字符设备驱动结构
cdev结构体
Linux内核中cdev结构体描述一个字符设备。
1 | struct cdev { |
dev_t
定义了设备号,为32位,其中12位为主设备号,20位为次设备号。下面的宏可以获得主设备号和次设备号:
1 | MAJOR(dev_t dev) |
使用下面的宏可以用主设备号和次设备号生成dev_t:
1 | MKDEV(int major, int minor) |
Linux内核提供了一组函数用于操作cdev结构体:
1 | void cdev_init(struct cdev *, struct file_operations *); // 用于初始化cdev的成员,并建立cdev和file_operations之间的连接 |
分配和释放设备号
在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()
或alloc_chrdev_region()
函数向系统申请设备号:
1 | int register_chrdev_region(dev_t from, unsigned count, const char *name); |
register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况。
file_operations结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用。
代码清单 include/linux/fs.h: file_operations
1 | struct file_operations { |
下面对file_operations结构体中的主要成员简要介绍:llseek()
函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。read()
函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。write()
函数向设备发送数据,成功时该函数返回写入的字节数。如果次函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。unlocked_ioctl()
提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。
字符设备驱动的组成
1.字符设备驱动模块加载与卸载函数
代码清单 加载与卸载函数模板
1 | // 设备结构体 |
2.字符设备驱动的file_operations结构体中的成员函数
file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。
代码清单 字符设备驱动读、写、I/O控制函数模板
1 | // 读设备 |
由于用户空间不能直接访问内核空间的内存,因此借助了函数copy_from_user()
完成用户空间缓冲区到内核空间的复制,copy_to_user()
完成内核空间到用户空间缓冲区的复制。它们的原型如下:
1 | unsigned long copy_from_user(void *to, const void __user *from, unsigned long count); |
完全复制成功返回值为0,如果复制失败,则返回负值。
读和写函数中的__user
是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释功能。
在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员:
1 | struct file_operations xxx_fops = { |
上述xxx_fops在 dev_init(&xxx_dev.cdev, &xxx_fops) 语句中建立与cdev的连接。
globalmem虚拟设备驱动
在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4K)的内存空间,并在驱动中提供对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。
头文件、宏及设备结构体
代码清单 globalmem设备结构体和宏
1 |
|
加载与卸载设备驱动
代码清单 globalmem设备驱动模块的加载与卸载函数
1 | static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) |
在cdev_init()函数中,与globalmem的cdev关联的file_operations结构体如下:
代码清单 globalmem设备驱动的文件操作结构体
1 | static const struct file_operations globalmem_fops = { |
读写函数
globalmem设备驱动的读写函数主要是让设备结构体的mem[]数组与用户空间交互数据,并随着访问的字节数变更更新文件读写偏移位置。
代码清单 globalmem设备驱动的读写函数
1 | static ssize_t globalmem_read(struct file *filp, char __user *buf, |
seek函数
seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),假设globalmem支持从文件开头和当前位置的相对偏移。
代码清单 globalmem设备驱动的seek()函数
1 | static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) |
ioctl函数
globalmem设备驱动的ioctl()函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl()函数返回-EINVAL。
1 | static long globalmem_ioctl(struct file *filp, unsigned int cmd, |
使用文件私有数据
大多数Linux驱动遵循一个”潜规则”,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。对于globalmem驱动而言,私有数据的设置是在globalmem_open()中完成的。
1 | static int globalmem_open(struct inode *inode, struct file *filp) |
globalmem驱动在用户空间中的验证
在globalmem源代码目录中通过make
命令编译globalmem驱动,运行:
1 | $ sudo insmod globalmem.ko |
reference
《Linux设备驱动开发详解——基于最新的Linux 4.0内核》