linux驱动摘要

来源:互联网 发布:潭州seo研究中心 编辑:程序博客网 时间:2024/05/20 19:46
(一) 写Linux Device Driver的基本思路

  写驱动,其实主要就是三件事:

实现struct file_operations中的函数。一般至少需要实现六个:
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t);
ssize_t (*write) (struct file *, const char __user, size_t, loff_t);
loff_t (*llseek) (struct file *, loff_t, int);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
实现两个模块中必须的函数:
static int __init my_init(void);
static void __exit my_exit(void);
模块,无外乎就是多了init和exit这两个函数,可以把驱动程序中实现的函数加载进内核中,和把驱动程序从内核中卸载。
写一个加载模块的脚本,里面要在用insmod加载了驱动模块之后,用mknode创建/dev中相应的文件。今后用户操作设备时都是通过访问/dev中的设备文件进行的。
(二) 一些重要的数据结构

  还有一些数据结构也比较关键,关系也比较复杂:

dev_t
这个不是structure,是简单变量,只用于保存一组major number和minor number。Linux提供一组Macro对其进行读写:
MAJOR(dev_t dev); /* 取设备的major number */
MINOR(dev_t dev); /* 取设备的minor number */
MKDEV(int major, int minor); /* 从一组指定的major number和minor number创建一个dev_t */
struct cdev
用于表示一个char型的设备。里厢内容不详
struct file_operations
用于定义一组在某类文件上操作的函数,根据文件类型不同,需要实现的接口也不同。
struct file
用于表示某个“打开的”文件,是与进程相关的。每次在有程序对文件执行open系统调用时创建。也就是说,同一个文件/设备,对应在其上操作的不同的进程,会创建多个file结构,而实际上操作的文件(或者说inode)是同一个。struct file里面有当前的指针位置和一些标记位等信息;还有指向一个file_operations结构的指针,对于设备文件,这用于提供到驱动程序的接口。
struct inode
用于表示文件系统树形结构中的一个节点,不论是目录还是文件。它里面保存的是文件的具体信息,每个文件只对应一个inode结构。对于char型设备文件,这里面主要有两个field有用:
dev_t i_dev; /* 在表示设备文件的inode中用于存放major number和minor number */
struct cdev *i_cdev; /* 在表示char型设备文件的inode中用于存放指向对应的cdev的指针 */
(三) scull工作的大致过程:

Linux启动时运行加载驱动模块的脚本。脚本首先做insmod。insmod时会调用驱动模块的init函数。在init中,进行了一些与设备本身相关的初始化设置以后(比如scull需要分配内存空间),会调用cdev_init()和cdev_add()来进行字符设备的初始化,并把这个设备添加进系统。这个过程会创建/proc/modules、/proc/devices两个文件和/sys/devices/目录中相应的项目。接下来脚本用mknod命令创建/dev/目录下的文件。这里创建的/dev/scull0,对应的就是前面说到的inode结构。
用户空间的程序通过系统调用open打开设备文件(比如在程序中fopen("/dev/scull0", "w" )),Linux会生成一个file结构,其中会包含f_pos(位置指针)、f_mode(打开方式是否只读等)等状态信息,然后调用驱动模块中定义的open()函数,把刚刚生成的file结构作为参数传给open()。open()通常需要根据情况做一些诸如设置互斥标记位之类的工作。
用户空间的程序通过系统调用进行读写操作(比如使用fprintf()等函数),会调用驱动程序中的read()、write()、llseek()等函数。这些操作会改变file结构中的信息,比如f_pos。
这时如果有另一个用户空间的程序打开这个设备文件,会再创建一个file结构,因为两个进程在访问时file结构中的信息是不同的,比如位置指针就不同。所以Linux把创建file结构的工作放在open系统调用时,而不是系统加载驱动模块时。但是需要注意的是,和第一个打开这个设备的文件不同,这次的file结构是从第一个进程中fork出来。这样,这个fork出来的进程不需要调用驱动程序中定义的release函数,在其返回时,file结构就会自动销毁。也就是说,只有一个进程会调用驱动程序中的open和release函数(虽然可能不只一个进程会试图关闭设备文件),也就保证了文件打开和关闭的次数不会有不同。
每一个程序完成操作,关闭设备文件时(比如使用fclose()函数),会销毁对应的file结构。但是只有最后一个进程关闭文件时才会调用驱动程序的release()函数。
关机时自然就是调用驱动模块中的exit函数,释放资源了。

