linux 内核有实时互斥体(锁),名为rt_mutex即realtime mutex。说到realtime一定离不开priority(优先级)。所谓实时,就是根据优先级的不同对任务作出不同速度的响应。rt_mutex也就是依据任务(task,process)的priority进行排队的锁,同时使用PI(priority Inheritance,优先级继承)算法解决proirty inversion(优先级逆转)的问题。
这里有三个角色:rt_mutex,rt_mutex_waiter,以及task_struct。(pi算法与进程结构偶合在一起)
rt_mutex:实时锁,使用优先级排队,使用优先级继承算法解决优先级逆转问题,与task_struct强偶合。
task_struct:要求锁的任务,rt_mutex用其指针作为上锁状态。
rt_mutex_waiter:任务阻塞等待锁,即任务与锁的关联,代表任务对锁的一次需求。
rt_mutex和task_struct都有一个waiter有序队列,并且都实现为rb树,并以优先级prio进行排序。rt_mutex_waiter->tree_entry入队到rt_mutex(锁)的waiter队列,即rt_mutex->waiters。而rt_mutex_waiter->pi_tree_entry入队到task_struct(任务)的waiter队列,即task_struct->pi_waiters。
rt_mutex->waiters:用于阻塞等待排队,与pi chain无关。
task_struct->pi_waiters:并非用于排队,而是作为一个最小值计算器,为task_struct计算可继承的最小prio(最高),并且提供一个是否为空的判断属性。它收集了task持有的所有rt_mutex的leftmost_waiter,即每当一个task在一个rt_mutex上了锁,这个rt_mutex的waiters中prio最小(最高)的waiter将被入队到task_struct->pi_waiters。与pi chain无关。
rt_mutex_waiter->prio:rt_mutex->waiters和task_struct->pi_waiters两个队列都使用这个值进行排序。一个在pi chain上阻塞的任务,当优先级被boost后,同样会反映在waiter结构的prio上。
pi chain:从一个任务的task_struct->pi_block_on开始,依次进行rt_mutex = rt_mutex_waiter->lock,task_struct = rt_mutex->owner,rt_mutex_waiter = task_struct->pi_block_on访问遍历。这个链并非我们通过指的用链表结构进行链接的链表。而是 task -> waiter -> lock -> ... 的chain。虽然rt_mutex->waiters 和 task_struct->pi_waiters 同为队列(由rbtree实现),但是跟pi chain无关,pi chain walk路径不通过这两个队列。
以内核的设计文档rt-mutex-design.txt举例,有pi chain如下
Example:
Process: A, B, C, D, E
Mutexes: L1, L2, L3, L4
A owns: L1
B blocked on L1
B owns L2
C blocked on L2
C owns L3
D blocked on L3
D owns L4
E blocked on L4
The chain would be:
E->L4->D->L3->C->L2->B->L1->A
在上面的例子中,A为最终的阻塞源,它继承了阻塞链上所有其它任务的优先级。图左则的任务被右则的任务阻塞,图右则的任务持有锁并继承图左则的任务的优先级。当一个任务被一个锁阻塞,并且这个锁在图上。以任务E为例,当E加入到pi chain后,就会从图的左则向图的右则进行pi chain walk,直到到达任务A,在walk所经节点,都会将优先级作最优继承。通常地pi chain由图最右则的A释放锁,一路唤醒pi chain上阻塞的任务,而消减pi chain。A首先离开pi chain,由于继承了优先级,必须马上恢复原有的优先级,但是并不需要进行pi chain walk,因为它是继承者不是继承源(没有pi_block_on),不影响其它继承者(pi chain上的任务)的继承。 但是如C,超时不阻塞了,要离开pi chain,因为它不但是继承者还是继承源(pi_block_on -> B),所以C必须对B进行pi chain walk。
下面是进行walk chain的调用拓扑,rt_mutex_adjust_prio_chain执行pi chain walk。
pi算法解决优先级逆转问题。
高(high)优先级任务由于等待低(low)优先级任务拥有的锁,在这过程中低优先级被其它优先级(界于高低之间的中级medium)的任务无限抢占,从而有机会释放锁,导致高优先级的任务无限等待。
下面设两轴,水平轴为时间,纵轴为优先级。
当一个high prio任务进来,抢占一个low prio任务后却发现有资源要同步又阻塞了,而这个同步的资源low prio任务正在使用,那么必须等待low prio任务释放掉同步的资源,high prio任务才能再次运行。但是有一种坏的情况是,low prio任务被medium prio任务抢占,不能及时释放同步的资源,使用high prio任务阻塞时间增加。
这还不算最坏的,最坏的是low prio任务被不定数目或不定回目的medium prio任务无限地进行抢占,最后导致high prio任务无限地被阻塞。
解决的方法就是,当high prio任务阻塞在一个同步资源时,将正在使用同步资源的任务临时提升 (boost)跟自己一样的优先级,赶紧释放资源,好让(方便)high prio任务通过。
rt_mutex->waiters 和 task_struct->pi_waiters 如何参与pi算法。
前面已经谈及的pi chain,也是pi算法的一部分。而pi算法的另一部分就是如何选出最高的优先级进行继承。这是通过rt_mutex->waiters和task_struct->pi_waiters队列来实现的。每当一个任务持有一个rt_mutex时,就会从rt_mutex->waiters中选出排在首位的waiter纳入到自己的task_struct->pi_waiters队列,这个同样是一个排序队列。这个队列代表着任务持有哪些rt_mutex,并且以及阻塞在这些rt_mutex的最高优先级(的任务)。这个队列将这些优先级进行排序选出最高的优先级进行继承。
一个对rt_mutex的lock或者unlock都可能会引发rt_mutex->waiters队列的入队和出队,从而改变了rt_mutex->waiters的排序,继而改变了task_struct->pi_waiters队列的成员,必须重新排序,选择新的优先级来继承。当pi chain上的一个任务的优先级继承受影响后,就必须要反映在它所阻塞的rt_mutex的waiters队列上,并按pi chain阻塞的方向传递变化。
任务 P 持有两个锁 L1 和 L2。A,B,C 阻塞在 L1;而 C,D,F 阻塞在 L2。 B 和 F 分别是 L1 和 L2 的 leftmost waiter,以最好的优先级 1 和 0,被任务 P 选中放在自己的 pi_waiters 队列。排序后,F 的优先级最高,作为继承的优先级。
当一个优先级更高的任务 X 需要阻塞在锁 L1 时,必然引发锁 L1的leftmost waiter改变,这时就要通知 P 选择 X 代替 B,重新对比 X 和 F 的优先级,选最高的优先级来继承。如果任务 P 还阻塞等待一个锁 L3 ,那么就必须进行 pi chain walk,通知优先级继承变更。
一个阻塞任务从其task_struct->pi_block_on开始进行 pi chain walk 通知优先级继承变更。
一个任务释放锁,从其task_struct->pi_waiters队列找出锁对应的任务进行唤醒。