Android底层开发技术实战详解
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.5 分析Linux内核源代码很有必要

长期以来,学习内核的最好方法就是分析内核代码。内核代码本身就是最好的参考资料,其他任何经典或非经典的书籍都只是起辅助作用,不能、也不应该取代内核代码在我们学习过程中的主导地位。学习内核是一项浩大的工程,在学习之前需要首先做到以下三个方面。

(1)熟练使用Linux操作系统

Linux操作系统是Linux内核在用户层面的具体体现,只有熟练掌握Linux的基本操作,才能在内核学习的过程中达到事半功倍的效果。

(2)掌握操作系统理论基础

要掌握操作系统中比较基础的理论,比如分时(time-shared)和实时(real-time)的区别、进程的概念、CPU和系统总线、内存的关系等。

(3)掌握C语言基础

不需要很精通C语言,但必须能够理解链表、散列表等数据结构的C语言实现,并熟练运用GCC编译器。总之对C语言越熟悉,对我们的内核学习越有帮助。

1.5.1 源代码目录结构

下载Linux内核源代码的官方地址为http://www.kernel.org/,如图1-4所示。

图1-4 Linux内核官方下载界面

当下载内核代码后,很有必要知道内核源代码的整体分布情况。通常其内核代码保存在“/usr/src/linux”目录下,该目录下的每一个子目录都代表了一个特定的内核功能性子集,接下来将针对2.6.23版本进行简单描述。

(1)目录“Documentation”

此目录下面没有内核代码,只有很多质量参差不齐的文档,但往往能够为我们提供很多帮助。

(2)目录“arch”

所有与体系结构相关的代码都在此目录及“include/asm-*/”目录中,Linux支持的每种体系结构在arch目录下都有对应的子目录,而在每个体系结构特有的子目录下又至少包含如下三个子目录。

  • kernel:存放支持体系结构特有的诸如信号量处理和SMP之类特征的实现。
  • lib:存放体系结构特有的对诸如strlen和memcpy之类的通用函数的实现。
  • mm:存放体系结构特有的内存管理程序的实现。

除了上述这三个子目录之外,大多数体系结构在必要的情况下还有一个boot子目录,其中包含了硬件平台上启动内核所使用的部分或全部平台特有代码。另外在大部分体系结构所特有的子目录中,还应该根据需要包含供附加特性使用的其他子目录。例如在i386目录中包含一个“math-emu”子目录,在里面包含了在缺少数学协处理器(FPU)的CPU上运行模拟FPU的代码。

(3)目录“drivers”

此目录是内核中最庞大的目录,可以在其中找到显卡、网卡、SCSI适配器、PCI总线、USB总线和其他任何Linux支持的外围设备或总线的驱动程序。

(4)目录“fs”

在此目录中保存了虚拟文件系统(VFS,Virtual File System)的代码,还有各个不同文件系统的代码。Linux支持的所有文件系统在fs目录下面都有一个对应的子目录。例如ext2文件系统对应的是fs/ext2目录。

文件系统是存储设备和需要访问存储设备的进程之间的媒介。存储设备可能是本地的物理上可访问的,比如硬盘或CD-ROM驱动器,它们分别使用ext2/ext3和isofs文件系统。也可能是通过网络访问的,使用NFS文件系统。

还有一些虚拟文件系统,例如proc是以一个标准文件系统出现的,然而它其中的文件只存在于内存中,并不占用磁盘空间。

(5)目录“include”

在此目录中包含了内核中的大部分头文件,它们按照“include/asm-*/”的子目录格式进行分组。这种格式的子目录有多个,每一个都对应着一个arch的子目录,例如“include/asm-alpha”、“include/asm-arm”和“include/asm-i386”等。在每个子目录下的文件中,都定义了支持给定体系结构所必需的预处理器宏和内联函数,这些内联函数多数都是全部或部分使用汇编语言实现的。

在编译内核时,系统会建立一个从“include/asm”目录到目标体系结构特有的目录的符号链接。比如对于arm平台,就是“include/asm-arm”到“include/asm”的符号链接。因此,体系结构无关部分的内核代码可以使用如下形式包含体系相关部分的头文件。

#include<asm/some-file>

(6)目录“init”

在此目录中保存了内核的初始化代码,包括文件main.c、创建早期用户空间的代码,以及其他初始化代码。

(7)目录“ipc”