第四章和第五章的笔记。这两章分别是:

Debugging Techniques
Concurrency and Race Conditions
  第四章里全是Debug的方法,实际开发时这是必须掌握的,包括:

内核中实现的某些检查机制
  内核中提供的检查机制需要在menuconfig时enable,可以检查出很多书中介绍的常见错误,例如Spinlock未初始化、sleeping with a Spinlock、堆栈溢出、定义为__init的函数在被卸掉以后又被调用等。比较有用!
使用printk()打印状态信息
  打印状态信息,这个上学的时候就知道了!Just for completeness吧!
使用proc文件系统
  这个用处应该也比较广的。原理已经掌握了,就是先在/proc文件系统下注册一个文件,然后提供当有人读这个文件时,用于提供文件内容的方法。作者推荐使用seq_file的方式,也就是start、next、show、stop四个函数。相对于原来的方式,seq_file的方式处理大量信息的时候比较容易。
使用strace工具
  strace工具可以显示一个程序执行过程中的各个系统调用的参数和返回值。比较厉害!
如何理解Oops信息
  Oops信息是内核中出现比较严重的错误,但是Linux没有被hung up时,显示的信息。其内容主要是:IP指针、所在的函数名、CPU寄存器信息、堆栈中的内容、Call trace。比较牛B!
gdb、kdb、kgdb的介绍
  这部分可能是最有用的了吧,尤其是使用gdb调试内核。但是我目前没有那么复杂的项目可做,所以暂时不想学。
  现在可以说掌握了的只有使用printk()、读Oops信息和实现/proc文件系统三个。其他方法等到用到的时候再回过头来查吧。

  第五章讲了进程并发有关的内容,包括怎么控制race。作者把控制方法分成几类:

Semaphore
  操作系统课里叫“信号量”(奇怪的翻译方法),提供P、V两种操作的那个。
Completion
  不知道咋译,也没仔细看。
Spinlock
  译为“自旋锁”,也没仔细看呢。
Atomic Variable
  应该是译为“原子变量”,在其上的操作很简单,但都是atomic的。
  Completion和Spinlock都是由Semaphore变形而来的,所以没仔细看。Semaphore和Atomic Variable两部分了解了,但是还没实际用它们写过代码。

  这章里有一个地方郁闷了很久:Semaphore的一系列P操作函数中,有个down_interruptible()。这个家伙在进入asleep状态等待semaphore被释放的时候,有可能被Signal唤醒。所以使用这个函数时,要判断它的返回值,看它是因为啥醒的。如果返回值为零,则说明是取得了semaphore而返回,相当于“自然醒”;不然则是被某个Signal给唤醒了,那么驱动程序需要返回-ERESTARTSYS(意思是告诉上一层的函数“重新尝试这个系统调用”)。这段得看了第六章的“IO Blocking”和“Asynchronouns Notification”两部分,还得了解Linux进程管理中的信号机制(Signal),才能明白。可折腾死我了。

  总结这章,其实也挺简单。就是一句话:你正在操作的数据随时可能被别的进程强奸,所以不要对数据报有任何假设,除非你hold了她的semaphore。


第六章的笔记。

Advanced Char Driver Operations

  看完第六章之后,时间安排有了变化。接下来的第七、八章(Time, Delays, and Deffered Work 和 Allocating Memory)只能暂时略过,从第九章继续看。

  第六章中介绍的是Char设备驱动中,除读写操作以外的常见操作,包括:

ioctl
Blocking I/O
poll and select
Asynchronous Notification
Seeking a Device
Access Control on a Device File
ioctl

  ioctl以前给我感觉挺神秘的,原来就是选好cmd的号码,然后写个switch就行了。关键是这一章还没介绍如何真正操作硬件,所以用的ioctl的例子也只是在软件部分的一些动作。这样当然就没什么神秘的了。

Blocking I/O

  使用sleep时应该注意的一点是,必须十分确定自己一定会被wake up,才可以开始sleep。其实前面讲Semaphore的时候,已经涉及到了sleep的概念。但是到了这里才开始真正介绍sleep的种类,以及进程如何使自己开始sleep的两种方式(simply调用wait_event()的方式,和所谓“manual”的方式)。

  第一种,也是比较简单的sleep基本上是这样:

