【摘要】

rtmutex作为futex的底层实现,有两个比較重要的特性。一个是优先级继承,一个是死锁检測。本文对这两个特性的实现进行说明。

一、优先级继承

2007年火星探路者号的vxworks上发生了优先级反转。导致设备不断重新启动。

http://research.microsoft.com/en-us/um/people/mbj/mars_pathfinder/mars_pathfinder.html),

优先级反转问题在大多数操作系统教材上都有提及,大概意思就是,A、B、C三个进程,优先级各自是Pa<Pb<Pc,如果有资源S,被A持有,某个时刻C来尝试获取S。被堵塞,接着B进程抢占A进程。而B又是一个死循环进程,这样C永远得不到调度的机会。

看上去就是优先级低的B比优先级相对高的C抢占了。

优先级反转的解决方式主要有两种。一种是优先级继承,还有一种是优先级天花板。

优先级继承的思路就是在进程获取资源假设被堵塞,则改动资源持有者的优先级(大多数情况是提升优先级),让资源持有者尽快完毕资源的操作后释放资源。优先级天花板则须要事先知道竞争资源的全部进程的优先级,当当中一个进程获取到资源后,则将进程优先级提升至最高的那个进程。这两者的差别是。前者是在获取资源堵塞时改动优先级,后者是获取资源成功后改动优先级。前者对系统调度影响较小,但实现较复杂。后者对系统调度影响大,但实现较简单。

Linux在2006年引入了优先级继承方案,在rtmutex中完毕。内核文档文件夹的rt-mutex-design.txt介绍了优先级反转和优先级继承的概念。并描写叙述了rtmutex的实现方案。本节以一种更白话的方式介绍rtmutex的优先级继承实现。

rtmutex.c有几个重要的数据结构,我们以因果顺序来描写叙述这些结构。

首先。你得有一把锁,这用struct rt_mutex来表示。有了锁之后,锁就可能有一个拥有者。于是struct rt_mutex内就有一个成员叫structtask_struct owner;这把锁可能会堵塞一些进程,那么struct rt_mutex里有一个链表。叫structplist_head wait_list,能够看出。这是一个优先级队列。队列的元素,是一些被封装成struct rt_mutex_waiter的进程描写叙述符,按进程的优先级来排序。既然一些进程会堵塞在这把锁上面,依据优先级继承的原理。锁的持有者owner,就必须參考一个优先级最高的堵塞进程。将owner的优先级提升至最高的这个堵塞进程,那么owner就须要维护一个链表,这个链表里保存了owner进程拥有的资源里,被堵塞的优先级最高的那些进程。这就是task
struct里struct plist_head pi_waiters的由来;另外,怎样知道一个进程是否被rt_mutex堵塞?于是又在task_struct里引入了structrt_mutex_waiter *pi_blocked_on,用来指示该进程被堵塞在哪个rt_mutex上。

以下,我们以样例来说明一下上面的数据结构是怎样联系起来的。

内核里,一个task_struct P,可能拥有n个资源,然后这n个资源堵塞了T个其它进程(T>=n)。

对于P拥有的某个资源,堵塞了总共T[i]个进程(∑T[i] = T。 0<=i<n)。这些进程都以rt_mutex_waiter的形式,通过按优先级顺序挂接到资源rt_mutex的wait_list链表上。接着,还要将这T[i]个进程中,优先级最高的那一个m(rt_mutex_waiter),通过pi_list_entry。链接到P的pi_waiters队列中。 也就是说,P的pi_waiters队列拥有n个元素,每一个元素都是一个封装成rt_mutex_waiter的task_struct,P的优先级。为这n个进程最高的那个。

为什么要维护这么一个pi_waiters链表,为什么不只保存一个P堵塞的最高优先级进程?

考虑这样的情况:

优先级为p的进程P。先后占有资源s1、s2,优先级为p1、p2的进程先后堵塞在这两个资源上。