IPC即进程间通信(Inter-Process Communication),在此目录中包含了共享内存、信号量以及其他形式IPC的代码。

(8)目录“kernel”

此目录是内核中最核心的部分,包括进程的调度(kernel/sched.c),以及进程的创建和撤销(kernel/fork.c和kernel/exit.c)等,和平台相关的另外一部分核心的代码在arch/*/kernel目录中。

(9)目录“lib”

此目录中保存了库代码,这些代码实现了一个标准C库的通用子集,包括字符串和内存操作的函数(strlen、mmcpy和其他类似的函数),以及有关sprintf和atoi的系列函数。与arch/lib下的代码不同,这里的库代码都是使用C语言编写的,在内核新的移植版本中可以直接使用。

(10)目录“mm”

在此目录中包含了体系结构无关部分的内存管理代码,体系相关的部分位于arch/*/mm目录下。

(11)目录“net”

在此目录中保存了和网络相关的代码,实现了各种常见的网络协议,如TCP/IP、IPX等。

(12)目录“scripts”

在该目录中没有内核代码,只包含了用来配置内核的脚本文件。当运行make menuconfig或者make xconfig之类的命令配置内核时,用户就是和位于这个目录下的脚本进行交互的。

(13)目录“block”

在此目录中保存了block层的实现代码。最初block层的代码一部分位于drivers目录,一部分位于fs目录,从2.6.15开始,block层的核心代码被提取出来放在了顶层的block目录中。

(14)目录“crypto”

在此目录中保存了内核本身所用的加密API信息,实现了常用的加密和散列算法,还有一些压缩和CRC校验算法。

(15)目录“security”

在此目录中包括不同的Linux安全模型的代码,比如NSA Security-Enhanced Linux。

(16)目录“sound”

在此目录中保存了声卡驱动及其他声音相关的代码。

(17)目录“usr”

此目录实现了用于打包和压缩的cpio等。

1.5.2 浏览源代码的工具

俗话说“工欲善其事,必先利其器”。一个功能强大并方便的代码浏览工具,会有助于我们学习内核代码这项“工程”。接下来将简单介绍浏览Linux内核源代码的常用工具。

1.Source Insight

在Windows下,最为方便快捷的代码浏览工具是Source Insight,这是一款商业软件。Source Insight是一个面向项目开发的程序编辑器和代码浏览器,它拥有内置的对C/C++、C#和Java等程序的分析。Source Insight可以分析源代码并在工作的同时动态维护它自己的符号数据库,并自动为我们显示有用的上下文信息。Source Insight不仅仅是一个强大的程序编辑器,还能显示reference trees、class inheritance diagrams和call trees等信息。Source Insight提供了最快速的对源代码的导航和任何程序编辑器的源信息。Source Insight提供了快速和革新的访问源代码和源信息的能力。与众多其他编辑器产品不同,Source Insight能在你编辑的同时分析源代码,提供实用的信息并立即进行分析。

安装Source Insight后,需要先打开Source Insight并创建一个工程,然后将内核代码加入到该工程中,并进行文件同步,这样就可以在代码之间进行关联阅读了。

Source Insight的缺点是并没有对应Linux的版本。因此对于很多Linux初学者来说,在一个完全的Linux环境下进行学习,需要寻找一个可以取代Source Insight的代码浏览工具。

2.Vim+Cscope

Linux环境下的最佳浏览工具是Vim,各种Linux发行版都会默认进行安装。虽然Vim默认的编辑界面很普通,甚至丑陋,但是可以通过配置文件“.vimrc”添加不同的界面效果。同时还可以配合TagList、WinManager等很多好用的插件或工具,将Vim打造成一个不次于Source Insight的代码浏览编辑工具。

3.LXR

LXR(Linux Cross Reference)也是一款比较流行的Linux内核源代码浏览工具,其下载地址为http://lxr.linux.no/。

如果我们的目的只是浏览Linux内核代码,那么并不需要去安装LXR。因为在网站http://lxr.linux.no/上已经提供了几乎所有版本的Linux内核在线阅读代码,我们只需要登录该网站,选择某一特定的内核版本后,就可以在内核代码之间进行关联阅读。

当登录网站并选择内核版本后,在查找框内输入要查找的内核代码符号名称,就可以搜索到所有以超链接形式给出的对该符号定义和引用的确切位置。

1.5.3 为什么用汇编语言编写内核代码

