成于坚持,败于止步
Linux 字符设备驱动结构
cdev 结构体
在 Linux 2.6 内核中使用 cdev 结构体描述字符设备,cdev 结构体的定义如代码所示。
1 struct cdev 2 { 3 struct kobject kobj; /* 内嵌的 kobject 对象 */ 4 struct module *owner; /*所属模块*/ 5 struct file_operations *ops; /*文件操作结构体*/ 6 struct list_head list; 7 dev_t dev; /*设备号*/ 8 unsigned int count; 9 };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 定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux 2.6 内核提供了一组函数用于操作 cdev 结构体,如下所示:
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接,其源代码如代码所示。
1 void cdev_init(struct cdev *cdev, struct file_operations *fops) 2 { 3 memset(cdev, 0, sizeof *cdev); 4 INIT_LIST_HEAD(&cdev->list); 5 cdev->kobj.ktype = &ktype_cdev_default; 6 kobject_init(&cdev->kobj); 7 cdev->ops = fops; /*将传入的文件操作结构体指针赋值给 cdev 的 ops*/ 8 }cdev_alloc()函数用于动态申请一个 cdev 内存,其源代码如代码所示。
1 struct cdev *cdev_alloc(void) 2 { 3 struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL); /*分配 cdev的内存*/ 4 if (p) { 5 memset(p, 0, sizeof(struct cdev)); 6 p->kobj.ktype = &ktype_cdev_dynamic; 7 INIT_LIST_HEAD(&p->list); 8 kobject_init(&p->kobj); 9 } 10 return p; 11 }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 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()、write()、read()、close()等系统调用时最终被调用。file_operations 结构体目前已经比较庞大,它的定义如代码所示。
1 struct file_operations 2 { 3 struct module *owner; 4 // 拥有该结构的模块的指针,一般为 THIS_MODULES 5 loff_t(*llseek)(struct file *, loff_t, int); 6 // 用来修改文件当前的读写位置 7 ssize_t(*read)(struct file *, char _ _user *, size_t, loff_t*); 8 // 从设备中同步读取数据 9 ssize_t(*aio_read)(struct kiocb *, char _ _user *, size_t, loff_t); 10 // 初始化一个异步的读取操作 11 ssize_t(*write)(struct file *, const char _ _user *, size_t, loff_t*); 12 // 向设备发送数据 13 ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t); 14 // 初始化一个异步的写入操作 15 int(*readdir)(struct file *, void *, filldir_t); 16 // 仅用于读取目录,对于设备文件,该字段为 NULL 17 unsigned int(*poll)(struct file *, struct poll_table_struct*); 18 // 轮询函数,判断目前是否可以进行非阻塞的读取或写入 19 int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); 20 // 执行设备 I/O 控制命令 21 long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); 22 // 不使用 BLK 文件系统,将使用此种函数指针代替 ioctl 23 long(*compat_ioctl)(struct file *, unsigned int, unsigned long); 24 // 在 64 位系统上,32 位的 ioctl 调用将使用此函数指针代替 25 int(*mmap)(struct file *, struct vm_area_struct*); 26 // 用于请求将设备内存映射到进程地址空间 27 int(*open)(struct inode *, struct file*); 28 // 打开 29 int(*flush)(struct file*); 30 int(*release)(struct inode *, struct file*); 31 // 关闭 32 int(*synch)(struct file *, struct dentry *, int datasync); 33 // 刷新待处理的数据 34 int(*aio_fsync)(struct kiocb *, int datasync); 35 // 异步 fsync 36 int(*fasync)(int, struct file *, int); 37 // 通知设备 FASYNC 标志发生变化 38 int(*lock)(struct file *, int, struct file_lock*); 39 ssize_t(*readv)(struct file *, const struct iovec *, unsigned long, loff_t*); 40 ssize_t(*writev)(struct file *, const struct iovec *, unsigned long, loff_t*); 41 // readv 和 writev:分散/聚集型的读写操作 42 ssize_t(*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void*); 43 // 通常为 NULL 44 ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int); 45 // 通常为 NULL 46 unsigned long(*get_unmapped_area)(struct file *,unsigned long, unsigned long, 47 unsigned long, unsigned long); 48 // 在进程地址空间找到一个将底层设备中的内存段映射的位置 49 int(*check_flags)(int); 50 // 允许模块检查传递给 fcntl(F_SETEL...)调用的标志 51 int(*dir_notify)(struct file *filp, unsigned long arg); 52 // 仅对文件系统有效,驱动程序不必实现 53 int(*flock)(struct file *, int, struct file_lock*); 54 };下面对 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.字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和 cdev 的注册,而在卸载函数中应实现设备号的释放和 cdev 的注销。
工程师通常习惯将设备定义为一个设备相关的结构体,其包含该设备所涉及的cdev、私有数据及信号量等信息。常见的设备结构体、模块加载和卸载函数形式如代码所示。
1 //设备结构体 2 struct xxx_dev_t 3 { 4 struct cdev cdev; 5 ... 6 } xxx_dev; 7 //设备驱动模块加载函数 8 static int _ _init xxx_init(void) 9 { 10 ... 11 cdev_init(&xxx_dev.cdev, &xxx_fops); //初始化 cdev 12 xxx_dev.cdev.owner = THIS_MODULE; 13 //获取字符设备号 14 if (xxx_major) 15 { 16 register_chrdev_region(xxx_dev_no, 1, DEV_NAME); 17 } 18 else 19 { 20 alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME); 21 } 22 23 ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); //注册设备 24 ... 25 } 26 /*设备驱动模块卸载函数*/ 27 static void _ _exit xxx_exit(void) 28 { 29 unregister_chrdev_region(xxx_dev_no, 1); //释放占用的设备号 30 cdev_del(&xxx_dev.cdev); //注销设备 31 ... 32 }设备驱动的读函数中,filp 是文件结构体指针,buf 是用户空间内存的地址,该地址在内核空间不能直接读写,count 是要读的字节数,f_pos 是读的位置相对于文件开头的偏移。
设备驱动的写函数中,filp 是文件结构体指针,buf 是用户空间内存的地址,该地址在内核空间不能直接读写,count 是要写的字节数,f_pos 是写的位置相对于文件开头的偏移。
由于内核空间与用户空间的内存不能直接互访,因此借助函数 copy_from_user()完成用户空间到内核空间的复制,函数 copy_to_user()完成内核空间到用户空间的复制。
copy_from_user()和 copy_to_user()的原型如下所示:
unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为 0。
如果要复制的内存是简单类型,如 char、int、long 等,则可以使用简单的 put_user()和 get_user(),如下所示:
int val; //内核空间整型变量
...
get_user(val, (int *) arg); //用户空间到内核空间,arg 是用户空间的地址
...
put_user(val, (int *) arg); //内核空间到用户空间,arg 是用户空间的地址
读和写函数中的_ _user 是一个宏,表明其后的指针指向用户空间,这个宏定义如下:
#ifdef _ _CHECKER_ _
# define _ _user _ _attribute_ _((noderef, address_space(1)))
#else
# define _ _user
#endif
I/O 控制函数的 cmd 参数为事先定义的 I/O 控制命令,而 arg 为对应于该命令的参数。例如对于串行设备,如果 SET_BAUDRATE 是一个设置波特率的命令,那后面的arg 就应该是波特率值。
在字符设备驱动中,需要定义一个 file_operations 的实例,并将具体设备驱动的函数赋值给 file_operations 的成员,如代码所示。
1 struct file_operations xxx_fops = 2 { 3 .owner = THIS_MODULE, 4 .read = xxx_read, 5 .write = xxx_write, 6 .ioctl = xxx_ioctl, 7 ... 8 };图 6.1 所示为字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。
就到这里了,O(∩_∩)O~
我的专栏地址:http://blog.csdn.net/column/details/linux-driver-note.html
待续。。。。