第五章 信号量和互斥体
第三章介绍了一个以内存为设备的字符设备驱动。需要注意的是,第三章介绍的只是这个驱动的最基本的四个函数,open release read和write 有了这四个函数,这个驱动也就可以使用了。不过,从书本附带的源码中得到的scull工程的代码可远远不止这四个函数,它的很多代码时为了后面章节的扩展。同时,由于内核版本跟作者所使用的内核版本不一致,在编译这个scull的时候会出现很多的问题,http://www.linuxidc.com/Linux/2011-06/37818.htm这篇文章介绍了在编译是可能遇到的问题以及解决方法,必须得感谢这篇文章的作者,我终于成功地把scull编译出来了。
第四章介绍的是调试技术,我看的也是模模糊糊的,貌似也没什么机会尝试,打算先进入后面章节的学习,待到用时在回头补充。这里提供一个很给力的文章
http://www.cnblogs.com/yangnas/archive/2010/03/18/1688885.html,这里介绍了如何使用eclipse和qemu虚拟机来单步调试内核,就跟调试一个应用程序一样的。按照步骤,我最终达到效果了。特别提一下,eclipse的版本不要使用最新的3.7,用3.5。在这个地方我可是吃了很大苦头的。还有就是源码存放的路劲尽量不要使用中文。这些都是我的经验吧。下面就进入第五章了。
————————————————————————————————————————
第三章的scull工程是很简单的,它完全没有考虑并发问题而形成的竞态。作为一个程序员,对于并发来说应该是不陌生的,编写一些应用程序也要考虑程序的并发执行。进而到了linux的设备驱动程序,并发问题是更加的显著的。之前就多次提到过,驱动只是提供功能(机制),至于如何使用功能,是由用户应用程序决定的。这样也就是说,你的驱动代码可能在任意时刻被调用,“任何时刻可能发生任何事情”。
回过头看下scull的write函数:
1 if (!dptr->data[s_pos]) {
2
3 dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
假如要写入的位置为空,就申请一片内存空间来写。显而易见的,当有两个或者两个以上的进程同时执行这一段代码的话,就会出现竞态。产生的后果就是后者的赋值覆盖了前者,而前者所申请的内存也就泄露了。太糟糕了。
产生竞态的原因其实很明显,就是两个执行线程同时需要访问相同的数据结构。取消这些共享的数据结构明显是不现实的,解决问题的关键就在于对于这些共享数据结构的访问管理。就是说要有一个第三方的管理员吧,他负责管理这些共享资源,使得在同一时刻,资源只能被一个线程访问。同时,这个管理者还得负责把这个资源的状态告知那些申请了这个资源的线程。作为一个资源的访问者来说,他要么得到资源的访问权,要么就被告知资源当前不可用。
为了达到这个目的,操作系统引入了信号量的概念。当一个进程要访问共享的资源时,他就要调用P,如果当前信号量的值大于0,则进程可以访问共享资源,同时信号量的值减一;如果当前信号量的值小于0,则进程不能访问共享资源,进程进入等待队列,信号量的值还是要减一。因此,当信号量的值为负时,这个值的绝对值就是当前等待队列中进程的数量。当一个进程要释放共享的资源时,他就要调用V,如果当前信号量的值大于0,信号量的值加一;如果当前信号量的值小于0,他会唤醒一个等待队列中的进程,信号量的值加一。
特殊的,如果要求临界区里头在同一时刻只有一个进程运行,型号量的值就必须初始化为1,这种情况就称为互斥。
linux中使用struct semaphore结构体(定义在<asm/semaphore.h>中)来实现信号量。信号量的初始化函数如下:
1 void sema_init(struct semaphore *sem, int val);
第二个参数val是信号量的初值,当然,要用于互斥的话,这个值就为1了。至于对信号量的P函数和V函数操作,就有对应的down函数和up函数了。
下面看看scull中对信号量的使用,首先每一个scull设备的scull_dev结构中友信号量的定义:
1 struct scull_dev {
2
3 struct scull_qset *data; /* Pointer to first quantum set */
4
5 int quantum; /* the current quantum size */
6
7 int qset; /* the current array size */
8
9 unsigned long size; /* amount of data stored here */
10
11 unsigned int access_key; /* used by sculluid and scullpriv */
12
13 struct semaphore sem; /* mutual exclusion semaphore */
14
15 struct cdev cdev; /* Char device structure */
16
17 };
在设备驱动的初始化函数中,对每一个scull设备进行初始化,同时也包括了每一个scull设备的信号量的初始化:
1 for (i = 0; i < scull_nr_devs; i++) {
2
3 scull_devices[i].quantum = scull_quantum;
4
5 scull_devices[i].qset = scull_qset;
6
7 init_MUTEX(&scull_devices[i].sem);
8
9 scull_setup_cdev(&scull_devices[i], i);
10
11 }
注意,作者给出的源码是使用init_MUTEX宏,如果由于内核版本原因而无法编译通过的话,这里就好改成使用sema_init了,详情在开头给出的文章外链中有解释。
在申请共享数据时,要先调用P,也就是down_interruptible,如果返回值为负,则说明申请失败,返回:
1 if (down_interruptible(&dev->sem))
2
3 return -ERESTARTSYS;
最后,在完成了访问数据后,一定要调用V,即
1 up(&dev->sem);
编写代码时应当特别小心,确保即使在出错后,这个V操作也要执行到,这是非常重要的。