Linux Kernel

走近Linux内核

Monday, 15. January 2007, 13:54:04





# 走近Linux内核

作者:王聪

>xiyou.wangcong@gmail.com<


>
>
> 不要理会任何一个告诉你内核开发是困难,特别或者不同的人。它是一个大的程序,而且bug修复或驱动编写是一个最佳起点。它也没有什么魔力,也不是使用只有留着络腮胡的老手才能读懂的语言编写。
>
> ──Alan Cox



## 简介

这篇文章是专门写给那些对Linux内核感兴趣,却又不知道如何着手去读懂那么多代码的内核新手。也许你刚刚了解Linux,又急于探索Linux的内部秘密;也许你是一个Linux开发者,熟悉应用程序的开发,又雄心勃勃准备向内核世界进发。那么这篇文章正是你需要的,它会带你走进内核的世界,伴你渡过危险的沼泽。通过分享我们自己的经历,希望有更多的人能够加入到Linux内核开发者行列。

内核开发向来被视为非常神秘的工作,仿佛只有传说中的留着长长的络腮胡的黑客们才能从事它。其实不然,Linux内核的开发和其它大型项目没有多少差别,只不过它的调试确实有点特别,需要一些特别的技巧。不要恐慌(Don’t Panic!),只要你下功夫,你也能参与内核的开发,它的确是一件非常好玩的事。

## 需要准备什么

当然,你首先要有一台可供支配的电脑,最好装有Linux。如果可以,最好再有一台专门供你调试代码的机器,因为没人能保证调试内核的过程中不会让你的文件系统崩溃。或者,至少有一块专门给调试内核使用的硬盘。

最好还有一个固定的互联网接口,毕竟Linux内核开发是在网络上进行的,而且你也会经常在互联网上搜索一些有用的信息。

如果你是一位超级geek的话,再准备一根双机串口线,它能帮助你从一台机器上聆听另一台机器上内核运行中的抱怨。嗯,有点像是外科医生给病人听诊,这看起来很酷,不是吗?

如果你准备在一台非计算机设备上调试你的内核(这没什么奇怪的,Linux早已经被移植到千奇百怪的系统上),那么你还需要准备相应的硬件,或者它的模拟器,或者其它一些工具。如果你有在非计算机设备上调试Linux内核的经验,请在这里自由添加相应的内容。

## 开始

我们假设上面的东西你都准备好了,整装待发,现在可以正式进军内核了。当然了,如果你对Linux上的开发已经很熟悉了,你可以安全地跳过这一节。好了,出发,水手们!

### 1. 精通C语言编程

不是我们一味推崇C语言,而是C语言的的确确太适合做内核开发了。C语言的诞生源于编写Unix内核代码,它精练的设计哲学确实做到了这一点。甚至有人这样评价C语言──“它联合了汇编的所有威力。如果你还不懂C,赶快去学吧。

如果你是一名编程新手,不推荐用C作为你的入门语言,原因如下:

编程新手最需要了解的是编程的概念和对编程的基本认识,而过多的接触C语言往往会把你引出这一目的,会让你把注意力集中到一些奇怪的语言特性上,而不是编程语言本身。

编程新手往往对计算机了解不够深刻,不清楚计算机的内部结构,而C语言恰恰就是和计算机内存/编码/CPU打交道,最起码,调试那些隐晦的错误时如此。(想想你是不是没有把一个指向指针的指针的指针指向正确的位置。)

学好C语言需要下很大的功夫,最起码不能低于两年。(当然如果你不打算学好那得另说了。)

所以,最好先学一门比较简单的编程语言作为铺垫。不妨试一下Python,它比Java还要简单。当然了,这并非绝对,因人而异。如果你真的决定开始学习C语言,那么推荐的入门书籍仍然是K&R《The C Programming Language。过去这么多年了,它仍然被奉为入门的首选,可见其有多么经典。

