【摘要】

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结构:

  1. pi_state = alloc_pi_state();

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

  1. 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来实现。

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

1.2 futex_unlock_pi

  1. futex_unlock_pi(unsigned long uaddr)
  2. {
  3. struct futex_hash_bucket *hb;
  4. //依据futex地址获取页框,来计算key
  5. get_user_pages_fast(addr, 1, 1, &page);
  6. q.key->both.offset |= FUT_OFF_INODE; /* inode-based key */
  7. q.key->shared.inode = page->mapping->host;
  8. q.key->shared.pgoff = page->index;
  9.  
  10. //以key为基准,查找出hash冲突链里第一个被堵塞的futex_q
  11. //并尝试唤醒
  12. hb = hash_futex(&key);
  13. head = &hb->chain;
  14. plist_for_each_entry_safe(this, next, head, list) {
  15. if (!match_futex (&this->key, &key))
  16. continue;
  17. ret = wake_futex_pi(uaddr,uval,this);
  18. goto out_unlock;
  19. }
  20.  
  21. }
  22. //详细的唤醒函数,尝试唤醒futex_q *this指向的进程。
  23. //并调整优先级
  24. wake_futex_pi(u32 __user *uaddr, unsigned long uval,struct futex_q *this)
  25. {
  26. //获取到该futex_q(进程)所持有的锁pi_state->rtmutex对象
  27. struct futex_pi_state *pi_state = this->pi_state;
  28. //获取下一个优先级最高的被堵塞者
  29. new_owner = rt_mutex_next_owner(&pi_state->pi_mutex);
  30. //将用户态lock字段更新owner为下一个持有者
  31. newval = FUTEX_WAITERS | task_pid_vnr(new_owner);
  32. cmpxchg_futex_value_locked(uaddr, uval, newval);
  33.  
  34. //眼下,此锁的全部者已经不是当前进程了。因此将它从本进程
  35. //的链表中取下。加入到下一个owner的链表中
  36. list_del(&pi_state->list);
  37. list_add(&pi_state->list, &new_owner->pi_state_list);
  38. pi_state->owner = new_owner;
  39. //释放锁,优先级调整
  40. rt_mutex_unlock(&pi_state->pi_mutex);
  41. }
  42. rt_mutex_unlock(struct rt_mutex* rtmutex)
  43. {
  44. //唤醒一个最高优先级堵塞者
  45. wakeup_next_waiter(lock, 0);
  46. //调整当前进程的优先级。由于已经释放资源了,须要往下调一下优先级
  47. rt_mutex_adjust_prio(current);
  48. }
  49.  
  50. static void wakeup_next_waiter(struct rt_mutex *lock)
  51. {
  52. //找出最高优先级的等待者(前面futex流程里也找过一次,用来更新用户态owner值)
  53. struct rt_mutex_waiter *waiter;
  54. waiter = rt_mutex_top_waiter(lock);
  55. //找到后,先从lock的堵塞队列里摘下来,由于该进程立即就不会被堵塞了
  56. plist_del(&waiter->list_entry, &lock->wait_list);
  57. //接着从当前进程的最高优先级堵塞队列里摘除。由于该进程是lock的最高优先级等待者,
  58. //也一定会被链接到锁持有者的最高优先级堵塞队列里
  59. pendowner = waiter->task;
  60. plist_del(&waiter->pi_list_entry, ¤t->pi_waiters);
  61. wake_up_process(pendowner);
  62. //设置rt_mutex的owner
  63. rt_mutex_set_owner(lock, pendowner, RT_MUTEX_OWNER_PENDING);
  64.  
  65. //还没完。新的owner的pi_waiters链表还须要更新,由于新owner获取到锁之后,也開始
  66. //堵塞别人了。
  67. //注意,新owner不须要调高优先级,由于新owner已经是眼下为止,持有该锁
  68. //的最高优先级。仅仅有当新的高优先级进程尝试获取该锁被堵塞时,
  69. //才须要继续往上调整优先级
  70. next = rt_mutex_top_waiter(lock);
  71. plist_add(&next->pi_list_entry, &pendowner->pi_waiters);
  72. }
  73.  
  74. void rt_mutex_adjust_prio(task)
  75. {
  76. prio = min(task_top_pi_waiter(task)->pi_list_entry.prio,
  77. task->normal_prio);
  78. task->prio = prio;
  79. }

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

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

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

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

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

