• rtmutex赏析


    【摘要】

    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层。以避免这样的情况。


  • 相关阅读:
    触发器基本使用
    查询结果合并用逗号分隔
    查询报表增加小计功能
    sql语句格式化数字(前面补0)
    如何在选择画面中创建下拉列表(drop down list)-as list box
    如何更改函数的函数组(function group)
    ABAP语言中如何定义嵌套内表(nested internal table)
    [REUSE_ALV_GRID_DISPLAY]如何指定单元格颜色
    如何创建嵌套动态内表(Nested dynamic internal table)
    如何根据方法名(method)查找所在类(class)-SE84
  • 原文地址:https://www.cnblogs.com/jhcelue/p/7057970.html
Copyright © 2020-2023  润新知