本篇基于ldd3中第三章,原书自带的源码随着内核版本更新已经不能运行,代码需要进行升级,文章参考代码能在内核版本4.17.2运行。
1. 分配设备编号
建立一个字符驱动时,需要做的第一件事是获取一个或多个设备编号来使用.此目的必要的函数是 register_chrdev_region.
注册字符设备函数执行后会出现在/proc/devices和sysfs中:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
first是要分配的起始设备编号. first 的次编号部分常常是 0, 但是没有要求是那个效果. count 是请求的连续设备编号的总数. 注意, 如果 count 太大,要求的范围可能溢出到下一个次编号; 但是只要要求的编号范围可用, 一切都仍然会正确工作. name 是应当连接到这个编号范围 的设备的名子; 它会出现在 /proc/devices 和 sysfs 中.
一些主设备编号是静态分派给最普通的设备的. 一个这些设备的列表在内核源码树的 Documentation/devices.txt 中.
对于新驱动,建议使用动态分配来获取你的主设备编号, 而不是随机选取一个当前空闲的编号.使用 alloc_chrdev_region, 不是 register_chrdev_region.
这个alloc_chrdev_region是动态分配主设备号的,因为你可能不知道系统中哪些主设备号可以给你的驱动程序使用,动态分配的一个缺点就是不能提前分配设备节点(注:通过MKNOD来创建节点的):
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
两个函数从名字上也可以区分,一个是注册,表示我有了主设备号只是想内核注册,而alloc_则是分配,意味着让内核来帮着allocate一下。
注册对应的是注销,这个是驱动程序卸载时候必须做的,从哪里来还那里去,对应的函数是:
void unregister_chrdev_region(dev_t from, unsigned count)
另外,获取设备主设备号和次设备号,使用如下宏:
MAJOR(dev_t dev);
MINOR(dev_t dev);
将主、次设备号转换成一个设备号,如下:
MKDEV(int major, int minor);
2. 基础性的驱动操作
基础性的驱动操作包括 3 个重要的内核数 据结构, 称为 file_operations, file, 和 inode.
2.1 file_operation
传统上, 一个 file_operation 结构或者其一个指针称为 fops( 或者它的一些变体). 结构中的每个成员 必须指向驱动中的函数, 这些函数实现一个特别的操作
其中定义的操作函数并不是需要全部实现,根据具体驱动实现针对的函数功能即可。字符设备主要有一下函数需要实现:
owner,llseek,read,write,ioctl,open,release.
2.2 file
file结构表示一个打开的文件。在内核中指向file的指针经常叫做filp,就是file pointer,以免是file搞混。
2.3 inode
inode结构由内核在内部用来表示文件,代表打开文件描述符的文件结构是不同的。多个打开的描述符可能指向一个单个inode结构。
相对于字符设备驱动程序,我们先使用i_rdev和i_cdev。
dev_t i_rdev;//实际设备的节点
Struct cdev *i_cdev; //指向字符设备驱动程序指针
现在可以通过宏如下,来获取节点的主、次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
3. 字符设备注册
内核中使用cdev结构体来表示字符设备。可以通过cdev_alloc来分配。
然后使用cdev_init来初始化。
我们可以将cdev结构体嵌入到我们自己的设备结构体中,这也正是例子所使用的方法。
最后告诉内核添加进去,如果不告诉内核就是空有一身资源而无施展之处,通过函数cdev_add。一旦添加,那么内核就可能来骚扰设备,所以要确保所有都准备好的时候调用cdev_add函数。
去除设备调用函数cdev_del.
4. 设备布局
设备由内存来模拟,其设备中的布局如下图。
数据结构如下,scull_qset结构体非常简单,其实现一个链表的同时,每个元素同时指向块内存:
struct scull_qset {
void **data;
struct scull_qset *next;
};
设备的结构体如下:
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* 互斥所*/
struct cdev cdev; /* Char device structure */
};
设备的大小为quantum*qset。
5. 代码解析
5.1 初始化
初始化函数为scull_init_module
如果指定了主设备号,调用register_chrdev_region,否则调用alloc_chrdev_region,并获取主设备号。
然后分配设备scull_dev结构体数组scull_devices,数量为SCULL_NR_DEVS子设备号数量,并初始化为0。接着根据需要分配的内存空间大小正式初始化scull_devices,其中会调用scull_setup_cdev函数(该函数中会使用cdev_init,cdev_add函数,初始化设备结构中嵌入的cdev,同时绑定scull_fops),scull_fops结构如下。
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.unlocked_ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
然后调用
初始化时候分配了指定数量的设备数据结构,并初始化后增加到了内核,可以在/proc/devices中看到,此时其实并没有分配设备的内存空间,因为还不需要。
5.2 退出
退出函数是scull_cleanup_module,该函数先获取设备号。然后根据设备数量循环调用scull_trim,cdev_del函数来删除cdev设备,最后调用kfree释放在初始化中分配的结构体数据。
其中scull_trim函数负责释放分配的内存空间。
5.3 操作函数集合
驱动的open函数
通过inode获取设备结构体的指针,这个通过内核中的container_of函数来实现,并将其保存到文件对象的private_data中以备后用。
如果是写模式打开,则将之前该设备分配的空间使用scull_trim函数清空。
llseek
返回当前读写位置。
release
直接返回0,并不做操作。
read
先从filp->private_data中获取设备地址。获取设备总空间大小。
如果要读的位置大于空间大小则退出。否则计算要读取的正确位置,因为每个scull_qset结构体指向的item空间大小是固定的,其相互之间是链表方式连接的。
然后会调用scull_follow函数,该函数中会通过kmalloc函数动态分配scull_qset结构体(如果没有被分配过),直到包含的item累计理论空间能包含要读取的地址。然后返回最后一个scull_qset结构体,如果返回null,说明系统内存空间不够了。
最后调用copy_to_user函数,将内容复制到用户的buf中。然后更新文件读取位置,并返回所读取字节大小。
write
获取设备结构体的指针,以及对应的设备空间相关大小。如item,quantum。
计算文件读取位置,调用scull_follow,返回位置所在的那个item的scull_qset结构体,如果该结构体对应的数据指针为NULL,说明之前没有给其分配内存空间,则调用kmalloc分配qset指向quantum的指针数数组。
然后根据读取位置,通过函数kmalloc函数分配quantum的内存空间。
最后调用用copy_from_user函数将数据复制到内存中的quantum片段。
unlocked_ioctl
对ioctl的实现。
6. 使用测试
加载驱动后,执行如下,其中247是主设备号,在/proc/devices中可以查看到:
mknod /dev/scull0 c 247 0
mknod /dev/scull1 c 247 1
mknod /dev/scull2 c 247 2
mknod /dev/scull3 c 247 3
然后可以使用dd命令或者cp命令复制内容到设备中。
# echo "hello" > scull0
# cat scull0
hello
7. 代码
https://github.com/kernel-z/ldd3/tree/master/scull