很多读者可能禁不住要问,Java、C++和C#功能强大,Visual Basic易于使用,为什么还要使用古老的汇编语言来编写内核代码呢?这是因为出于以下三个方面的考虑。

  • Linux内核中的底层代码直接和硬件打交道,需要一些专用的指令,而这些指令在C语言中并无对应的语言成分。
  • 内核中实现某些操作的过程、代码段或函数,在运行时会被很频繁地调用,这时用汇编语言编写,其时间效率会有大幅度提高。
  • 在某些特别的场合,一段代码的空间效率也很重要,比如操作系统的引导程序一定要容纳在磁盘的第一个扇区中,多一个字节都不行。这时只能用汇编语言编写。

在Linux内核代码中,以汇编语言编写的代码有如下两种不同的形式。

  • 完全的汇编代码,这样的代码采用“.s”作为文件名的后缀。
  • 嵌入在C代码中的汇编语言片段。

对于新接触Linux内核源代码的读者,即使比较熟悉i386汇编语言,在理解内核中的汇编代码时都会感到困难。原因是在内核的汇编代码中,采用的是不同于常用Intel i386汇编语言的AT&T格式的汇编语言,而在嵌入C代码的汇编语言片段中,更是增加了一些指导汇编工具如何分配使用寄存器、如何与C代码中定义的变量相结合的语言成分。这些成分使得嵌入C代码的汇编语言片段实际上变成了一种介于汇编和C之间的中间语言。

1.5.4 Linux内核的显著特性

在下载Linux内核代码并安装浏览工具后,就可以浏览并分析Linux内核的源代码了。接下来将简要讲解Linux内核的显著特性,为读者学习本书后面的知识打下基础。

1.GCC特性

Linux内核使用GNU Compiler Collection(GCC)套件的几个特殊功能。这些功能包括提供快捷方式和简化以及向编译器提供优化提示等。GCC和Linux是出色的组合。尽管它们是独立的软件,但是Linux完全依靠GCC在新的体系结构上运行。Linux还利用GCC中的特性(称为扩展)实现更多功能和优化。

(1)基本功能

概括来说,GCC具有如下两大功能。

  • 功能性:扩展提供新功能。
  • 优化:扩展帮助生成更高效的代码。

GCC允许通过变量的引用识别类型,这种操作支持泛型编程。在C++、Ada和Java语言等许多现代编程语言中都可以找到相似的功能。例如Linux使用typeof构建min和max等依赖于类型的操作。使用typeof构建一个泛型宏的代码如下所示。

#define min(x,y)({
    typeof(x)_min1=(x);
    typeof(y)_min2=(y);
    (void)(&_min1==&_min2);
    _min1<_min2?_min1:_min2;})

GCC还支持范围,在C语言的许多方面都可以使用范围。最常见的是在switch/case块中的case语句中使用。使用switch/case也可以通过使用跳转表实现进行编译器优化。在复杂的条件结构中,通常依靠嵌套的if语句实现与下面代码相同的结果,但是下面的代码更简洁。

static int sd_major(int major_idx){
    switch(major_idx){
    case 0:
        return SCSI_DISK0-MAJOR;
    case 1...7:
        return SCSI_DISK1-MAJOR + major_idx - 1;
    case 8...15:
        return SCSI_DISK8-MAJOR + major_idx - 8;
    default:
        BUG();
        return 0;/*shut up gcc*/
    }
}

(2)属性

GCC允许声明函数、变量和类型的特殊属性,以便指示编译器进行特定方面的优化和更仔细的代码检查。使用方式非常简单,只需在声明后面加上如下代码即可(代码中的__是两个英文下画线)。

attribute__((ATTRIBUTE))

其中ATTRIBUTE是属性的说明,多个说明之间以逗号分隔。GCC可以支持十几个属性,接下来将介绍一些比较常用的属性。

·属性noreturn

属性noreturn用在函数中,表示该函数从不返回。它能够让编译器生成较为优化的代码,消除不必要的警告信息。

·属性format(archetype,string-index,first-to-check)

属性format用在函数中,表示该函数使用printf、scanf、strftime或strfmon风格的参数,并可以让编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。

·archetype:指定是哪种风格。

·string-index:指定传入函数的第几个参数是格式化字符串。

·first-to-check:指定从函数的第几个参数开始按照上述规则进行检查。

例如下面的内核代码:

++++ include/linux/kernel.h
 static inline int printk(const char*s,...)__
     attribute__((format(printf,1,2)));

