scull简单驱动程序阅读
一、 驱动程序加载
module_init(scull_init_module);
指定了使用insmod加载模块时,调用scull_init_module进行初始化,在该函数中做的事情就是分配设备号等工作,具体如下:
1、 如果指定了主设备号,则使用register_chrdev_region()函数尝试静态分配设备号,否则使用alloc_chrdev_region()函数动态分配一个;
2、 为scull_nr_devs个设备分配scull_dev的内存空间,并清空;
3、 在循环中初始化每个scull_dev设备:初始化quantnum、qset等变量,初始化信号量、调用函数scull_setup_cdev();
(在scull_setup_cdev()函数中,先调用cdev_init()函数初始化scull_dev结构中的cdev机构,并将其与scull_fops联系起来,之后通过调用cdev_add()函数通知内核。)
4、 调用scull_create_proc函数创建/proc/scullmen和/proc/scullseq两个文件。
(在 scull_create_proc函数有点复杂,在“十、使用/proc”中再叙述)
二、 驱动程序卸载
module_exit(scull_cleanup_module);
指定了rmmod卸载模块时,调用函数scull_cleanup_module,主要用来释放已分配的内存、设备号等,具体如下:
1、 在循环中调用scull_trim()函数释放每个设备中存储数据的内存区域,然后调用cde_del()函数移除该设备对应的字符设备结构;
(scull_dev结构中*data指向一个有scull_qset的链表,链表中每个scull_qset结构中的**data指向一个指针数组,数组中每个指针指向一片内存区域,这些内存区域用来 存储数据。而scull_trim函数则使用循环的方式释放这些内存区域和scull_qset结构)
2、 释放设备scull_dev结构;
3、 调用scull_reomve_proc()函数/proc中的入口项;
(具体的内容在 “十、使用/proc” 中再叙述)
4、 调用unregister_chrdev_region()函数释放设备号。
三、文件操作
在设备驱动中有一个重要的数据结构——file_operations,这个结构指明了与设备相关的函数(Unix系列把设备当成文件)。scull中的file_operations如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
于是指定了打开、关闭、写、读、定位、控制该设备的函数分别为scull_open、scull_release、scull_write、scull_read、scull_llseek、scull_ioctl。
四、 scull_open
在scull中,scull_open的工作如下:
1、使用container_of(),通过inode->i_cdev得到当前设备的指针,然后将其保存到filp->private_date中;
2、如果是以可写的方式打开该设备,则使用down_interruptible()函数尝试获取信号量,获取信号量后调用scull_trim()函数清空存储数据的区域,之后up()释放信号量。
(这样的直接结构是,每次用vi打开文件后,文件都是空的)
五、 scull_release
该函数除了返回0,之外没有进行任何操作。但ldd中提到一般的设备都会维护一个使用计数,release函数中一般会减一。
六、 scull_write
该函数的具体过程如下:
1、 获取互斥信号量,down_interruptible();
2、 计算当前偏移对应第几个scull_qset、在scull_qset中data的下标、以及在*data指向的内存区域中的偏移;
3、 调用scull_follow()函数获得指向scull_qset的指针;
(scull_follow()函数主要的工作就是根据当前要操作的sucll_qset的索引,返回其指针,如果在该索引之前的scull_qset结构不存在的话,为之分配内存)
4、 分配内存,拷贝数据,重置偏移量;
5、 释放信号量,up()。
七、 scull_read
该函数具体过程如下:
1、获取互斥信号量,down_interruptible();
2、计算偏移对应的内存区域指针,也调用函数scull_follow();
3、拷贝数据,重置偏移量;
4、释放信号量,up()。
八、 scull_llseek
该函数使用一个switch语句,根据whence(0:文件开始;1:当前偏移;2:文件末)重置filp->f_ops的值。
文件对象中的f_ops保存文件当前的位移量。
九、 scull_ioctl
这个函数中的内容我怎么看,主要是用来回应一些设备命令,大致流程为:
1、 提取“幻数”和虚幻,拒绝不恰当的ioclt(-ENOTTY);
2、 如果有数据传输,则使用access_ok来验证地址是否可用;
3、 之后,通过switch根据具体的命令进行具体的操作。
十、 使用/proc
ldd中提到的使用/proc接口的方法有两种,在scull中都是用到了:
1、使用create_proc_read_entry
这种方法相对比较简单,只需先定义一个处理读/proc文件的函数(这里是scull_read_procmen),然后调用create_proc_read_entry()函数创建对应的/proc文件,指定读取函数即可。
最后,在卸载模块时,调用remove_proc_entry()函数即可。
2、使用seq_file接口
(1)使用seq_file接口,需要定义四个迭代器操作函数:start、next、stop、show:
start函数用于指定seq_file文件的读开始位置,如果指定的位置超过文件末尾,则返回NULL;
next函数 用于将迭代器移动到下一个位置;
stop函数 在内核使用迭代器后,用来进行清理工作;
show函数用于 将数据 格式化输出 到用户空间,在scull中于scull_read_procmem()类似 。
(2)scull将这些迭代器操作函数填充到了一个seq_operation 结构中。之后定义创建了一个file_operation结构,以在略低的层次上连接到/proc。在这个file_operation结构中自己实现的方法只有一个(scull_proc_open连接到.open上),其余的使用的是fs/seq_file.c中定义的seq_read、seq_lseek和seq_release。
在scull_proc_open()函数中,调用seq_open将文件与seq_operation连接。
(3)最后,调用create_proc_entry()函数创建/proc文件,之后将目录项中proc_fops指向scull_proc_ops。
scullp阻塞型驱动程序
一、 scull_p_init()
1、如果指定了主设备号,则尝试调用 register_chrdev_region() 分配设备号,否则调用函数 alloc_chrdev_region() 函数动态分配一个;
2、为 scull_p_dev 分配内存;
3、在循环中初始化每个 scull_p_dev 设备:
先调用init_waitqueue_head() 函数动态初始化写等待队列、和读等待队列;然后初始化互斥信号量;再对每个 scull_p_dev 调用 scull_p_setup_cdev() 。
(在scull_p_setup_cdev() 中所做的工作只是调用 cdev_init() 分配和初始化 cdev 结构,将其与 file_operation 结构相连,之后调用 cdev_add() 通知系统。
二、 scull_p_cleanup()
1、在循环中调用 cdev_del() 从系统中删除每个 cdev 结构,之后释放每个设备的环形缓冲区;
2、释放 scull_p_dev 的内存;
3、释放设备号。
三、 文件操作
struct file_operations scull_pipe_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = scull_p_read,
.write = scull_p_write,
.poll = scull_p_poll,
.open = scull_p_open,
.release = scull_p_release,
.fasync = scull_p_fasync,
};
在系统调用poll 、 epoll 或 select 查询某个或多个文件描述符上的读取或写入是否会被阻塞时, scull_p_poll 返回一个掩码,用来指出非阻塞的读取和写入是否可能。
fasync用于异步通知,在执行 F_SETFL 启用 FASYNC 时,驱动程序中的该方法就会被调用。而具体工作将由 fasync_helper 完成。
四、 scull_p_open
1、获得设备号,保存到文件描述符的 private 成员中;
2、尝试获得信号量, down_interruptible() ,之后为环形缓冲区分配内存,并初始化缓冲区头、尾指针和读、写指针;
3、根据打开文件的类型(读 / 写),为读者或写者计数加以;
4、释放信号量, up() ;
5、最后返回 noneseekable_open(inode,filp); 该函数的功能就是修改文件描述符的 f_mode ,具体为: filp->f_mode &= ~(FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE) 。
五、 scull_p_fasync
scull_p_fasync的所有工作都有 fasync_hepler 完成,以从相关的进程队列表中添加或删除文件;
六、 scull_p_release
1、调用 scull_p_fasync 方法,从活动的异步读取进程列表中删除该文件。( ldd 中提到这只有 FASYNC 标志设置了才有必要,但是调用它不会有坏处)。
2、获取信号量,之后对读者或写者计数减一;
3、如果读者和写者计数和为 0 ,则释放环形缓冲的内存;
4、释放信号量。
七、 scull_p_read
1、获取信号量;
2、在 while 的条件语句中判断读写指针是否重合,如果读写指针重合,则表示无数据可读,于是先释放信号量,然后:
如果读操作是不可阻塞的,则返回-EAGIN ;
调用wait_event_interruptible() ,将进程加入 inq 等待队列,并设置休眠前后要对表达式 dev->rp!=dev->wp ;
唤醒后,重新获得信号量。
3、 拷贝数据,重置读指针位置;
4、 释放信号量
5、 调用wake_up_interruptible() 唤醒 outq 上的写者进程;
八、 scull_p_write
1、获取信号量;
2、调用 scull_getwritespace() 函数,该函数中包含休眠的代码,主要过程如下:
在while 循环条件中,调用函数 spacefree() 函数判断可写空间大小是否为 0 ,如果为 0 则进行以下操作:
使用DEFINE_WAIT ( wait )设置等待队列入口;
释放信号量;
如果写是不可阻塞的,返回-EAGAIN ;
调用prepare_to_wait() 函数将等待队列入口添加到队列中,并设置进程状态为 TASK_INTERRUPTIBLE( 可中断睡眠 ) 。
再次调用spacefree() 函数检测是可写空间是否为 0 (不想被睡眠啊!),如果仍然没有空间则调用 schedule() 。
schedule()函数返回后调用 finish_wait() 函数进行一些清理工作;
调用signal_pending() 函数判断是否是因为信号而被唤醒,如果是则返回 -ERESTARTSYS 通知 fs 层做相应处理;
重新获取信号量。
3、 传输数据、重置写指针位置等
4、 释放信号量
5、 调用wake_up_inerruptible() 函数,唤醒 inq 队列上的等待的读进程;
6、 如果dev->async_queue 非空,说明异步通知队列中有文件,则调用 kill_fasync() 函数通知所有相关进程。
九、 no_llseek
scullp设备不支持定位,但因为默认是、方法是允许定位的,所以不能不写(不写则使用默认的),于是指定 no_llseek 。
十、 scull_p_poll
我从来没有使用过poll 、 epoll 、 select 等系统调用,对 poll 的作用不是很理解。
结合ldd 的讲解,我的理解是驱动程序要做的是两件事:
1、对该驱动程序中能改变 poll 所查询状态有关等待队列调用 poll-wait() 函数,将等待队列加到 poll_table 中(为什么要这么做呢?是不是说如果不能执行无阻塞的读写,那么进程会在该队列上等待呢?)
2、返回一个位掩码来指示操作是否可以立即无阻塞执行。
scull_p_poll中步骤如下:
1、获得信号量;
2、对 inq 和 outq 队列进行 poll_wait() 调用;
3、根据具体情况返回是否可以立即进行读写;
4、释放信号量。