创建一个wait_queue_head。两种方法:

DECLARE_WAIT_QUEUE_HEAD(my_queue);
  或
wait_queue_head_t my_queue;init_waitqueue_head(my_queue);
调用wait_event()(或者个系列的其他函数):
wait_event(myqueue, condition);

其中的condition就是这个进程wake up的条件。

  然后,当然了,应该会有另一个进程调用wake up进程的函数“wake_up(&my_queue)”。这个函数会wake up所有my_queue上的进程。(对应wait_event()系列的其他函数,wake_up()也有若干变形。)

  第二种,manual sleep实际上就是把wait_event()这个宏拆开。这样可以做出更复杂的sleep(比如可以用prepare_to_wait_exclusive())。

  使用第一种sleep方式时,一般的情况下,当wake_up(&my_queue)被调用时,如果my_queue上面有多个进程在等待condition这个条件,则它们将同时被wake up。在看了wait_event()的定义以后,发现其实它内部就是一个循环,一旦被wake_up,就去检查condition是否确实为真,以判断是否需要继续sleep。但是必须注意的是,由于race的存在,这也并不意味着从wait_event()返回后condition就一定是真。

  另外,这一章里,还学到了schedule()函数。schedule()就是告诉Linux,你现在应该马上重新考虑下应该把CPU交给哪个进程使用,也就是当年操作系统课中讲的“从用户态切换到系统态”。这个动作就是一个进程在把自己的state设置为TASK_INTERRUPTIBLE或TASK_UNITERRUPTIBLE,并把自己加入sleep队列以后,要做的事情。这里还有一个有意思的事情就是,如果在进程完成了这两项动作以后,而还没有调用schedule()之前的空隙,有人调用了wake_up()试图唤醒这个进程,Linux并不会出乱子。因为wake_up()正好做了和那两项动作相反的事,所以当这个进程重新获得CPU,并继续执行到schedule()时,scheduler会直接返回。

  回顾一下Blocking I/O这部分的内容,感觉最重要的就是要想清楚流程。down_interruptible()以后进入一个循环,除非condition成立才可以退出循环。在循环体内,当condition不成立时,要先up(),再开始sleep,睡醒了之后再down_interruptible()并回到循环开始,重新判断condition。而循环体内的sleep那部分也挺烦。要先prepare_to_wait(),然后在睡前最后确认一次condition,确实仍然不成立,才调用schedule()。schedule()返回以后先finish_wait(),再看看是怎么醒的。如果是某Signal给吵醒的,要return -ESYSRESTART;重新来过。否则是自然醒,那就算是睡眠结束,继续上面说的“睡醒了之后在down_interruptible()那段”。

poll and select

  poll和select是Unix时代传下来的一个系统调用的两个版本,在file_operations结构中都是用poll()实现的。poll用来查看当前某设备可用的操作(读、写等)。在程序不希望进入sleep状态等待某个I/O设备时,它们通常会在操作前调用poll来查看设备状态。

  poll涉及到一些乱七八糟的数据结构,什么poll_table之类的。这部分只了解了poll的作用,更深的没仔细看。

Asynchronous Notification

  异步通知。也就是POSIX进程通信机制中的“Signal”,信号机制。其目的是为了让(尤其是低优先级的)进程可以更迅速地响应某些事件。这部分也没仔细看,只是知道进程需要用fcntl()在设备上注册自己。

Seeking a Device

  就是讲了一些和fops.llseek()有关的内容,说有些设备不支持seek操作的,该怎么办。

Access Control on a Device File

  scull系列的最后几个终于出场了。这章介绍了几种常见的访问控制方式:

Single Open
  很直接的方法。在设备的结构中加个锁,第一次open就锁住,以后再open就返回一个-EBUSY。这样一个设备就只能open一次了。
Restricting Access to a Single User at a Time
  有时候要求同一个设备可以被open多次,但必须只能是由同一个用户。有一种可能就是为了确保在进行多个进程在某个设备上的race时,不希望其他用户的进程打扰。这可以在open中判断uid,至于判断之后的处理方法有几种:
Returning -EBUSY
直接返回“忙”,没啥说的。
Blocking open
让后来的用户的进程sleep。
Cloning the Device on open
为后来的用户的进程创建一个副本。一个例子就是console设备。
  这就是第六章的内容了,很杂。单独写一篇看来是对了。 
原创粉丝点击