当前位置: 首页 > 科技观察

说说Linux内核信号量的概念

时间:2023-03-20 14:00:10 科技观察

Linux内核信号量的概念和原理和SystemV在用户态的IPC机制信号量是一样的,但是绝对不可能在内核之外使用,所以他和SystemVV的IPC机制信号量是无关紧要的。如果有任务想要获取一个已经被占用的信号量,信号量会把它放到一个等待队列中(不是站在外面死守着等待,而是把自己的名字写在任务队列中),然后让它休眠。当持有信号量的进程释放信号时,等待队列中的一个任务会被唤醒(因为队列中可能有多个任务),让它获得信号量。这与自旋锁不同,处理器可以执行其他代码。应用场景由于争夺信号量的进程会在等待锁再次可用时进入休眠状态,因此信号量适用于需要长时间持有锁的情况;相反,当持有锁的时间很短时,就不需要使用信号量了。完美,因为睡眠、维护等待队列和唤醒的开销可能比锁占用的整个调度时间更长。举两个生活中的例子:我们从南京坐火车到新疆需要2天时间。这个“任务”非常耗时。我们只能坐在车里等火车到站,但不需要一直睁着眼睛。等等,最理想的情况是我们上车直接睡觉,醒来就到车站了(看过《异形》的读者会深有体会),所以从人的角度来说(用户),体验是最好的。对于进程来说,当程序在等待某个耗时事件时,它不必一直占用CPU。它可以暂停当前任务使其进入休眠状态。当等待事件发生时,会被其他任务唤醒。与此场景类似,使用了信号量。更合适。我们有时会等电梯,等厕所,这种场景不需要很多等待时间。如果我们一定要找个地方睡觉,然后等电梯来了或者有卫生间再醒来,那显然不是。有必要的,我们只需要排队刷抖音就可以了。与计算机程序相比,比如驱动程序进入中断例程等待某个寄存器被置位,这种场景的等待时间往往非常长。短时间内,系统开销甚至比进入休眠的开销小很多,所以这种场景使用自旋锁比较合适。关于信号量、自旋锁、死锁问题,后面会详细讨论。如何使用如果一个任务想要访问共享资源,它必须首先获得信号量。获取信号量的操作会将信号量的值减1,如果当前信号量的值为负数,则表示无法获取信号量,必须挂起任务。在信号量的等待队列中等待信号量可用;如果当前信号量的值为非负数,则表示可以获得该信号量,从而可以立即访问被该信号量保护的共享资源。任务访问受信号量保护的共享资源后,必须释放信号量。信号量的释放是通过将信号量的值加1来实现的。如果信号量的值为非正数,说明有任务在等待当前信号量。因此它也唤醒所有等待信号量的任务。内核信号量组合内核信号量类似于自旋锁,因为它不允许内核控制路径在锁关闭时继续进行。然而,当内核控制路径试图获取受内核信号量锁保护的繁忙资源时,相应的进程将被挂起。只有当资源被释放时,进程才能再次运行。只有可以休眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。内核信号量是一个结构信号量类型的对象。它位于内核源代码中的include\linux\semaphore.h文件中。方法和spin_lock系列完全一样,只是参数spinlock_t改为raw_spinlock_tcount,相当于信号量的值。如果大于0,则资源空闲;如果等于0,则资源繁忙,但没有进程在等待受保护的资源;如果小于0,则资源不可用,至少有一个进程在等待资源wait_list内核链表,当前获取信号量的任务会在等待链表中注册到该成员。信号量的API初始化DECLARE_MUTEX(name)该宏声明了一个信号量名称,并将其值初始化为1,即声明了一个互斥量。DECLARE_MUTEX_LOCKED(name)这个宏声明了一个互斥体的名字,但是将它的初始值设置为0,即锁在创建时处于锁定状态。所以,对于这种锁,一般都是先释放后获取。voidsema_init(structsemaphore*sem,intval);该函数用于初始化设置信号量的初始值,他将信号量sem的值设置为val。注:val设置为1表示只有一个holder。这种信号量称为二进制信号量或互斥信号量。我们还允许信号量有多个持有者。该信号量称为计数信号量。初始化的时候需要指定最多允许持有多少个holder。您还可以将信号量中的val初始化为任何正值n,在这种情况下最多n个进程可以同时访问该资源。voidinit_MUTEX(structsemaphore*sem);这个函数是用来初始化一个mutex的,也就是他把信号量sem的值设置为1。voidinit_MUTEX_LOCKED(structsemaphore*sem);这个函数也是用来初始化一个mutex的,只不过他把信号量sem的值设置为0,也就是一开始就加锁。PV操作获取信号量(P)voiddown(structsemaphore*sem);该函数用于获取信号量sem,会导致调用该函数的进程进入休眠状态,因此该函数不能用于中断上下文(包括IRQ上下文和softirq上下文)。该函数会将sem的值减1,如果信号量sem的值非负,则直接返回,否则调用者会被挂起,直到其他任务释放信号量继续运行。intdown_interruptible(structsemaphore*sem);这个函数的作用和down类似,不同的是down不会被signal(信号)打断,但是down_interruptible是可以被signal打断的,所以这个函数有一个返回值来区分是正常返回还是If被信号中断,如果返回0,表示获取到信号量,正常返回;如果它被信号中断,它返回-EINTR。intdown_trylock(structsemaphore*sem);此函数尝试获取信号量sem。如果可以立即获取,则获取信号量并返回0。否则,表示无法获取信号量sem,返回值为非零。因此,它不会导致调用者休眠并且可以在中断上下文中使用。intdown_killable(structsemaphore*sem);intdown_timeout(structsemaphore*sem,longjiffies);intdown_timeout_interruptible(structsemaphore*sem,longjiffies);释放内核信号量(V)voidup(structsemaphore*sem);该函数释放信号量sem,即sem的值加1。如果sem的值为非正数,说明有任务在等待信号量,所以唤醒这些waiters。补充intdown_interruptible(structsemaphore*sem)的作用是获取信号量。如果获取不到信号量,它就会休眠。如果此时没有信号中断,就会进入休眠状态。但是在sleep过程中可能会被信号打断,打断后返回-EINTR,主要用于进程间的互斥同步。下面是该函数的注解:/***down_interruptible-acquirethesemaphoreunlessinterrupted*@sem:thesemaphoretobeacquired**Attemptstoacquirethesemaphore.Ifnomoretasksareallowedto*acquirethesemaphore,callingthisfunctionwillputthetasktosleep.*Ifthesleepisinterruptedbyasignal,thisfunctionwillreturn-EINTR.*Ifthesemaphoreissuccessfullyacquired,thisfunctionreturns0.*/一个进程在调用down_interruptible()Afterwards,ifsem<0,itentersaninterruptiblesleepstateandschedulesotherprocesses运行,但是一旦进程接收到信号,它就会从down_interruptible函数返回。并将错误号标记为:-EINTR。一个形象的比喻:传入的信号量是1,就像黎明一样。如果当前信号量为0,则进程会一直睡到天亮(信号量为1),但中间可能会有一个闹钟(信号)把你叫醒。又如:小强下午放学回家,回到家就要开始吃饭了。这时候会出现两种情况:情况一:饭做好了,可以开饭了;情况二:他去厨房,发现妈妈还在忙,就对他说:“你先睡吧,忙完了我叫你。”小强同意去睡觉,但又说:“睡觉的时候,如果小红来陪我玩,你可以把我叫醒。”小强是down_interruptible,他要吃饭是为了拿到信号量,睡觉对应这里的休眠,小红来找我玩就是打断休眠。使用可中断的信号量版本意味着在出现信号量死锁的情况下,仍有机会使用ctrl+c发出软中断,从而使等待内核驱动返回的用户态进程退出。而不是锁定整个系统。休眠时,可以通过中断信号终止。这个进程可以接受中断信号!例如在命令行中输入#sleep10000,然后按下ctrl+c,就会向上面的进程发送一个进程终止信号。信号被发送到用户空间,然后通过系统调用传递给驱动程序。信号只能发送到用户空间,无权直接发送给内核。我们不能直接操作1G内核空间。内核信号量使用例程场景1在驱动程序中,当多个线程同时访问同一个资源时(驱动中的全局变量是典型的共享资源),可能会造成“race”,所以我们必须对它进行并发控制共享资源。Linux内核中最常用的解决并发控制的方法是自旋锁和信号量(大部分时间用作互斥量)。在这里插入图片来描述场景2有时我们希望一个设备只被一个进程打开,当设备被占用时,其他设备必须进入休眠状态。这里插入信号处理示意图,图片说明如上图所示:进程A首先通过open()打开设备文件,调用内核的hello_open(),调用down_interruptible(),因为信号量是此时没有被占用,所以进程A可以获得Semaphore;进程A获得信号量后继续处理原来的任务。此时进程B也需要通过open()打开设备文件,同样调用了内核函数hello_open(),但是此时获取不到信号量,所以进程B处于Blocking状态;进程A完成任务执行,关闭设备文件,通过up()释放信号量,于是进程B被唤醒,可以继续执行剩下的任务,进程B完成任务,释放设备文件,传递up()释放信号量的代码如下:#include#include#include#include#include#include#includestaticintmajor=250;staticintminor=0;staticdev_tdevno;staticstructcdevcdev;staticstructclass*cls;staticstructdevice*test_device;staticstructsemaphoresem;staticinthello_open(结构节点*file*inode,struct){if(down_interruptible(&sem))//p{return-ERESTARTSYS;}return0;}staticinthello_release(structinode*inode,structfile*filep){up(&sem);//vreturn0;}staticstructfile_operationshelo_ops={.open=hello_open,.release=hello_release,};staticinthello_init(void){intresult;interror;printk("hello_init\n");result=register_chrdev(major,"hello",&hello_ops);if(result<0){printk("register_chrdevfail\n");returnresult;}devno=MKDEV(major,minor);cls=class_create(THIS_MODULE,"helloclass");if(IS_ERR(cls)){unregister_chrdev(major,"hello");returnresult;}test_device=device_create(cls,NULL,devno,NULL,"test");if(IS_ERR(test_device)){class_destroy(cls);unregister_chrdev(major,"hello");returnresult;}sem_init(&sem,1);return0;}staticvoidhello_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("dan??iel.peng");测试程序test.c#include#include#include#includemain(){intfd;printf("beforeopen\n“);fd=open("/dev/test",O_RDWR);//原子变量0if(fd<0){perror("openfail\n");return;}printf("openok,sleep......\n");sleep(20);printf("wakeupfromsleep!\n");close(fd);//添加到1}编译步骤1make生成hello.ko2gcctest.c-oa3gcctest.c-ob测试步骤1、安装驱动insmodhello.ko2、先运行进程A,进程A在运行进程B时可以成功打开设备。在进程A休眠期间,会一直占用字符设备。因为B进程获取不到信号量,进入Leisure,结合代码可以看出B进程阻塞在函数open()中3.A进程结束睡眠,释放字符设备和信号量,B进程唤醒up获取信号量,成功打开字符设备。进程B执行完sleep函数后退出,释放字符设备和信号量。读写信号量和自旋锁一样,信号量也和读写信号量区分开来。如果一个读写信号量当前不为写入者所有,并且没有写入者在等待读取者释放信号量,则任何读取者都可以成功获取该读写信号量;否则,必须挂起读取器,直到写入器释放信号量。如果读写信号量当前不属于读者或写入者,并且没有写入者在等待信号量,则写入者可以成功获取读写信号量,否则写入者将被挂起,直到没有访问者。因此,作家是排他性的。读写信号量有两种实现,一种是通用的,不依赖于硬件架构,所以增加一个新的架构不需要重新实现,但缺点是性能低,开销大获取和释放读写信号量较大;另一种是架构相关的,所以性能高,获取和释放读写信号量的开销小,但是增加一个新的架构需要重新实现。配置内核时,可以使用选项来控制使用哪个实现。读写信号量的相关API:DECLARE_RWSEM(name)该宏声明了一个读写信号量名称,并对其进行了初始化。voidinit_rwsem(structrw_semaphore*sem);该函数初始化读写信号量sem。voiddown_read(structrw_semaphore*sem);读者调用该函数获取读写信号量sem。该函数会导致调用者休眠,因此只能在进程上下文中使用。intdown_read_trylock(structrw_semaphore*sem);此函数类似于down_read,只是它不会导致调用者休眠。它会尽力获取读写信号量sem。如果可以立即获取,则获取读写信号量并返回1,否则表示不能立即获取信号量,返回0。因此,也可以用于中断上下文中。voiddown_write(structrw_semaphore*sem);writer使用该函数获取读写信号量sem,同样会导致调用者休眠,所以只能在进程上下文中使用。intdown_write_trylock(structrw_semaphore*sem);此函数类似于down_write,只是它不会导致调用者休眠。该函数尽量获取读写信号量。如果可以立即获取,则获取读写信号量并返回1,否则表示不能立即获取,返回0。可以在中断上下文中使用。voidup_read(structrw_semaphore*sem);reader使用这个函数来释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,则不需要调用up_read释放读写信号量,因为根本没有获取到信号量。voidup_write(structrw_semaphore*sem);作者调用这个函数来释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,则不需要调用up_write,因为返回0表示还没有获取到读写信号量。voiddowngrade_write(structrw_semaphore*sem);此功能用于将编写器降级为读取器,这有时是必要的。因为写者是独占的,当写者维护读写信号量时,任何读者或写者都不能访问受读写信号量保护的共享资源。对于那些在当前条件下不需要写入权限的写者,降级为读者,可以让等待访问的读者立即访问,从而增加并发,提高效率。读写信号量适合在读多写少的情况下使用。在Linux内核中,进程对内存映像描述结构的访问受到读写信号量的保护。本文转载自微信?“一口Linux”,可通过以下二维码关注。转载本文请联系易口Linux公众号。