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的。

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

原文地址:https://www.cnblogs.com/bitCoin/p/5573511.html