zoukankan      html  css  js  c++  java
  • Linux内核完全注释阅读笔记1:O(1)时间复杂度查找timeout定时器

    
    

    前言

    一直有Linux kernel情节,之前也一直在看Linux kernel相关的书和代码,但是每次到最后又由于兴趣转变而荒废了。这次终于静下心来想把Linux内核相关的代码好好看看,算是对自己的一个沉淀吧。由于之前工作做的是分布式调度这块的东西,也稍微有过分布式文件系统相关的实习经历,所以阅读Linux内核代码的重心可能会往调度和文件系统这两大块倾斜。

    个人感觉读读Linux kernel还是蛮有必要的,其实现在各种分布式框架、各种云计算,其实很多的思想都是借鉴Linux kernel的。只是现在人比较浮躁,都比较急功近利,都恨不得一下子把什么分布式框架完全研究透,而

    忽略了底层的基础。可是到后面又只是停留在理论,而根本不知道如何深入。

    Linux定时器

    Linux通过系统定时器用以计算流逝的时间,使用全局jiffies用来记录自系统启动以来产生的节拍的总数。由于Linux系统所有的时钟、进程的调度、运行队列的负载均衡都依赖于定时器。

    所以Linux定时器的添加和timeout定时器的查找性能要求尤其重要,所以能够在O(1)查找timeout定时器还是蛮重要的,看了下Linux内核定时器添加这块的逻辑。不得不说,这块的代码

    写得还是蛮好的,而且算法非常简单。

    单纯从timeout定时器的查找来看,可以有以下一些方法:

    1. 最容易想到的,而且也是时间复杂度最高的:维护一个定时器链表,每次插入一个定时器的时候,可以把定时器插入到链表最前面,这样插入时间负责度是O(1),但是查找的时候就必须遍历的,

    时间负责度会O(N)。

    2. 维护一个定时器数组,数组中的每个元素严格按照超时jiffies有序。这样查找的时候可以使用二分查找,查找时间复杂度为O(lgN)。但是,但是对于定时器的插入和删除,时间复杂度却很高,

    必须移动元素,时间复杂度在O(N)。

    3. 使用堆或者红黑树,这样插入、查找和删除时间复杂度都会O(lgN)。还是可以接受的,不过编程稍微复杂些。

    4. Linux内核的实现,插入时间复杂度会O(N),查找和删除时间复杂度都为O(1),性能还是非常好的。而且都是内核的使用场景,插入相对于查找来说肯定频率低很多。比如,一个定时器的

    timeout时间相对于现在有10jiffies,那么就是在这10个jiffies中会涉及10次查找但是只有一次插入和删除。

    Linux定时器插入代码

    看下Linux定时器的插入逻辑,你就会发现它这块在插入的时候估计为查找做了很多优化,就会发现timeout定时器在O(1)复杂度就能够找到。

    前面那些判断的逻辑就不用多说了,这块会讲下核心的代码。首先会判断当前带插入的定时器合不合法,如果合法就插入到链表头部。然后,就是优化。核心代码如下:

    while (p->next && p->next->jiffies < p->jiffies) {
        p->jiffies -= p->next->jiffies;
        fn = p ->fn;
        p->fn = p->next->fn;
        p->next->fn = fn;
    
        jiffies = p->jiffies;
        p->jiffies = p->next->jiffies;
        p->next->jiffies = jiffies;
    
        p = p->next;
    }
    

    p为定时器链表表头指针,代码中比较核心的逻辑是 p->jiffies -= p->next->jiffies; 每个定时器真正的timeout时间是前面所有定时器jiffies值和自身jiffies之和。并且链表的是按照jiffies值有序的。

    但是这块代码中还是有一处问题的,如果仔细想想的话。如果刚开始插入的元素很小的话,后面插入的元素都比较大,这个确实是没有问题。但是,如果之前插入的元素比较大,后面插入的比较小。

    如果,插入1、2、3、4,严格最小的最开始插入式没有问题的,这样最终的链表元素值依次是1、1、1、1。OK,肯定是正常的,每次jiffies到期时,会把第一个元素的值减1,到0就清除掉,

    并且调用对应的注册方法。

    但是,对于后面插入的不是当前链表中最大的值,例如按4、3、2、1或者其他的顺序,就存在问题了。像这个例子,最终链表元素值依次是1、2、3、4。也就是最后一个元素本来是需要4jiffies到期的,现在却需要变成是10jiffies。明显不合理。

    那这个问题怎么解决呢,有一个办法,元素插到确定的位置之后,再把后面那个元素的值减去前面那个元素的值。比如,4已经插好了,并且链表中只有4,再插入3的时候,3插入到4之前,然后把后面的所有的元素值减去前面元素的值。

    所以,3插入到链表后,链表中的值为3、1。插入2之后,链表元素的值为2、3、1,再把2后面的那个元素的值减去见面的,就是2、1、1。插入1之后,链表中的元素值为1、2、1、1,然后再把2减去1,

    所以链表中最终的元素值为1、1、1、1。

    while (p->next && p->next->jiffies < p->jiffies) {
        p->jiffies -= p->next->jiffies;
        fn = p ->fn;
        p->fn = p->next->fn;
        p->next->fn = fn;
    
        jiffies = p->jiffies;
        p->jiffies = p->next->jiffies;
        p->next->jiffies = jiffies;
    
        p = p->next;
    }
    if (p->next) {
        p->next->jiffies -= p->jiffies;
    }

    不知道为什么源代码中反而没有后面的判断,

    if (p->next) {
        p->next->jiffies -= p->jiffies;
    }
    

    取而代之的是,调用了sti()函数,然后add_timer函数就结束了。没细看sti()函数的逻辑,不过感觉Linux内核中肯定做了这件事情,否则肯定是有Bug的。

    不过,不管怎样,这块的代码写的还是蛮好的。可以借鉴,不过在借鉴之前要明白它这么做的场景和原理。

  • 相关阅读:
    poj 2187 Beauty Contest(旋转卡壳)
    poj 2540 Hotter Colder(极角计算半平面交)
    poj 1279 Art Gallery(利用极角计算半平面交)
    poj 3384 Feng Shui(半平面交的联机算法)
    poj 1151 Atlantis(矩形面积并)
    zoj 1659 Mobile Phone Coverage(矩形面积并)
    uva 10213 How Many Pieces of Land (欧拉公式计算多面体)
    uva 190 Circle Through Three Points(三点求外心)
    zoj 1280 Intersecting Lines(两直线交点)
    poj 1041 John's trip(欧拉回路)
  • 原文地址:https://www.cnblogs.com/bitCoin/p/5573511.html
Copyright © 2011-2022 走看看