【摘要】

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. URAL 1517 Freedom of Choice

    Freedom of Choice Time Limit: 2000ms Memory Limit: 32768KB This problem will be judged on Ural. Orig ...

  2. 【转】 C# ListView实例:文件图标显示

    [转] C# ListView实例:文件图标显示 说明:本例将目录中的文件显示在窗体的ListView控件中,并定义了多种视图浏览.通过调用Win32库函数实现图标数据的提取. 主程序: 大图标: 列 ...

  3. spring boot启动原理步骤分析

    spring boot最重要的三个文件:1.启动类 2.pom.xml 3.application.yml配置文件 一.启动类->main方法 spring boot启动原理步骤分析 1.spr ...

  4. Java线程演示样例 - 继承Thread类和实现Runnable接口

    进程(Process)和线程(Thread)是程序执行的两个基本单元. Java并发编程很多其它的是和线程相关. 进程 进程是一个独立的执行单元,可将其视为一个程序或应用.然而,一个程序内部同事还包括 ...

  5. 从头认识java-16.4 nio的读与写(ByteBuffer的使用)

    这一章节我们来讨论一下nio的读与写. 1.nio的读 package com.ray.ch16; import java.io.IOException; import java.io.RandomA ...

  6. Linux操作系统是如何工作的

    <实验五——Linux操作系统是如何工作的?破解操作系统的奥秘> 姓名:方超 学号:SA12**6201 Linux操作系统工作的基础 存储程序计算机.堆栈(函数调用堆栈)机制和中断机制是 ...

  7. 8.boost_array_any

    #include <iostream> #include <string> #include <boost/array.hpp> //异构的容器 #include ...

  8. Gradle学习总结

    Gradle学习系列 (1). Gradle快速入门 (2). 创建Task的多种方法 (3). 读懂Gradle语法 (4). 增量式构建 (5). 自定义Property (6). 使用java ...

  9. solarwinds之网络发现

    1.  首先需要添加网络发现   2.  使用public   3.  添加主机   4.  网络地址选择   5.  默认下一步   6.  运行发现   7.  扫描结构如下   8.  下一步 ...

  10. POJ 3660 Cow Contest【传递闭包】

    解题思路:给出n头牛,和这n头牛之间的m场比赛结果,问最后能知道多少头牛的排名. 首先考虑排名怎么想,如果知道一头牛打败了a头牛,以及b头牛打赢了这头牛,那么当且仅当a+b+1=n时可以知道排名,即为 ...