不过仅仅了解C的语法,能编写一些小的程序是远远不够的。你必须能够熟练地操纵C语言,了解它的一些缺陷和陷阱,让它变成你的利器。有句话说得好:“C语言就像一把刻刀,简单,锋利,并且在技师手中非常有用。和任何锋利的工具一样,C会伤到那些不能驾驭它的人。读一读《C

Traps and Pitfalls
》和《Expert C Programming》吧,它们能让你有一个大的提升,成为一名C语言高手。

如果碰巧你是一位C++的推崇者,那么下面的一些引用或许能说服你开发Linux内核不使用C++(摘自LKML FAQ)。

> Linus2004年说:
>
> In fact, in Linux we did try C++ once already, back in 1992.
>
> It sucks. Trust me - writing kernel code in C++ is a BLOODY STUPID IDEA.
>
>
他认为:
>
> C++
编译器是不可靠的,1992年的时候更糟,有一些基础性的东西没有改变:
>
> (不知道怎么翻译这个词好)的,对内核来说它更是broken
>
> 任何一个喜欢把内存分配藏到你背后的编译器或者语言,都不是你编写内核的好的选择。
>
> 你可以用C来编写OO代码,而不用C++的一些废话
>
> Andrew D. Balsa如是说:
>
> Linux
一开始的时候gcc还没有很好的C++实现,而且当时C程序员比C++程序员要多。
>
> Richard E. Gooch
解释到:
>
>
Linux诞生之前,也有内核在g++下编译,但是人们抱怨它的性能。据证明,把C代码放到g++下编译会产生更糟糕的代码。它不应该有差别,但的的确确有!

### 2. 熟悉常用的工具

#### 掌握至少一种编辑器

掌握一种你喜欢的编辑器是非常有必要的,它能为你节省很多时间。在Linux上,最著名的编辑器莫过于emacsvi了。一些内核开发者介绍,他们大多数人就是在使用这两种编辑器中的一种。可能一开始你并不能适应这种环境,没关系,熟练之后你的效率会有质的飞跃。

>pvi


在内核开发中使用vi有如下好处:

1.
占用内存少,加载速度快;

2.
高度可定制化,经配置的vi可以很好地重映射命令按键;

3.
可以自动补齐一些函数名和变量名;

4.
可以和ctags很好地配合使用,通过ctrl+]能很快定位函数的定义位置。

了解更多的vi使用说明,请参考:《Learning the vi Editor, Fifth Edition》, L.Lamb, 1990, O’Reilly

emacs

emacs的优点如下:

1. Lisp
扩展可以大幅度提高效率;

2.
etags的良好整合;

3.
按键组合更具指导性。

更多的emacs使用知识请参考:《Learning GNU Emacs, Third Edition

, Debra Cameron, James Elliott and

Marc Loy, 2004, O’Reilly


#### 熟悉Linux命令行工具

内核开发者一般都在命令行下面工作,是的,图形界面有时会让事情变得更糟,对内核开发尤为如此。你应该能在命令行下面进行一些简单的工作,比如:编译程序,提交补丁,控制版本,收发邮件等。一些常用的命令行技巧会有帮助,所以也不妨去了解一些shell编程知识。下面仅介绍一些常用的:

gcc

gccGNU提供的优秀的软件之一,其性能不亚于任何商业编译器。它具有惊人的可移植性,而它也号称其最优化功能非常强大,所以Linux内核就是采用了gcc作为编译器。gcc的选项这里无法一一列举,只能介绍一些常用的选项。

-o filename
:这可能是你最常用的一个选项了。它用来产生以指定名字的可执行输出文件。如果不指定文件名,gcc产生默认的a.out

-c:
只编译不链接,产生以.o结尾的目标文件。当然它也可以同时编译多个文件。

> $ gcc -c hello.c

-Idir: 要求gcc在搜寻头文件时除了默认的目录/usr/include,也要到指定的目录dir中去找。

比如,编译test.c时要用到/home/myname/test.h,我们可以:

$gcc

-I/home/myname test.c -o test


-On: 最优化选项。后面的n越大最优化程度越大。一般最常用的是-O2-O0是不优化,这也是默认的。

