当前位置:文档之家› Linux设备驱动第五章(并发和竞争)读书笔记

Linux设备驱动第五章(并发和竞争)读书笔记

第5章 并发和竞争
参考文章:https://www.doczj.com/doc/754863398.html,/u1/34474/showart.php?id=408682
5.3旗标和互斥体
1)旗标的核心是一个单个整型值。结合一对函数来实现,也称“PV操作”。
一个想进入临界区的进程在相关旗标上调用P,如果旗标值大于0,这个值递减1,并且进程继续。如果旗标小 于或者等于0,说明该旗标已经被其他进程占用,此时进程必须等待知道旗标被释放。
解锁旗标,也就是释放旗标,通过调用V操作来完成,V操作递增旗标的值,并且可以唤醒等待该旗标的进程 。
2)互斥体 作用是:互斥,阻止多个进程同时在同一临界区内运行。互斥体也就是值初始化为1的旗标。因为 :初始化为1,当第一个进程P操作时,1>0,所以进程得以运行,同时旗标递减1,值为0,此时第2个进程想 获得该旗标用来访问临界区,却发现旗标值为0,因此进程2不能运行一直到进程1释放该旗标。

5.3.1 LINUX旗标实现
1)要使用旗标,必须包含.相关的类型是 struct semaphore;
2)旗标的实现:创建一个旗标, 接着使用 sema_init 来设定它
例如:
struct semaphore *sem;//定义旗标
sema_init(sem,val);//初始化旗标 val为初始化旗标的值。
3)通常,旗标是以互斥锁的模式使用。因此内核提供了一系列的宏来初始化。
DECLARE_MUTEX(name); //初始化一个互斥锁,值为1
DECLARE_MUTEX_LOCKED(name);//初始化一个互斥锁,值为0 就是:旗标初始化后就不可用。
任何要使用该旗标的进程都要先解锁它再使用。
当我们需要在运行时间初始化(也就是动态创建)我们使用:
void init_MUTEX(struct semaphore *sem);//初始化一个互斥锁,值为1
void init_MUTEX_LOCKED(struct semaphore *sem);初始化一个互斥锁,值为0
例如:我们要创建一个初始化为0的互斥锁:
struct semaphore *sem;//定义旗标
ECLARE_MUTEX_LOCKED(sem);//初始化旗标值为0。

以上是初始化旗标,初始化后我们当然就要使用旗标了,不然创建它有何意义?