(p<p1<p2)根据优先级继承协议。P的优先级先后变为p1、p2。 当P释放资源s2后。优先级应该降为多少?毫无疑问。应该减少为p1而不是p。 这也就是链表的来由。即我们须要跟踪该进程获取资源的一个路径,以此作为优先级调整的根据。

须要注意的是。一个rt_mutex_waiter,同一时间仅仅可能被链接进一个rt_mutex的wait_list里,由于一个进程m不能同一时候等待两个资源而被堵塞。

以下以futex的加解锁为例,说明rt_mutex的流程。

1.1 futex_lock_pi

能够看出。优先级继承属性的锁。须要严重关注锁的owner属性,以便实现优先级传递。

进程加锁的函数是futex_lock_pi,当进程进入内核态。发现自己是第一个挂起在此锁的

进程时,会通过 lock & FUTEX_TID_MASK获取用户态设置的owner的pid,

然后find_task_by_pid得到owner的task struct。

接着新分配一个pi_state结构:

pi_state = alloc_pi_state();

接下来,初始化pi_state中的rtmutex,特别是owner字段赋值:

rt_mutex_init_proxy_locked(&pi_state->pi_mutex)->rt_mutex_set_owner(lock, proxy_owner, 0);

这样就给rtmutex lock赋值了owner了。这些操作是在函数lookup_pi_state中完毕的。

这里我们引入了一个结构struct futex_pi_state ,该结构主要作用就是内置了一个rtmutex。

而全部涉及到优先级继承、传递等概念的实现,事实上都靠这个rtmutex来实现。

futex_lock_pi(unsigned long uaddr)
{
struct rt_mutex_waiter waiter;
struct futex_q q;
//依据futex地址获取页框,来计算key
get_user_pages_fast(addr, 1, 1, &page);
q.key->both.offset |= FUT_OFF_INODE; /* inode-based key */
q.key->shared.inode = page->mapping->host;
q.key->shared.pgoff = page->index;
//第一步,就是依据uaddr来找到相应的rtmutex。
//首先。依据uaddr和共享内存相应的inode、page frame的组合为key。找到曾被该锁堵塞的futex_q对象。
//(假设其它进程,线程以前在这把锁上堵塞过一次,
//就至少能找到一个key匹配的futex_q对象)
//找到futex_q对象后,就借用他的pi_state成员。也即rtmutex成员
struct futex_q *find_q = find_match_key(q.key,hash_bucket[hash(uaddr)]);
struct futex_pi_state *pi_state;
//假设找不到匹配的 futex_q,说明我们是第一个堵塞在此锁的对象,
//就分配futex_q里的pi_state成员
//总之,到眼下为止,得到一个可用的pi_state也即rtmutex
if(!find_q){
q->pi_state = alloc_pi_state();
pi_state = q->pi_state;
}else
pi_state = find_q->pi_state; //当然每次都须要将本次堵塞的对象以futex_q的形式增加hash冲突链
q->task = current;
plist_add(&q->list, &hash_bucket[hash(uaddr)]->chain); //開始将当前进程封装task struct
waiter->task = current;
struct rt_mutex *lock = &pi_state->pi_mutex;
//获取原先的最高等待优先级任务,留待兴许比較
old_top_waiter = rt_mutex_top_waiter(lock);
//将本次rt_mutex_waiter加到futex_state->rtmutex的等待链表中
plist_add(&waiter->list_entry, &lock->wait_list);
//假设本次增加的waiter是该lock堵塞的最高优先级的进程,则须要改动
//lock持有者task struct的pi_waiters链表。并提高lock持有者优先级。
//这个就是优先级继承实现的精华所在。
struct task_struct *owner = rt_mutex_owner(lock); if (waiter == rt_mutex_top_waiter(lock)) {
//这里把以前的那个最高优先级的等待进程从持有者链表删除
//有个疑问,这里是否会存在内存泄露?
//不会,由于rt_mutex_waiter 是局部栈变量
//这里也能够看出。为什么rt_mutex_waiter 要做成局部变量而不是动态分配变量,
//是为了避免内存泄露。
plist_del(&old_top_waiter->pi_list_entry, &owner->pi_waiters);
plist_add(&waiter->pi_list_entry, &owner->pi_waiters);
//一连串复杂的优先级修正
__rt_mutex_adjust_prio(owner);
} }

