750 likes | 940 Views
第9章 模块的动态加载和系统配置. 第9章 模块的动态加载和系统配置. 本章介绍了 Linux 内核动态加载功能模块的工作原理。分析了 Linux 内核中的系统配置结构,解释了 Makefile 和配置文件的格式以及配置语句的含义。最后给出一个简单的例子,说明如何将自行开发的代码加入到 Linux 内核中。. 9.1 模块的动态加载. 操作系统通常由内核和一些系统服务程序(命令解释、库文件、链接和编译程序等)组成。内核是操作系统的灵魂,它为用户进程提供了一个虚拟机接口。用户进程可以并行运行、公平的占用系统资源而互不干扰。. 9.1 模块的动态加载.
E N D
第9章 模块的动态加载和系统配置 • 本章介绍了Linux内核动态加载功能模块的工作原理。分析了Linux 内核中的系统配置结构,解释了Makefile 和配置文件的格式以及配置语句的含义。最后给出一个简单的例子,说明如何将自行开发的代码加入到Linux 内核中。
9.1 模块的动态加载 • 操作系统通常由内核和一些系统服务程序(命令解释、库文件、链接和编译程序等)组成。内核是操作系统的灵魂,它为用户进程提供了一个虚拟机接口。用户进程可以并行运行、公平的占用系统资源而互不干扰。
9.1 模块的动态加载 • 操作系统通常由内核和一些系统服务程序(命令解释、库文件、链接和编译程序等)组成。内核是操作系统的灵魂,它为用户进程提供了一个虚拟机接口。用户进程可以并行运行、公平的占用系统资源而互不干扰。 • 从结构上来分,可将操作系统分为微内核结构和单块结构两类。Windows NT 和MINIX是典型的微内核操作系统,而Linux 则是单块结构的操作系统。微内核结构可方便地在系统中添加新的组件,而单块结构却不容易做到这一点。为此,Linux系统使用可动态加载和卸载的内核模块(Loadable Kernel Modules,LKMs),可方便地在内核中添加新的组件或卸载不再需要的内核组件。Linux使用insmod来显式加载内核模块,使用rmmod来卸载模块。同时内核自身也可以请求内核后台进程kerneld来加载与卸载模块。Linux模块大多数是设备驱动程序以及伪设备驱动程序模块, 如网络设备和文件系统等。
动态可加载代码的优点是可以让内核保持很小的尺寸并非常灵活。模块机制可以无需重构内核并频繁重新启动来尝试运行新内核代码。用户可以根据自己系统的需要构筑自己的私有内核。Linux源码的公开更是为改造其内核、重建有特殊要求的操作系统提供了可能。动态可加载代码的优点是可以让内核保持很小的尺寸并非常灵活。模块机制可以无需重构内核并频繁重新启动来尝试运行新内核代码。用户可以根据自己系统的需要构筑自己的私有内核。Linux源码的公开更是为改造其内核、重建有特殊要求的操作系统提供了可能。
模块必须能够找到其需要使用的内核资源。例如模块需要分配内存时,要调用内核的内存分配例程kmalloc()。但在构造模块时并不知道kmalloc()在内存中何处,这样内核必须在使用这些模块前修改模块中对kmalloc()的引用地址。内核在其内核符号表中维护着一个内核资源链表这样当加载模块时它能够解析出模块中对内核资源的引用。Linux还允许存在模块堆栈,它在模块之间相互调用时使用。模块必须能够找到其需要使用的内核资源。例如模块需要分配内存时,要调用内核的内存分配例程kmalloc()。但在构造模块时并不知道kmalloc()在内存中何处,这样内核必须在使用这些模块前修改模块中对kmalloc()的引用地址。内核在其内核符号表中维护着一个内核资源链表这样当加载模块时它能够解析出模块中对内核资源的引用。Linux还允许存在模块堆栈,它在模块之间相互调用时使用。 例如,因为VFAT(VirtuaI File Allocation Table)文件系统是从FAT(File Allocation Table)文件系统中扩展而来,VFAT文件系统模块可能需要FAT文件系统模块的服务。某个模块对其他模块的服务或资源的需求类似于模块对内核本身资源或服务的请求。不过此时所请求的服务是来自另外一个事先已加载的模块。当加载模块时,内核就把新近加载模块输出的所有资源和符号添加到内核符号表 (Kernel-Symbol-Table)中。
在/proc/ksyms.里面的每一个表项代表着一个公共的内核符号,这就是内核符号表。这些内核符号是可以被LKM引用的。LKM中所存取的每一个符号(像函数名)也会被列在这个文件里面。在该文件中可以看到LKM到底可以调用那些函数。在/proc/ksyms.里面的每一个表项代表着一个公共的内核符号,这就是内核符号表。这些内核符号是可以被LKM引用的。LKM中所存取的每一个符号(像函数名)也会被列在这个文件里面。在该文件中可以看到LKM到底可以调用那些函数。 当试图卸载某个模块时,内核需要知道此模块是否已经没有被使用,同时它需要有种方法来通知此将卸载模块。模块必须能够在从内核种删除之前释放其分配的所有系统资源,如内核内存或中断。当模块被卸载时,内核将从内核符号表中删除所有与之对应的符号。
但是,内核模块的引入也带来了如下问题: (1) 有可能同时带来与内核模块相关的性能与内存损失。可加载模块的代码一般较长,且额外的数据结构可能会占据一些内存,对内核资源的间接使用也可能带来效率方面的问题。对系统性能和内存利用有负面影响;
但是,内核模块的引入也带来了如下问题: (1) 有可能同时带来与内核模块相关的性能与内存损失。可加载模块的代码一般较长,且额外的数据结构可能会占据一些内存,对内核资源的间接使用也可能带来效率方面的问题。对系统性能和内存利用有负面影响; (2) 一旦Linux模块被加载,则和普通内核代码一样成为内核的一部分,具有与其他内核代码相同的权限与职责。因此,Linux内核模块也可以象所有内核代码和设备驱动一样使内核崩溃;
但是,内核模块的引入也带来了如下问题: (1) 有可能同时带来与内核模块相关的性能与内存损失。可加载模块的代码一般较长,且额外的数据结构可能会占据一些内存,对内核资源的间接使用也可能带来效率方面的问题。对系统性能和内存利用有负面影响; (2) 一旦Linux模块被加载,则和普通内核代码一样成为内核的一部分,具有与其他内核代码相同的权限与职责。因此,Linux内核模块也可以象所有内核代码和设备驱动一样使内核崩溃; (3) 为了内核模块访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改这些符号表;
但是,内核模块的引入也带来了如下问题: (1) 有可能同时带来与内核模块相关的性能与内存损失。可加载模块的代码一般较长,且额外的数据结构可能会占据一些内存,对内核资源的间接使用也可能带来效率方面的问题。对系统性能和内存利用有负面影响; (2) 一旦Linux模块被加载,则和普通内核代码一样成为内核的一部分,具有与其他内核代码相同的权限与职责。因此,Linux内核模块也可以象所有内核代码和设备驱动一样使内核崩溃; (3) 为了内核模块访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改这些符号表; (4) 有些模块要求利用其他模块的功能,因此,内核要维护模块之间的依赖性。 (5) 内核必须能够在卸载模块时通知模块,并且要释放分配给模块的内存和中断等资源; (6) 内核版本和模块版本的不兼容,也可能导致系统崩溃,因此,严格的版本检查是必需的。
9.1.1模块的加载 有两种方法可用来加载模块: • (1) 利用insmod命令手工将模块插入内核;
9.1.1模块的加载 有两种方法可用来加载模块: • (1) 利用insmod命令手工将模块插入内核; • (2) 由内核在必要时加载模块,称为“需求加载”。
当内核发现有必要加载某个模块时,如用户安装了内核中不存在的文件系统时,内核将请求内核后台进程(kerneld)准备加载适当的模块。这个内核后台进程仅仅是一个带有超级用户权限的普通用户进程。当系统启动时它也被启动并为内核打开了一个进程间通讯(IPC)通道。内核可以利用该通道向Kerneld进程发送任务的执行请求。Kerneld进程的主要功能是加载和卸载模块,另外,该进程也负责其他一些任务,例如打开和关闭PPP 链接等。kerneld自身并不执行这些任务,它通过某些程序如insmod来做此工作。因此,该进程实际是代表内核进行调度的代理。
执行insmod命令时,必须指定要加载模块的位置;对需求加载的内核模块,通常保存在/lib/modules/kernel-version。和系统的其他程序一样,内核模块实际是经连接的目标文件,但模块是可重定位的,也就是说,为了让装入的模块和已有的内核组件之间可以互相访问,模块不能连接为从特定地址执行的映像文件。模块可以是a.out或elf格式的目标文件。insmod利用一个特权系统调用,可找到内核的导出符号表,符号成对出现,一个是符号名称,另外一个是符号的值,例如符号的地址。内核维护一个由module_list指针指向的module链表,其中第一个module数据结构保存有内核的导出符号表(见图 9.1)。并不是所有的内核符号均在符号表中导出,而只有一些特殊的符号才被添加到符号表中。例如,“request_irq”是一个导出符号,它是一个内核例程,可由驱动程序申请控制某个特定的系统中断。利用ksyms命令或查看/proc/ksyms文件内容,可非常方便地看到所有的内核导出符号及其符号值。利用ksyms命令,不仅可以看到内核的所有符号,也可以看到只由以加载模块导出的符号。insmod命令将模块读到它本身的虚拟内存中,然后利用内核导出的符号表,修正尚未解析的对内核例程的引用。这种修正实际是对模块在内存中的映像进行修正,insmod将符号的地址写入模块中适当的位置而实现修正。
insmod命令修正模块对内核符号的引用之后,再次利用特权系统调用请求内核分配足够的物理内存空间保存新的模块。内核将分配新的module数据结构以及足够的内核内存,并将新模块添加在内核模块表的末尾。新的内核模块标记为Uninitialized(未初始化)。图 9.1是装入VFAT 和FAT 模块之后的内核模块表。图中并没有表示出第一个模块,它实际是一个伪模块,仅仅用来保存内核的导出符号表。利用lsmod命令可列出所有已加载的内核模块以及它们的内在依赖性。内核为新模块分配的内核内存映射到insmod进程的地址空间中,这样,insmod就可以将模块复制到新分配的内存中。insmod还对模块进行重新定位,经重定位之后,新的模块就可以从新分配的内核地址开始运行了。显然,模块不能期望自己能够在不同的Linux 系统,或前后两次装入时被加载到相同地址,重定位操作可通过对模块映像中适当的地址进行修正而解决这一问题。
新的模块也要向内核导出符号,由insmod建立相应的符号表。每个内核模块必须包含模块的初始化和清除例程,这些例程作为每个模块均具备的例程而不被导出,但insmod必须知道它们的地址,并将地址传递给内核。insmod同样利用特权系统调用将模块的初始化和清除例程地址传递给内核。 新的模块添加到内核之后,它必须更新内核符号集并修改使用新模块的模块。由其他模块依赖的模块必须在自身符号表的末尾维护一个引用表,并指向其他模块的module结构。例如,图 9.1 表明VFAT 文件系统模块依赖于FAT 文件系统模块,因此,FAT 模块包含一个对VFAT 模块的引用,该引用在装入VFAT 模块时添加。
内核成功调用模块的初始化例程之后继续模块的安装,最后,模块状态被设置为Running(运行)。模块的清除例程保存在module数据结构中,在卸载模块时由内核调用。
9.1.2 模块的卸载 • 和模块的加载类似,可利用rmmod命令手工卸载模块,当对需求加载的模块则由kerneld在不再需要时自动卸载。每次kerneld的空闲定时器到期时,它会利用系统调用将当前不再使用的需求加载模块从内核中移走。启动kerneld时指定该定时器的时间,通常的时间为 180 秒。
如果内核的其他部分依赖于装入的模块时,该模块不能卸载。例如,如果挂装了FAT 文件系统,则不能卸载已装入的FAT 文件系统模块。lsmod命令的输出会显示已安装模块的使用计数,例如: Module: #pages: Used by: msdos 5 1 vfat 4 1 (autoclean) fat 6 2 (autoclean)
使用计数就是依赖于该模块的内核实体个数。模块的使用计数保存在模块映像的第一个长整型中。但是,这一长整型中还包含有AUTOCLEAN和VISITED标志。这两个标志均由需求加载的模块使用。具有AUTOCLEAN标志的模块是系统认为可以自动卸载的模块。具有VISITED标志的模块表明正由其他内核组件使用,当任何其他内核组件使用该模块是设置该标志。当kerneld请求系统移走不使用的需求加载模块时,系统首先寻找可以移走的模块,但系统只查看标记为AUTOCLEAN,并且状态处于RUNNING的模块。如果上述模块的VISITED标志被清除,则系统将卸载该模块,否则系统会清除VISITED标志并查看下一个模块。 假定某个模块是可卸载的,则系统调用其清除例程释放分配该模块的内核资源。相应的module数据结构被标志为DELETED并从内核模块链表中断开。所有由该模块依赖的模块,系统会修改它们的引用表以便取消依赖性。最后,系统释放模块的内核内存。
9.1.3 内核模块的管理 在Linux里,除了直接修改系统核心的源代码,把设备驱动程序加进核心里以外,还可以把设备驱动程序作为可加载的模块,由系统管理员动态地加载它,使之成为核心的一部分。也可以由系统管理员把已加载地模块动态地卸载下来。 Linux的模块可以用C语言编写,用gcc编译成目标文件(不进行链接,作为*.o文件存在),为此需要在gcc命令行里加上-c的参数。在编译时,还应该在gcc的命令行里加上这样的参数:-D_KERNEL_-DMODULE。由于在不链接时,gcc只允许一个输入文件,因此一个模块的所有部分都必须在一个文件里实现。
编译好的模块*.o放/lib/modules/xxxx/misc下(xxxx表示核心版本,如在核心版本为2.0.30时应该为/lib/modules/2.0.30/misc),然后用depmod-a使此模块成为可加载模块。模块用insmod命令加载,用rmmod命令来卸载,并可以用lsmod命令来查看所有已加载的模块的状态。 利用insmod命令可手工装入内核模块;利用lsmod可查看当前装入的内核模块以及需求加载模块的使用计数及标志信息;利用rmmod则可以卸载指定的模块。
编写模块程序的时候,必须提供两个函数,一个是intinit_module(void),供insmod在加载此模块的时候自动调用,负责进行设备驱动程序的初始化工作。init_module返回0以表示初始化成功,返回负数表示失败。另一个函数是void cleanup_module(void),在模块被卸载时调用,负责进行设备驱动程序的清除工作。 在成功的向系统注册了设备驱动程序后(调用register_chrdev成功后),就可以用mknod命令来把设备映射为一个特别文件,其它程序使用这个设备的时候,只要对此特别文件进行操作就行了。
随着Linux 操作系统的广泛应用,特别是Linux 在嵌入式领域的发展,越来越多的人开始投身到Linux 内核级的开发中。面对日益庞大的Linux 内核源代码,开发者在完成自己的内核代码后,都将面临着同样的问题,即如何将源代码融入到Linux 内核中,增加相应的Linux 配置选项,并最终被编译进Linux 内核。这就需要了解Linux 的内核配置系统。 Linux 内核是由分布在全球的Linux 爱好者共同开发的,Linux 内核每天都面临着许多新的变化。但是,Linux 内核的组织并没有出现混乱的现象,反而显得非常的简洁,而且具有很好的扩展性,开发人员可以很方便的向Linux 内核中增加新的内容。原因之一就是Linux 采用了模块化的内核配置系统,从而保证了内核的扩展性。
9.2.1 配置系统的基本结构 Linux内核的配置系统由三个部分组成: (1)Makefile:分布在Linux 内核源代码中的Makefile 定义Linux 内核的编译规则; (2)配置文件(config.in):给用户提供配置选择的功能; (3)配置工具:包括配置命令解释器(对配置脚本中使用的配置命令进行解释)和配置用户界面(提供基于字符界面、基于Ncurses 图形界面以及基于Xwindows 图形界面的用户配置界面,各自对应于Make config、Make menuconfig 和Make xconfig)。
这些配置工具都是使用脚本语言,如Tcl/TK、Perl 编写的(也包含一些用C 编写的代码)。除非是配置系统的维护者,一般的内核开发者无须了解配置系统的原理,只需要知道如何使用配置系统,如何编写Makefile 和配置文件就可以。所以,本节只对Makefile 和配置文件进行讨论。凡是涉及到与具体CPU 体系结构相关的内容,都以ARM 为例,这样不仅可以将讨论的问题明确化,而且对内容本身不产生影响。
9.2.2 Makefile • Makefile 的作用是根据配置的情况,构造出需要编译的源文件列表,然后分别编译,并把目标代码链接到一起,最终形成Linux 内核二进制文件。
由于Linux 内核源代码是按照树形结构组织的,所以Makefile 也被分布在目录树中。Linux 内核中的Makefile 以及与Makefile 直接相关的文件有: Makefile:顶层Makefile,是整个内核配置、编译的总体控制文件。 .config:内核配置文件,包含由用户选择的配置选项,用来存放内核配置后的结果(如make config)。 arch/*/Makefile:位于各种CPU 体系目录下的Makefile,如arch/arm/Makefile,是针对特定平台的Makefile。 各个子目录下的Makefile:比如drivers/Makefile,负责所在子目录下源代码的管理。 Rules.make:规则文件,被所有的Makefile 使用。
用户通过make config 配置后,产生了 .config。顶层Makefile 读入 .config 中的配置选择。顶层Makefile 有两个主要的任务:产生vmlinux 文件和内核模块(module)。为了达到此目的,顶层Makefile 递归的进入到内核的各个子目录中,分别调用位于这些子目录中的Makefile。至于到底进入哪些子目录,取决于内核的配置。在顶层Makefile 中,有一句:include arch/$(ARCH)/Makefile,包含了特定CPU 体系结构下的Makefile,这个Makefile 中包含了平台相关的信息。 位于各个子目录下的 Makefile 同样也根据 .config 给出的配置信息,构造出当前配置下需要的源文件列表,并在文件的最后有 include $(TOPDIR)/Rules.make。
Rules.make 文件起着非常重要的作用,它定义了所有Makefile 共用的编译规则。比如,如果需要将本目录下所有的c 程序编译成汇编代码,需要在Makefile 中有以下的编译 规则: %.s: %.c $(CC) $(CFLAGS) -S $< -o $@ 有很多子目录下都有同样的要求,就需要在各自的Makefile 中包含此编译规则,这会比较麻烦。而Linux 内核中则把此类的编译规则统一放置到Rules.make 中,并在各自的Makefile 中包含进了Rules.make(include Rules.make),这样就避免了在多个Makefile 中重复同样的规则。对于上面的例子,在Rules.make 中对应的规则为: %.s: %.c $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$(*F)) $(CFLAGS_$@) -S $< -o $@
Makefile 中的变量 • 顶层Makefile 定义并向环境中输出了许多变量,为各个子目录下的Makefile 传递一些信息。有些变量,比如SUBDIRS,不仅在顶层Makefile 中定义并且赋初值,而且在arch/*/Makefile 还作了扩充。
常用的变量有以下几类: (1)版本信息 版本信息有:VERSION、PATCHLEVEL、SUBLEVEL、 EXTRAVERSION和KERNELRELEASE。版本信息定义了当前内核的版本,例如VERSION=2,PATCHLEVEL=4,SUBLEVEL=18,EXATAVERSION=-rmk7,它们共同构成内核的发行版本KERNELRELEASE:2.4.18-rmk7 (2)CPU 体系结构:ARCH 在顶层Makefile 的开头,用ARCH 定义目标CPU 的体系结构,比如ARCH:=arm 等。许多子目录的Makefile 中,要根据ARCH 的定义选择编译源文件的列表。 (3)路径信息:TOPDIR和SUBDIRS TOPDIR 定义了Linux 内核源代码所在的根目录。例如,各个子目录下的Makefile 通过 $(TOPDIR)/Rules.make 就可以找到Rules.make 的位置。 SUBDIRS 定义了一个目录列表,在编译内核或模块时,顶层Makefile 就是根据SUBDIRS 来决定进入哪些子目录。SUBDIRS 的值取决于内核的配置,在顶层Makefile 中SUBDIRS 赋值为kernel drivers mm fs net ipc lib;根据内核的配置情况,在arch/*/Makefile 中扩充了SUBDIRS 的值。
(4)内核组成信息:HEAD、CORE_FILES、NETWORKS、DRIVERS、LIBS(4)内核组成信息:HEAD、CORE_FILES、NETWORKS、DRIVERS、LIBS Linux 内核文件vmlinux 是由以下规则产生的: vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs $(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \ --start-group \ $(CORE_FILES) \ $(DRIVERS) \ $(NETWORKS) \ $(LIBS) \ --end-group \ -o vmlinux 可以看出,vmlinux 是由HEAD、main.o、version.o、CORE_FILES、DRIVERS、NETWORKS 和LIBS 组成的。这些变量(如HEAD)都是用来定义连接生成vmlinux 的目标文件和库文件列表。其中,HEAD在arch/*/Makefile 中定义,用来确定被最先链接进vmlinux 的文件列表。
例如,对于ARM 系列的CPU,HEAD 定义为: HEAD := arch/arm/kernel/head-$(PROCESSOR).o \ arch/arm/kernel/init_task.o 表明head-$(PROCESSOR).o 和init_task.o 需要最先被链接到vmlinux 中。PROCESSOR 为armv 或armo,取决于目标CPU。 CORE_FILES,NETWORK,DRIVERS 和LIBS 在顶层Makefile 中定义,并且由arch/*/Makefile 根据需要进行扩充。CORE_FILES 对应着内核的核心文件,有kernel/kernel.o,mm/mm.o,fs/fs.o,ipc/ipc.o,可以看出,这些是组成内核最为重要的文件。同时,arch/arm/Makefile 对CORE_FILES 进行了扩充: # arch/arm/Makefile # If we have a machine-specific directory, then include it in the build. MACHDIR := arch/arm/mach-$(MACHINE) ifeq ($(MACHDIR),$(wildcard $(MACHDIR))) SUBDIRS += $(MACHDIR) CORE_FILES := $(MACHDIR)/$(MACHINE).o $(CORE_FILES) endif HEAD := arch/arm/kernel/head-$(PROCESSOR).o \ arch/arm/kernel/init_task.o SUBDIRS += arch/arm/kernel arch/arm/mm arch/arm/lib arch/arm/nwfpe CORE_FILES := arch/arm/kernel/kernel.o arch/arm/mm/mm.o $(CORE_FILES) LIBS := arch/arm/lib/lib.a $(LIBS)
(5)编译信息:CPP, CC, AS, LD, AR,CFLAGS,LINKFLAGS 在Rules.make 中定义的是编译的通用规则,具体到特定的场合,需要明确给出编译环境,编译环境就是在以上的变量中定义的。针对交叉编译的要求,定义了CROSS_COMPILE。如: CROSS_COMPILE = arm-linux- CC = $(CROSS_COMPILE)gcc LD = $(CROSS_COMPILE)ld ...... CROSS_COMPILE 定义了交叉编译器前缀arm-linux-,表明所有的交叉编译工具都是以arm-linux- 开头的,所以在各个交叉编译器工具之前,都加入了 $(CROSS_COMPILE),以组成一个完整的交叉编译工具文件名,比如arm-linux-gcc。 CFLAGS 定义了传递给C 编译器的参数。 LINKFLAGS 是链接生成vmlinux 时,由链接器使用的参数。 LINKFLAGS 在arm/*/Makefile 中定义,如: # arch/arm/Makefile LINKFLAGS :=-p -X -T arch/arm/vmlinux.lds
(6)配置变量CONFIG_* .config 文件中有许多的配置变量等式,用来说明用户配置的结果。例如CONFIG_MODULES=y 表明用户选择了Linux 内核的模块功能。 .config 被顶层Makefile 包含后,就形成许多的配置变量,每个配置变量具有确定的值:y 表示本编译选项对应的内核代码被静态编译进Linux 内核;m 表示本编译选项对应的内核代码被编译成模块;n 表示不选择此编译选项;如果根本就没有选择,那么配置变量的值为空。
2. Rules.make 变量 Rules.make 是编译规则文件,所有的Makefile 中都会包括Rules.make。Rules.make 文件定义了许多变量,最为重要是那些编译、链接列表变量。 O_OBJS,L_OBJS,OX_OBJS,LX_OBJS:本目录下需要编译进Linux 内核vmlinux 的目标文件列表,其中OX_OBJS 和LX_OBJS 中的 "X" 表明目标文件使用了EXPORT_SYMBOL 输出符号。 M_OBJS,MX_OBJS:本目录下需要被编译成可装载模块的目标文件列表。同样,MX_OBJS 中的 "X" 表明目标文件使用了EXPORT_SYMBOL 输出符号。 O_TARGET,L_TARGET:每个子目录下都有一个O_TARGET 或L_TARGET,Rules.make 首先从源代码编译生成O_OBJS 和OX_OBJS 中所有的目标文件,然后使用 $(LD) -r 把它们链接成一个O_TARGET 或L_TARGET。O_TARGET 以 .o 结尾,而L_TARGET 以 .a 结尾。
3. Makefile子目录 子目录Makefile 用来控制本级目录以下源代码的编译规则。下面通过一个例子来讲解子目录Makefile 的组成:
# # Makefile for the linux kernel. # # All of the (potential) objects that export symbols. # This list comes from 'grep -l EXPORT_SYMBOL *.[hc]'. export-objs := tc.o # Object file lists. obj-y := obj-m := obj-n := obj- := obj-$(CONFIG_TC) += tc.o obj-$(CONFIG_ZS) += zs.o obj-$(CONFIG_VT) += lk201.o lk201-map.o lk201-remap.o # Files that are both resident and modular: remove from modular. obj-m := $(filter-out $(obj-y), $(obj-m))
# Translate to Rules.make lists. L_TARGET := tc.a L_OBJS := $(sort $(filter-out $(export-objs), $(obj-y))) LX_OBJS := $(sort $(filter $(export-objs), $(obj-y))) M_OBJS := $(sort $(filter-out $(export-objs), $(obj-m))) MX_OBJS := $(sort $(filter $(export-objs), $(obj-m))) include $(TOPDIR)/Rules.make (1)注释 对Makefile 的说明和解释,由#开始。 (2)编译目标定义 类似于obj-$(CONFIG_TC) += tc.o 的语句是用来定义编译的目标,是子目录Makefile 中最重要的部分。编译目标定义那些在本子目录下,需要编译到Linux 内核中的目标文件列表。为了只在用户选择了此功能后才编译,所有的目标定义都融合了对配置变量的判断。
前面说过,每个配置变量取值范围是:y,n,m 和空,obj-$(CONFIG_TC) 分别对应着obj-y,obj-n,obj-m,obj-。如果CONFIG_TC 配置为y,那么tc.o 就进入了obj-y 列表。obj-y 为包含到Linux 内核vmlinux 中的目标文件列表;obj-m 为编译成模块的目标文件列表;obj-n 和obj- 中的文件列表被忽略。配置系统就根据这些列表的属性进行编译和链接。 export-objs 中的目标文件都使用了EXPORT_SYMBOL() 定义了公共的符号,以便可装载模块使用。在tc.c 文件的最后部分,有“EXPORT_SYMBOL(search_tc_card);”,表明tc.o 有符号输出。 这里需要指出的是,对于编译目标的定义,存在着两种格式,分别是老式定义和新式定义。老式定义就是前面Rules.make 使用的那些变量,新式定义就是obj-y,obj-m,obj-n 和obj-。Linux 内核推荐使用新式定义,不过由于Rules.make 不理解新式定义,需要在Makefile 中的适配段将其转换成老式定义。
(3)适配段 适配段的作用是将新式定义转换成老式定义。在上面的例子中,适配段就是将obj-y 和obj-m 转换成Rules.make 能够理解的L_TARGET,L_OBJS,LX_OBJS,M_OBJS,MX_OBJS。 L_OBJS := $(sort $(filter-out $(export-objs), $(obj-y))) 定义了L_OBJS 的生成方式:在obj-y 的列表中过滤掉export-objs(tc.o),然后排序并去除重复的文件名。这里使用到了GNU Make 的一些特殊功能,具体的含义可参考Make 的文档(info make)。 (4) include $(TOPDIR)/Rules.make