4)旗标的获取与释放(也可以成为互斥锁,因为LINUX中基本上所有的旗标都用来互斥)
获取旗标也称P操作,也叫down。down字面意思就是下降的意思。相对于旗标来说就是减1。
p函数如下:
void down(struct semaphore *sem); /*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore *sem);/*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore *sem);/*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*

/
我们一般使用down_interruptible函数就好。书中推荐滴。
一旦获取了旗标,则获取旗标的进程就可以存取该旗标保护的临界区了。当使用完后,要怎么办?当然是丢弃对临界区的控制,也就是释放旗标。
释放旗标也称V操作,也叫UP,up字面意思就是上升的意思。相对于旗标来说就是加1。(down和up形象的描述了对旗标的操作)。
V函数如下:
void up(struct semaphore *sem);
一旦调用UP,进程就不在拥有旗标了。
5)使用旗标的容易犯错误
获得旗标的进程使用对UP的调用来释放旗标,但不能多次调用UP,就是说:一个down对于一个UP。
在持有旗标遇到错误时,我们必须在return(错误状态)是调用UP释放旗标,不然临界区一直被该进程占有,但也许该进程已经被kill了,而其他要使用临界区的进程就会因一直得不到临界区而一直挂起。

5.3.2. 在 scull 中使用旗标
正确使用加锁原语的关键是严密地指定要保护哪个资源并且确认每个对这些资源的存取都使用了正确的加锁方法.
首先看看一个scull结构体:
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; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
struct semaphore sem是我们的旗标,而这个结构体是我们要保护的对象。

旗标在使用前必须初始化. scull 在加载时进行这个初始化, 在这个循环中:
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i], i);
}
我们定义了scull_nr_devs个字符驱动,因此我们建立了scull_nr_devs个旗标,有人问:为什么不是建立一个全局旗标就可以了?答案是:每个字符驱动之间不共享资源,为了效率,我们没有理由在使用其中一个SCULL设备时,也使其他进程不能使用其他的scull设备。
该循环一般出现在驱动的__init加载函数里面,是对变量的初始化以及字符设备的加载。
init_MUTEX 在 scull_setup_cdev 前被调用. 以相反的次序进行这个操作可能产生一个竞争情况, 旗标可能在它准备好之前被存取.(一定要注意)。
当初始化完后,我们要使用该驱动了,我们必须确认在没有持有旗标时没有对 scull_dev 数据结构的存取.换句话说也就是:只有在持有旗标时才能对数据进行存取。
在scull_write 代码中我们可以看到:
if (down_interruptible(&dev->sem))
return -ERESTARTSYS

;
注意对 down_interruptible 返回值的检查; 如果它返回非零, 操作被打断了. 在这个情况下通常要做的是返回 -ERESTARTSYS. 看到这个返回值后, 内核的高层要么从头重启这个调用要么返回这个错误给用户. 如果你返回 -ERESTARTSYS, 你必须首先恢复任何用户可见的已经做了的改变, 以保证当重试系统调用时正确的事情发生. 如果你不能以这个方式恢复, 你应当替之返回 -EINTR.
当我们使用完临界区后,wirte函数一定要释放旗标,不管它的操作是否成功,或者当我们使用write函数时发生错误,比如:kmalloc分配内存失败,copy_form_uesr从用户空间copy数据失败。记住:一定要释放旗标。
代码:
out:
up(&dev->sem);
return retval;
在遇到不同的错误时,使用goto语句来跳转到out.
另:必须确保在不拥有信号量的时候不会访问scull_dev结构体。

5.3.3读者/写者旗标
旗标为所有调用者进行互斥,但有时我们可以想想:旗标的作用是什么?一般情况下,我们用在对临界区的存取上,因为如果没有旗标,可能进程1刚修改完一个变量,进程2又修改了同一个变量,导致后来的数据把前面的数据给覆盖了。如同5.1. scull 中的缺陷中所举得例子一样。 但是但我们只读的时候,我们也只允许一个进程读,这样做就显的没有效率。因此,有了读者/写者旗标。
读者/写者旗标实现的结果:我们可以并发的读,但是只能互斥的写。提高了效率。只读的任务可以并行进行它们的工作而不必等待其他读者退出临界区.
1)使用rwsem要包含.
2)初始化一个rwsem.
void init_rwsem(struct rw_semaphore *sem);
3)函数如下:
需要只读存取的代码的接口是:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
对 down_read 的调用提供了对被保护资源的只读存取, 与其他读者可能地并发地存取. 注意 down_read 可能将调用进程置为不可中断的睡眠. down_read_trylock 如果读存取是不可用时不会等待; 如果被准予存取它返回非零, 否则是 0. 注意 down_read_trylock 的惯例不同于大部分的内核函数, 返回值 0 指示成功. 一个使用 down_read 获取的 rwsem 必须最终使用 up_read 释放.

读者的接口类似:
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
down_write, down_write_trylock, 和 up_write 全部就像它们的读者对应部分, 除了, 当然, 它们提供写存取. 如果你处于这样的情况, 需要一个写者锁来做一个快速改变, 接着一个长时间的只读存取, 你可以使用

downgrade_write 在一旦你已完成改变后允许其他读者进入.
4)rwsem 在驱动中的使用相对较少, 但是有时它们有用。rwsem允许多个读来持有旗标,而写着有优先权,当一个写者试图获得旗标,就不允许读者进入直到写者完成工作。但是它可以导致读者饥饿,因为如何大量的写者来竞争旗标,因为写者有优先权,所以读者被长时间的拒绝. 因此:rwsem最好用的很少写并且写占用很短时间的情况下。

5.4. Completions 机制
上面讲述了旗标的互斥使用,但是旗标还有一种方式就是:同步。同步的含义就是:顺序执行,只有事件A运行结束,事件B才开始运行。这叫同步。
例如:
struct semaphore sem;
init_MUTEX_LOCKED(&sem);//初始化旗标为0 不可用
start_external_task(&sem);//在该任务中调用UP。
down(&sem);//获得旗标
但是事实证明:这种情况旗标不是最好的工具,因此Completions 机制产生了。
1)要使用Completions 机制,必须包含.
2)创建completions的2种方式:
1:DECLARE_COMPLETION(my_completion);
2:struct completion my_completion;//动态创建
/* ... */
init_completion(&my_completion);
3)函数:
void wait_for_completion(struct completion *c); //等待completion
该函数是一个不可打断的等待,如果被调用但没有进程来完成条件,则会是一个不可杀死的进程,因此必须有函数来完成条件,也就是发出completion事件。

void complete(struct completion *c);//只唤醒一个等待的进程
void complete_all(struct completion *c);//唤醒所有等待的进程
以上2个函数被用来发出completion事件。
如果我们使用了complete_all()函数,那么我们必须再重新使用它之前调用INIT_COMPLETION(struct completion c)来快速初始化 completion 结构。
4)实例程序
DECLARE_COMPLETION(comp);//初始化completion
ssize_t complete_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) going to sleep\n",current->pid, current->comm);
wait_for_completion(&comp);//等待completion事件
printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
return 0; /* EOF */
}

ssize_t complete_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) awakening the readers...\n", current->pid, current->comm);
complete(&comp);//发出completion事件
return count; /* succeed, to avoid retrial */
}
该程序实现的是:对设备的write使确切的read完成,也许有多个“读”,但是我们不知道是哪个“读”。
5)completion机制的典型使用
completion 机制的典型使用是在模块退出时与内核线程的终止一起. 在这个原型例子里, 一些驱动的内部工

作是通过一个内核线程在一个 while(1) 循环中进行的. 当模块准备好被清理时, exit 函数告知线程退出并且等待结束. 为此目的, 内核包含一个特殊的函数给线程使用:

void complete_and_exit(struct completion *c, long retval);

5.5. 自旋锁(未完待续)

















相关主题
文本预览
相关文档 最新文档