加锁。

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

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

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

来获取资源。这就叫偷。

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

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

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

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

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

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

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

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

  1. static inline int try_to_steal_lock(struct rt_mutex *lock,
  2. struct task_struct *task)
  3. {
  4. struct task_struct *pendowner = rt_mutex_owner(lock);
  5. if (!rt_mutex_owner_pending(lock))
  6. return 0;
  7.  
  8. if (pendowner == task)
  9. return 1;
  10.  
  11. if (task->prio >= pendowner->prio) {
  12. return 0;
  13. }
  14.  
  15. /* No chain handling, pending owner is not blocked on anything: */
  16. //找到lock的下一个最高优先级堵塞者。
  17. //这个堵塞者已经被挂在pending owner的pi_waiters最高优先级堵塞进程队列上了。
  18. //须要将其改挂到当前偷取者的pi_waiters上。让后调整pending owner的优先级,
  19. //由于pending owner已经不持有该锁了
  20. next = rt_mutex_top_waiter(lock);
  21. plist_del(&next->pi_list_entry, &pendowner->pi_waiters);
  22. __rt_mutex_adjust_prio(pendowner);
  23.  
  24. //将pending owner改挂后,当前偷取者的优先级也得
  25. //依据偷取者的pi_waiters优先级来调整。
  26. plist_add(&next->pi_list_entry, &task->pi_waiters);
  27. __rt_mutex_adjust_prio(task);
  28.  
  29. return 1;
  30. }

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

成员被改动后,运行。

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


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

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

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

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

这套代码

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

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

  1. static int rt_mutex_adjust_prio_chain(struct task_struct *task,
  2. int deadlock_detect,
  3. struct rt_mutex *orig_lock,
  4. struct rt_mutex_waiter *orig_waiter,
  5. struct task_struct *top_task)
  6. {
  7. struct rt_mutex *lock;
  8. struct rt_mutex_waiter *waiter, *top_waiter = orig_waiter;
  9.  
  10. retry:
  11. //当前锁持有者task0是否被其它锁lock1堵塞,
  12. //假设堵塞的话则须要调整lock1->owner ,即task1的优先级
  13. //否则返回不须要处理。
  14. waiter = task->pi_blocked_on;
  15. if (!waiter)
  16. goto out;
  17. //得到lock1
  18. lock = waiter->lock;
  19. //死锁检測:假设遍历过程中,出现了一个环,
  20. //即要么锁反复了。要么进程反复了,就是一个死锁
  21. /* Deadlock detection */
  22. if (lock == orig_lock || rt_mutex_owner(lock) == top_task) {
  23. ret = deadlock_detect ?
  24.  
  25. -EDEADLK : 0;
  26. goto out;
  27. }
  28. //获取lock1的最高优先级被堵塞者
  29. top_waiter = rt_mutex_top_waiter(lock);
  30. //将task0的优先级调整后。又一次加到lock1的等待者队列
  31. /* Requeue the waiter */
  32. plist_del(&waiter->list_entry, &lock->wait_list);
  33. waiter->list_entry.prio = task->prio;
  34. plist_add(&waiter->list_entry, &lock->wait_list);
  35.  
  36. //获取lock1的持有者task1,作为下一个须要遍历的节点
  37. /* Grab the next task */
  38. task = rt_mutex_owner(lock);
  39.  
  40. //假设改动优先级后插入lock1等待队列的task0,是最高优先级等待者。则
  41. //须要把task0插入到task1的最高优先级等待者队列,即task1->pi_waiters
  42. //然后继续尝试改动task1的优先级后。继续遍历链表。
  43.  
  44. if (waiter == rt_mutex_top_waiter(lock)) {
  45. /* Boost the owner */
  46. plist_del(&top_waiter->pi_list_entry, &task->pi_waiters);
  47. waiter->pi_list_entry.prio = waiter->list_entry.prio;
  48. plist_add(&waiter->pi_list_entry, &task->pi_waiters);
  49. __rt_mutex_adjust_prio(task);
  50. //否则。说明task0改动优先级后。不是lock1的最高优先级等待者,
  51. //而且,task0以前是lock1的最高优先级等待者(即下句推断)
  52. //那么说明task0的优先级被减少了,须要将task0从task1的最高优先级
  53. //等待队列中删去。取下一个lock1的最高优先级等待者,加入到
  54. //task1的最高优先级等待队列pi_waiter中,再调整task1的优先级,
  55. //最后进行下一次节点遍历。
  56.  
  57. } else if (top_waiter == waiter) {
  58. /* Deboost the owner */
  59. plist_del(&waiter->pi_list_entry, &task->pi_waiters);
  60. waiter = rt_mutex_top_waiter(lock);
  61. waiter->pi_list_entry.prio = waiter->list_entry.prio;
  62. plist_add(&waiter->pi_list_entry, &task->pi_waiters);
  63. __rt_mutex_adjust_prio(task);
  64. }
  65. goto again;
  66.  
  67. out
  68. return ret;
  69. }

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

