信号量的定义如下。
信号量也是对自旋锁的组合应用,与其他锁有显著的不同之处,信号量并不是并发保护,而是资源并发数量限制。count域代表的是该信号量的可用并发数,每一个逻辑获得了信号量都会减少1个count,当count到0的时候,额外的并发就会进入wait_list中等待。这里的等待也是可睡眠等待,lock自旋锁用于保护数据结构本身的临界操作。每当有逻辑释放信号量,count就会增加1,整个信号量相当于一个令牌桶,一共有count个令牌,表示同时只能并发进入count个逻辑。count个令牌代表的语义是并发限流,而不是阻止并发发生,并发操作对信号量内的内容的变更如果不能做到互不影响,就仍然需要额外加并发锁。
信号量的接口函数是down和up,down函数代表获得一个count,up函数代表释放一个count。在up函数时由于已经持有信号量,所以可以不用阻塞随时进行,而在down函数时是获得信号量,需要保证count不为0,还可能需要操作链表,所以可能阻塞。由于down函数的语义是关中断(信号中断,不是硬件中断),也就是在阻塞等待期间是不允许中断发生的,所以该函数的性能会有严重的问题。因为信号量用光是不可预期的,一旦处于阻塞状态,阻塞的时间就是不可预期的。信号量的语义并不像spinlock那样严格限制逻辑的长度,因为信号量允许被其他线程抢占,因此可能出现长时间的关中断的现象。所以down函数在内核中已经处于deprecated状态,应该使用允许中断发生的down_interruptible或者down_killable函数替代。
无论哪种情况都只是线程睡眠等待的状态不同,但所用的函数路径是相同的,代码如下。
在上述信号量代码中通过一个关中断的自旋锁来锁住操作链表,由于使用了raw_spin_lock_irqsave,使得信号量可以在不确定当前中断状态的逻辑中进行,但是信号量能导致线程睡眠,在中断上下文中不应该使用。将当前线程的状态设置为非TASK_RUNNING,就相当于让CPU在调度选择的时候不选择该线程,只是等待信号超时或条件满足被其他线程显式唤醒,从而达到睡眠的目的。显式唤醒对应的逻辑是up函数,代码如下。
up函数与down函数是成对出现的,信号量有获得就必然有释放。有一个快速路径是阻塞等待和up显式唤醒等待线程的情况,并不会更新count的计数。wake_up_process是显式唤醒等待的线程,这个逻辑还有性能优化效果,相当于精准交付执行权限,在有了新的信号量产生的时候,通过这种方法只唤醒一个线程,精准地让等待的线程得到信号量。
由于在等待的时候可以处理信号,而在信号处理中可以退出线程,等待的数据结构又位于栈上,所以就需要保证在信号到达和信号处理之间完成信号量等待的返回,否则链表操作就有可能出现线程已经退出、栈内存释放的错误。Linux下的信号是主要给用户空间程序设计的一种事件响应的机制,纯粹的内核线程是默认不处理任何信号的,所有的信号都会被静默丢弃,除非主动打开。信号处理函数一定是在用户空间的栈中执行的,因为只有用户才会注册信号处理函数,而信号的检查和信号处理上下文也是在进出用户空间时进行的。在内核内部,只有收到信号这一种通知。这里并不在乎收到了什么信号,只要收到信号,调度系统就会首先检查处于TASK_INTERRUPTIBLE的线程,唤醒该线程并让其继续运行,在继续运行时逻辑仍然在信号量函数中,就可以完成对信号量的判断。信号处理要等到返回用户空间时才会判断,或者纯内核线程会静默忽略处理,在这里无论是什么信号,都只是起到唤醒线程的作用。