本文也即《Linux Device Drivers》,LDD3的第五章Concurrency and Race Conditions的读书笔记之二,但我们不限于此内容。
信号量(Semaphore)
信号量和互斥锁
Kernel提供不同的原语来处理不同的情况,最常用的是采用信号量的方式。如果不能获得资源将进入sleep状态,等待资源释放,也即block的方
式。通过加锁的原语使之sleep,例如在scull的write()的例子中,kmalloc很适合,但是不是所有情况都适合sleep的方式。信号量
是一种sleep机制。它包含一个整数,以及一对函数P和V。进程如果需要进入将调用P,如果信号量的值大于0,那么该值减一,继续执行,如果信号量等于
或者小于0,进程将等待其他人释放型号了。Unlock信号量即调用V,它将信号量的值加1,如果可能唤醒正在等待的进程。
如果信号量用户互斥(mutex:mutual exclusion),将信号量的值初始化的1,这样只允许一个进程或者线程执行。这种情况下,信号量也成为互斥锁。在linux kernel中基本上是由于的信号量都是互斥锁。
在scull中,我们已经以互斥锁的方式使用过信号量,在scull_write()中有竞争导致的内存泄漏的危险:
if(!dptr->data[s_pos]){
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if(dptr->data[s_pos] == NULL)
goto out;
… …
}
这里分配了内存空间kmalloc,然后进行copy内容,如果在执行的过程中,有多个进程同时触发了scull_write,就发生竞争,例如A执行第
一句判断,当尚未执行第二句kmalloc时,B执行第一句,因此B也需要执行第二句,因此两个进程对data[s_pos]都需要kmalloc空间,
并执行copy内容,导致内容混乱是一个方面,A分配的空间,他的指针消失了,我们无法对A的kmalloc进行释放,它将占据内存的一个空间,直至系统
关闭,这就是内存泄漏。
信号量的使用例子见kernel module编程(五):设备读写
,相关操作已经重点标出,这里不再重复。使用方式如下:
- 头文件加载:#include <asm/semaphore.h>,
相关的数据结构为struct semaphore
。
- 创建信号量:void sema_init(struct semaphore * sem
, int val
);
当val设置为1时就成为互斥锁。对于互斥锁,也可以采用如下的方式:
- 声明和初始化一个互斥锁:DECLARE_MUTEX(
name
)
或者DECLARE_MUTEX_LOCKED(
name
),
其中name是一个struct semaphore的变量。 在后一种方式,起始状态就是locked,其他线程/进程需要等待其unlocked才能进入。
- 如果一个互斥锁需要实时初始化,即动态产生,使用:void int_MUTEX(struct semaphore *
sem
)
;或者void init_MUTEX_LOCKED(struct semaphore *
sem
);
- 对于信号量获取,即P函数,通过down来调用,对信号量进行减一操作,如果信号量不满足要求,则将调用者置为sleep状态直至资源获取。方式如下:
- void down
(struct semaphore * sem); 对信号量减一,并一直等到资源获取。
- int down_interruptible
(struct semaphore *sem);
同上,但是采用中断的方式(interruptible),这是最为常用的使用方式,它允许用户中断一个正在等待信号量获取的用户空间的进程。如果采用非
中断方式,则这个这个期间,进程是无法killed的。我们需要注意的是,采用这种方式,如果在等待期间用户终止了进程,则将返回一个非零的值,所以必须
判断返回值,并作出相应处理。一般返回为-ERESTARTSYS
,在返回之前,应当确保undo之前用户可察觉的所有操作,如果不能确保,返回-EINTR
.
- int down_trylock(struct semaphore *
sem
);
这种方式不会sleep,如果不能成功会的信号量,将马上返回非零值。
- 当一个线程成功调用down,即获得的semahore,将进入这段敏感代码区,执行完后,必须释放信号量。通过up来调用V操作。void up(struct semaphre *
sem)
。我们要非常注意,如果在处理过程中出现异常或者错误而需要return,必须要保证信号量的释放,即无论是正常的还是异常地离开这段敏感代码区,都必须释放信号量
。否则任何线程/进程将无法获取信号量。
我们需要注意,一定要在获取信号量之前确保已经初始化。在语句先后执行顺序中必须要优先处理。
读写信号量
在scull
的例子中,读和写都通过信号量进行保护,防止一起写,也防止在写和读同时操作,这些都是我们应当避免的,当时它同时也不允许两个读的操作一通进行,而这种
情况是不会产生危害的。Linux
kernel提供了一个rwsem的特别的信号量用于处理这种情况,允许多个读同时存在以提高程序处理能力。读写信号量在驱动中一般较少使用,但是有时会
非常有效。使用方式如下:
- 头文件加载:#include
<linux/rwsem.h>
,相应的的数据结构为struct rw_semaphore
,
- 初始化操作:void init_rw_sem(struct rw_semaphore *
sem
);
- 对于只读操作,相关函数如下:
-
void down_read(struct rw_semaphore *
sem
);
提供只读获取的保护,可以同时有其他的只读操作。他将置调用者与非中断的sleep,这是需要特别注意的,即如果另外有写的操作,而引起sleep,是非中断,用户不能在此刻中断进程。
-
int down_read_trylock(struct rw_semaphore *
sem
);
马上返回,如果可读,返回非零,不可读,返回0。注意这个返回和一般的kernel函数的方式方式不一样。也可信号量的返回方式不一样。
-
void up_read(struct rw_semaphore *
sem
);
释放读写信号量。
- 对于写的保护,相关函数如下:
-
void down_write(struct rw_semaphore *
sem
);
类似down_read
-
int down_write_trylock(struct rw_semaphore *
sem
);
类似down_read_trylock
-
void up_write(struct rw_semaphore *
sem
);
类似up_read
-
void downgrade_write(struct rw_semaphore *
sem
);
当一个写保护后,跟着一个耗费时间长的读保护,我们可以在使用能够downgrade_write,它允许其他读操作在你结束写后,马上获得读写型号了。否侧通常的处理是需要等待这个紧跟的漫长的读操作。
读写信号量允许一个写用户或者无限个读用户来获得,写用户将具备优先具备,当一个写试图进入这段关键操作代码时,其他读者都无法获得信号量,必须等待所有的写完成。因此它适合于写操作比较少,且写的过程比较短的情况,不适合存在大量写,这会阻碍读的处理。
completion(不知道中文名字,可能是完成量,^_^)
一般信号量的的处理会限制在一个函数内,但是有时会函数A的处理的前提条件是函数B,A必须等待B处理后才能继续,可以用信号量来进行处理,但linux kernel提供complete的方式。使用方式如下:
- 头文件#include
,数据结构为struct completion
,初始化为init_completion(struct completion *
comp
)
,也可以直接使用DECLARE_COMPLETION(
comp
);
- 在A函数中,如果需要等待其他的处理,使用void wait_for_completion(struct completion *
comp
);
则在这个位置上将处于非中断的sleep,进行等待,也就是相关的线程/进程,用户是无法kill的。
- 在B函数,如果已经处理完,可以交由A函数处理,有下面两种方式
-
void complete(struct completion *
comp
);
如果要执行A必须等待B先执行,B执行后,A可以继续执行。如果A需要再次执行,则需要确保下一次B执行完。如果连续执行两次B,则可以执行两次A,第三次A要等第三次B执行完。
-
void complete_all(struct completion *
comp
);
只要B执行完,A就可以执行,无论执行多少次。如果需要再等待B的直系个可以使用INIT_COMPLETION(struct completion *
comp
)
。重新初始化completion即可。
-
void complete_and_exit(struct completion *
comp
,long
retval
)
; 这个处理具有complete的功能外,还将调用它的线程/进程终止。可用于一些无限循环的场景,例如受到某个cleaned up的信息后,e通知用户程序终止,允许A函数执行。
针对我们的例子Scull,其实completion并不是合适的场景,但我们可以通过它来试验一下。我们希望是在scull的读操作之前都先完成一次写操作。
#include <linux/completion.h>
... ...
DECLARE_COMPLETION(comp); //为了试验方便,就不分每个scull都持有一个完成量,本来应当如此
int scull_read(... ...){
struct scull_qset * dptr;
... ...
printk("scull_read waiting for completed from write function.\n");
wait_for_completion
(&comp);
printk("scull_read awake for reading,continue ....\n");
... ....
}
int scull_write(... ...){
... ...
complete
(&comp);
// complete_all
(&comp);
// complete_and_exit
(&comp);
}
对于scull0,我们原来在用户空间有一个读写测试例子,将其分为读测试和写测试。当我们调用读测试是,例子sleep,只有调用写测试时,读测试才能
继续进行。但是发现,scull_read的使用发现scull的内核模块发生crash,这是因为在wait_for_completion()之前,
对一些变量进行赋值,例如dptr,而这些变量在write的时候是发现改变的,因此出现错误,需要在wait_for_completion后面对这些
变量进行赋值。这样解决了crash的问题。但是我们发现在写测试后,读测试可以继续进行,但是很快又陷入了等待状态。下面是读测试的有关代码:
file = fopen("dev/scull0","r");
... ...
while((len = fread(read_buf,1,512,file)) > 0){
t
otal_len
+=len;
printf("%s",read_buf);
memset(read_buf,0,512);
}
会调用scull_read直至scull_read返回0或者<0为止。因为已经进行了写操作,所以第一次调用返回内容长度,会出现第二次调用。
这样会将程序搞得很混乱,实际上我们写测试,因为写的内容少,可以一次写完,也很可能分为多次写。所以在这里加完成量是不合适的,可以在fopen中进行
处理,即scull_open中进行处理,例如:
int scull_open(....)
{
... ...
if((filp->f_flags & O_ACCMODE) == O_RDONLY){
printk("waiting for completed from write function.\n");
wait_for_completion(&comp);
printk("awake for reading,continue ....\n");
}
... ...
}
这是更为合理的方式。如果complete的位置仍然放在scull_write中,我们试验三种方式。complete_and_exit(),可以要
求写测试中写入大量的内容,我们将发现只写了部分的内容(第一次调用scull_write)
,测试程序就退出。scull并不是个合适的completion的例子,completion可能会引起这样或者那样的问题,需要仔细规划。
分享到:
相关推荐
网盘文件永久链接 ...20:可睡眠锁:读写信号量rwsem_rec 21:可睡眠锁:完成变量completion_rec 22:可睡眠锁:SRCUsleepable_read-copy-update_rec 23:原子操作_rec 24:内存屏障_rec ...........
利用互斥锁和计数信号完成生产者消费者问题 一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中...
Linux中提供一把互斥锁mutex(也称之为互斥量)。 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。 资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作...
程,熟练掌握线程的创建pthread_create(),线程终止pthread_exit(),等待线程合 并pthread_join()等线程控制的操作,利用信号量或者互斥锁实现线程间的同步。 二、实验内容 问题:求 100000 个浮点数(精确小数点右 ...
Linux 的 进程间同步异步的程序,互斥锁,信号量,完成量,poll 机制
2、用信号量机制解决进程(线程)的同步与互斥问题。 二、实验目的 1.掌握基本的同步互斥算法,理解生产者和消费者模型。 2.了解Windows 2000/XP中多线程的并发执行机制,线程间的同步和互斥。 3.学习使用Windows ...
μC/OS-III还允许无限数量的内核对象,如任务,信号量,互斥,信号旗,消息队列,计时器和内存分区。μC/OS-III大部分是运行时可以配置。 μC/OS-III提供接近零的中断停用时间。μC/OS-III有一些内部数据结构和变量...
4.9.1 获取和释放信号量 221 4.9.2 读/写信号量 224 4.9.3 补充信号量 225 4.10 禁止本地中断 226 4.10.1 禁止本地中断 227 4.10.2 禁止下半部(可延迟函数) 229 4.11 一些避免竞争条件的实例 231 4.11.1 引用...
信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程...
使用互斥体来防止竞争状况作业4:比较带有和不带有互斥锁的多线程程序作业5:使用信号量解决了生产者使用者问题,该信号量仅在Ubuntu上运行(在VirtualBox中测试) 作业6:使用read(),write(),open()和...
员+1,要求利用互斥锁,使每次输出a[0]==a[99]。 4)创建两线程,A线程每2秒打印一次字母A,B线程每秒打印一次 字母B,要求利用同步信号量,使输出字母B总是在A之后。 任务4、Linux应用程序开发实践,任选以下任务之一...
//文件锁保证读写互斥 CCriticalSection csReadCount; //读者记数互斥 CCriticalSection csPreWriteLock;//写者优先锁 int g_iReadCount = 0; bool g_bPreWriter= false; //写者优先标志 HANDLE hReadSemaphore=...
6)信号量Semaphore 31 7)ReentrantLock可重入的互斥锁定 Lock 32 8)阻塞队列BlockingQueue 34 9)已完成任务队列CompletionService 36 10)计时器CountDownLatch 37 11)周期性同步工具CyclicBarrier 38 12)异步计算的...
(1)二值信号量可用作互斥体(mutex) (2)实现资源池,例如数据库连接池 (3)使用信号量将任何一种容器变成有界阻塞容器 栅栏 能够阻塞一组线程直到某个事件发生 栅栏和闭锁的...
设计和实现需要通过许多同步机制(如自旋锁、互斥锁、信号量和监视器)共享计算机资源的应用程序 使用标准 C 库实现 C 语言程序实现通过操作系统调用与计算机平台交互的应用程序 使用 POSIX API 实现应用程序 使用 C...
1.该文件夹下所有的文件示例,都是基于宋宝华《 linux设备驱动程序开发详解,基于最新4.0内核》写的,所有的代码都经过了调试和验证并附有日志文档。2.globalfifo是在堆内存里面通过kzalloc()函数申请的一段内存,...
2 基本线程编程.............................................................................................................................................23 线程库.......................................