字符设备是个能够像字节流一样被访问的设备,字符终端和串口就是两个字符设备。
这些设备文件和普通文件的区别是,对于普通文件的访问可以通过前后移动访问位置来实现随机存取,而大多数的字符设备只能够顺序访问。
它不具备缓冲区,因此对这种设备的读写是实时的。
字符设备驱动程序通常至少要实现open、close、read和write等操作接口。
2、字符设备驱动框架
Linux的一个重要特点就是将所有的设备都看成文件来处理,它们可以使用和操作文件相同的系统调用接口来完成打开、关闭、读写和I/О控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。如下图所示。
字符设备驱动程序的实现方式分为两种:一种是直接编译进内核,另一种是以模块方式加载,然后在需要使用驱动时加载。
字符设备在Linux内核中使用struct cdev结构来表示,这个结构体在整个字符驱动程序设计中起着关键的作用。在struct cdev结构中包含着字符设备需要的全部信息,其中最主要的是设备号(dev_t)和文件操作(file_operations)。
当驱动程序以模块的形式加载到内核中时,模块加载函数会初始化cdev结构,并且将其与文件操作函数绑定在一起,然后向内核中添加这个结构。而模块卸载函数则负责从内核中删除cdev结构。
a -- 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;
b -- 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
c -- 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;
用户空间访问该设备的程序:
通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;
3、字符设备驱动组成
如下图所示。
字符设备的注册和注销:
(1)字符设备驱动关键数据结构及内核函数
在Linux内核中使用cdev结构体来描述字符设备。
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //操作字符设备所能实现的方法
struct list_head list; //用来向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
内核还提供了操作cdev结构体的一组函数,只能通过这些函数来操作字符设备,例如初始化、注册、添加以及移除字符设备。
void cdev_init(struct cdev *, const struct file_operations *)
struct cdev *cdev_alloc(void)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
void cdev_del(struct cdev *p)
分别介绍如下:
1)void cdev_init(struct cdev *, const struct file_operations *)
其源代码如代码清单如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
该函数主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接:
1) 将整个结构体清零;
2) 初始化list成员使其指向自身;
3) 初始化kobj成员;
4) 初始化ops成员;
2)struct cdev *cdev_alloc(void)
该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即: .ops=xxx_ops)。
其源代码清单如下:
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
3)int cdev_add(struct cdev *p, dev_t dev, unsigned count)
该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。
当然这里还需提供两个参数:第一个设备号 dev;和该设备关联的设备编号的数量。
这两个参数直接赋值给struct cdev 的dev成员和count成员。
(2)字符设备驱动主要组成
字符设备创建过程:
创建一个字符设备的时候,首先要得到一个设备号,分配设备号的途径有静态分配和动态分配;拿到设备的唯一ID,将设备的file_operations并保存到cdev中,实现cdev的初始化;然后我们需要将所做的工作告诉内核,使用cdev_add()注册cdev;最后还需要创建设备节点,以便后面调用file_operations接口。
设备号:
对字符设备的访问是通过文件系统内的设备文件进行的,或者称为设备节点。它们通常位于/dev目录。表示字符设备的设备文件可以通过“ls -l”命令输出的第一列中的“c”来识别,而块设备则用“b”标识。
主设备号用来标识该设备的种类,也标识了该设备所使用的驱动程序;次设备号由内核使用,标识使用同一设备驱动程序的不同硬件设备。
当应用程序对某个设备文件进行系统调用时, Linux内核会根据该设备文件的设备类型和主设备号调用相应的驱动程序,并从用户态进入到内核态,再由驱动程序判断该设备的次设备号,最终完成对相应硬件的操作。
所有已经注册(即已经加载了驱动程序)的硬件设备的主设备号可以从/proc/devices文件中得到,如下所示:
设备号类型:
在Linux内核中,使用dev_t类型来表示设备号,这个类型在<linux/types.h>头文件中定义。
dev_t是一个32位的无符号数,其高12位用来表示主设备号,低20位用来表示次设备号。因此,在2.6内核中,可以容纳大量的设备,而不像先前的内核版本最多只能使用255个主设备号和255个次设备号。
内核主要提供了三个操作dev_t类型的函数,它们分别是:MAJOR(dev), MINOR(dev)和 MKDEV(ma,mi)。其中 MAJOR(dev)用于获取主设备号,MINOR( dev)则用于获取次设备号。而相反的过程是通过MKDEV( ma,mi)来完成的,它根据主设备号ma和次设备号mi构造dev_t设备号。
注册和注销设备号:
在建立一个字符设备之前,驱动程序首先要做的一件事是向内核请求分配一个或多个设备号。内核专门提供了字符设备号管理的函数接口,完成分配和释放字符设备号的函数主要有三个,它们都是在<linux/fs.h>头文件中声明,如下所示:
register_chrdev_region()函数和alloc_chrdev_region()函数用于分配设备号,这两个函数最终都会调用_register_chrdev_region()函数来注册一组设备编号范围,它们的区别是后者是以动态的方式分配的。unregister_chrdev_region()函数则用于释放设备号。
(3)设备操作关键数据结构
基本的驱动程序操作都会涉及内核提供的三个关键数据结构,分别是file_operations、file和inode,它们都在<linux/fs.h>头文件中定义。
1)file_operations
file_operations结构体描述了一个文件操作所需要的所有函数。这组函数是以函数指针的形式给出的,它们是字符设备驱动程序设计的主要内容。
每个打开的文件,在内核里都用file结构体表示,这个结构体中有一个成员为f_op,它是指向一个file_operations结构体的指针。通过这种形式将一个文件同它自身的操作函数关联起来,这些函数实际上是系统调用的底层实现。在用户空间的应用程序调用内核提供的open、close、read、write等系统调用时,实际上最终会调用这些函数。
对于一个字符设备来说,驱动一般只要实现open、release、read、write、mmap、ioctl这几个函数。
struct file_operations {
//指向拥有该结构的模块的指针,一般初始化为THIS_MODULE
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int); //用来改变文件中的当前读/写位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //用来从设备中读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //用来向设备写入数据
//初始化一个异步读取操作
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
//初始化一个异步写入操作
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
//用来读取目录,对于设备文件,该成员应当为NULL
int (*readdir) (struct file *, void *, filldir_t);
//轮询函数,查询对一个或多个文件描述符的读或写是否会阻塞
unsigned int (*poll) (struct file *, struct poll_table_struct *);
//用来执行设备I/O操作命令
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
//不使用BKL文件系统,将使用此函数代替ioctl
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
//在64位系统上,使用32位的ioctl调用将使用此函数代替
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
//用来将设备内存映射到进程的地址空间
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *); //用来打开设备
//执行并等待设备的任何未完成的操作
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *); //用来关闭设备
//用来刷新待处理的数据
int (*fsync) (struct file *, struct dentry *, int datasync);
//fsync的异步版本
int (*aio_fsync) (struct kiocb *, int datasync);
//通知设备FASYNC标志的改变
int (*fasync) (int, struct file *, int);
//用来实现文件加锁,通常设备文件不需要实现此函数
int (*lock) (struct file *, int, struct file_lock *);
}
file_operations结构体中的几个主要的函数:
llseek()函数用于改变文件中的读写位置,并将新位置返回。
open()函数负责打开设备和初始化I/O。例如,检查设备特定的错误,首次打开设备则对其初始化,更新f_op指针等。
release()函数负责释放设备占用的内存并关闭设备。
read()函数用来从设备中读取数据,调用成功则返回实际读取的字节数。
write()函数用来向设备上写入数据,调用成功则返回实际写入的字节数。
ioctl函数实现对设备的控制。除了读写操作外,应用程序有时还需要对设备进行控制﹐这可以通过设备驱动程序中的ioctl()函数来完成。
mmap()函数将设备内存映射到进程的地址空间。
此外,aio_read()和aio_write()函数分别实现对设备进程异步的读写操作。
2)file
Linux中的所有设备都是文件,在内核中使用file结构体来表示一个打开的文件。file结构体代表一个打开的文件,系统每个打开的文件在内核空间都有一个关联的file结构体。此结构体在内核打开文件时创建,并传递给在文件上操作的所有函数。
在这里,只介绍一些file结构体中的重要成员。
a. fmode_tf_mode:对文件的读写模式,对应系统调用open的mod_t mode参数。
b. loff_t f_pos:表示文件当前的读写位置。
c. unsigned int f_flags:表示文件标志,对应系统调用open的 int flags参数。
d. const struct file_operations * f _op:指向和文件关联的操作,如open、read、write等;
e. void * private_data:open系统调用重置这个指针为NULL,在调用驱动程序的open函数之前,可以自由使用这个成员或者忽略它。
f. unsigned int f_flags:表示文件标志,对应系统调用open 的 int flags参数。
g. struct dentry *f_dentry:关联到文件的目录入口( dentry )结构。
3)inode
在file_opreations结构体中的open和release函数﹐它们的第一个参数都是inode结构体。这是一个内核文件系统索引节点对象,它包含了内核在操作文件或目录时所需要的全部信息。在内核中inode结构体用来表示文件,它与表示打开文件的file结构体的区别是,同个文件可能会有多个打开文件,因此一个inode结构体可能会对应着多个file结构体。
对于字符设备驱动来说,需要关心的是如何从inode结构体中获取设备号。与此相关的两个成员分别是:
a. dev_ti_rdev:对于设备文件而言,此成员包含实际的设备号。
b. struct cdev * i_cdev:字符设备在内核中是用cdev结构来表示的。此成员是指向cdev结构的指针。
内核开发者提供了两个函数来从inode对象中获取设备号,它们的定义如下: