Refer to <<linux
内核源代码情景分析
>> and <<Linux kernel Version:2.4.0>>
Having any problems, send mails to viloner@163.com
Linux
内核源代码中的
C
语言代码
Linux
内核的主体是以
GNU
的
C
语言编写的,
GNU
为此提供了编译工具
gcc
。
一、
inline
函数大量的使用:
Gcc
从
C++
语言中吸收了“
inline
”和“
const
”。其实,
GNU
的
C
和
C++
是合为一体的,
gcc
既是
C
编译又是
C++
编译,所以从
C++
中吸收一些东西到
C
中是很自然的。从功能上说,
inline
函数的使用与
#define
宏定义相似,但更有相对的独立性,也更安全。使用
inline
函数也有利于程序调试。如果编译时不加优化,则这些
inline
函数就是普通的,独立的函数,更便于调试。调试好以后,再采用优化重新编译一次,这些
inline
函数就像宏操作一样融入了引用处的代码中,有利于提高运行效率。由于
inline
函数的大量使用,相当一部分代码从
.c
文件移入了
.h
文件中。
二、奇怪的宏操作定义:
Linux
内核代码中使用了大量的
inline
函数,但这并未消除对宏操作的使用,内核中仍有许多宏操作定义。并常对内核代码中一些宏操作定义方式感到迷惑不解,先看一个实例,取自
fs/proc/kcore.c:
163#define DUMP_WRITE(add,nr) do {memcpy(bufp,addr,nr);buf +=nr;} while(0)
这个循环体只执行一次,为什么要这样通过一个
do-while
循环来定义呢?首先能不能定义成如下式样:
163#define DUMP_WRITE(add,nr) memcpy(bufp,addr,nr);buf +=nr;
不行。如果有一段程序在一个
if
语句中引用这个宏操作就会出问题:
if (add)
DUMP_WRITE(addr,nr);
Else
Do_something_else();
经过预处理以后,这段代码就会变成这样:
if (add)
memcpy(bufp,addr,nr);buf +=nr;
Else
Do_something_else();
编译这段代码
gcc
会失败,并报语法出错。因为
gcc
认为
if
语句在
memcpy()
以后就结束了,然后却又碰到了一个
else
。如果把
DUMP_WRITE(addr,nr)
和
Do_something_else()
换一下位置,编译倒是可以通过,但问题却更严重了,因为不管条件满足与否
bufp+=nr
都会得到执行。马上会想到要在定义中加上花括号,成为这样:
163#define DUMP_WRITE(add,nr) {memcpy(bufp,addr,nr);buf +=nr;}
可是,上面那段程序是通不过编译,因为经过预处理后就变成这样:
if (add)
{memcpy(bufp,addr,nr);buf +=nr;};
Else
Do_something_else();
同样,
gcc
在碰到
else
前面的“
;
”时就认为
if
语句已经结束了,因而后面的
else
不在
if
语句中。相比之下,采用
do-while
的定义在任何情况下都没有问题。
三、队列的使用:
内核中大量地使用着队列和队列操作。
如果我们有一种数据结构
foo
,并且需要维持一个这种数据结构的双链队列,最简单的、也是最常用的办法就是在这个数据结构的类型定义中加入两个指针,例如:
typedef struct foo
{
struct foo *prev;
struct foo *next;
……
}foo_t;
然后为这种数据结构写一套用于各种队列操作的子程序。由于用来维持队列的这两个指针的类型是固定的(都是指向
foo
数据结构),这些子程序不能用于其它数据结构的队列操作。换言之,需要维持多少种数据结构的队列,就得有多少套的队列操作子程序。对于使用队列较少的应用程序或许不是个大问题,但对于使用大量队列的内核就成问题了。所以,
Linux
内核中采用了一套能用的、一般的、可以用到各种不同数据结构的队列操作。为此,代码的作者们把指针
prev
和
next
从具体的“宿主”数据结构中抽象出来成为一种数据结构
list_head,
这种数据结构既可以“寄宿”在具体的宿主结构内部,成为该数据结构的一个“连接件”
;
也可以独立存在而成为一个队列的头。这个数据结构定义在
include/linux/list.h
中。
16
struct list_head {
17
struct list_head *next, *prev;
18
};
如果需要某种数据结构的队列,就在这种结构内部放上一个
list_head
数据结构。以用于内存页面管理的
page
数据结构为例,其定义为:(见
include/linux/mm.h
)
134
typedef struct page {
135
struct list_head list;
……
138
struct page *next_hash;
……
141
struct list_head lru;
……
148
}mem_map_t;
可见,在
page
数据结构中寄宿了两个
list_head
结构,或者说有两个队列操作的连接件,所以
page
结构可以同时存在于两个双链队列中。此外,结构中还有个单链指针
next_hash,
用来维持一个单链的杂凑队列。
对于宿主数据结构内部了每个
list_head
数据结构都要加以初始化,可以通过一个宏操作
INIT_LIST_HEAD
进行,要将一个
page
结构通过其“队列头”链入(有时候也说“挂入”)一个队列时,可以使用
list_add()
。从队列中脱链用
list_del()
。
但这里存在一个问题:队列操作都是通过
list_head
进行的,但那不过是个连接件,如果我们手上有个宿主结构,那当然就知道了它的某个
list_head
在那里,从而以此为参数调用
list_add()
或
list_del();
可是,反过来,当我们顺着一个队列取得其中一项
list_head
结构时,又怎样找到其宿主结构呢?在
list_head
结构中并没有指向宿主结构的指针呀。毕竟,我们真正关心的是宿主结构,而不是连接件。
下面通过一个实例来看这个问题是如何解决的。下面是取自
mm/page_alloc.c
中的一行代码:
[rmqueue()]
188
page = memlist_entry(curr,struct page,list);
这里的
memlist_entry()
将一个
list_head
指针
curr
换算成其宿主结构的起始地址,也就是取得指向其宿主结构的指针。那
memlist_entry()
是如何实现的呢?因为其调用参数
page
是个类型,而不是具体的数据。如果看一下函数
rmqueue()
的整个代码,就可以发现在那里
list
竟是无定义的。
事实上,在同一文件中将
memlist_entry
定义成
list_entry
,所以实际引用的是
list_entry():
48
#define memlist_entry list_entry
而
list_entry
的定义则在
include/linux/list.h
中:
135/**
136
* list_entry : get the struct for this entry
137
* @ptr: the &struct list_head pointer
138
* @type: the type of the struct this is embedded in
139
* @member: the name of the list_struct within the struct
140
*/
141
#define list_entry(ptr, type, member) \
142
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
将前面的
188
行与此对照,就可以看出其中的奥秘:经过
C
预处理的文字替换,这一行的内容就成为:
page=((struct page*) ((char )(curr)-(unsigned long)(&((struct page*)0)->list)));
这里的
curr
是一个
page
结构内部的成分
list
的地址,而我们所需要的却是那个
page
结构本身的地址,所以要从地址
curr
减去一个位移量,即成分
list
在
page
内部的位移量,才能达到要求。那么,这个位移量到底是多少呢?&
((struct page*)0)->list
就表示当结构
page
正好在地址
0
上时其成分
list
的地址,这就是位移。同样道理,如果是在
page
结构的
lru
队列里,则传下来的
member
为
lru
,一样能算出宿主结构的地址。
可见,这一套操作既普遍适用,又保持了较高效率。但是,对于阅读代码的人却是有个缺点,那就是光从代码中不容易看出一个
list_head
的宿主结构是什么,而以前只要看一下
next
的类型就知道了。