$gcc -O2 -o foo

foo.c


-Wall/-w:

-Wall
将打开所有警告,而-w将关闭所有警告。在编译C程序时,强烈建议你加上-Wall选项,因为gcc给出的很多警告都是相当有用的。当然还有折中的选择,具体请参阅man手册。

-W:
打开一些额外的警告。

-S:
用来产生相应的汇编源文件,默认的文件名是filename.s

make

make是一个通用的工程管理工具,它可以自动化编译,极大地提高了软件开发的效率。make的使用是根据预先设定的规则来运行的,这些设定的规则记录在一个文件中,即makefile

make
命令的格式是:

> make [ -f makefile ] [ option ] …
>
> target …


-c dir :将指定目录设为make开始运行后的工作目录。

-f filename
:将指定的文件作为makefile

-k
:使make尽可能地编译,即使命令调用返回一个错误,make也不会停止运行。这个功能很有用,例如,当需要移植的时候;你可以创建尽可能多的目标文件,然后可以在不用等待中间文件创建的同时,移植到不能创建的文件处。

makefile
的书写规则是一个大的话题,我们在这里难以展开论述。这里>http://www.wlug.org.nz/MakefileHowto<有一篇介绍makefile书写的Howto,不妨仔细读一下。要对make进行更深入的了解,请参考《Managing

Projects with GNU make
一书。



grep

grep用来在文件中查找一个给定的字符串模式,比如可以快速定位函数或结构体定义。它还可以自动匹配一些正则表达式,非常强大。比如:你要搜索taskstruct的定义, 你可以:

> $grep -r
>
> task_struct | less


-r选项或许是你需要的,它可以帮你扫描源代码树中的所有源代码文件。查看grepman手册来了解更多信息。



*diff/patch

如果你在维护包含很多源文件的一个大型项目,每次更新后都推出完整的源程序是不太可行的,所以最好就用patch程序来更新原来的程序。这样,当你的程序升级时,你只要推出源文件对应的patch文件,其他人就能通过执行patch来使用那个patch文件,以获得最新版本的程序。

比如我们有一个程序foo,我们把它更新为bar,我们可以用diff程序这样产生patch文件:

> $diff -u foo bar
>
> < foo.patch
-u参数指定使用特殊的diff输出格式,否则得到的patch格式怪异,一般人都没法看懂。foo.patch就包含了描述foobar不同的信息,我们将用这个文件来更新。

提示:一定要注意diff命令后面的文件名顺序,一定是旧的文件在前,新的文件在后!



接下来,我们用patch来更新:

$patch foo >

foo.patch


使用过某个patch文件后,若要再次使用该patch文件,patch程序会产生警告信息:询问是否以-R方式执行,-R将还原文件。这样很好,防止进行了误操作。当然,有时你想更新的是整个目录中的所有源文件,比如foobar两个目录,我们可以:

$diff -cr foo

bar <foo.patch

$patch -p0 > foo.patch


diff
-c参数表示输出上下文格式,-r参数表示递归的比较两个目录。 -p0告诉patch程序被更新的文件的路径名并没改变。

diffstat
是一个很有用的工具,它可以列出patch 所引起的变更的统计(加入或移出的代码行)。输出关于patch的信息,执行:

$diffstat -p1

foo.patch


更多选项请参阅patchdiff的使用手册。

### 3. 良好的英文阅读能力和表达能力

不管是在Linux内核中,还是在Linux大多数应用程序中,文档基本上都是用英语写成的,到目前为止,大多数还没有对应的汉语翻译。而且Linux内核黑客之间也是用英语交流的,虽然他们可能来自世界各地。熟练地驾驭英文肯定让你受益非浅。

引用ESR《如何成为一名黑客》一文中的话:

> 当前英语有着比其他语言丰富得多的技术词汇,因此是一个对于工作来说相当好的工具。基于类似的原因,英文技术书籍的翻译通常不令人满意(如果有翻译的话)。
>
> “Linus Torvalds
,一个芬兰人,用英语注释他的代码(很明显这对他来说不是凑巧)。他流利的英语成为他能够管理全球范围的Linux开发人员社区的重要因素。这是一个值得学习的例子。