表示printk的第一个参数是格式化字符串,从第二个参数开始根据格式化字符串检查参数。

·属性unused

属性unused用于函数和变量,表示该函数或变量可能并不使用,这个属性能够避免编译器产生警告信息。

·属性aligned(ALIGNMENT)

属性aligned常用在变量、结构或联合中,用于设定一个指定大小的对齐格式,以字节为单位,例如下面的内核代码。

++++ drivers/usb/host/ohci.h
struct ohci_hcca {
#define NUM_INTS 32
   __hc32 int_table [NUM_INTS];/*periodic schedule*/
   /*
    *OHCI defines u16 frame_no,followed by u16 zero pad.
    *Since some processors can't do 16 bit bus accesses,
    *portable access must be a 32 bits wide.
    */
   __hc32 frame_no;/*current frame number*/
   __hc32 done_head;/*info returned for an interrupt*/
   u8 reserved_for_hc [116];
   u8 what [4];/*spec only identifies 252 bytes:)*/
}__attribute__((aligned(256)));

在上述代码中,表示结构体ohci_hcca的成员以256字节对齐。如果aligned后面不紧跟一个指定的数字值,那么编译器将依据目标机器情况使用最大、最有益的对齐方式。

需要注意的是,属性attribute的效果与我们的连接器也有关,如果连接器最大只支持16字节对齐,那么此时定义32字节对齐也是无济于事的。

·属性packed

属性packed用在变量和类型中,当用于变量或结构体成员时表示使用最小可能的对齐,当用于枚举、结构体或联合类型时表示该类型使用最小的内存。属性packed多用于定义硬件相关的结构时,使元素之间不会因对齐产生问题。例如下面的内核代码。

++++ include/asm-i386/desc.h
 struct usb_interface_descriptor {
   __u8 bLength;
    __u8 bDescriptorType;
      u8 bInterfaceNumber;
      u8 bAlternateSetting;
      u8 bNumEndpoints;
    __u8 bInterfaceClass;
    __u8 bInterfaceSubClass;
    __u8 bInterfaceProtocol;
      u8 iInterface;
 }__attribute__((packed));

在上述代码中,__attribute__((packed))告诉编译器usb_interface_descriptor的元素为1字节对齐,不要再添加填充位。因为定义此结构的代码和usb spec中的是完全一样的。如果不给编译器这个暗示,则编译器会依据平台的类型在结构的每个元素之间添加一定的填充位,使用这个结构时就不能达到预期的结果。

(3)内建函数

在GCC中提供了大量的内建函数,其中有很多是标准C库函数的内建版本,例如memcpy(),它们的功能与对应的C库函数的功能相同,在此不再进行讲解。

在内建函数中,还有很多函数的名字是以__builtin开始的。接下来将对__builtin_expect()进行详细分析,其他__builtin_xxx()函数的原理和此函数类似,本书中不再一一介绍。

函数__builtin_expect()的格式如下所示。

__builtin_expect(long exp,long c)

为什么Linux会推出__builtin_xxx()函数呢?这是因为大部分代码在分支预测方面做得比较糟糕,所以GCC提供了此内建函数来帮助处理分支预测并优化程序。

  • 第一个参数exp:是一个整型的表达式,返回值也是此exp。
  • 第二个参数c:其值必须是一个编译期的常量。

由此可见,此内建函数的意思就是exp的预期值为c,编译器可以根据这个信息适当地重排条件语句块的顺序,将符合这个条件的分支放在合适的地方。

我们看此函数在Linux内核中的应用,具体代码如下所示。

++++ include/linux/compiler.h
#define likely(x)__builtin_expect(!!(x),1)
#define unlikely(x)__builtin_expect(!!(x),0)

代码unlikely(x)用于告诉编译器条件x发生的可能性不大,likely(x)用于告诉编译器条件x发生的可能性很大。它们一般用在条件语句里,if语句不变,当if条件为1的可能性非常小的时候,可以在条件表达式外面包装一个unlikely(),那么这个条件块里语句的目标码可能就会被放在一个比较远的位置,以保证经常执行的目标码更紧凑。如果可能性非常大,则使用likely()包装。

2.链表的重要性

链表和本书中讲解的驱动密切相关,例如USB驱动。鉴于链表在内核中的特殊地位,有必要在此对其做一番陈述。内核中链表的实现位于“include/linux/list.h”文件中,链表数据结构的定义也很简单。具体代码如下所示。