1.2 futex_unlock_pi

futex_unlock_pi(unsigned long uaddr)
{
struct futex_hash_bucket *hb;
//依据futex地址获取页框,来计算key
get_user_pages_fast(addr, 1, 1, &page);
q.key->both.offset |= FUT_OFF_INODE; /* inode-based key */
q.key->shared.inode = page->mapping->host;
q.key->shared.pgoff = page->index; //以key为基准,查找出hash冲突链里第一个被堵塞的futex_q
//并尝试唤醒
hb = hash_futex(&key);
head = &hb->chain;
plist_for_each_entry_safe(this, next, head, list) {
if (!match_futex (&this->key, &key))
continue;
ret = wake_futex_pi(uaddr,uval,this);
goto out_unlock;
} }
//详细的唤醒函数,尝试唤醒futex_q *this指向的进程。
//并调整优先级
wake_futex_pi(u32 __user *uaddr, unsigned long uval,struct futex_q *this)
{
//获取到该futex_q(进程)所持有的锁pi_state->rtmutex对象
struct futex_pi_state *pi_state = this->pi_state;
//获取下一个优先级最高的被堵塞者
new_owner = rt_mutex_next_owner(&pi_state->pi_mutex);
//将用户态lock字段更新owner为下一个持有者
newval = FUTEX_WAITERS | task_pid_vnr(new_owner);
cmpxchg_futex_value_locked(uaddr, uval, newval); //眼下,此锁的全部者已经不是当前进程了。因此将它从本进程
//的链表中取下。加入到下一个owner的链表中
list_del(&pi_state->list);
list_add(&pi_state->list, &new_owner->pi_state_list);
pi_state->owner = new_owner;
//释放锁,优先级调整
rt_mutex_unlock(&pi_state->pi_mutex);
}
rt_mutex_unlock(struct rt_mutex* rtmutex)
{
//唤醒一个最高优先级堵塞者
wakeup_next_waiter(lock, 0);
//调整当前进程的优先级。由于已经释放资源了,须要往下调一下优先级
rt_mutex_adjust_prio(current);
} static void wakeup_next_waiter(struct rt_mutex *lock)
{
//找出最高优先级的等待者(前面futex流程里也找过一次,用来更新用户态owner值)
struct rt_mutex_waiter *waiter;
waiter = rt_mutex_top_waiter(lock);
//找到后,先从lock的堵塞队列里摘下来,由于该进程立即就不会被堵塞了
plist_del(&waiter->list_entry, &lock->wait_list);
//接着从当前进程的最高优先级堵塞队列里摘除。由于该进程是lock的最高优先级等待者,
//也一定会被链接到锁持有者的最高优先级堵塞队列里
pendowner = waiter->task;
plist_del(&waiter->pi_list_entry, ¤t->pi_waiters);
wake_up_process(pendowner);
//设置rt_mutex的owner
rt_mutex_set_owner(lock, pendowner, RT_MUTEX_OWNER_PENDING); //还没完。新的owner的pi_waiters链表还须要更新,由于新owner获取到锁之后,也開始
//堵塞别人了。
//注意,新owner不须要调高优先级,由于新owner已经是眼下为止,持有该锁
//的最高优先级。仅仅有当新的高优先级进程尝试获取该锁被堵塞时,
//才须要继续往上调整优先级
next = rt_mutex_top_waiter(lock);
plist_add(&next->pi_list_entry, &pendowner->pi_waiters);
} void rt_mutex_adjust_prio(task)
{
prio = min(task_top_pi_waiter(task)->pi_list_entry.prio,
task->normal_prio);
task->prio = prio;
}

好,到这一步。锁的持有者已经变成了新的owner,BUT!,

新的owner还不一定获取到了这把锁,仅仅是一个pending状态。

假设要真正获取到这把锁,还须要新owner被唤醒后,走

