重点linux源码分析.docx

上传人:b****6 文档编号:8110251 上传时间:2023-05-12 格式:DOCX 页数:9 大小:20.92KB
下载 相关 举报
重点linux源码分析.docx_第1页
第1页 / 共9页
重点linux源码分析.docx_第2页
第2页 / 共9页
重点linux源码分析.docx_第3页
第3页 / 共9页
重点linux源码分析.docx_第4页
第4页 / 共9页
重点linux源码分析.docx_第5页
第5页 / 共9页
重点linux源码分析.docx_第6页
第6页 / 共9页
重点linux源码分析.docx_第7页
第7页 / 共9页
重点linux源码分析.docx_第8页
第8页 / 共9页
重点linux源码分析.docx_第9页
第9页 / 共9页
亲,该文档总共9页,全部预览完了,如果喜欢就下载吧!
下载资源
资源描述

重点linux源码分析.docx

《重点linux源码分析.docx》由会员分享,可在线阅读,更多相关《重点linux源码分析.docx(9页珍藏版)》请在冰点文库上搜索。

重点linux源码分析.docx

重点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的类型就知道了

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 求职职场 > 简历

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2