因此内核给了一个上限。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. Spring中使用Quartz之MethodInvokingJobDetailFactoryBean配置任务

    Quartz是一个强大的企业级任务调度框架,Spring中继承并简化了Quartz. Spring中使用Quartz的3种方法(MethodInvokingJobDetailFactoryBean,i ...

  2. springboot项目封装为docker镜像

    1.本次镜像的基础镜像是:https://www.cnblogs.com/JoeyWong/p/9173265.html 2.将打包好的项目文件放在与Dockerfile同级的目录下 3.Docker ...

  3. Jenkins学习总结(5)——免费DevOps开源工具简介

    一:开发工具 1.版本控制系统 Git Git是一个开源的分布式版本控制系统,用以有效.高速的处理从很小到非常大的项目版本管理. 2.代码托管平台 GitLab GitLab是一个利用Ruby on ...

  4. MySQL创建表时加入的约束以及外键约束的的意义

    1,创建表时加入的约束 a) 非空约束,not null b) 唯一约束,unique c) 主键约束,primary key d) 外键约束,foreign key 1,非空约束,针对某个字段设置其 ...

  5. Mybatis解决了JDBC编程哪些问题

    一:Mybatis简介 MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动.创建connection.创 ...

  6. 使用iTools、PP助手清理垃圾前后文件夹对照图

    1.1 documents清理前 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveHl4am4=/font/5a6L5L2T/fontsize/400/fi ...

  7. libLAS1.8.0 编译和配置(VS2013+Win7 64)(一)

    libLAS 是一个用来读写三维激光雷达数据(LiDAR) 的 C++ 库.在学习.科研和研发中都会广泛运用.怎样编译和配置自己所须要版本号的libLAS库确是一件麻烦耗时的事情. 笔者在Win7 6 ...

  8. [JZOJ 5894] [NOIP2018模拟10.5] 同余方程 解题报告(容斥)

    题目链接: http://172.16.0.132/senior/#contest/show/2523/0 题目: 题解:(部分内容来自https://blog.csdn.net/gmh77/arti ...

  9. sicily 题目分类

    为了方便刷题,直接把分类保存下来方便来找. 转自:http://dengbaoleng.iteye.com/blog/1505083 [数据结构/图论] 1310Right-HeavyTree笛卡尔树 ...

  10. BZOJ 2793: [Poi2012]Vouchers(调和级数)

    Time Limit: 20 Sec  Memory Limit: 64 MBSubmit: 582  Solved: 250[Submit][Status][Discuss] Description ...