struct list_head {
    struct list_head*next,*prev;
};

结构list_head包含两个指向list_head结构的指针prev和next,由此可见,内核中的链表实际上都是双链表。学过C语言的读者应该知道,链表的定义结构如下所示。

struct list_node {
   struct list_node*next;
   ElemType data;
};

以上述格式使用链表时,对于每一种数据类型都要定义它们各自的链表结构。而内核中的链表却与此不同,它并没有数据域,不是在链表结构中包含数据,而是在描述数据类型的结构中包含链表。

如果在hub驱动中使用struct usb_hub来描述hub设备,hub需要处理一系列的事件,比如当探测到一个设备连进来时,就会执行一些代码去初始化该设备,所以hub就创建了一个链表来处理各种事件,这个链表的结构如图1-5所示。

图1-5 hub链表的结构

在Linux代码中完整展示了链表的操作过程,接下来将简单剖析对应的Linux代码。

(1)声明与初始化

可以使用如下两种方式来声明链表。

  • 使用LIST_HEAD宏在编译时静态初始化。
  • 使用INIT_LIST_HEAD()在运行时进行初始化。

对应的Linux代码如下所示。

#define LIST_HEAD_INIT(name){ &(name),&(name)}
#define LIST_HEAD(name)\
     struct list_head name=LIST_HEAD_INIT(name)
static inline void INIT_LIST_HEAD(struct list_head*list)
{
     list->next=list;
     list->prev=list;
}

无论采用哪种方式,新生成的链表头的两个指针next、prev都初始化为指向自己。

(2)判断链表是否为空

对应的Linux代码如下所示。

static inline int list_empty(const struct list_head*head)
{
    return head->next==head;
}

(3)插入操作

建立链表后,就不可避免地要对其进行操作,例如向其中添加数据。使用函数list_add()和list_add_tail()可以完成添加数据的工作。对应的Linux代码如下所示。

static inline void list_add(struct list_head*new,struct list_head*head)
{
     __list_add(new,head,head->next);
}
static inline void list_add_tail(struct list_head*new,struct list_head*head)
{
     __list_add(new,head->prev,head);
}

其中,函数list_add()将数据插入head之后,函数list_add_tail()将数据插入head->prev之后。对于循环链表来说,因为表头的next、prev分别指向链表中的第一个和最后一个节点,所以函数list_add()和list_add_tail()的区别并不大。

(4)删除操作

可以使用函数list_replace_init()从链表中删除一个元素,并将其初始化。对应的Linux代码如下所示。

static inline void list_replace_init(struct list_head*old,struct list_head*new)
{
    list_replace(old,new);
    INIT_LIST_HEAD(old);
}

(5)遍历操作

在内核中的链表仅保存了list_head结构的地址,我们应该如何通过它获取一个链表节点真正的数据项呢?此时需要使用list_entry宏,通过它可以很容易地获得一个链表节点的数据。对应的Linux代码如下所示。

#define list_entry(ptr,type,member)\container_of(ptr,type,member)

以hub驱动为例,当要处理hub的事件的时候,需要知道具体是哪个hub触发了这起事件。而list_entry的作用是从struct list_head event_list中得到它所对应的struct usb_hub结构体变量。例如下面的代码。

struct list_head*tmp;
struct usb_hub*hub;
tmp=hub_event_list.next;
hub=list_entry(tmp,struct usb_hub,event_list);

通过上述代码,从全局链表hub_event_list中取出了结构体变量tmp,然后通过tmp获得它所对应的struct usb_hub。

3.Kconfig和Makefile

Kconfig和Makefile是浏览内核代码时最为倚仗的两个文件之一。几乎Linux内核中的每一个目录下边都有一个Kconfig文件和一个Makefile文件。通过Kconfig和Makefile,我们可以了解一个内核目录下面的结构。在研究内核的某个子系统、某个驱动或其他某个部分之前,需要仔细阅读目录下对应的Kconfig和Makefile文件。

(1)Kconfig结构

每种平台对应的目录下面都有一个Kconfig文件,例如“arch/i386/Kconfig”。Kconfig文件通过source语句可以构建一个Kconfig树。文件“arch/i386/Kconfig”的代码片段如下所示。

mainmenu "Linux Kernel Configuration"
config X86-32
   bool
   default y
   help
     This is Linux's home port.Linux was
