概念Linux内核的信号量在概念和原理上和用户态的System V的IPC机制信号量是相同的,不过他绝不可能在内核之外使用,因此他和System V的IPC机制信号量毫不相干。
如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列(它不是站在外面痴痴地等待而是将自己的名字写在任务队列中)然后让其睡眠。
当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。这一点与自旋锁不同,处理器可以去执行其它代码。
应用场景由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况;相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间表还要长。
举2个生活中的例子:
我们坐火车从南京到新疆需要2天的时间,这个'任务'特别的耗时,只能坐在车上等着车到站,但是我们没有必要一直睁着眼睛等,理想的情况就是我们上车就直接睡觉,醒来就到站(看过《异形》的读者会深有体会),这样从人(用户)的角度来说,体验是最好的,对比于进程,程序在等待一个耗时事件的时候,没有必须要一直占用CPU,可以暂停当前任务使其进入休眠状态,当等待的事件发生之后再由其他任务唤醒,类似于这种场景采用信号量比较合适。
我们有时候会等待电梯、洗手间,这种场景需要等待的时间并不是很多,如果我们还要找个地方睡一觉,然后等电梯到了或者洗手间可以用了再醒来,那很显然这也没有必要,我们只需要排好队,刷一刷抖音就可以了,对比于计算机程序,比如驱动在进入中断例程,在等待某个寄存器被置位,这种场景需要等待的时间往往很短暂,系统开销甚至远小于进入休眠的开销,所以这种场景采用自旋锁比较合适。
关于信号量和自旋锁,以及死锁问题,我们后面会再详细讨论。
使用方法一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在 该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示能获得信号量,因而能即时访问被该信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此他也唤醒所有等待该信号量的任务。
内核信号量的构成内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。
只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。
内核信号量是struct semaphore类型的对象,在内核源码中位于include\linux\semaphore.h文件
<span]DECLARE_MUTEX(name)
该宏声明一个信号量name并初始化他的值为1,即声明一个互斥锁。
DECLARE_MUTEX_LOCKED(name)
[p=26,]DECLARE_MUTEX_LOCKED(name)
[p=26,]void sema_init (struct semaphore *sem, int val);
该函用于数初始化设置信号量的初值,他设置信号量sem的值为val。
注意:
val设置为1说明只有一个持有者,这种信号量叫二值信号量或者叫互斥信号量。
我们还允许信号量可以有多个持有者,这种信号量叫计数信号量,在初始化时要说明最多允许有多少个持有者也可以把信号量中的val初始化为任意的正数值n,在这种情况下,最多有n个进程可以并发地访问这个资源。
<span]void init_MUTEX_LOCKED (struct semaphore *sem);
该函数也用于初始化一个互斥锁,但他把信号量sem的值设置为0,即一开始就处在已锁状态。
PV操作获取信号量(P)<span]int down_interruptible(struct semaphore * sem);
该函数功能和down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。
<span]int down_killable(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
int down_timeout_interruptible(struct semaphore *sem, long jiffies);
释放内核信号量(V)<span]int down_interruptible(struct semaphore *sem)
这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步。
下面是该函数的注释:
- <span]<span]#include
- #include
- #include
- #include
- #include
- #include
- #include
- static int major = 250;
- static int minor = 0;
- static dev_t devno;
- static struct cdev cdev;
- static struct class *cls;
- static struct device *test_device;
- static struct semaphore sem;
- static int hello_open (struct inode *inode, struct file *filep)
- {
-
- if(down_interruptible(&sem))//p
- {
- return -ERESTARTSYS;
- }
- return 0;
- }
- static int hello_release (struct inode *inode, struct file *filep)
- {
- up(&sem);//v
- return 0;
- }
- static struct file_operations hello_ops =
- {
- .open = hello_open,
- .release = hello_release,
- };
- static int hello_init(void)
- {
- int result;
- int error;
- printk("hello_init \n");
- result = register_chrdev( major, "hello", &hello_ops);
- if(result < 0)
- {
- printk("register_chrdev fail \n");
- return result;
- }
- devno = MKDEV(major,minor);
- cls = class_create(THIS_MODULE,"helloclass");
- if(IS_ERR(cls))
- {
- unregister_chrdev(major,"hello");
- return result;
- }
- test_device = device_create(cls,NULL,devno,NULL,"test");
- if(IS_ERR(test_device ))
- {
- class_destroy(cls);
- unregister_chrdev(major,"hello");
- return result;
- }
- sem_init(&sem,1);
- return 0;
- }
- static void hello_exit(void)
- {
- printk("hello_exit \n");
- device_destroy(cls,devno);
- class_destroy(cls);
- unregister_chrdev(major,"hello");
- return;
- }
复制代码module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("daniel.peng");
测试程序 test.c
<span]insmod hello.ko
可见进程A成功打开设备,在进程A sleep期间会一直占有该字符设备,进程B由于无法获得信号量,进入休闲,结合代码可知,进程B被阻塞在函数open()中。
- 进程A 结束了sleep,并释放字符设备以及信号量,进程B被唤醒获得信号量,并成功打开了字符设备。
- 进程B执行完sleep函数后退出,并释放字符设备和信号量。
读-写信号量
跟自旋锁一样,信号量也有区分读-写信号量之分。
如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;否则,读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。因此,写者是排他性的,独占性的。
读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因此,增加新的架构不需要重新实现它,但缺点是性能低,获得和释放读写信号量的开销大;另一种是架构相关的,因此性能高,获取和释放读写信号量的开销小,但增加新的架构需要重新实现。在内核配置时,可以通过选项去控制使用哪一种实现。
读写信号量的相关API:
DECLARE_RWSEM(name)
[p=26,]DECLARE_RWSEM(name)
[p=26,]void init_rwsem(struct rw_semaphore *sem);
该函数对读写信号量sem进行初始化。
<span]int down_read_trylock(struct rw_semaphore *sem);
该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。
<span]int down_write_trylock(struct rw_semaphore *sem);
该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。
<span]void up_write(struct rw_semaphore *sem);
写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。
void downgrade_write(struct rw_semaphore *sem);
该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。
读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。