如果你不懂实用性的英语,赶快去学习吧。

## 进阶

如果你能来到这里,那说明你已经是一名合格的Unix/Linux程序员了。但是,如果你要成为一名内核程序员,还需要下面的一些知识。

### 1. 理解操作系统原理

内核是操作系统的核心和灵魂,要理解内核的原理和运作,必须具备基本的操作系统知识。操作系统课上的一些理论性的东西很有帮助,比如信号量的定义和使用,死锁的预防,内存页面的置换等等。列举如下:

1. 进程管理

进程的定义和PCB,进程和线程的区别,进程的三个基本状态及它们之间的转换关系,进程的同步,竞争和死锁,进程间通信

2. 内存管理

分页式管理,分段式管理,虚拟内存的概念,页面置换算法,内存分配算法

3. 设备管理

中断的概念,中断处理,I/O控制方式,缓冲区管理,设备驱动,磁盘调度和高速缓存

4. 文件管理

文件的概念,文件的管理,文件系统

5. 系统调用

系统调用的概念,系统调用的处理,系统调用类型

关于操作系统理论方面的书籍,推荐Andrew S. Tanenbaum编写的《Operating Systems: Design and Implementation》一书。

### 2. 一些硬件知识

由于内核本身的特殊性,一些时候你不得不和计算机硬件打交道,尤其是CPU。以i386为例说明:

常用寄存器,常见指令

实模式和保护模式

分段和分页机制

TSS和任务管理

中断机制

时钟机制

高速缓存

关于Intel CPU最权威的资料莫过于Intel的官方手册,它们可以在Intel网站上免费下载: http://www.intel.com/products/processor/manuals/index.htm。其实你只要读一下System Programming Guide就够了。

如果你在做嵌入式Linux或者Linux设备驱动,相关的硬件知识更是必须的,可惜这里无法一一列举。

### 3. 其它

你还需要足够的耐心和一些运气,毕竟内核调试不像用户程序那么容易,一些bug让整个社区好几天都食不甘味了。用户程序的调试经验或许有用,但用处不大。这就更要求你对内核本身有足够多的了解了,实际上,你对内核越是了解,bug就越容易清除。

最后,你可能还需要一些想像力。没错,Linux内核开发者有着丰富的想像力,他们经常使用一些神来之笔来解决令旁人苦恼的问题,或者为内核添加一项新奇的功能来让内核运行更流畅。

## 推荐相关书籍:

《Linux内核完全剖析》

讲述Linux 0.11版的源代码,是一个不错的起点。

Understanding the Linux Kernel, 3rd Edition

中文版是《深入理解Linux内核》,第三版即将上市。第三版主要是讲解2.6版的内核,保持了前两版的风格,是深入学习内核的必读书籍。

_Linux Kernel Development, 2nd Edition


中文版是《Linux内核开发》。其内容朴实,风格简练,便于整体把握Linux内核架构。当了解操作系统原理后,便可阅读这本书。

The Linux Kernel Primer - A Top Down Approach For x86 and PowerPC Architectures

中文版是《Linux内核编程》,此书最大的特色是能够把x86ppc两个平台结合起来讲述内核,从用户程序讲起,深入到内核,而且每章后面还配有练习题。

Linux Device Drivers, 3rd Edition

中文译本是《Linux设备驱动程序》,该书是设备驱动开发的最好工具书。其中的例子对于内核开发者来讲是最好的启蒙教材。

《Linux操作系统原理与应用》

陈老师写的一本不错的内核书籍,从原理、设计思想的角度对Linux操作系统的核心内容进行全面的阐述。

《Linux内核情景分析》

本书采取类似于英语教学中行之有效的情景会话的教学方法,全面深入地剖析了Linux 2.4.0内核源代码,并对Linux核心的独特优点和需要进一步改进的问题作了精辟的评述。 全书分上下两册。

## 一些有用的链接:

KernelNewbies

