重点linux源码分析.docx
《重点linux源码分析.docx》由会员分享,可在线阅读,更多相关《重点linux源码分析.docx(9页珍藏版)》请在冰点文库上搜索。
重点linux源码分析
[重点]linux源码分析
linux源码分析
Linux内核源代码中的C语言代码
Linux内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc。
GNU对C语言本身
(在ANSIC基础上)做了不少扩充,可能是读者尚未见到过的。
另一方面,由于内核代码,往往会用
到一些在应用程序设计中不常见的语言成分或编程技巧,也许使读者感到陌生。
本书并非介绍GNUC
语言的专著,也非技术手册,所以不在这里一一列举和详细讨论这些扩充和技巧。
再说,离开具体的情
景和上下文,罗列一大堆规则,对于读者恐怕也没有多大帮助。
所以,我们在这里只是对可能会影响读
者阅读Linux内核源程序,或使读者感到困惑的一些扩充和技巧先作一些简单的介绍。
以后,随着具体
的情景和代码的展开,在需要时还会结合实际加以补充。
首先,gcc从C++语言中吸收了“inline”和“const”。
其实,GNU的C和C++是合为一体的,gcc
既是C编译又是C++编译,所以从C++中吸收一些东西到C中是很自然的。
从功能上说,inline函数的使用与#define宏定义相似,但更有相对的独立性,也更安全。
使用inline函数也有利于程序调试。
如果编译时不加优化,则这些inline就是普通的、独立的函数,更便于调试。
调试好了以后,再采用优化重
新编译一次,这些inline函数就像宏操作一样融入了引用处的代码中,有利于提高运行效率。
由于inline函数的大量使用,相当一部分的代码从.c文件移入了.h文件中。
还有,为了支持64位的CPU结构(Alpha就是64位的),gcc增加了一种新的基本数据类型“long
longint”,该类型在内核代码中常常用到。
许多C语言都支持一些“属性描述符”(attribute),如“aligned”、“packed”等等;gcc也支持不少这样的描述符。
这些描述符的使用等于是在C语言中增加了一些新的保留字。
可是,在原来的C语言(如ANSIC)中这些词并非保留字,这样就有可能产生一些冲突。
例如,gcc支持保留字inline,可是由于“inline”原非保留字(在C++中是保留字),所以在老的代码中可能已经有一变量名为inline,这样就产生了冲突。
为了解决这个问题,gcc允许在作为保留字使用的“inline”前、后都加上“__”(注意,是两个下划线),因而“__inline__”等价于保留字“inline”。
同样的道理,“__asm__”等价于“asm”。
这就是我们在代码中有时候看到“asm”,而有时候又看到“__asm__”的原因。
gcc还支持一个保留字“attribute”,用来作属性描述。
如:
structfoo{
chara;
intx[2];
}__attribute__((packed));
这里属性描述“packed”表示在字符a与整形数组x之间不应为了与32位长整数边界对齐而留下空
洞。
这样“packed”就不会与变量名发生冲突了。
由于在Linux的内核中使用了gcc对C的扩充,很自然地Linux的内核就只能用gcc编译。
不仅如
此,由于gcc和Linux内核在平行地发展,一旦在Linux内核中使用了gcc,在其较新版本中有了新增加新扩充,就不能再使用较老的gcc来编译。
也就是说,Linux内核的各个版本有着对gcc版本的依赖关系。
读者自然会问:
“这样,Linux内核的可移植性是否会受到损害,”
回答是:
“是的,但这是经过权衡利
弊以后作出的决定。
”首先,在可移植性与本身的质量之间,GNU选择了以质量为优先。
再说,将gcc
移植(其实就是扩充)到新的CPU上应非难事。
回顾一下Unix的历史。
最初的Unix是以汇编和B语言书写的,正是因为Unix的需要才有了C语言。
所以,C语言可说是Unix的孪生物。
Unix要发展,C语言自然也要发展。
对于Unix来说,C语言不过是工具,而工具当然要服从目的本身的需要。
其次,可
移植性问题看似重大,其实并不太严重。
如前所述,目前的Linux内核源代码已经支持几乎所有重要的、
常用的CPU,gcc支持的CPU就更多了。
而且,gcc还支持对各种CPU的交叉编译。
如前所述,Linux内核的代码中使用了大量的inline函数。
不过,
这并未消除对宏操作的使用,内核
中仍有许多宏操作定义。
人们常常会对内核代码中一些宏操作的定义方式感到迷惑不解,有必要在这里
作一些解释。
先看一个实例,取自fs/proc/kcore.c第163行。
00163:
#defineDUMP_WRITE(addr,nr)do{memcpy(bufp,addr,nr);
bufp+=nr;}while(0)Linux内核源代码情景分析
读者想必知道,do-while循环是先执行后判断循环条件。
所以这个定义意味着每次引用这个宏操作
是会执行循环体一次,而且只执行一次,可是,为什么要这样通过一个do-while循环来定义呢,这似乎
有点怪。
我们不妨看看其他几种可能。
首先,能不能改成如下式样,
00163:
#defineDUMP_WRITE(addr,nr)memcpy(bufp,addr,nr);
bufp+=nr;
不行。
如果有一段程序在一个if语句中引用这个宏操作就会出问题,让我们通过一个假想的例子来
说明:
if(addr)
DUMP_WRITE(addr,nr);
else
do_something_else();
经过预处理后,这段代码就会变成这样:
if(addr)
memcpy(bufp,addr,nr);bufp+=nr;else
do_something_else();
编译这段代码时gcc会失败,并报告语法错误。
因为gcc认为if
语句在memcpy()以后就结束了,然
后却又碰到了一个else。
如果把DUMP_WRITE()和
do_something_else()换一下位置,编译倒是可以通过,
问题却更严重了,因为不管条件满足与否bufp+=nr都会得到执行。
读者马上会想到要在定义中加上括号,成为这样:
00163:
#defineDUMP_WRITE(addr,nr){memcpy(bufp,addr,nr);
bufp+=nr;}
可是,上面那段程序还是通不过编译,因为经过预处理就变成这样:
if(addr)
{memcpy(bufp,addr,nr);bufp+=nr;};else
do_something_else();
同样,gcc在碰到else前面的“;”时就认为if语句已经结束,因而后面的else不在if语句中。
相比
之下,采用do-while的定义在任何一种情况下都没有问题。
了解了这一点之后,再来看对“空操作”的定义。
由于Linux内核的代码要考虑到各种不同的CPU
和不同的系统配置,所以常常需要在一定的条件下把某些宏操作定义为空操作。
例如在
include/asm-i386/system.h中第14行的prepare_to_switch():
00014:
#defineprepare_to_switch()do{}while(0)
内核在调度一个进程运行,进行切换之际,在有些CPU上需要调用prepare_to_switch()作些准备,
而在另一些CPU上就不需要,所以要把它定义为空操作。
读者在学习数据结构时一定学习过队列(指双链队列)操作。
内核中大量地使用着队列和队列操作,
而这又不是专门属于哪一个方面的内容(如进程管理、文件系统、存储管理等等),所以我们在这里作一
如果我们有一种数据结构foo,并且需要维持一个这种数据结构的双链队列,最简单的办法、也是
最常用的办法就是在这个数据结构的类型定义中加入两个指针,例如:
typedefstructfoo{
structfoo*prev;
structfoo*next;
„„„„„„„„
}foo_t;
然后为这种数据结构写一套用于各种队列操作的子程序。
由于用来维持队列的这两个指针的类型是
固定的(都指向foo数据结构),这些子程序不能用于其他数据结构
的队列操作。
换言之,需要维持多少
种数据结构的队列,就得有多少套的队列操作子程序。
对于使用队列较少的应用程序或许不是个大问题,
但对于使用大量队列的内核就成了问题了。
所以,Linux内核中采用了一套通用的、一般的、可以用到
各种不同数据结构的队列操作。
为此,代码的作者们把指针prev和next从具体的“宿主”数据结构中抽象出来成为一种数据结构list_head,这种数据结构既可以“寄宿”在具体的宿主数据结构内部,成为该
数据结构的一个“连接件”;也可以独立存在而成为一个队列的头。
这个数据结构的定义在
include/linux/list.h中(实际上是数据结构类型的申明,为行文方便,本书采取不那么“学究”,或者说不那么严格的态度。
对“定义”和“申明”,还有对“数据结构类型”
和“数据结构”,乃至“结构”这些
词也常常不加严格区分。
当然,我们并不鼓励读者这样做)。
00016:
structlist_head{
00017:
structlist_head*next,*prev;00018:
};
这里我们把结构名以粗体字排出,目的仅在于醒目,并没有特别的含义。
如果需要有某种数据结构
的队列,就把这种结构内部放上一个list_head数据结构。
以用于内存页面管理的page数据结构为例,其定义为:
(见include/linux/mm.h)00134:
typedefstructpage{
00135:
structlist_headlist;„„„„„„„„
00138:
structpage*next_hash;
„„„„„„„„
00141:
structlist_headlru;
„„„„„„„„
00148:
}mem_map_t;
可见,在page数据结构中寄宿了两个list_head结构,或者说有两个队列操作的连接件,所以page
结构可以同时存在于两个双链队列中。
此外,结构中还有个单链指针next_hash,用来维持一个单链的杂
凑队列,不过我们在这里并不关心。
对于宿主数据结构内部的每个list_head数据结构都要加以初始化,可以通过一个宏操作
INIT_LIST_HEAD进行:
00025:
#defineINIT_LIST_HEAD(ptr)do{\00026:
(ptr)->next=(ptr);(ptr)->prev=(ptr);\
00027:
}while(0)
参数ptr为指向需要初始化的list_head结构。
可见初始化后两个指针都指向该list_head结构自身。
要将一个page结构通过其“队列头”list链入(有时候我们也说“挂入”)一个队列时,可以使用
list_add(),这是一个inline函数,其代码在
include/linux/list.h中:
00053:
static__inline__voidlist_add(structlist_head
*new,structlist_head*head)
00054:
{
00055:
__list_add(new,head,head->next);
00056:
}
参数new指向欲链入队列的宿主数据结构内部的list_head数据结构。
参数head则指向链入点,也
是个list_head结构,它可以是一个独立的、真正意义上的队列头,也可以是在另一个宿主数据结构(甚
至可以是不同类型的宿主结构)内部。
这个inline函数调用另一个inline函数__list_add()来完成操作:
[list_add()>__list_add()]
00029:
/*
00030:
*Insertanewentrybetweentwoknownconsecutive
entries.
00031:
*
00032:
*Thisisonlyforinternallistmanipulationwhere
weknow
00033:
*theprev/nextentriesalready!
00034:
*/
00035:
static__inline__void__list_add(structlist_head
*new,
00036:
structlist_head*prev,00037:
structlist_head*next)00038:
{
00039:
next->prev=new;
00040:
new->next=next;
00041:
new->prev=prev;
00042:
prev->next=new;
00043:
}
对于辗转调用的函数,为帮助读者随时了解其来龙去脉,本书通常在函数的代码前面用方括号和大
括号列出其调用路径。
这种路径通常以一个比较重要或常用的函数为起点,例如这里就是以list_add()为起点。
不过,读者要注意,对同一个函数的不同调用路径往往有很多,我们列出的只是具体的情景或讨
论中的路径。
例如,有些函数也许跳过list_add()而直接调用__list_add(),而形成另一条不同的路径。
至于__list_add()本身的代码,我们就把它留给读者了。
再来看从队列中脱链的操作list_del():
00090:
static__inline__voidlist_del(structlist_head
*entry)
00091:
{
00092:
__list_del(entry->prev,entry->next);
00093:
}
同样,这里也是调用另外一个inline函数__list_del()来完成操作:
[list_del()>__list_del()]
00078:
static__inline__void__list_del(structlist_head
*prev,
00079:
structlist_head*next)Linux内核源代码情景分析
2006-12-31版权所有,XX第24页,共1482页
00080:
{
00081:
next->prev=prev;
00082:
prev->next=next;
00083:
}
注意在__list_del()中的操作对象是队列中在entry之前和之后的两个list_head结构。
如果entry是队列中的最后一项,则二者相同,就是队列的头,那也是一个list_head结构,不过不在任何宿主结构内部。
读者也许已经等不及要问了:
队列操作都是通过list_head进行的,但是那不过是个连接件,如果我
们手上有个宿主结构,那当然就知道了它的某个list_head在哪里,从而以此为参数调用list_add()或
list_del();可是,反过来,当我们顺着一个队列取得其中一项的list_head结构时,又怎样找到其宿主结
构呢,在list_head结构中并没有指向宿主结构的指针啊。
毕竟,
我们真正关心的是宿主结构,而不是连
接件。
是的,这是个问题。
我们还是通过一个实例来看这个问题是怎样解决
的。
下面是取自mm/page_alloc.c
中的一行代码:
[rmqueue()]
00188:
page=memlist_entry(curr,structpage,list);
这里的memlist_entry()将一个list_head指针curr换算成其宿
主结构的起始地址,也就是取得指向其
宿主page结构的指针。
读者可能会对memlist_entry的实现和调
用感到困惑。
因为其调用参数page是个
类型,而不是具体的数据。
如果看一下函数rmqueue()整个代码,还
可以发现在那里list竟是无定义的。
事实上,在同一个文件中将memlist_entry定义成list_entry,所
以实际引用的是list_entry():
00048:
#definememlist_entrylist_entry
而list_entry的定义则在include/linux/lish.h中:
00135:
/*
00136:
*list_entry–getthestructforthisentry
00137:
*@ptr:
the&structlist_headpointer.
00138:
*@type:
thetypeofthestructthisisembedded
in.
00139:
*@member:
thenameofthelist_structwithinthe
struct.
00140:
*/
00141:
#definelist_entry(ptr,type,member)\
00142:
((type*)((char*)(ptr)-(unsignedlong)(&((type
*)0)->member)))
将前面的188行与此对照,就可以看出其中的奥秘:
经过C预处理的文字替换,这一行的内容就成
为:
page=
((structpage*)((char*)(curr)-(unsignedlong)(&((struct
page*)0)->list)));
这里的curr是一个page结构内部的成分list的地址,而我们所需要的却是那个page结构本身的地址,所以要从地址curr中减去一个位移量,即成分list在page内部的位移量,才能达到要求。
那么,这位移量到底是多少呢,&((structpage*)0)->list就表示当结构
page正好在地址0上时其成分list的地址,
这就是位移。
同样的道理,如果是在page结构的lru队列里,则传下来的member为lru,一样能算出宿主结构的地址。
Linux内核源代码情景分析
2006-12-31版权所有,XX第25页,共1482页
可见,这一套操作既普遍适用,又保持了较高的效率。
但是,对于阅读代码的人却有个缺点,那就
是光从代码中不容易看出一个list_head的宿主结构是什么,而以前只要看一下指针next的类型就知道了