try_to_take_rt_mutex,将锁真正抓到。这个道理也是能够理解的。

新owner从堵塞到被唤醒。会走try_to_take_rt_mutex再次尝试

加锁。

static int try_to_take_rt_mutex(struct rt_mutex *lock)
{
//假设该锁有一个owner。那么就尝试偷取。 //如何算一次偷取呢?为什么要有偷取的概念呢?
//以下再看。
if (rt_mutex_owner(lock) && !try_to_steal_lock(lock, current))
return 0;
/* We got the lock. */
//抓到锁,设置锁真正持有者,并清空可能的锁pending状态。
rt_mutex_set_owner(lock, current, 0);
return 1;
}

什么叫偷锁?  当owner是pending状态,且当前进程的优先级比pending的

owner还要大,那么非常明显,应该让当前进程而不是pending的那个进程

来获取资源。这就叫偷。

这个情况在什么时候会发生?futex_unlock_pi时,选取了一个当时最高优先级

的进程作为候选者,但候选者没有唤醒时,这个时候又来了一个更高优先级

的进程尝试抓这把锁。结果更高优先级的进程就把这个锁抓走了。

能够类比一下。比方,某个时刻。你去面试一家公司,面试也通过了。这个公司就

会给你一个口头offer,但在这个书面offer下来之前,那家公司又面试了一个更牛逼

的程序猿,公司就找了个理由拒绝给你发书面offer,而是把书面offer给了那个更牛逼

的程序猿。这就是说,那个牛逼程序猿偷走了你的offer。于是你又不得不等待那个

牛逼程序猿辞职后,再次面试这家公司。

static inline int try_to_steal_lock(struct rt_mutex *lock,
struct task_struct *task)
{
struct task_struct *pendowner = rt_mutex_owner(lock);
if (!rt_mutex_owner_pending(lock))
return 0; if (pendowner == task)
return 1; if (task->prio >= pendowner->prio) {
return 0;
} /* No chain handling, pending owner is not blocked on anything: */
//找到lock的下一个最高优先级堵塞者。
//这个堵塞者已经被挂在pending owner的pi_waiters最高优先级堵塞进程队列上了。
//须要将其改挂到当前偷取者的pi_waiters上。让后调整pending owner的优先级,
//由于pending owner已经不持有该锁了
next = rt_mutex_top_waiter(lock);
plist_del(&next->pi_list_entry, &pendowner->pi_waiters);
__rt_mutex_adjust_prio(pendowner); //将pending owner改挂后,当前偷取者的优先级也得
//依据偷取者的pi_waiters优先级来调整。
plist_add(&next->pi_list_entry, &task->pi_waiters);
__rt_mutex_adjust_prio(task); return 1;
}

能够看出,进程优先级调整的时机,主要是在进程堵塞的最高优先级进程链pi_waiters,

成员被改动后,运行。

当我们改动完锁持有进程的优先级后,事实上还没完。由于这个持有者非常可能被另外一把锁堵塞。
于是须要改动另外一把锁的持有进程的优先级(可能提升,也可能减少)。这样就形成了一个链式反应。
死锁检測就是在这个链式反应中进行的,什么时候算是一个死锁呢?
依据经典操作系统死锁检測的方案。对有向资源图的每一个节点进行深度优先搜索,
仅仅要找到一个回环。就算检測到死锁,例如以下图所看到的:


可是这个搜索的代价非常高。有点得不偿失,由于经典死锁检測会关注进程的全部可能路径

(如上图的节点D就是一个进程,他尝试去获取S和T),经典死锁检測会遍历S和T方向的路径。

而linux对这点做了简化。进程D仅仅须要关注他被堵塞的那个资源所在的路径就能够了,

并且不须要对资源图的全部节点搜索。仅须要以D为起点,进行一次遍历。

这套代码

正好嵌入在链式反应的函数实现中。

以下我们对链式反应的函数进行分析。