KernelTrap

Linux Weekly News

Linux内核之旅

Kernel Links

The Linux Kernel

What is the Kernel

## 结束语

讲了这么多了,现在该轮到你了。如果你对上面的知识还有没全部掌握,那么不妨从你不熟悉的地方入手,一步步去了解和熟悉Linux内核。如果你是一位高手,对Linux内核有初步的了解和认识,不妨去动手写一些驱动程序或者内核模块,尝试对Linux内核做一些自己的修改。如果可以,加入Linux内核邮件列表,看看里面的内核黑客在做些什么,试着帮他们测试新的内核,修复一些bug

祝你在Linux内核世界走得更远!

两个和gcc相关的脚本

Sunday, 25. February 2007, 07:16:44

Linux内核源代码中有这么一个脚本文件:scripts/gcc-version.sh。它的主要代码如下:


9 compiler=”$*”
10
11 MAJOR=$(echo GNUC | $compiler -E -xc - | tail -n 1)
12 MINOR=$(echo GNUC_MINOR | $compiler -E -xc - | tail -n 1)
13 printf “%02d%02dn” $MAJOR $MINOR
14

从上面我们很容易看出它的用法,它要带一个参数,指明该平台上 GNU C编译器的命令(可能有些平台是cc)。它会给出GNU C编译器的版本号,以如下格式:XXYY,其中XX是主版本号,YY是次版本号,比如gcc-4.1的版本号就是0401。

这个脚本的实现很简单,它通过把GNU C编译器预先定义的宏GNUCGNUC_MINOR展开后交给编译器的预处理器处理,处理后其实就应该是想要的结果了,但gcc会自动在前面插入自己的一些东西,所以,要截取最后一行才是真正的结果。-E选项是指明只进行预处理,注意:如果没有-o,-E默认的输出是标准输出;-x选项是要指明所使用的语言,这里指明的是c;-是说明输入来自标准输入,这主要是照顾管道。

另一个和gcc相关的脚本是scripts/gcc-x86_64-has-stack-protector.sh,它用来测试x86_64(x86_64是AMD的,IA-64才是Intel的)上是不是有堆栈保护,代码如下:


3 echo “int foo(void) { char X[200]; return 3; }” | $1 -S -xc -c -O0 -mcmodel=kernel -fstack-protector - -o - 2< /dev/null | grep -q “%gs”
4 if [ “$?” -eq “0” ] ; then
5 echo $2
6 fi

