几个重要的内核文件介绍
--几个重要的RedHat Linux内核文件介绍
在网络中,不少服务器采用的是Linux系统。为了进一步提高服务器的性能,可能需要根据特定的硬件及需求重新编译Linux内核。编译Linux内核,需要根据规定的步骤进行,编译内核过程中涉及到几个重要的文件。比如对于RedHat Linux,在/boot目录下有一些与Linux内核有关的文件,进入/boot执行:ls –l,如图所示。编译过RedHat Linux内核的人对其中的System.map 、vmlinuz、initrd-2.4.7-10.img印象可能比较深刻,因为编译内核过程中涉及到这些文件的建立等操作。那么这几个文件是怎么产生的?又有什么作用呢?本文对此做些介绍。
一、vmlinuz
vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制。Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。vmlinuz是可执行的Linux内核,它位于/boot/vmlinuz,它一般是一个软链接,比如图中是vmlinuz-2.4.7-10的软链接。
vmlinuz的建立有两种方式。一是编译内核时通过“make zImage”创建,然后通过:
“cp /usr/src/linux-2.4/arch/i386/linux/boot/zImage /boot/vmlinuz”产生。zImage适用于小内核的情况,它的存在是为了向后的兼容性。二是内核编译时通过命令make bzImage创建,然后通过:“cp /usr/src/linux-2.4/arch/i386/linux/boot/bzImage /boot/vmlinuz”产生。bzImage是压缩的内核映像,需要注意,bzImage不是用bzip2压缩的,bzImage中的bz容易引起误解,bz表示“big zImage”。 bzImage中的b是“big”意思。
zImage(vmlinuz)和bzImage(vmlinuz)都是用gzip压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。所以你不能用gunzip 或 gzip –dc解包vmlinuz。
内核文件中包含一个微型的gzip用于解压缩内核并引导它。两者的不同之处在于,老的zImage解压缩内核到低端内存(第一个640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么可以采用zImage 或bzImage之一,两种方式引导的系统运行时是相同的。大的内核采用bzImage,不能采用zImage。
vmlinux是未压缩的内核,vmlinuz是vmlinux的压缩文件。
二、 initrd-x.x.x.img
initrd是“initial ramdisk”的简写。initrd一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。图中的initrd-2.4.7-10.img主要是用于加载ext3等文件系统及scsi设备的驱动。比如,使用的是scsi硬盘,而内核vmlinuz中并没有这个scsi硬件的驱动,那么在装入scsi模块之前,内核不能加载根文件系统,但scsi模块存储在根文件系统的/lib/modules下。为了解决这个问题,可以引导一个能够读实际内核的initrd内核并用initrd修正scsi引导问题。initrd-2.4.7-10.img是用gzip压缩的文件,下面来看一看这个文件的内容,操作步骤如下图所示:
从图中linuxrc这个脚本的内容可以看到,initrd实现加载一些模块和安装文件系统等。
initrd映象文件是使用mkinitrd创建的。mkinitrd实用程序能够创建initrd映象文件。这个命令是RedHat专有的。其它Linux发行版或许有相应的命令。这是个很方便的实用程序。具体情况请看帮助:man mkinitrd
下面的命令创建initrd映象文件:
三、 System.map
System.map是一个特定内核的内核符号表。它是你当前运行的内核的System.map的链接。
内核符号表是怎么创建的呢? System.map是由“nm vmlinux”产生并且不相关的符号被滤出。对于本文中的例子,编译内核时,System.map创建在/usr/src/linux-2.4/System.map。像下面这样:
nm /boot/vmlinux-2.4.7-10 > System.map
下面几行来自/usr/src/linux-2.4/Makefile:
nm vmlinux | grep -v '(compiled)|(.o$$)|( [aUw] )|(..ng$$)|(LASH[RL]DI)' | sort > System.map
然后复制到/boot:
cp /usr/src/linux/System.map /boot/System.map-2.4.7-10
下图是System.map文件的一部分:
在进行程序设计时,会命名一些变量名或函数名之类的符号。Linux内核是一个很复杂的代码块,有许许多多的全局符号。
Linux内核不使用符号名,而是通过变量或函数的地址来识别变量或函数名。比如不是使用size_t BytesRead这样的符号,而是像c0343f20这样引用这个变量。
对于使用计算机的人来说,更喜欢使用那些像size_t BytesRead这样的名字,而不喜欢像c0343f20这样的名字。内核主要是用c写的,所以编译器/连接器允许我们编码时使用符号名,当内核运行时使用地址。
然而,在有的情况下,我们需要知道符号的地址,或者需要知道地址对应的符号。这由符号表来完成,符号表是所有符号连同它们的地址的列表。上图就是一个内核符号表,由上图可知变量名checkCPUtype在内核地址c01000a5。
Linux 符号表使用到2个文件:
/proc/ksyms
System.map
下图是/proc/ksyms的一部分。
/proc/ksyms是一个“proc file”,在内核引导时创建。实际上,它并不真正的是一个文件,它只不过是内核数据的表示,却给人们是一个磁盘文件的假象,这从它的文件大小是0可以看出来。然而,System.map是存在于你的文件系统上的实际文件。当你编译一个新内核时,各个符号名的地址要发生变化,你的老的System.map具有的是错误的符号信息。每次内核编译时产生一个新的System.map,你应当用新的System.map来取代老的System.map。
虽然内核本身并不真正使用System.map,但其它程序比如klogd, lsof和ps等软件需要一个正确的System.map。如果你使用错误的或没有System.map,klogd的输出将是不可靠的,这对于排除程序故障会带来困难。没有System.map,你可能会面临一些令人烦恼的提示信息。
另外少数驱动需要System.map来解析符号,没有为你当前运行的特定内核创建的System.map它们就不能正常工作。
Linux的内核日志守护进程klogd为了执行名称-地址解析,klogd需要使用System.map。System.map应当放在使用它的软件能够找到它的地方。执行:man klogd可知,如果没有将System.map作为一个变量的位置给klogd,那么它将按照下面的顺序,在三个地方查找System.map:
/boot/System.map
/System.map
/usr/src/linux/System.map
System.map也有版本信息,klogd能够智能地查找正确的映象(map)文件。
Linux 内核配置系统浅析 | ||||
随着 Linux 操作系统的广泛应用,特别是 Linux 在嵌入式领域的发展,越来越多的人开始投身到 Linux 内核级的开发中。面对日益庞大的 Linux 内核源代码,开发者在完成自己的内核代码后,都将面临着同样的问题,即如何将源代码融入到 Linux 内核中,增加相应的 Linux 配置选项,并最终被编译进 Linux 内核。这就需要了解 Linux 的内核配置系统。 众所周知,Linux 内核是由分布在全球的 Linux 爱好者共同开发的,Linux 内核每天都面临着许多新的变化。但是,Linux 内核的组织并没有出现混乱的现象,反而显得非常的简洁,而且具有很好的扩展性,开发人员可以很方便的向 Linux 内核中增加新的内容。原因之一就是 Linux 采用了模块化的内核配置系统,从而保证了内核的扩展性。 本文首先分析了 Linux 内核中的配置系统结构,然后,解释了 Makefile 和配置文件的格式以及配置语句的含义,最后,通过一个简单的例子--TEST Driver,具体说明如何将自行开发的代码加入到 Linux 内核中。在下面的文章中,不可能解释所有的功能和命令,只对那些常用的进行解释,至于那些没有讨论到的,请读者参考后面的参考文献。 Linux内核的配置系统由三个部分组成,分别是:
这些配置工具都是使用脚本语言,如 Tcl/TK、Perl 编写的(也包含一些用 C 编写的代码)。本文并不是对配置系统本身进行分析,而是介绍如何使用配置系统。所以,除非是配置系统的维护者,一般的内核开发者无须了解它们的原理,只需要知道如何编写 Makefile 和配置文件就可以。所以,在本文中,我们只对 Makefile 和配置文件进行讨论。另外,凡是涉及到与具体 CPU 体系结构相关的内容,我们都以 ARM 为例,这样不仅可以将讨论的问题明确化,而且对内容本身不产生影响。 2.1 Makefile 概述 Makefile 的作用是根据配置的情况,构造出需要编译的源文件列表,然后分别编译,并把目标代码链接到一起,最终形成 Linux 内核二进制文件。 由于 Linux 内核源代码是按照树形结构组织的,所以 Makefile 也被分布在目录树中。Linux 内核中的 Makefile 以及与 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 ___FCKpd___0lt; -o $@ 有很多子目录下都有同样的要求,就需要在各自的 Makefile 中包含此编译规则,这会比较麻烦。而 Linux 内核中则把此类的编译规则统一放置到 Rules.make 中,并在各自的 Makefile 中包含进了 Rules.make(include Rules.make),这样就避免了在多个 Makefile 中重复同样的规则。对于上面的例子,在 Rules.make 中对应的规则为: %.s: %.c $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$(*F)) $(CFLAGS_$@) -S ___FCKpd___1lt; -o $@ 2.2 Makefile 中的变量 顶层 Makefile 定义并向环境中输出了许多变量,为各个子目录下的 Makefile 传递一些信息。有些变量,比如 SUBDIRS,不仅在顶层 Makefile 中定义并且赋初值,而且在 arch/*/Makefile 还作了扩充。 常用的变量有以下几类: 1) 版本信息 2) CPU 体系结构:ARCH 3) 路径信息:TOPDIR, SUBDIRS 4) 内核组成信息:HEAD, CORE_FILES, NETWORKS, DRIVERS, LIBS 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)endifHEAD := arch/arm/kernel/head-$(PROCESSOR).o / arch/arm/kernel/init_task.oSUBDIRS += arch/arm/kernel arch/arm/mm arch/arm/lib arch/arm/nwfpeCORE_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 CROSS_COMPILE = arm-linux-CC = $(CROSS_COMPILE)gccLD = $(CROSS_COMPILE)ld......CROSS_COMPILE 定义了交叉编译器前缀 arm-linux-,表明所有的交叉编译工具都是以 arm-linux- 开头的,所以在各个交叉编译器工具之前,都加入了 $(CROSS_COMPILE),以组成一个完整的交叉编译工具文件名,比如 arm-linux-gcc。 CFLAGS 定义了传递给 C 编译器的参数。 LINKFLAGS 是链接生成 vmlinux 时,由链接器使用的参数。LINKFLAGS 在 arm/*/Makefile 中定义,比如: # arch/arm/MakefileLINKFLAGS :=-p -X -T arch/arm/vmlinux.lds 6) 配置变量CONFIG_* 2.3 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 结尾。 2.4 子目录 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.oobj-$(CONFIG_ZS) += zs.oobj-$(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.aL_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 a) 注释 b) 编译目标定义 c) 适配段 d) include $(TOPDIR)/Rules.make 3.1 配置功能概述 除了 Makefile 的编写,另外一个重要的工作就是把新功能加入到 Linux 的配置选项中,提供此项功能的说明,让用户有机会选择此项功能。所有的这些都需要在 config.in 文件中用配置语言来编写配置脚本,
以字符界面配置(make config)为例,顶层 Makefile 调用 scripts/Configure, 按照 arch/arm/config.in 来进行配置。命令执行完后产生文件 .config,其中保存着配置信息。下一次再做 make config 将产生新的 .config 文件,原 .config 被改名为 .config.old 3.2 配置语言 1) 顶层菜单 2) 询问语句 bool /prompt/ /symbol/ hex /prompt/ /symbol/ /word/ int /prompt/ /symbol/ /word/ string /prompt/ /symbol/ /word/ tristate /prompt/ /symbol/询问语句首先显示一串提示符 /prompt/,等待用户输入,并把输入的结果赋给 /symbol/ 所代表的配置变量。不同的询问语句的区别在于它们接受的输入数据类型不同,比如 bool 接受布尔类型( y 或 n ),hex 接受 16 进制数据。有些询问语句还有第三个参数 /word/,用来给出缺省值。
3) 定义语句 define_bool /symbol/ /word/ define_hex /symbol/ /word/ define_int /symbol/ /word/ define_string /symbol/ /word/ define_tristate /symbol/ /word/不同于询问语句等待用户输入,定义语句显式的给配置变量 /symbol/ 赋值 /word/。
4) 依赖语句 dep_bool /prompt/ /symbol/ /dep/ ... dep_mbool /prompt/ /symbol/ /dep/ ... dep_hex /prompt/ /symbol/ /word/ /dep/ ... dep_int /prompt/ /symbol/ /word/ /dep/ ... dep_string /prompt/ /symbol/ /word/ /dep/ ... dep_tristate /prompt/ /symbol/ /dep/ ...与询问语句类似,依赖语句也是定义新的配置变量。不同的是,配置变量/symbol/的取值范围将依赖于配置变量列表/dep/ …。这就意味着:被定义的配置变量所对应功能的取舍取决于依赖列表所对应功能的选择。以dep_bool为例,如果/dep/ …列表的所有配置变量都取值y,则显示/prompt/,用户可输入任意的值给配置变量/symbol/,但是只要有一个配置变量的取值为n,则/symbol/被强制成n。 不同依赖语句的区别在于它们由依赖条件所产生的取值范围不同。
5) 选择语句 choice /prompt/ /word/ /word/choice 语句首先给出一串选择列表,供用户选择其中一种。比如 Linux for ARM 支持多种基于 ARM core 的 CPU,Linux 使用 choice 语句提供一个 CPU 列表,供用户选择: choice 'ARM system type' / "Anakin CONFIG_ARCH_ANAKIN / Archimedes/A5000 CONFIG_ARCH_ARCA5K / Cirrus-CL-PS7500FE CONFIG_ARCH_CLPS7500 / …… SA1100-based CONFIG_ARCH_SA1100 / Shark CONFIG_ARCH_SHARK" RiscPCChoice 首先显示 /prompt/,然后将 /word/ 分解成前后两个部分,前部分为对应选择的提示符,后部分是对应选择的配置变量。用户选择的配置变量为 y,其余的都为 n。
6) if语句 if [ /expr/ ] ; then /statement/ ... fi if [ /expr/ ] ; then /statement/ ... else /statement/ ... fiif 语句对配置变量(或配置变量的组合)进行判断,并作出不同的处理。判断条件 /expr/ 可以是单个配置变量或字符串,也可以是带操作符的表达式。操作符有:=,!=,-o,-a 等。
7) 菜单块(menu block)语句 mainmenu_option next_commentcomment '…..'…endmenu引入新的菜单。在向内核增加新的功能后,需要相应的增加新的菜单,并在新菜单下给出此项功能的配置选项。Comment 后带的注释就是新菜单的名称。所有归属于此菜单的配置选项语句都写在 comment 和 endmenu 之间。
8) Source 语句 3.3 缺省配置 Linux 内核支持非常多的硬件平台,对于具体的硬件平台而言,有些配置就是必需的,有些配置就不是必需的。另外,新增加功能的正常运行往往也需要一定的先决条件,针对新功能,必须作相应的配置。因此,特定硬件平台能够正常运行对应着一个最小的基本配置,这就是缺省配置。 Linux 内核中针对每个 ARCH 都会有一个缺省配置。在向内核代码增加了新的功能后,如果新功能对于这个 ARCH 是必需的,就要修改此 ARCH 的缺省配置。修改方法如下(在 Linux 内核根目录下):
如果新增的功能适用于许多的 ARCH,只要针对具体的 ARCH,重复上面的步骤就可以了。 3.4 help file 大家都有这样的经验,在配置 Linux 内核时,遇到不懂含义的配置选项,可以查看它的帮助,从中可得到选择的建议。下面我们就看看如何给给一个配置选项增加帮助信息。 所有配置选项的帮助信息都在 Documentation/Configure.help 中,它的格式为: <description><variable name><help file> <description> 给出本配置选项的名称,<variable name> 对应配置变量,<help file> 对应配置帮助信息。在帮助信息中,首先简单描述此功能,其次说明选择了此功能后会有什么效果,不选择又有什么效果,最后,不要忘了写上"如果不清楚,选择 N(或者)Y",给不知所措的用户以提示。 对于一个开发者来说,将自己开发的内核代码加入到 Linux 内核中,需要有三个步骤。首先确定把自己开发代码放入到内核的位置;其次,把自己开发的功能增加到 Linux 内核的配置选项中,使用户能够选择此功能;最后,构建子目录 Makefile,根据用户的选择,将相应的代码编译到最终生成的 Linux 内核中去。下面,我们就通过一个简单的例子--test driver,结合前面学到的知识,来说明如何向 Linux 内核中增加新的功能。 4.1 目录结构 test driver 放置在 drivers/test/ 目录下: $cd drivers/test$tree.|-- Config.in|-- Makefile|-- cpu| |-- Makefile| `-- cpu.c|-- test.c|-- test_client.c|-- test_ioctl.c|-- test_proc.c|-- test_queue.c`-- test |-- Makefile `-- test.c 4.2 配置文件 1) drivers/test/Config.in ## TEST driver configuration#mainmenu_option next_commentcomment 'TEST Driver'bool 'TEST support' CONFIG_TESTif [ "$CONFIG_TEST" = "y" ]; then tristate 'TEST user-space interface' CONFIG_TEST_USER bool 'TEST CPU ' CONFIG_TEST_CPUfiendmenu由于 test driver 对于内核来说是新的功能,所以首先创建一个菜单 TEST Driver。然后,显示 "TEST support",等待用户选择;接下来判断用户是否选择了 TEST Driver,如果是(CONFIG_TEST=y),则进一步显示子功能:用户接口与 CPU 功能支持;由于用户接口功能可以被编译成内核模块,所以这里的询问语句使用了 tristate(因为 tristate 的取值范围包括 y、n 和 m,m 就是对应着模块)。
2) arch/arm/config.in 4.3 Makefile 1)drivers/test/Makefile # drivers/test/Makefile## Makefile for the TEST.#SUB_DIRS :=MOD_SUB_DIRS := $(SUB_DIRS)ALL_SUB_DIRS := $(SUB_DIRS) cpuL_TARGET := test.aexport-objs := test.o test_client.oobj-$(CONFIG_TEST) += test.o test_queue.o test_client.oobj-$(CONFIG_TEST_USER) += test_ioctl.oobj-$(CONFIG_PROC_FS) += test_proc.osubdir-$(CONFIG_TEST_CPU) += cpuinclude $(TOPDIR)/Rules.makeclean: for dir in $(ALL_SUB_DIRS); do make -C $dir clean; done rm -f *.[oa] .*.flagsdrivers/test 目录下最终生成的目标文件是 test.a。在 test.c 和 test-client.c 中使用了 EXPORT_SYMBOL 输出符号,所以 test.o 和 test-client.o 位于 export-objs 列表中。然后,根据用户的选择(具体来说,就是配置变量的取值),构建各自对应的 obj-* 列表。由于 TEST Driver 中包一个子目录 cpu,当 CONFIG_TEST_CPU=y(即用户选择了此功能)时,需要将 cpu 目录加入到 subdir-y 列表中。
2)drivers/test/cpu/Makefile # drivers/test/test/Makefile## Makefile for the TEST CPU #SUB_DIRS :=MOD_SUB_DIRS := $(SUB_DIRS)ALL_SUB_DIRS := $(SUB_DIRS)L_TARGET := test_cpu.aobj-$(CONFIG_test_CPU) += cpu.oinclude $(TOPDIR)/Rules.makeclean: rm -f *.[oa] .*.flags 3)drivers/Makefile ……subdir-$(CONFIG_TEST) += test……include $(TOPDIR)/Rules.make在 drivers/Makefile 中加入 subdir-$(CONFIG_TEST)+= test,使得在用户选择 TEST Driver 功能后,内核编译时能够进入 test 目录。
4)Makefile ……DRIVERS-$(CONFIG_PLD) += drivers/pld/pld.oDRIVERS-$(CONFIG_TEST) += drivers/test/test.aDRIVERS-$(CONFIG_TEST_CPU) += drivers/test/cpu/test_cpu.aDRIVERS := $(DRIVERS-y)……在顶层 Makefile 中加入 DRIVERS-$(CONFIG_TEST) += drivers/test/test.a 和 DRIVERS-$(CONFIG_TEST_CPU) += drivers/test/cpu/test_cpu.a。如何用户选择了 TEST Driver,那么 CONFIG_TEST 和 CONFIG_TEST_CPU 都是 y,test.a 和 test_cpu.a 就都位于 DRIVERS-y 列表中,然后又被放置在 DRIVERS 列表中。在前面曾经提到过,Linux 内核文件 vmlinux 的组成中包括 DRIVERS,所以 test.a 和 test_cpu.a 最终可被链接到 vmlinux 中。 |
1.Linux内核源码结构:
内核源码中主要包含以下子目录:
arch :包含了与体系结构相关的代码
对应于每一个支持的体系结构,有一个相应的子目录如i386、arm、alpha等。
其每个体系结构子目录下包含几个主要的子目录:
kernel :包含与体系结构相关的内核代码
mm : 包含与体系结构相关的内存管理代码
lib : 包含与体系结构相关的库代码
documentation:包含内核的文档
drivers :包含设备驱动代码。每类设备有相应的子目录,如char 、block、net等
fs : 包含文件系统的代码。每个支持的文件系统有相应的子目录,如 ext2、proc等
include :内核头文件,对每一种体系结构,分别有相应的子目录。
init : 包含内核初始化代码
lib : 包含内核的库代码
mm :包含内存管理代码
kernel :包含内核管理代码
net :包含网络部分的代码
2.系统引导的过程
在pc机上系统启动过程:
系统加电以后bois对系统完成监测设置后将控制权交给硬盘上MBR中的 BootLoader在这里即是lilo或grub等。
BootLoader 将操作系统代码调入内存,然后将控制权交给arch/i386/boot中的Setup.S这段程序。
Setup.S 这段程序在386实模式下对系统进行基本的检测和设置后转入保护模式把控制权交给Head.S
Head.S 建立内存管理和中断管理的框架后调用init/main.c中的start_kernel()函数在start_kernel执行完成后用户就可以登录和使用linux了。Start_kernel()函数在init/main.c 中定义。
Start_kernel的流程中的主要步骤:
setup_arch(&command_line); 用于和处理器、内存等最基本的硬件相关部分的初始化。 在 arch/i386/kernel/setup.c 中定义;
parse_options(command_line); 把启动时得到的参数从命令行的字符串中分离出来并赋给相应的变量。在 init/main.c 中定义;
trap_init(); 对中断向量表进行初始化。在 arch/i386/kernel/trap.c 中定义;
init_IRQ(); 与中断有关的初始化, 在 arch/i386/kernel/i8259.c 中定义;
sched_init(); 进程调度初始化。 在 kernel/sched.c 中定义;
softirq_init(); 在 kernel/softirq.c 中定义;
time_init(); 时间部分初始化。 在 arch/i386/kernel/time.c 中定义;
console_init(); 对终端初始化。 在 drivers/char/tty_io.c 中定义;
buffer_init(mempages); 对用于指示块缓存的buff free list 初始化。 在fs/buffer.c 中定义;
mem_init(); 内存管理初始化。 在 arch/i386/mm/init.c中定义;
rest_init(); 此函数中调用
kernel_thread(init,NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)函数时会调用init/main.c中的init()函数 在init()函数中将会建立dbflush、kswapd两个新的内核线程。初始化tty1设备。寻找/etc/init或/sbin/init 或/bin/init来建立一个init进程。
Init进程根据/etc/inittab文件进行文件系统检查、启动系统守护进程为联机终端建立getty进程,执行/etc/rc下的命令文件。
此后getty会在终端上显示login提示符,以等待用户登录。
3.使用make建立内核
1.使用make menuconfig命令:
使用以下编译选项:
Processor type and features --->
(Pentium-Pro/Celeron/Pentium-II) Processor family
(3GB) Maximum Virtual Memory
General setup --->
(ELF) Kernel core (/proc/kcore) format
[*] Kernel support for ELF binaries
File systems --->
[*] /proc file system support
[*] Second extended fs support
ATA/IDE/MFM/RLL support --->
[*] ATA/IDE/MFM/RLL support
[*] Enhanced IDE/MFM/RLL disk/cdrom/tape/floppy support
[*] Include IDE/ATA-2 DISK support
Character devices --->
[*] Virtual terminal
Console drivers --->
[*] VGA text console
生成内核bzImage 大小为 309892 byte;
此内核可在pc上成功引导系统
此命令生成一个文件 .config 其中根据你在menuconfig中的选择定义了相应的变量。在Makefile文件中将会包含这个文件。
2.使用make dep命令 建立依赖关系。
3.使用make bzImage 命令建立内核。
如设置正确将在arch/i386/boot/目录下生成内核bzImage文件
4.make bzImage的流程简单说明
当我们使用make命令时,make程序将首先找到当前目录下的Makefile文件。根据Makefile文件的语法进行处理。
在主Makefile文件中包含了arch/i386/Makefile我们make的目标bzImage即在该文件中定:
bzImage: vmlinux
@$(MAKEBOOT) bzImage #此命令将解释为:make -C arch/i386/boot bzImage
现在make需要先去建立目标 vmlinux 然后再执行 arch/i386/boot/ 目录下的 make bzImae。我们现在假设vmlinux目标已生成,则 arch/i386/boot目录下的make程序将执行如下操作:
tools/build -b bbootsect bsetup compressed/bvmlinux.out ./ bzImage
即将vmlinux 用 tools/build工具压缩成目标文件 bzImage(在此过程中,还会构建build 程序,将vmlinux转换成bvmlinux.out等,可参考tools 和 compressed目录下的Makefile文件)。
生成vmlinux目标:
在主目录下Makefile文件中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
$(NM) vmlinux |grep –v /(compiled/)/|/(/.o$$/)/|/( [aUw] /)/|/(/./.ng$$/)/|/(LASH[RL]DI/) | sort > System.map #此命令根据vmlinux生成System.map文件
在当前设置下此ld连接命令被解释为:
ld -m elf_i386 -T /home/arm/linux/arch/i386/vmlinux.lds -e stext arch/i386/kernel/head.o arch/i386/kernel/init_task.o init/main.o init/version.o --start-group arch/i386/kernel/kernel.o arch/i386/mm/mm.o kernel/kernel.o mm/mm.o fs/fs.o ipc/ipc.o drivers/char/char.o drivers/block/block.o drivers/misc/misc.o drivers/net/net.o drivers/media/media.o drivers/ide/idedriver.o drivers/video/video.o net/network.o /home/arm/linux/arch/i386/lib/lib.a /home/arm/linux/lib/lib.a /home/arm/linux/arch/i386/lib/lib.a --end-group -o vmlinux
即连接程序ld 将各个.o文件连接成目标文件 vmlinux 。
此命令中用到的各个 .o文件make程序会根据Makefile文件的规则去自动生成,下面简单介绍一下由ipc目录生成ipc.o过程:
其ipc目录下Makefile文件内容如下:
O_TARGET := ipc.o
obj-y := util.o
obj-$(CONFIG_SYSVIPC) += msg.o sem.o shm.o
include $(TOPDIR)/Rules.make #包含的Rules.make文件中为通用的规则;
如我们在make menuconfig时选中了SYSVIPC选项,则 .config文件中将定义变量 CONFIG_SYSVIPC=y; 则obj-y 就等于util.o msg.o sem.o shm.o;根据Rules.make将四个.c文件编译为 .o文件。再将四个.o文件连接成 ipc.o文件。在我们的当前设置中没有选择SYSVIPC;将只使用util.o一个文件生成目标 ipc.o ;而util.o由util.c生成。
当所有需要的.o文件生成以后,由ld将其连接生成vmlinux文件,再将其压缩成我们所需要的内核文件 bzImage。Make程序就执行完了。
Linux内核启动地址
$(wildcard include/config/cpu/32.h) /
$(wildcard include/config/cpu/26.h) /
$(wildcard include/config/arch/*.h)
@echo ' Generating $@'
@sed 's/TEXTADDR/$(TEXTADDR)/;s/DATAADDR/$(DATAADDR)/' $(LDSCRIPT) >$@
ENTRY(stext)
SECTIONS
{
. = TEXTADDR;
.init : { /* Init code and data */
_stext = .;
__init_begin = .;
*(.text.init)
__proc_info_begin = .;
*(.proc.info)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist)
__tagtable_end = .;
*(.data.init)
. = ALIGN(16);
*(.setup.init)
__setup_end = .;
__initcall_start = .;
*(.initcall.init)
__initcall_end = .;
. = ALIGN(4096);
__init_end = .;
}
其中TEXTADDR就是内核启动的虚拟地址,定义在kernel/arch/arm/Makefile中:
ifeq ($(CONFIG_CPU_32),y)
PROCESSOR = armv
TEXTADDR = 0xC0008000
LDSCRIPT = arch/arm/vmlinux-armv.lds.in
endif
需要注意的是这里是虚拟地址而不是物理地址。
下载到flash中的是压缩后的zImage文件,zImage是由压缩后的vmlinux和解压缩程序组成,如下图所示:
| | / | |
| | / | decompress code |
| vmlinux | / |------------------| zImage
| | /| |
| | | |
| | | |
| | | |
| | /|------------------|
| | /
| | /
| | /
|------------------|/
是由同一目录下的vmlinux.lds.in文件生成的,内容如下:
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = LOAD_ADDR;
_load_addr = .;
. = TEXT_START;
_text = .;
.text : {
_start = .;
其中LOAD_ADDR就是zImage中解压缩代码的ram偏移地址,TEXT_START是内核ram启动的偏移地址,这个地址是物理地址。
在kernel/arch/arm/boot/Makefile文件中定义了:
ZTEXTADDR =0
ZRELADDR = 0xa0008000
明显是不正确的,因为我的平台上的ram起始地址是0xa0000000,在Makefile文件中看到了对该地址设置的几行注释:
# We now have a PIC decompressor implementation. Decompressors running
# from RAM should not define ZTEXTADDR. Decompressors running directly
# from ROM or Flash must define ZTEXTADDR (preferably via the config)
他的意识是如果是在ram中进行解压缩时,不用指定它在ram中的运行地址,如果是在flash中就必须指定他的地址。所以这里将ZTEXTADDR指定为0,也就是没有真正指定地址。
SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/LOAD_ADDR/$(ZRELADDR)/;s/BSS_START/$(ZBSSADDR)/
使得TEXT_START = ZTEXTADDR,LOAD_ADDR = ZRELADDR。
vmlinux.lds: vmlinux.lds.in Makefile $(TOPDIR)/arch/$(ARCH)/boot/Makefile $(TOPDIR)/.config
@sed "$(SEDFLAGS)" < vmlinux.lds.in > $@
以上就是我对内核启动地址的分析,总结一下内核启动地址的设置:
1、设置kernel/arch/arm/Makefile文件中的
TEXTADDR = 0xC0008000
内核启动的虚拟地址
ZRELADDR = 0xa0008000
内核启动的物理地址
如果需要从flash中启动还需要设置
ZTEXTADDR地址