rtmutex赏析

来源:互联网 发布:java web文件上传 编辑:程序博客网 时间:2024/04/26 22:08

【摘要】

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;        //得到lock1lock = 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; outreturn ret;}
当然,这个链式反应也是有深度限制的,如果层数太多,可能会内核栈溢出,

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


0 0