-fstack-protector选项是指明要检查堆栈是否会溢出,这是为了保护程序免于缓冲区溢出的攻击;-mcmodel=kernel是指明要为内核模式生成代码,Linux内核似乎要使用此选项。第3行的命令使用得更妙,还把中间命令的错误重定向到了/dev/null,而且还为grep开启了安静模式。似乎是从生成的汇编中找到”%gs”这个寄存器就说明有堆栈保护,但原理还是不明白;-(。

Linux内核中的红黑树

Friday, 2. February 2007, 06:07:27

王聪@西邮
红黑树是平衡二叉树的一种,它有很好的性质,树中的结点都是有序的,而且因为它本身就是平衡的,所以查找也不会出现非常恶劣的情况,基于二叉树的操作的时间复杂度是O(log(n))。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的。

先到include/linux/rbtree.h中看一下rbtree的定义,如下:

struct rb_node
{
        unsigned long  rb_parent_color;
#define RB_RED          0
#define RB_BLACK        1
        struct rb_node *rb_right;
        struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
struct rb_root只是struct rb_node*的一个包装,这样做的好处是看起来不用传递二级指针了。不错,很简单。再看一下下面几个重要的宏,细心的你一定会发现,rb_parent_color其实没那么简单,Andrea Arcangeli在这里使用了一个小的技巧,不过非常棒。正如名字所暗示,这个成员其实包含指向parent的指针和此结点的颜色!它是怎么做到的呢?很简单,对齐起了作用。既然是sizeof(long)大小的对齐,那么在IA-32上,任何rb_node结构体的地址的低两位肯定都是零,与其空着不用,还不如用它们表示颜色,反正颜色就两种,其实只要一位就够了(估计此处也是照顾16位的机器)。 这样,提取parent指针只要把rb_parent_color成员的低两位清零即可:
#define rb_parent(r)   ((struct rb_node *)((r)-
取颜色只要看最后一位即可:
#define rb_color(r)   ((r)-
测试颜色和设置颜色也是水到渠成的事了。需要特别指出的是下面的一个内联函数:
static inline void rb_link_node(struct rb_node * node, struct rb_node * parent, struct rb_node ** rb_link);
它把parent设为node的父结点,并且让rb_link指向node。 我们把重点集中在lib/rbtree.c上,看看一些和红黑树相关的重要算法。开始之前我们一起回忆一下红黑树的规则: 1\. 每个结点要么是红色要么是黑色; 2\. 根结点必须是黑色; 3\. 红结点如果有孩子,其孩子必须都是黑色; 4\. 从根结点到叶子的每条路径必须包含相同数目的黑结点。 这四条规则可以限制一棵排序树是平衡的。 __rb_rotate_left是把以root为根的树中的node结点进行左旋,__rb_rotate_right是进行右旋。这两个函数是为后面的插入和删除服务,而不是为外部提供接口。 新插入的结点都设为叶子,染成红色,插入后如果破坏了上述规则,通过调整颜色和旋转可以恢复,二叉树又重新平衡。插入操作的接口函数是
void rb_insert_color(struct rb_node *node, struct rb_root *root);
它把已确定父结点的node结点融入到以root为根的红黑树中,具体算法的分析可以参考MIT的《算法导论》第14.3节,这里的实现和书中的讲解几乎完全一样!怎么确定node的父结点应该在调用rb_insert_color之前通过手工迭带完成。值得指出的一点是,虽然插入操作需要一个循环迭代,但是总的旋转次数不会超过两次!所以效率还是很乐观的。 删除操作多多少少都有点麻烦,它要先执行像普通二叉查找树的“删除”,然后根据删除结点的颜色来判断是否执行进一步的操作。删除的接口是
void rb_erase(struct rb_node *node, struct rb_root *root);
其实它并没有真正删除node,而只是让它和以root为根的树脱离关系,最后它还要判断是否调用__rb_erase_color来调整。具体算法的讲解看参考《算法导论》13.3和14.4节,__rb_erase_color对应书中的RB-DELETE-FIXUP。此处的实现和书上也基本上一致。 其余的几个接口就比较简单了。
struct rb_node *rb_first(struct rb_root *root);
在以root为根的树中找出并返回最小的那个结点,只要从根结点一直向左走就是了。
struct rb_node *rb_last(struct rb_root *root);
是找出并返回最大的那个,一直向右走。
struct rb_node *rb_next(struct rb_node *node);
返回node在树中的后继,这个稍微复杂一点。如果node的右孩子不为空,它只要返回node的右子树中最小的结点即可;如果为空,它要向上查找,找到迭带结点是其父亲的左孩子的结点,返回父结点。如果一直上述到了根结点,返回NULL。
struct rb_node *rb_prev(struct rb_node *node);
返回node的前驱,和rb_next中的操作对称。
void rb_replace_node(struct rb_node *victim, struct rb_node *new, struct rb_root *root);
用new替换以root为根的树中的victim结点。 红黑树接口使用的一个典型例子如下:
static inline struct page * rb_search_page_cache(struct inode * inode,
                                                 unsigned long offset)
{
        struct rb_node * n = inode-_
        struct page * page;

        while (n)
        {
                page = rb_entry(n, struct page, rb_page_cache);

                if (offset > page-
                        n = n-
                else if (offset < page-
                        n = n-
                else
                        return page;
        }
        return NULL;
}

static inline struct page * __rb_insert_page_cache(struct inode * inode,
                                                   unsigned long offset,
                                                   struct rb_node * node)
{
        struct rb_node ** p = &inode-
        struct rb_node * parent = NULL;
        struct page * page;

        while (*p)
        {
                parent = *p;
                page = rb_entry(parent, struct page, rb_page_cache);

                if (offset > page-
                        p = &(*p)-
                else if (offset < page-
                        p = &(*p)-
                else
                        return page;
        }

        rb_link_node(node, parent, p);

        return NULL;
}

static inline struct page * rb_insert_page_cache(struct inode * inode,
                                                 unsigned long offset,
                                                 struct rb_node * node)
{
        struct page * ret;
        if ((ret = __rb_insert_page_cache(inode, offset, node)))
                goto out;
        rb_insert_color(node, &inode-
 out:
        return ret;
}
_

Linux进程管理中队列的使用

Friday, 2. February 2007, 05:58:24

王聪@西邮

Linux内核中大量使用了队列,这里仅列举它在进程管理中的几处应用。

状态为TASK_RUNNING的进程都会被放入运行队列(runqueue)中,这是通过task_struct(定义在include/linux/sched.h)中的run_list成员来链接的。不过,为了让内核每次都能选取优先级最合适的进程,Linux为每个优先级构建了一个queue!这是通过struct prio_array来实现的,struct prio_array的定义(在kernel/sched.c)大致如下:


struct prio_array {
int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
};

queue成员就是队列数组。每个CPU有各自的runqueue,每一个runqueue又有包含两个prio_array,一个是活动队列,一个是时间片耗尽的队列。当运行队列空时,内核便会交换两个队列的指针!原来的耗尽队列就成了新的活动队列!这和prio_array中的bitmap是决定调度算法为O(1)的关键!

状态为TASK_STOPPED,EXIT_ZOMBIE或EXIT_DEAD的进程不会被放入专门的队列中,它们直接通过pid或者通过父进程的孩子队列来访问。

TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程会被放入等待队列。不同的是,每个等待事件都会有一个等待队列,该队列中的进程等待同一事件的完成(不要惊慌!“事件”一个动态的过程,不好通过具体的结构来定义一个“事件”。这里等待一个事件就是查看某个条件是否为真,比如某个标志变量是否为真。)。wait_queue_head_t的定义(include/linux/wait.h)如下:


typedef struct _ _wait_queue_head {
spinlock_t lock;
struct list_head task_list;
}wait_queue_head_t;

wait_queue_t的定义如下:


typedef struct _ _wait_queue {
unsigned int flags;
struct task_struct task;
wait_queue_func_t func;
struct list_head task_list;
}wait_queue_t;

进入等待状态的接口有两类:
prepare_to_wait()/finish_wait()
wait_event()
其实wait_event
()内部也是调用prepare_to_wait(),把它放入一个循环中。而且wait_event()在事件完成时会自动调用finish_wait()。决定使用哪个要看情况而定。sleep_on*()是遗弃的接口,现在已经不再使用,虽然还支持。等待队列中的进程有两种,一种是exclusive的进程,另一种是nonexclusive的进程。所谓exclusive是指唤醒的进程等待的资源是互斥的,每次只唤醒一个(唤醒多个也可以,不过最后还是只有一个会被唤醒,其余的又被重新添加到等待队列中,这样效率会大打折扣)。一般,等待函数会把进程设为nonexclusive和uninterruptible,带“interruptible”的会专门指定状态为interruptible;而带“timeout”的会在超时后退出,因为它会调用schedule_timeout();带“exclusive”的则会把进程设为exclusive。

唤醒的接口虽然只有wake_up*(),但它内部也分成好几种。带“interruptible”的唤醒函数只会唤醒状态是TASK_INTERRUPTIBLE的进程,而不带的则会唤醒TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程;所有唤醒函数都会把等待同一事件的nonexclusive进程全部唤醒,或者把其中一个exclusive的进程唤醒;而带“nr”的则会唤醒指定个数的exclusive的进程,带“all”的会把全部exclusive进程唤醒。带“sync”会忽略优先级的检查,高优先级的进程可能会被延迟。最后,持有自旋锁时只能调用wait_up_locked()。