分类: C/C++
2008-03-09 22:01:43
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 的类型就知道了。