originally native to the Intel
     386,and runs on all the later x86
processors including the Intel
     486,586,Pentiums,and various
instruction-set-compatible chips by
     AMD,Cyrix,and others.
……
source "init/Kconfig"
menu "Processor type and features"
source "kernel/time/Kconfig"
……
config KTIME_SCALAR
   bool
   default y

Kconfig的详细语法规则可以参看内核文档Documentation/kbuild/kconfig-language.txt,对其有简单介绍。

①菜单项

关键字config可以定义一个新的菜单项,比如下面的代码。

config MODVERSIONS
   bool "Set version information on all module symbols"
   depends on MODULES
   help
     Usually,modules have to be recompiled whenever you switch to a new
     kernel……

后面的代码定义了该菜单项的属性,包括类型、依赖关系、选择提示、帮助信息和默认值等。常用的类型有bool、tristate、string、hex和int。类型为bool的只能被选中或不选中,类型tristate的菜单项多了编译成内核模块的选项。

依赖关系是通过“depends on”或“requires”定义的,指出此菜单项是否依赖于另外一个菜单项。

帮助信息需要使用“help”或“---help---”来指出。

②菜单组织结构

菜单选项通过如下两种方式来组成树状结构。

·第一种方式:使用关键字“menu”显式声明为菜单,例如下面的代码。

menu "Bus options(PCI,PCMCIA,EISA,MCA,ISA)"
 config PCI
……
 endmenu

·第二种方式:也可以使用依赖关系确定菜单结构,例如下面的代码。

config MODULES
   bool "Enable loadable module support"
config MODVERSIONS
   bool "Set version information on all module symbols"
   depends on MODULES
comment "module support disabled"
   depends on !MODULES

因为菜单项MODVERSIONS依赖于MODULES,所以它就是一个子菜单项。这要求菜单项和它的子菜单项同步显示或不显示。

③关键字Kconfig

Kconfig文件描述了一系列的菜单选项,除帮助信息外,文件中的每一行都以一个关键字开始,主要有config、menuconfig、choice/endchoice、comments、menu/endmenu、if/endif、source等,只有前5个可以用在菜单项定义的开始,它们都可以结束一个菜单项。

(2)Makefile

Linux内核的Makefile分为如下5个组成部分。

  • Makefile:顶层的Makefile。
  • .config:内核的当前配置文档,编译时成为顶层Makefile的一部分。
  • arch/$(ARCH)/Makefile:和体系结构相关的Makefile。
  • Makefile.*:一些特定Makefile的规则。
  • kbuild级别Makefile:各级目录下的大约500个文档,编译时根据上层Makefile传下来的宏定义和其他编译规则,将源代码编译成模块或编入内核。顶层的Makefile文档读取.config文档的内容,并总体上负责build内核和模块。Arch Makefile则提供补充体系结构相关的信息。其中.config的内容是在运行make menuconfig的时候,通过Kconfig文档配置的结果。

假如想把自己写的一个Flash驱动程序加载到工程中,并且能够在通过menuconfig配置内核时选择该驱动,此时该怎么办呢?方法是借助Makefile实现,解决流程如下所示。

  • 将编写的flashtest.c文档添加到“/driver/mtd/maps/”目录下。
  • 修改“/driver/mtd/maps”目录下的kconfig文档,修改代码如下所示。
config MTD_flashtest
tristate "ap71 flash"

这样当运行make menuconfig时会出现“ap71 flash”选项。

·修改该目录下makefile文档,添加下面的代码内容。

obj-$(CONFIG_MTD_flashtest)+=flashtest.o

此时当运行make menucofnig时会发现“ap71 flash”选项,假如选择了此选项,该选择就会保存在“.config”文档中。当编译内核时会读取“.config”文档,当发现“ap71 flash”选项为yes时,系统在调用“/driver/mtd/maps/”下的makefile时,会把flashtest.o加入内核中。

1.5.5 学习Linux内核的方法

学习Linux内核的最大工作就是对内核代码进行分析,如果抱着走马观花、得过且过的态度,最终结果很可能是没有多大的收获。学习内核应该遵循科学、严谨的态度,要做到真正理解每一段代码的实现,并且在学习过程中多问、多想、多记。

上述学习Linux内核的方法非常重要。接下来将通过两个具体的应用来演示学习Linux内核的过程。

1.分析USB子系统的代码