static int rt_mutex_adjust_prio_chain(struct task_struct *task,
int deadlock_detect,
struct rt_mutex *orig_lock,
struct rt_mutex_waiter *orig_waiter,
struct task_struct *top_task)
{
struct rt_mutex *lock;
struct rt_mutex_waiter *waiter, *top_waiter = orig_waiter; retry:
//当前锁持有者task0是否被其它锁lock1堵塞,
//假设堵塞的话则须要调整lock1->owner ,即task1的优先级
//否则返回不须要处理。
waiter = task->pi_blocked_on;
if (!waiter)
goto out;
//得到lock1
lock = waiter->lock;
//死锁检測:假设遍历过程中,出现了一个环,
//即要么锁反复了。要么进程反复了,就是一个死锁
/* Deadlock detection */
if (lock == orig_lock || rt_mutex_owner(lock) == top_task) {
ret = deadlock_detect ? -EDEADLK : 0;
goto out;
}
//获取lock1的最高优先级被堵塞者
top_waiter = rt_mutex_top_waiter(lock);
//将task0的优先级调整后。又一次加到lock1的等待者队列
/* Requeue the waiter */
plist_del(&waiter->list_entry, &lock->wait_list);
waiter->list_entry.prio = task->prio;
plist_add(&waiter->list_entry, &lock->wait_list); //获取lock1的持有者task1,作为下一个须要遍历的节点
/* Grab the next task */
task = rt_mutex_owner(lock); //假设改动优先级后插入lock1等待队列的task0,是最高优先级等待者。则
//须要把task0插入到task1的最高优先级等待者队列,即task1->pi_waiters
//然后继续尝试改动task1的优先级后。继续遍历链表。 if (waiter == rt_mutex_top_waiter(lock)) {
/* Boost the owner */
plist_del(&top_waiter->pi_list_entry, &task->pi_waiters);
waiter->pi_list_entry.prio = waiter->list_entry.prio;
plist_add(&waiter->pi_list_entry, &task->pi_waiters);
__rt_mutex_adjust_prio(task);
//否则。说明task0改动优先级后。不是lock1的最高优先级等待者,
//而且,task0以前是lock1的最高优先级等待者(即下句推断)
//那么说明task0的优先级被减少了,须要将task0从task1的最高优先级
//等待队列中删去。取下一个lock1的最高优先级等待者,加入到
//task1的最高优先级等待队列pi_waiter中,再调整task1的优先级,
//最后进行下一次节点遍历。 } else if (top_waiter == waiter) {
/* Deboost the owner */
plist_del(&waiter->pi_list_entry, &task->pi_waiters);
waiter = rt_mutex_top_waiter(lock);
waiter->pi_list_entry.prio = waiter->list_entry.prio;
plist_add(&waiter->pi_list_entry, &task->pi_waiters);
__rt_mutex_adjust_prio(task);
}
goto again; out
return ret;
}

当然,这个链式反应也是有深度限制的。假设层数太多,可能会内核栈溢出,

因此内核给了一个上限。1024层。以避免这样的情况。

