本文简介
在Linux设备驱动中,字符设备驱动较为基础。本文主要讲解Linux字符设备驱动程序的结构及其主要组成部分的编程方法。
6.1节讲解了Linux字符设备驱动的关键数据结构cdev及file_operations结构体的操作方法,并分析了Linux字符设备的整体结构,给出了简单的设计模板。
6.2节讲解了本章及后续各章所基于的globalmem虚拟字符设备,第6~9章都将基于该虚拟设备实例进行字符设备驱动及并发控制等知识的讲解。
6.3节依据6.1节的知识讲解了globalmem设备的驱动编写方法,对读写函数、seek()函数和I/O控制函数等进行了重点分析。该节的最后改造globalmem的驱动程序以利用文件私有数据。
6.4节给出了6.3节的globalmem设备驱动在用户空间的验证。
6.1 Linux字符设备驱动结构
一、cdev结构体
Linux2.6内核中使用cdev结构体描述字符设备,cdev结构体的定义如代码清单6.1所示。
代码清单6.1 cdev结构体
struct cdev{
struct kobject kobj; /*内嵌的kobject对象*/
struct module *owner; /*所属模块*/
struct file_operations *ops; /*文件操作结构体*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中高12位为主设备号,低20位为次设备号。使用下列宏可以从dev_t获得主设备号和次设备号。
MAJOR(dev_t dev)
MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成dev_t。
MKDEV(int major, int minor)
cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux2.6内核提供了一组函数用于操作cdev结构体,如下所示:
void cdev_init(struct cdev *, struct file_operations *);
/*用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接*/
struct cdev *cdev_alloc(void); /*用于动态申请一个 cdev 内存*/
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned); /*向系统添加cdev,完成字符设备的注册*/
void cdev_del(struct cdev *); /*从系统删除cdev,完成字符设备的注销*/
cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的连接。其源代码如代码清单6.2所示。
代码清单6.2 cdev_init()函数
void cdev_init(struct cdev *cdev, struct file_operations *fops){
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdec->list);
cdev->kobj.ktype = &ktype_cdev_default;
kobject_init(&cdec->kobj);
cdev->ops = fops; /*将传入的文件操作结构体指针赋值给cdev的ops*/
}
cdev_alloc()函数用于动态申请一个cdev内存,其源代码如代码清单6.3所示。
代码清单6.3 cdev_alloc()函数
struct cdev *cdev_alloc(void){
struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL); /*分配cdev的内存*/
if(p){
memset(p, 0, sizeof(struct cdev));
p->kobj.ktype = &ktype_cdev_dynamic;
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj);
}
return p;
}
cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。
二、分配和释放设备号
在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
register_chrdev_region()函数用于已知起始设备的设备号的情况。而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功后,会把得到的设备号放入第一个参数dev中。alloc_chrdev_region()与register_chrdev_region()对比的优点在于它会自动避开设备号重复的冲突。
相反地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型如下:
void unregister_chrdev_region(dev_t from, unsigned count);
三、file_operations结构体
file_operations中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行open()、write()、read()、close()等系统调用时最终被调用。file_operations结构体目前已经比较庞大,它的定义如代码清单6.4所示。
代码清单6.4 file_operations结构体
struct file_operations{
struct module *owner;
//拥有该结构的模块的指针,一般为THIS_MODULES
loff_t (*llseek)(struct file *, loff_t, int);
//用来修改文件当前的读写位置
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
//从设备中同步读取数据
ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);
//初始化一个异步的读取操作
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
//向设备发送数据
ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t);
//初始化一个异步的写入操作
int (*readdir)(struct file *, void *, filldir_t);
//仅用于读取目录,对于设备文件,该字段为NULL
unsigned int(*poll)(struct file *, struct poll_table_struct *);
//轮询函数,判断目前是否可以进行非阻塞的读取或写入
int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
//执行设备I/O控制命令
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
//不使用BLK文件系统,将使用此种函数指针替代ioctl
long (*compat_ioctl)(struct file*, unsigned int, unsigned long);
//在64位系统上,32位的ioctl调用将使用此函数指针替代
int (*mmap)(struct file *, struct vm_area_struct *);
//用于请求将设备内存映射到进程地址空间
int (*open)(struct inode *, struct file *);
//打开
int (*flush)(struct file *);
int (*release)(struct inode *, struct file *);
//关闭
int (*synch)(struct file *, struct dentry *, int datasync);
//刷新待处理的数据
int (*aio_fsync)(struct kiocb *, int datasync);
//异步fsync
int (*fasync)(int, struct file *, int);
//通知设备FASYNC标志发生变化
int (*lock)(struct file *, int, struct file_lock *);
ssize_t (*readv)(struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev)(struct file *, const struct iovec *, unsigned long, loff_t *);
//readv和writev:分散/聚集型的读写操作
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
//通常为NULL
ssize_t (*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);
//通常为NULL
unsigned long(*get_unmapped_area)(struct file *, unsigned long, unsigned long,
unsigned long, unsigned long);
//在进程地址空间找到一个将底层设备中的内存段映射的位置
int (*check_flags)(int);
//允许模块检查传递给fcntl(F_SETEL...)调用的标志
int (*dir_notify)(struct file *filp, unsigned long arg);
//仅对文件系统有效,驱动程序不必实现
int (*flock)(struct file *, int, struct file_lock *);
};
下面对 file_operations 结构体中的主要成员进行讲解。
llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行 write()系统调用时,将得到-EINVAL 返回值。
readdir()函数仅用于目录,设备节点不需要实现它。
ioctl()提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。内核本身识别部分控制命令,而不必调用设备驱动中的 ioctl()。如果设备不提供 ioctl()函数,对于内核不能识别的命令,用户进行 ioctl()系统调用时将获得-EINVAL 返回值。
mmap()函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,用户进行 mmap()系统调用时将获得-ENODEV 返回值。这个函数对于帧缓冲等设备特别有意义。
当用户空间调用 Linux API 函数 open()打开设备文件时,设备驱动的 open()函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与 open()函数对应的是 release()函数。
poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行 select()和 poll()系统调用将引起进程的阻塞。
aio_read()和 aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符调用 aio_read()、 aio_write()等系统调用进行读写。
四、Linux字符设备驱动的组成
在Linux系统中,字符设备驱动由如下几个部分组成。
1、字符设备驱动模块加载与卸载函数