Linux内核中USB子系统的代码位于“drivers/usb”目录下,进入该目录,执行命令ls后将会显示如下结果。

atm class core gadget host image misc mon serial storage Kconfig
Makefile README usb-skeleton.c

目录“drivers/usb”一共包含10个子目录和4个文件,为了理解每个子目录的作用,有必要首先阅读README文件。根据README文件的描述,得知“drivers/usb”目录下各个子目录的作用,具体说明如下所示。

(1)core

这是内核开发者针对部分核心的功能特意编写的代码,用于为其他的设备驱动程序提供服务,比如申请内存,实现所有的设备都会需要的一些公共函数,并命名为USB core。

(2)host

早期的内核结构并不像现在这般富有层次感,几乎所有的文件都直接堆砌在“drivers/usb/”目录下,这其中包括“usb core”和其他各种设备驱动程序的代码。

后来在“drivers/usb/”目录下面单独列出了“core”子目录,用于存放一些比较核心的代码,比如整个USB子系统的初始化、root hub的初始化、host controller的初始化代码。

后来随着技术的发展,出现了多种USB host controller,于是内核开发者把host controller有关的公共代码保留在“core”目录下,而其他各种host controller对应的特定代码则移到“host”目录下,让相应的负责人去维护。为此针对host controller单独创建了子目录“host”,用于存放与其相关的代码。

(3)gadget

用于存放USB gadget的驱动程序,控制外围设备如何作为一个USB设备和主机通信。比如,嵌入式开发板通常会支持SD卡,使用USB连接线将开发板连接到PC时,通过USB gadget架构的驱动,可以将该SD卡模拟成U盘。

除core、host和gadget之外,其他几个目录分门别类存放了各种USB设备的驱动,例如U盘的驱动位于“storage”子目录,触摸屏和USB键盘鼠标的驱动位于“input”子目录。

因为我们的目的是研究内核对USB子系统的实现,而不是特定设备或host controller的驱动,所以通过对README文件的分析,应该进一步关注“core”子目录。

2.分析USB系统的初始化代码

分析Kconfig和Makefile文件可以帮助我们在庞大复杂的内核代码中定位及缩小目标代码的范围。为了研究内核对USB子系统的实现,需要在目标代码中找到USB子系统的初始化代码。

Linux内核针对某个子系统或某个驱动,使用subsys_initcall或module_init宏来指定初始化函数。在内核文件“drivers/usb/core/usb.c”中,可以发现下面的代码。

subsys_initcall(usb_init);
module_exit(usb_exit);

在上述代码中,可以将subsys_initcall理解为module_init,只不过因为该部分代码比较核心,开发者们把它看做一个子系统,而不仅仅是一个模块。在Linux中,类似此类别的设备驱动被归结为一个子系统,例如PCI子系统和SCSI子系统。通常“drivers/”目录下第一层的每个子目录代表一个子系统,因为它们分别代表了一类设备。

subsys_initcall(usb_init)表示函数usb_init()是USB子系统的初始化函数,而module_exit则表示usb_exit函数是USB子系统结束时的清理函数。为了研究USB子系统在内核中的实现,需要从函数usb_init()开始分析,对应的内核代码如下所示。