rtmutex赏析的更多相关文章

  1. 关注经典:CSS Awards 获奖网站作品赏析《第一季》

    每天都有很多新的网站推出,其中不乏一些设计极其优秀的作品.这个系列的文章,我为大家挑选了2012年赢得 CSS Awards 大奖的50个最佳网站.这些鼓舞人心的网站作品代表了网页设计的最高水平,相信 ...

  2. chart.js图表库案例赏析,饼图添加文字

    chart.js图表库案例赏析,饼图添加文字 Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上.目前,它支持6种图表类型(折线图,条形图, ...

  3. 计算机网络协议包头赏析-UDP

    之前我们已经针对以太网.IP.TCP协议,进行了包头赏析.本次,我们继续UDP协议包头赏析. 提到TCP,想必大家会有所了解,它早已是家喻户晓的一个网络协议了,而UDP远没有他的大哥那么的有名,所以, ...

  4. 国际C语言混乱代码大赛代码赏析(一)【转】

    本文转载自:http://blog.csdn.net/ce123_zhouwei/article/details/9073869 国际C语言混乱代码大赛代码赏析(一) 近段时间在看<C专家编程& ...

  5. DC游戏《斑鸠》原创赏析[转载]

    游戏背景:      凤来之国本来只是边远地区的一个小国.但现在他们却自称为得到“神之力”的“神通者”,在“选民思想”“和平统一”之类的名义下开始了对各地的武力侵略.      事情的起因是因为凤来之 ...

  6. 老李分享:qtp自动化测试框架赏析-关键字自动化测试框架

    老李分享:qtp自动化测试框架赏析-关键字自动化测试框架   QTP从2005年继winrunner,robot逐渐退出历史舞台之后,占领主流自动化测试工具市场已经10年之久.当初为了提高在自动化测试 ...

  7. 漫画赏析:Linux 内核到底长啥样(转)

    知乎链接:https://zhuanlan.zhihu.com/p/51679405 来自 http://TurnOff.us 的漫画 “InSide The Linux Kernel” 本文转载自: ...

  8. Cocos2dx源码赏析(4)之Action动作

    Cocos2dx源码赏析(4)之Action动作 本篇,依然是通过阅读源码的方式来简单赏析下Cocos2dx中Action动画的执行过程.当然,这里也只是通过这种方式来总结下对Cocos2dx引擎的理 ...

  9. Cocos2dx源码赏析(3)之事件分发

    Cocos2dx源码赏析(3)之事件分发 这篇,继续从源码的角度赏析下Cocos2dx引擎的另一模块事件分发处理机制.引擎的版本是3.14.同时,也是学习总结的过程,希望通过这种方式来加深对Cocos ...

随机推荐

  1. ES6特性:(阮一峰老师)学习总结

    ES6(阮一峰)学习总结   1.块级作用域的引入 在ES6之前,js只有全局作用域和函数作用域,ES6中let关键字为其引入了块级作用域. { var a = 5; let b = 6; } con ...

  2. IE9以下版本兼容h5标签

    随着html5(后面用h5代表)标签越来越广泛的使用,IE9以下(IE6-IE8)不识别h5标签的问题让人很是烦恼. 在火狐和chrome之类的浏览器中,遇到不认识的标签,只要给个display:bl ...

  3. Http请求和相应模式(B/S)(1)

    B/S模式 服务器端的接受数据 :浏览器端 表单格式 <form></form> GET:请求方式, /index.html  Web浏览器上的资源路径 uname=bjxt& ...

  4. iis解析json

    一. windows XP 1. MIME设置:在IIS的站点属性的HTTP头设置里,选MIME 映射中点击”文件类型”-”新类型”,添加一个文件类型:关联扩展名:*.json内容类型(MIME):a ...

  5. HDU 1023

    卡特兰数.把进栈看成是+1,出栈看成是-1,任何时候部分和都有a1+a2+....ak>=0.求这样的数列的个数.这明显是卡特兰数的一个解释嘛.在<组合数学>这本书就有这样的原本的证 ...

  6. 玩转oracle学习第六天

     1.上节回想 2.PL/SQL的介绍 3.PL/SQL的基础 理解oracle的pl/sql概念 掌握PL/SQL编程技术(包含编写过程,函数,触发器.包... ) PL/SQL是什么? PL/ ...

  7. hdu 4544 湫湫系列故事——消灭兔子 优先队列+贪心

    将兔子的血量从小到大排序,箭的威力也从小到大排序, 对于每仅仅兔子将威力大于血量的箭增加队列,写个优先队列使得出来数位价钱最少.. #include<stdio.h> #include&l ...

  8. 开源TT框架上的日志类

    public class Logger { /** * log tag */ private String tagName = "MoGuLogger";// tag name / ...

  9. hdu Escape

    Escape 题目: 非常裸的多重匹配. 可是点数较多,所以要用到状态压缩. . .. .. 第一次写. 好厉害的赶脚. #include <iostream> #include < ...

  10. POJ 3299 模拟

    水题,但是WA了一屏--- swap的时候忘了把读入的数字也swap了---------..[尴尬] // by SiriusRen #include <cmath> #include & ...