posts - 8, comments - 7, trackbacks - 0, articles - 64

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

只有注册用户登录后才能发表评论。