static int__init usb_init(void)
{
       int retval;
       if(nousb){
          pr_info("%s:USB support disabled
          ",usbcore_name);
          return 0;
       }
       retval=ksuspend_usb_init();
       if(retval)
          goto out;
       retval=bus_register(&usb_bus_type);
       if(retval)
          goto bus_register_failed;
   retval=usb_host_init();
       if(retval)
          goto host_init_failed;
       retval=usb_major_init();
       if(retval)
          goto major_init_failed;
   retval=usb_register(&usbfs_driver);
       if(retval)
          goto driver_register_failed;
       retval=usb_devio_init();
       if(retval)
          goto usb_devio_init_failed;
   retval=usbfs_init();
       if(retval)
          goto fs_init_failed;
       retval=usb_hub_init();
   if(retval)
          goto hub_init_failed;
   retval=usb_register_device_driver(&usb_generic_driver,THIS_MODULE);
       if(!retval)
          goto out;
       usb_hub_cleanup();
   hub_init_failed:
       usbfs_cleanup();
fs_init_failed:
       usb_devio_cleanup();
usb_devio_init_failed:
       usb_deregister(&usbfs_driver);
driver_register_failed:
       usb_major_cleanup();
major_init_failed:
   usb_host_cleanup();
host_init_failed:
       bus_unregister(&usb_bus_type);
bus_register_failed:
       ksuspend_usb_cleanup();
out:
       return retval;
}

接下来分析上述代码。

(1)标记__init

关于usb_init,第一个问题是上述第一行代码中的__init标记有什么意义?在前面讲解GCC扩展的特殊属性section时曾经提到,__init修饰的所有代码都会被放在.init.text节,当初始化结束后就可以释放这部分内存。但是内核是如何调用到__init所修饰的这些初始化函数的呢?要回答这个问题,需要用到subsys_initcall宏的知识,它在文件“include/linux/init.h”中的定义格式如下所示。

#define subsys_initcall(fn)__define_initcall("4",fn,4)

此时出现了一个新的宏__define_initcall,它用来将指定的函数指针fn存放到“.initcall.init”节。subsys_initcall宏,则表示把fn存放到“.initcall.init”的子节“.initcall4.init”。

要理解“.initcall.init”、“.init.text”和“.initcall4.init”之类的符号,还需要了解和内核可执行文件相关的概念。内核可执行文件由许多链接在一起的对象文件组成。对象文件有许多节,如文本、数据、init数据、bass等。这些对象文件都是由一个称为“链接器脚本”的文件链接并装入的,这个文件的功能是将输入对象文件的各节映射到输出文件中。换句话说,它将所有输入对象文件都链接到单一的可执行文件中,将该可执行文件的各节装入指定地址处。vmlinux.lds是保存在“arch/<target>/”目录中的内核链接器脚本,它负责链接内核的各个节并将它们装入内存中特定偏移量处。

打开文件“arch/i386/kernel/vmlinux.lds”,搜索关键字“initcall.init”后便会看到如下结果。

  inicall_start=.;
.initcall.init:AT(ADDR(.initcall.init)- 0xC0000000){
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
}
__initcall_end=.;

其中“__initcall_start”指向“.initcall.init”节的开始,“__initcall_end”指向“.initcall.init”节的结尾。而“.initcall.init”节又被分为如下7个子节。

.initcall1.init
.initcall2.init
.initcall3.init
.initcall4.init
.initcall5.init
.initcall6.init
.initcall7.init

宏subsys_initcall将指定的函数指针放在了“.initcall4.init”子节,至于其他宏,功能也类似,例如core_initcall将函数指针放在了“.initcall1.init”子节,device_initcall将函数指针放在了“.initcall6.init”子节等。

各个子节的顺序是确定的,即先调用“.initcall1.init”中的函数指针,然后调用“.initcall2.init”中的函数指针。__init修饰的初始化函数在内核初始化过程中调用的顺序和.initcall.init节里函数指针的顺序有关,不同的初始化函数被放在不同的子节中,因此就决定了它们的调用顺序。

(2)模块参数

在前面usb_init()函数代码中,代码nousb在文件“drivers/usb/core/usb.c”中定义为如下格式。

static int nousb;/*Disable USB when built into kernel image*/
module_param_named(autosuspend,usb_autosuspend_delay,int,0644);
MODULE_PARM_DESC(autosuspend,"default autosuspend delay");

从中可知nousb是个模块参数,用于在内核启动的时候禁止USB子系统。关于模块参数,可以在加载模块的时候指定,但是如何在内核启动的时候指定?打开系统的grub文件,找到kernel行,例如下面的代码。

kernel/boot/vmlinuz-2.6.18-kdb root=/dev/sda1 ro splash=silent vga=0x314

其中的root、splash、vga等都表示内核参数。当某一模块被编译进内核的时候,它的模块参数便需要在kernel行中指定,其格式为:

模块 名.参数=值

例如下面的代码。

modprobe usbcore autosuspend=2

对应到kernel行的代码如下所示:

usbcore.autosuspend=2

通过命令“modinfo -p ${modulename}”可以得知一个模块有哪些参数可以使用。而对于已经加载到内核里的模块,其模块参数会列举在“/sys/module/$ {modulename}/parameters/”目录下面,可以使用如下命令去修改。

echo -n ${value}>/sys/module/$ {modulename}/parameters/${parm}

关于函数usb_init(),除了上面介绍的代码外,余下的代码分别完成usb各部分的初始化,其他代码的具体分析工作可以参阅下载的Linux内核代码,具体含义可以参阅相关的书籍和资料。因为篇幅限制,在此不再进行详细介绍。