移植Linux内核
嵌入式Linux系统移植之移植Linux内核
《嵌入式Linux应用完全开发手册》第3篇第16章总结归纳
本章目标
- 了解内核源码结构,了解内核启动过程
- 掌握内核配置方法
- 移植内核同时支持S3C2410、S3C2440
- 掌握MTD设备的分区方法
- 掌握YAFFS文件系统的移植方法
Linux 版本及特点
Linux内核的版本号可以从源代码的顶层目录下的Makefile中看到,比如下面几行它们构成了Linux的版本号:2.6.22.6。
1 | VERSION = 2 |
其中的“VERSION”和“PATCHLEVEL”组成主版本号,比如2.4、2.5、2.6等,稳定版本的主版本号用偶数表示(比如2.4、2.6),每隔2-3年出现一个稳定版本。开发中的版本用技术表示(2.3、2.5),它是下一个稳定版本的前身。
“SUBLEVEL”称为此版本号,它不分奇偶顺序递增。每隔1-2个月发布一个稳定版本。
“EXTRAVERSION”称为扩展版本号,它不分奇偶,顺序递增。每周发布几次扩展版本号,修正最新的稳定版本的问题。
Linux内核的最初版本在1991年发布,这是Linus Torvalds为他开发的386开发的一个类Minix的操作系统。
Linux 1.0的官方版发行于1994年3月,包含了386的官方支持,仅支持单CPU系统。
Linux 1.2发行于1995年3月,它是第一个包含多平台(Alpha、Sparc、Mips等)支持的官方版本。
Linux 2.0发行于1996年6月,包含很多新的平台支持,但是最重要的是,它是第一个支持SMP(对称多处理器)体系的版本。
Linux 2.2发行于1999年1月,它带来了SMP系统性能的极大提升,同时支持更多的硬件。
Linux 2.2发行于2001年1月,它进一步提升了SMP系统的扩展性,同时它集成了很多用于支持桌面系统的特性:USB、PC卡(PCMCIA)的支持,内置的即插即用等。
Linux 2.6发行于2003年12月,在Linux 2.4的基础上作了极大的改进。2.6内核支持更多的平台,从小规模的嵌入式系统到服务器级的64位的系统;使用新的调度器,进程的切换更高效;内核可被抢占,使得用户的操作可以得到更快速的响应;I/O子系统也经历很大的修改,使得它在各种工作负荷下都更具响应性;模块子系统、文件系统都做了大量的改进。另外,以前使用Linux的变种μClinux来支持没有MMU的处理器,现在2.6版本的Linux中已经合入了μClinux的功能,也可以支持没有MMU的处理器。
Linux移植准备
获取Linux内核源码
登录Linux内核的官方网站http://www.kernel.org/
,可以看到下图所示的内容:
上面标明了Linux内核的最新稳定版本、正在开发的测试版本,图中间的版本号就是各种补丁的链接地址。各种标记符的意义如下表所示:
标记 | 描述 |
---|---|
F | 全部代码,单击“F”即可下载全部内核代码 |
B | 当前补丁基于哪个版本的内核,单击“B”可以下载这个内核 |
V | 查看补丁文件的信息,修改了哪些文件 |
VI | 查看与上一个扩展版本相比,修改了哪些文件 |
C | 当前的修改记录 |
ChangeLog | 正式的修改记录,由开发者提供 |
一般而言,各种补丁文件都是基于内核的某个正式版本生成的,除非使用标记符“B”指明了它所基于的版本。比如有补丁文件patch-2.6.xx.1、patch-2.6.xx.2、patch-2.6.xx.3,它们都是基于内核2.6.xx生成的补丁文件。使用时可以在内核2.6.xx上直接打补丁patch-2.6.xx.3,并不需要先打上补丁文件patch-2.6.xx.1、patch-2.6.xx.2;相应的,如果已经打上了补丁文件patch-2.6.xx.2,在打补丁之前,要先去除补丁文件patch-2.6.xx.2。
本书在Linux2.6.22.6上进行移植开发。下载linux-2.6.22.6.tar.bz2后如下解压即可得到目录linux-2.6.22.6,里面存放了内核源码,如下所示:
1 | tar xjf linux-2.6.22.6.tar.bz2 |
也可以先下载内核源文件linux-2.6.22.tar.bz2、补丁文件patch-2.6.22.6.bz2,然后解压、打补丁:
1 | tar xjf linux-2.6.22.tar.bz2 |
内核源码结构及Makefile分析
内核源码结构
Linux内核文件有近2万,出去其他架构CPU的相关文件,支持S3C2410、S3C2440这两款芯片的完整内核文件有1万多个。这些文件的组织结构并不复杂,它们分别位于顶层目录下的17个子目录,各个目录功能独立。下表描述各目录的功能,最后两个目录不包含代码。
目录名 | 描述 |
---|---|
arch | 体系结构相关的代码,对于每个架构的CPU,arch下有一个对应的子目录,比如arch/arm 、arch/i386 。 |
block | 块设备的通用函数 |
crypto | 常用的加密和散列算法(AES、SHA),还有一些压缩和CRC校验算法 |
drivers | 所有的设备驱动程序,里面的每一个子目录对应一类驱动程序,比如drivers/block 为块设备驱动程序,drivers/char 为字符设备驱动程序,drivers/mtd 为NOR Flash、NAND Flash等存储设备的驱动程序 |
fs | Linux支持的文件系统的代码,每一个子目录对应一种文件系统,比如fs/jffs2 、fs/ext2 、fs/ext3 |
include | 内核头文件,有基本头文件(存放在include/linux 目录下)、各种驱动或功能部件的头文件(比如include/media 、include/mtd 、include/net )、各种体系相关的头文件(比如include/asm-arm 、include/asm-i386 )。当配置内核之后,include/asm 是某个include/asm-xxx 的链接 |
init | 内核的初始化代码(不是系统的引导代码),其中的main.c 文件中的start_kernel 函数是内核引导后运行的第一个函数 |
ipc | 进程间的通信代码 |
Kernel | 内核管理的核心代码,与处理器相关的代码位于arch/*/kernel |
lib | 内核用到的一些库函数的代码,比如crc32.c 、string.c ,与处理器相关的库函数代码位于arch/*/lib 目录下 |
mm | 内存管理代码,与处理器相关的内存管理代码位于arch/*/mm 目录下 |
net | 网络支持代码,每个子目录对应与网络的一个方面 |
security | 安全、密钥相关的代码 |
sound | 音频设备的驱动代码 |
usr | 用来制作一个压缩的cpio归档文件:initrd的镜像,它可以作为内核启动后挂接的第一个文件系统 |
Documentation | 内核文档 |
scripts | 用于配置、编译内核的脚本文件 |
对于ARM架构的S3C2410、S3C2440,其体系相关的代码在arch/arm
目录下,在后面进行Linux移植时,开始的工作正是修改这个目录下的文件。内核代码的层次结构如下图:
Linux Makefile 分析
内核中的哪些文件将被编译,怎样被编译,连接顺序如何确定,哪个文件在最前面,哪些文件或函数先执行。这些都是通过Makefile来管理的。
- 决定编译哪些文件
- 怎样编译这些文件
- 怎样连接这些文件
Linux内核源码中含有很多个Makefile文件,这些Makefile文件又要包含其他一些文件(比如配置信息、通用的规则等 )。这些文件构成了Linux的Makefile体系:
名称 | 描述 |
---|---|
顶层Makefile | 它是所有Makefile文件的核心,从总体上控制着内核的编译、连接 |
.config | 配置文件,在配置内核时生成,所有的Makefile文件(包括顶层目录及各级子目录)都是根据.config文件来决定使用哪些文件 |
arch/$(ARCH)/Makefile | 对应体系结构的Makefile,它用来决定哪些体系结构相关的文件参与内核的生成,并提供一些规则来生成特定格式的内核映像 |
scipts/Makefile.* | Makefile共用的通用规则、脚本等 |
kbuild Makefiles | 各级子目录下的Makefile,它们相对简单,被上一层Makefile调用来编译当前子目录下的文件 |
内核文档Documentation/kbuild/makefiles.txt
对内核中的Makefile作用、用法讲解的非常透彻,以下根据前面总结的Makefile的3大作用分析这5类文件。
- 决定编译哪些文件
Linux 内核的编译过程从顶层Makefile开始,然后递归进入各级子目录调用它们的Makefile,分为3个步骤。
①顶层Makefile决定内核根目录下哪些子目录将被编进内核。
②arch/$(ARCH)/Makefile
决定arch/$(ARCH)
目录下哪些文件、哪些目录将被编进内核。
③各级子目录下的Makefile决定所在目录下哪些文件将被编进内核,哪些文件将被编成模块(即驱动程序),进入哪些子目录继续调用它们的Makefile。
先看步骤①,在顶层Makefile中可以看到如下内容:
1 | init-y : init/ |
可见,顶层Makefile将这13个子目录分为5类:init-y、drivers-y、net-y、libs-y和core-y。之前上表中的17个子目录,出去include
目录和后面两个不包含内核代码的目录外,还有一个arch
目录没有出现在内核中。它在arch/$(ARCH)/Makefile
中被包含进内核,在顶层Makefile中直接包含了这个Makefile,如下所示:
1 | include $(srctree)/arch/$(ARCH)/Makefile |
对于ARCH变量,可以在执行make命令时传入,比如“make ARCH=arm …”。另外,对于非x86平台,还需要指定交叉编译工具,这也可以在执行make命令时传入,比如“make CROSS_COMPILE=arm-linux- …”。为了方便,常在顶层Makefile中进行修改。
修改前:
1 | ARCH ?= $(SUBARCH) |
修改后:
1 | ARCH ?= arm |
对于步骤②的arch/$(ARCH)/Makefile
,以ARM体系为例,在arch/arm/Makefile
中可以看到如下内容:
1 | head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o |
由第1行可知,除前面的5类子目录外,又出现了另一类:head-y
,不过它是直接以文件名出现。MMUEXT在arch/arm/Makefile
前面定义,对于没有MMU的处理器,MMUEXT的值为-nommu,使用文件head-nommu.S;对于有MMU的处理器,MMUEXT的值为空,使用文件head.S。arch/arm/Makefile
中类似第3、4、5行的代码进一步扩展了core-y
的内容,第9行扩展了libs-y
的内容,这些都是体系结构相关的目录。第5-7行中的CONFIG_ARCH_S3C2410在配置内核时定义,它的值有3种:y、m、空。y表示编进内核,m表示编为模块,空表示不使用。
编译内核时,将依次进入init-y、core-y、libs-y、drivers-y和net-y所列出的目录执行它们的Makefile,每个子目录都会生成一个build-in.o(libs-y所列目录下,有可能lib.a文件)。最后,head-y
所表示的文件将和这些build-in.o、lib.a一起被连接成内核映像文件vmlinux。
最后,步骤③是如何进行的。
在配置内核时,生成配置文件.config
。内核顶层Makefile使用如下语句间接包含.config
文件,以后就根据.config
中定义的各个变量来决定编译哪些文件。之所以说是“间接”包含,是因为包含的是include/config/auto.conf
文件,而它只是将.config
文件中的注释去掉,并根据顶层Makefile中定义的变量增加一些变量而已。
1 | #Read in config |
include/config/auto.conf
文件的生成过程不再描述,它与.config
的格式相同,摘选部分内容如下:
1 | CONFIG_ARCH_SMDK2410=y |
在include/config/auto.conf
文件中,变量的值主要有两类:“y”和“m”。各级子目录的Makefile使用这些变量来决定哪些文件被编译进内核中,哪些文件被编成模块(即驱动程序),要进入哪些下一级子目录继续编译,这通过以下4种方法来确定(obj-y、obj-m、lib-y是Makefile中的变量)。
①obj-y用来定义哪些文件被编进(build-in)内核。
obj-y中定义的.o文件由当前目录下的.c或.S文件编译生成,它们连同下级子目录的build-in.o文件一起被组合成(使用“$(LD) -r”命令)当前目录下的build-in.o文件。这个build-in.o文件将被它的上一层Makefile使用。
obj-y中各个.o文件的顺序是有意义的,因为内核中用moudule_init
或__initcall
定义的函数将按照它们的连接顺序被调用。
例子1,当下面的CONFIG_ISDN、CONFIG_ISDN_PPP_BSDCOMP在.config
中被定义为y时,isdn.c或isdn.S、isdn_bsdcomp.c或isdn_bsdcomp.S被编译成isdn.o、isdn_bssdcomp.o。这两个.o文件被组合进buidl-in.o文件中,最后被链接进入内核。假如isdn.o、isdn_bsdcomp.o中分别用moudule_init(A)
、moudule_init(B)
定义了函数A、B,则内核启动时A先被调用,然后是B。
1 | obj-$(CONFIG_ISDN) += isdn.o |
②obj-m用来定义哪些文件被编译成可加载模块(Loadable module)。
obj-m中定义的.o文件有当前目录下的.c或.S文件编译生成,它们不会被编进build-in.o中,而是被编成可加载模块。
一个模块可以由一个或多个.o文件组成。对于只有一个源文件的模块,在obj-m中直接增加它的.o文件即可。对于有多个源文件的模块,除在obj-m中增加一个.o文件外,还要定义一个
例子2,当下面的CONFIG_ISDN_PPP_BSDCOMP在.config文件中被定义为m时,isdn_bsdcomp.c或isdn_bsdcomp.S将被编译成isdn_bsdcomp.o文件,它最后被制作成isdn_bsdcomp.ko模块。
1 |
|
例子3,当下面的CONFIG_ISDN在.config文件中被定义为m时,将会生成一个isdn.o文件,它由isdn-objs中定义的isdn-net-lib.o、isdn_v110.o、isdn_common.o等3个文件组合而成。isdn.o最后被制作成isdn.ko模块。
1 |
|
③lib-y用来定义哪些文件被编成库文件
lib-y中定义的.o文件由当前目录下的.c或.S文件编译生成,它们被打包成当前目录下的一个库文件:lib.a。
同时出现在obj-y、lib-y中的.o文件,不会被包含进lib.a中。
要把这个lib.a编译进入内核中,需要在顶层Makefile中libs-y变量中列出当前目录。要编成库文件的内核代码一般都在这两个目录下:lib/
、arch/$(ARCH)/lib
。
④obj-y、obj-m还可以用来指定要进入的下一级子目录。
Linux中一个Makefile文件只负责生成当前目录下的目标文件,子目录下的目标文件由子目录的Makefile生成。Linux的编译系统会自动进入这些子目录调用他们的Makefile,只是需要在进入之前指定这些子目录。
这要用到obj-y、obj-m,只要在其中增加这些子目录名即可。
例子4,fs/Makefile
中有如下一行,当CONFIG_JFFS2_FS被定义为y或m时,在编译时将会进入jffs2
目录进行编译。Linu的编译系统只会根据这些信息决定是否进入下一级目录,而下一级目录的文件如何编译成build-in.o或模块由它的Makefile决定。
1 | obj-$(CONFIG_JFFS2_FS) += jffs2 |
怎么编译这些文件
即编译选项、连接选项是什么。这些选项分为3类:全局的,适用于整个内核代码树;局部的,仅适用于某个Makefile中的所有文件;个体的,仅适用于某个文件。
全局选项在顶层Makefile和arch/$(ARCH)/Makefile
中定义,这些选项的名称为:CFLAGS、AFLAGS、LDFLAGS、ARFLAGS,它们分别是编译C文件的选项、编译汇编文件的选项、连接文件的选项、制作库文件的选项。
需要使用局部选项时,它们在各个子目录中定义,名称为:EXTRA_FLAGS、EXTRA_ALAGS、EXTRA_LDFLAGS、EXTRA_ARFLAGS,它们的用途与前述选项相同,只是适用范围比较小,它们针对当前的Makefile中的所有文件。
另外,如果想针对某个文件定义它的编译选项,可以使用CFLAGS_$@,AFLAGS_$@。前者用于编译某个C文件,后者用于编译某个汇编文件。$@表示某个目标文件名,比如以下代码表示编译aha152x.c时,选项中要额外加上“-DAHA152X_STAT -DAUTOCONF”。1
2
CFLAGS_aha152x.o = -DAHA152X_STAT -DAUTOCONF需要注意的是,这3类选项是一起用的,在
scropts/Makefile.lib
中可以看到。1
_c_flags = $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$(basetarget).o)
怎样连接这些文件,它们的顺序如何
前面分析有哪些文件需要编译进入内核时,顶层Makefile和arch/$(ARCH)/Makefile
定义了6类目录(或文件):head-y、init-y、drivers-y、net-y、libs-y和core-y。它们的初始值如下(以ARM体系为例):arch/arm/Makefile
中:1
2
3
4
5
6
7
8
9head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
...
core-y := arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
core-y := $(MACHINE)
core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2400/
core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2412/
core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2440/
...
libs-y := arch/arm/lib/ $(libs-y)顶层Makefile中:
1
2
3
4
5
6
7init-y := init/
drivers := drivers/ sound/
net-y := net/
libs-y := lib/
core-y := usr/
...
core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/可见,除head-y之外,其余的init-y、drivers-y等都是目录名。在顶层Makefile中,这些目录名的后面直接加上build-in.o或lib.a,表示要连接进内核的文件,如下所示:
1
2
3
4
5
6
7init-y := $(patsubst %/, %/build-in.o, $(init-y))
core-y := $(patsubst %/, %/build-in.o, $(core-y))
drivers-y := $(patsubst %/, %/build-in.o, $(drivers-y))
net-y := $(patsubst %/, %/lib.a, $(net-y))
libs-y1 := $(patsubst %/, %/build-in.o, $(libs-y))
libs-y2 := $(patsubst %/, %/build-in.o, $(libs-y))
libs-y := $(libs-y1 $(libs-y2))上面的patsubst是个字符串处理函数,它的用法如下:
1
$(patsubst pattern,replacement,text)
表示寻找“text”中符合格式“pattern”的字,用“replacement”替换它们。比如上面的init-y初值为“init/”,经过交换之后,“init-y”变为“init/build-in.o”。
顶层Makefile中,继续往下看:1
2
3
4vmlinux-init := $(head-y) $(init-y)
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
vmlinux-all := $(vmlinux-linux) $(vmlinux-main)
vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds第3行的
vmlinux-all
表示所有构成内核映像文件vmlinux的目标文件,从第1-3行可知这些目标文件的顺序为:head-y、init-y、core-y、libs-y、drivers-y、net-y,即arch/arm/kernel/head.o
(假设有MMU,否则为head-nommu.o)、arch/arm/kernel/init_task.o
、init/build-in.o
、usr/build-in.o
等。
第4行表示连接脚本为arch/$(ARCH)/kernel/vmlinux.lds
。对于ARM体系,连接脚本就是arch/arm/kernel/vmlinux.lds
,它由arch/arm/kernel/vmlinux.lds.S
文件生成。规则在scripts/Makefile.build
中,如下所示:1
2$(obj)/%.lds: $(src)/%.lds.S FORCE
$(call if_changed_dep,cpp_lds_S)现将生成的
arch/arm/kernel/vmlinux.lds
摘录如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41SECTIONS
{
...
. = 0xc0000000 + 0x00008000; /* 代码段起始地址,这是个虚拟地址 */
.text.head : {
_stext = .;
_sinittext = .;
*(.text.head)
}
.init : { /* 内核初始化的代码和数据 */
...
}
...
.text : { /* 真正的代码段 */
_text = .; /* 代码段和只读数据段的开始地址 */
...
}
/* 只读数据 */
. = ALIGN((4096)); .rodata:AT(ADDR(.rodata) - 0) {......} . = ALIGN((4096));
_etext = .; /* 代码段和只读数据段的结束地址 */
... ...
.data : AT(__data_loc) { /* 数据段 */
__data_start = .; /* 数据段起始地址 */
... ...
_edata = .; /* 数据段结束地址 */
}
_edata_loc = __data_loc + SIZEOF(.data); /* 数据段结束地址 */
.bss : { /* BSS段,没有初始化或初值为0的全局、静态变量 */
__bss_start = .; /* BSS段起始地址 */
*(.bss)
*(COMMON)
_end = .; /* BSS段结束地址 */
}
.stab 0 : { *(.stab) } /* 调试信息段 */
... ...下面对本节分析Makefile的结果作一下总结。
配置文件.config中定义了一系列的变量,Makefile将结合它们来决定哪些文件被编进内核、哪些文件被编成模块、涉及哪些子目录。
顶层Makefile和
arch/$(ARCH)/Makefile
决定根目录下哪些子目录、arch/$(ARCH)
目录下哪些文件和目录将被编进内核。最后,各级子目录下的Makefile决定所在目录下哪些文件将被编进内核,哪些文件将被编成模块(驱动程序),进入哪些子目录继续调用它们的Makefile。
顶层Makefile和
arch/$(ARCH)/Makefile
设置了可以影响所有文件的编译、连接选项:CFLAGS、AFLAGS、LDFLAGS、ARFLAGS。各级子目录下的Makefile中可以设置能够影响当前目录下所有文件的编译、连接选项:EXTRA_CFLAGS、EXTRA_AFLAGS、EXTRA_LDFLAGS、EXTRA_ARFLAGS;还可以设置可以影响某个文件的编译选项:CFLAGS_$@,AFLAGS_$@。
顶层Makefile按照一定的顺序组织文件,根据连接脚本
arch/$(ARCH)/kernel/vmlinux.lds
生成内核映像文件vmlinux。
内核的Kconfig分析
在内核目录下执行“make menuconfig ARCH=arm CROSS_COMPILE=arm-linux-”时,就会看到一个如下图所示的菜单:
这就是内核的配置界面。通过配置界面,可以选择芯片类型、选择需要支持的文件系统,去除不需要的选项等,这就称为“配置内核”。注意,也有其他形式的配置界面,比如“make config”命令启动字符配置界面,对于每个选项都会依次出现一行提示信息,逐个回答;“make xconfig”命令启动X-windows图形配置界面。
所有配置工具都是通过读取arch/$(ARCH)/Kconfig
文件来生成配置界面,这个文件是所有配置文件的总入口,它会包含其他目录的Kconfig文件。配置界面如上图所示。
内核源码的每个子目录中,都有一个Makefile文件和Kconfig文件。Makefile的作用前面已经讲述,Kconfig用于配置内核,它就是各种配置界面的源文件。内核的配置工具读取各个Kconfig文件,生成配置界面供开发人员配置内核,最后生成配置文件.config。
内核的配置界面以树状的菜单形式组织,主菜单下有若干子菜单,子菜单下又有子菜单或配置选项。每个子菜单或选项可以有依赖关系,这些依赖关系用来确定它们上是否显示。只有被依赖项的父项已经被选中,子项才会显示。
Kconfig文件的语法可以参考Documentation/kbuild/kconfig-language.txt
文件,下面讲述几个常用的语法,并在最后介绍菜单形式的配置界面操作方法。
Kconfig 文件的基本要素:config条目(entry)
config条目常被其他条目包含,用来生成菜单、进行多项选择等。
config条目用来配置一个选项,或者这么说,它用于生成一个变量,这个变量会连同它的值一起被写入配置文件.config中。比如有一个config条目用来配置CONFIG_LEDS_S3C24XX,根据用户的选择,.config文件中可能出现下面3种配置结果中的一个。
1 | CONFIG_LEDS_S3C24XX=y # 对应的文件被编进内核 |
以一个例子说明config条目格式,下面代码选自fs/Kconfig
文件,它用于配置CONFIG_JFFS2_FS_POSIX_ACL选项。
1 | config JFFS2_FS_POSIX_ACL |
代码中包含了几乎所有的元素,下面一一说明:
第1行中,config是关键字,表示一个配置选项的开始;紧跟着的JFFS2_FS_POSIX_ACL是配置选项的名称,省略了前缀“CONFIG_”。
第2行中,boot表示变量类型,即CONFIG_JFFS2_FS_POSIX_ACL的类型。有5种类型:bool、tristate、string、hex和int,其中的tristate和string是基本的类型,其他类型是它们的变种。boot变量取值有两种:y和n;tristate变量取值有3种:y、n和m;string变量取值为字符串;hex变量取值为十六进制的数据;int变量取值为十进制的数据。
“boot”之后的字符串是提示信息,在配置界面中上下移动光标选中它时,就可以通过按空格或回车来设置CONFIG_JFFS2_FS_POSIX_ACL的值。提示信息的完整格式如下,如果使用“if
1 | "prompt" <prompt> ["if" <expr>] |
第3行表示依赖关系,格式如下。只有JFFS2_FS_XATTR配置选项被选中时,当前配置选项中的提示信息才会出现,才能设置当前配置选项。注意,如果依赖条件不满足,则它取默认值。
1 | "depend on "/"requires" <expr> |
第4行的表示默认值为y,格式如下:
1 | "default" <expr> ["if" <expr>] |
第5行表示当前配置选项JFFS2_FS_POSIX_ACL被选中时,配置选项FS_POSIX_ACL也会被自动选中,格式如下:
1 | "select" <symbol> ["if" <expr>] |
第6行表示下面几行是帮助信息,帮助信息的关键字有如下两种,它们完全一样。当遇到一行的缩进距离比第一行帮助信息的缩进距离小时,表示帮助信息已经结束。
1 | "help" or "---help---" |
menu 条目
menu条目用于生成菜单,格式如下:
1 | "menu" <prompt> |
它的实际使用并不如它的标准格式那样复杂,下面是一个例子:
1 | menu "Floating point emulation" |
menu之后的字符串是菜单名,“menu”和“endmenu”之间有很多config条目。在配置界面会出现如下字样的菜单,移动光标选中它后按回车键进入,就会看到这些config条目定义的配置选项。
1 | Floating point emulation ---> |
choice 条目
choice条目将多个类似的配置选项组合在一起,供用户单选或多选,格式如下:
1 | "choice" |
实际使用中,也是在“choice”和“endchoice”之间定义多个config条目,比如arch/arm/Kconfig
中有如下代码:
1 | choice |
prompt“ARM system type”给出提示信息“ARM system type”,光标选中它之后按回车键进入,就可以看到多个config条目定义的配置选项。
choice条目中定义的变量类型只能有两种:bool和tristate,不能同时有这两种类型的变量。对于bool类型的choice条目,只能在多个选项中选择一个;对于tristate类型的choice条目,要么就把一个或多个选项色设为m;要么就像bool类型的choice条目一样,只能选择一个。这是可以理解的,比如对于同一个硬件,它有多个驱动程序,可以选择将其中之一编译进内核(配置选项设置为y),也可以都将它们编译成模块(配置选项设置为m)。
comment 条目
comment条目用于定义一些帮助信息,它在配置过程中出现界面的第一行;并且这些帮助信息会出现在配置文件中,格式如下:
1 | "comment" <prompt> |
实际使用也很简单,比如arch/arm/Kconfig
1 | menu "Floating point emulation" |
进入菜单“Floating point emulation —>”之后,在第一行会看到如下内容:
1 | --- At least one emulation must be selected |
而在.config文件中也会看到如下内容:
1 | # |
source 条目
source 条目用于读入另一个Kconfig文件,格式如下:
1 | "source" <prompt> |
下面是一个例子,取自arch/arm/Kconfig
,它读入net/Kconfig
文件。
1 | source "net/Kconfig" |
菜单形式的配置界面操作方法
配置界面的开始几行就是它的操作方法,如下图所示:
内核scripts/kconfig/mconf.c
文件的注释给出了更详细的操作方法,讲解如下:
一些特定功能的文件可以直接编译进内核中,或者编译成一个可加载模块,或者根本不使用它们。还有一些内核参数,必须给它们赋一个值:十进制数、十六进制数,或者一个字符串。
配置界面中,以[*]
、<M>
或[]
开头的选项表示相应的功能被编译进内核中、被编译成一个模块,或者没有使用。尖括号<>
表示相应功能的文件可以被编译成模块。↑
、↓
方向键用来高亮选中某个配置选项,如果要进入某个子菜单,先选中它,然后按回车键进入。配置选项的名字后有--->
表示它是一个子菜单。配置选项的名称中有一个高亮的字母,它被称为热键(hotkey)
,直接输入热键就可以选中该配置选项,或者循环选中具有相同热键的配置选项。
可以使用翻页键<Page Up>
、<Page Down>
来移动配置界面中的内容。
要退出配置界面,使用←
、→
方向键选中<Exit>
按钮,然后按回车键。如果没有配置选项使用后面这些按键作为热键的话,也可以按两次<ESC>
或<E><X>
键退出。
按<TAB>
键可以在<Select>
、Exit
、Help
这3个按钮中循环选中它们。
要想阅读某个配置选项的帮助信息,选中它之后,再选中<Help>
按钮,按回车键;也可以选中配置选项后,直接按<H>
或<?>
键。
对于choice条目中的多个配置选项,使用方向键高亮选中某个配置选项,按<S>
或空格键选中它;也可以通过输入配置选项的首字母,按<S>
或空格键选中它。
对于int、hex或string类型的配置选项,要输入它们的值时,先高亮选中选中它,按回车键,输入数据,再按回车键。对于十六进制数据,前缀0x可以省略掉。
配置界面的最下面,如下图红框所示:
前者用于加载某个配置文件,后者用于将当前的配置保存到某个配置文件中去。需要注意的是,如果不使用这两个选项,配置的加载文件、输出文件都默认为.config文件;如果加载了其他的文件(假设文件名为A),然后在它的基础上进行修改,最后退出保存时,这些变化会保存到A中去,而不是.config。
当然,可以先加载文件A,然后修改,最后保存到.config中去。
Linux 内核配置选项
Linux内核配置选项多达上千个,一个个的进行选择既耗费时间,对开发人员的要求也比较高(需要了解每个配置选项的作用)。一般的做法是在某个默认配置文件的基础上进行修改,比如我们可以先加载配置文件arch/arm/configs/s3c2410_defconfig
,再增加、去除某些配置选项。
配置界面主菜单的类别
下表讲解了主菜单的类别,以后读者配置内核时,可以根据自己所要设置的功能进入某个菜单,然后根据其中的各个配置选项的帮助信息进行配置。
配置界面主菜单 | 描述 |
---|---|
Code maturity level options | 代码成熟度选项:用于包含一些正在开发的或者不成熟的代码、驱动程序。一般不设置 |
Genaral setup | 常规设置:比如增加附加的内核版本号、支持 内存交换功能、System V进程间通信等。除非很熟悉其中的内容,否则一般使用默认配置 |
Loadable module support | 可加载模块支持:一般都会打开可加载模块支持(Enable loadable module support)、允许卸载已经加载到模块(Module unloading)、让内核通过运行modprobe来自动加载所需要的模块(Automatic kernel module loading) |
Block layer | 块设备层:用于设置块设备的一些总体参数,比如是否支持大于2TB的块设备、是否支持大于2TB的文件、设置I/O调度器等。一般使用默认值即可 |
System Type | 系统类型:选择CPU的架构、开发板类型等与开发板相关的配置选项 |
Bus support | PCMCIA/CardBus总线的支持 |
Kernel Features | 用于设置内核的一些参数,比如是否支持内核抢占、是否支持动态修改系统时钟等 |
Boot options | 启动参数:比如设置默认的命令行参数等 |
Floating point emulation | 浮点运算仿真功能:目前Linux 还不支持硬件浮点运算,所以要选择一个浮点仿真器,一般选择“NWFPE math emulation” |
Userspace binary formats | 可执行文件格式:一般都支持ELF、a.out格式 |
Power management options | 电源管理选项 |
Networking | 网络协议选项:一般选择“Networking support”以支持网络功能,选择“Packet socket”以支持socket接口功能,选择“TCP/IP networking”以支持TCP/IP网络协议。通常可以在选择“Networking support”后使用默认配置 |
Device Drivers | 设备驱动程序:几乎包含了Linux的所有驱动程序 |
File systems | 文件系统:可以在里面选择要支持的文件系统,比如EXT2、JFFS2等 |
Profiling support | 对系统的活动进行分析,仅供内核开发者使用 |
Kernel hacking | 调试内核时的各种选项 |
Security options | 安全选项,一般使用默认配置 |
Cryptographic options | 加密选项 |
Library routines | 库子程序:比如CRC32检验函数、zlib压缩函数等。不包含在内核源码中的第三方内核模块可能需要这些库,可以全不选,内核中若有其他部分依赖它,会自动选上 |
“System Type” 菜单:系统类型
对于arm平台(在顶层Makefile中修改“ARCH ?= arm”),执行“make menuconfig”后,在配置界面可以看到“System Type”字样,进入它得到另一个界面,如下图所示:
第一行“ARM system type”用来选择体系结构,进入它之后选中“Samsung S3C2410,S3C2412,S3C2413,S3C2440,S3C2442,S3C2443”,查看帮助信息可以知道它对应CONFIG_ARCH_S3C2410配置项。
下面几行用来设置S3C2410(包括S3C2412等)系统的特性,比如选中“S3C2410 UART to use for low-level message”后按回车键,可以输入数字,表示使用哪个串口来输入内核打印信息:选中“S3C2410 DMA support”表示支持DMA功能。
再往下的“S3C2410 Machine —>”、“S3C2440 Machine —>”表示这又是一个菜单,它们用来选择开发板类型。比如进入“S3C2410 Machine”菜单后,可以看到如下内容:
它们表示目前内核中支持S3C2410的8种开发板。选中某个开发板之后,它相应的文件就会被编译进内核中。比如对于开发板SMDK2410/A9M2410
,它的配置项为CONFIG_ARCH_SMDK2410,在arch/arm/mach-s3c2410/Makefile
中可以看到如下一行,表示如果选择支持该开发板,则arch/arm/mach-s3c2410/mach-smdk2410.c
文件被编进内核中。
1 | obj-$(CONFIG_ARCH_SMDK2410) += mach-smdk2410.o |
在移植内核时,可以选中某个配置相似的开发板,然后在上面进行修改。
“Device Drivers” 菜单:设备驱动程序
执行“make menuconfig”后,在配置界面可以看到“Device Drivers”字样,进入它得到另一个界面,如下图所示:
图中的各个子菜单与内核源码drivres/
目录下各个子目录一一对应,如下表所示,在配置过程中可以参考这个表格找到对应的配置选项,在添加新驱动时,也可以参考它来决定代码放在哪个目录下。
Device Drivers 子菜单 | 描述 |
---|---|
Generic Driver Options | 对应divers/base 目录,这是设备驱动程序中一些基本和通用的配置选项 |
Connector - undefined userspace <-> kernelspace linker | 对应divers/connector 目录 |
Memory Technology Device (MTD)support | 对应divers/mtd 目录,用于支持各种新型的存储设备,比如NOR Flash、NAND Flash等 |
Parallel port support | 对应divers/parport 目录,用于支持各种并口设备 |
Plug and Play support | 对应divers/pnp 目录,支持各种“即插即用”设备 |
Block devices | 对应divers/block 目录,包括回环设备、RAMDISK等的驱动 |
ATA/ATAPI/MFM/RLL support | 对应divers/ide 目录,它用来支持ATA/ATAPI/MFM/RLL 接口的硬盘、软盘、光盘等 |
SCSI device support | 对应divers/scsi 目录,支持各种scsi接口的设备 |
Serial ATA(prod)and parallel ATA (experiment)drivers | 对应divers/ata 目录,支持SATA与PATA设备 |
Multi-device support (RAID and LVM) | 对应divers/md 目录,表示多设备支持(RAID和LVM),RAID和LVM的作用是使多个物理设备组建成一个单独的逻辑磁盘 |
Network device support | 对应divers/net 目录,用来支持各种网络设备,比如CS8900、DM9000等 |
ISDN subsystem | 对应divers/isdn 目录,用来提供综合业务数字网(Integrate Service Digital Network)的驱动程序 |
Input device support | 对应divers/input 目录,支持各类输入设备,比如键盘、鼠标等 |
Character devices | 对应divers/char 目录,它包含各种字符设备驱动程序。串口的配置也是从这个菜单调用的,但是串口的代码在drivers/serial 下 |
I2C support | 对应divers/i2c 目录,支持各类I2C设备 |
SPI support | 对应divers/spi 目录,支持各类SPI设备 |
Dallas’s 1-wire bus | 对应divers/w1 目录,支持一线总线 |
Hardware Monitoring support | 对应divers/hwmon 目录 |
Misc devices | 对应divers/misc 目录,用来支持一些不好分类的设备,称为杂项设备 |
Multifuction device drivers | 对应divers/mfd 目录,用来支持多功能的设备,比如SM501,它既可以用于显示图像,也可以用作串口 |
LED devices | 对应divers/leds 目录,包含各种LED驱动程序 |
Multimedia devices | 对应divers/media 目录,包含多媒体驱动,比如V4L(Video for Linux),它用于向上提供统一的图像、声音接口 |
Graphics support | 对应divers/video 目录,提供图形设备/显卡的支持 |
Sound | 对应sound/ 目录(它不在drivers下),用来支持各种声卡 |
HID Devices | 对应divers/hid 目录,用来支持各种USB-HID设备,或者符合USB-HID规范的设备(蓝牙)。HID(Human Interface Device),比如各种USB接口的鼠标/键盘/游戏杆/手写板等输入设备 |
USB support | 对应divers/usb 目录,包括各种USB Host和USB Device 设备 |
MMC/SD card support | 对应divers/mmc 目录,用来支持各种MMC/SD卡 |
Real Time Clock | 对应divers/rtc 目录,用来支持各种实时时钟设备。比如S3C24x0上就集成了RTC芯片 |
Linux 内核移植
本节将修改Linux-2.6.22.6内核,使得它可以同时在本书使用的S3C2410、S3C2440开发板上运行,并修改相关驱动使它支持网络功能、支持JFFS2、YAFFS文件系统,同时修改MTD设备分区,使得内核可以挂接NAND Flash上的文件系统。
Linux 内核启动过程概述
与移植U-Boot的过程相似,在移植Linux之前,先了解它的启动过程。Linux的启动过程可以分为两部分:架构/开发板相关的引导过程、后续的通用启动过程。下图所示是ARM架构处理器上Linux内核vmlinux的启动过程。之所以强调是vmlinux,是因为其他格式的内核在进行与vmlinux相同的流程之前会有一些独特的操作。比如对于压缩格式的内核zImage,它首先进行自解压得到vmlinux,然后执行vmlinux开始正常的启动流程。
引导阶段通常使用汇编语言编写,它首先检查内核是否支持当前架构的处理器,然后检查是否支持当前开发板。通过检查后,就为调用下一阶段的start_kernel
函数准备了。主要分为如下两个步骤。
- 连接内核时使用的虚拟地址,所以要设置页表、使能MMU。
- 调用C函数
start_kernel
之前的常规工作,包括复制数据段、清除BSS段、调用start_kernel
函数。
第二阶段的关键代码主要使用C语言编写。它进行内核初始化的全部工作,最后调用rest_init
函数启动init过程,创建系统第一个进程:init进程。在第二阶段,仍有部分架构/开发板相关的代码,比如下图中的setup_arch
函数用于进行架构/开发板相关的设置(重新设置页表、设置系统时钟、初始化串口等)。
修改Linux内核以支持S3C2410/S3C2440开发板
首先配置、编译内核,确保内核可以正确编译。得到内核源码后,先修改顶层Makefile,如下所示:
1 | ARCH ?= $(SUBARCH) |
然后执行如下命令,使用arch/arm/configs/smdk2410_defconfig
文件来配置内核,它生成.config配置文件,后面就可以直接使用“make menuconfig”修改配置了。
1 | make smdk2410_defconfig |
最后是编译生成内核,执行“make”命令将在顶层目录下生成内核映像文件vmlinux;执行“make uImage”除生成vmlinux外,还在arch/arm/boot
目录下生成U-Boot格式的内核映像文件uImage。
对于S3C2410开发板,上面生成的uImage是可以使用的。在U-Boot控制界面中使用如下命令下载uImage并启动它:
1 | tftp 0x32000000 uImage 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage/ |
在串口可以看到内核的启动信息,只是在最后看到如下的panic信息,这是因为没有修改MTD分区,没有增加对yaffs文件系统的支持。
1 | VFS: Uable to mount root fs via NFS, trying flopping |
对于S3C2440开发板,使用同样的命令启动uImage,在打印如下信息之后(U-Boot打印),就会出现一大堆乱码:
所以,Linux 2.6.22.6还不支持本书所用的S3C2440开发板,这个开发板的配置与内核所支持的开发板不一致。
要让内核支持当前的S3C2410开发板,需要进行一些修改。
引导阶段代码分析
由前面对内核Makefile的分析,可知arch/arm/kernel/head.S
是内核执行的第一个文件。另外。U-Boot调用内核时,r1寄存器中存储“机器类型ID”,内核会用到它。
移植Linux内核时,对于arch/arm/kernel/head.S
,只需要关注开头几条命令,如下所示:
1 | ENTRY(stext) |
第2行通过设置CPSR寄存器来确保处理器进入管理(SVC)模式,并且禁止中断。
第3行读取协处理器CP15的寄存器C0获得CPU ID,CPU ID格式如下所示:
位 | 含义 |
---|---|
[31:24] | 厂商编号有如下值。 0x41 = A,表示ARM公司 0x44 = D,表示Digital Equipment公司 0x69 = 1,表示Intel公司 |
[23:20] | 由厂商定义,当产品编号相同时,使用子编号来区分不同的产品子类,如产品中不同的高速缓存大小等 |
[19:16] | ARM体系版本号,目前取值如下。 0x01,表示ARM体系版本4 0x02,表示ARM体系版本4T 0x03,表示ARM体系版本5 0x04,表示ARM体系版本5 0x05,表示ARM体系版本5T 0x06,表示ARM体系版本5TE |
[15:4] | 产品主编号 |
[3:0] | 处理器版本号 |
比如S3C2410的CPU ID为0x41129200,S3C2440的CPU ID也是0x41129200。注意,S3C2410和S3C2440称为片山系统(SOC),除CPU外,还集成了UART、USB控制器、NAND Flash控制器等设备。从它们的CPU ID可知,它们的CPU是相同的,只是片上外设不一样。
第4行调用__lookup_processor_type
函数,确定内核是否支持当前CPU。如果支持,r5寄存器将会返回一个用来描述处理器的结构体的地址,否则r5的值为0。
第7行调用__lookup_machine_type
函数,确定内核是否支持当前开发板。如果支持,r5寄存器将会返回一个用来描述这个开发板的结构体的地址,否则r5的值为0。
如果__lookup_processor_type
、__lookup_machine_type
这两个函数中有一个返回值为0,则内核不能启动,如果配置内核时选择了CONFIG_DEBUG_ALL,还会打印一些提示信息。__lookup_processor_type
、__lookup_machine_type
函数都是在arch/arm/kernel/head-common.S
中定义的。
内核映像中,定义了若干个proc_info_list
结构(结构体原型在include/asm-arm/procinfo.h中定义),表示它支持的CPU。对于ARM架构的CPU,这些结构体的源码在arch/arm/mm/
目录下,比如proc-arm920.S
中的如下代码,它表示arm920 CPU的proc_info_list结构。
1 | .section ".proc.info.init", #alloc, #execinstr |
不同的proc_info_list
结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中。在连接内核时,这些结构体被组织在一起,开始地址为__proc_info_begin
,结束地址为__proc_info_end
。这可以从连接脚本arch/arm/kernel/vmlinux.lds
中看出来。
1 | __proc_info_begin = .; //proc_info_init结构的开始地址 |
__lookup_processor_type
函数就是根据前面读出的CPU ID (存在r9 寄存器中),从这些proc_info_init
结构中找出匹配的,它的代码如下(arch/arm/kernel/head-common.S
):
1 | .type __lookup_processor_type, %function |
在调用__enable_mmu
函数之前使用的都是物理地址,而内核却是以虚拟地址连接的。所以在访问proc_info_list
结构前,先将它的虚拟地址转换为物理地址,第3-7行就是用来转换地址的。
第3行用来获得第26行代码的物理地址。adr指令基于pc寄存器计算地址值,由于这时候还没使能MMU,pc寄存器中使用的还是物理地址,所以执行“adr,r3,3f”后,r3寄存器中存放的就是第26行代码的物理地址。
第4行用来获得第24-26行定义的数据:__proc_info_begin、__proc_info_end和“.”。这3个数据都是在连接内核时确定,它们是虚拟地址,前两个表示proc_info_list
结构的开始地址和结束地址,“.”表示当前行的代码在编译连接后的虚拟地址。
第5行计算物理地址和虚拟地址的差值,第6-7行根据这个差值计算__proc_info_begin
、__proc_info_end
的物理地址。
下面的代码依次读取每个proc_info_list
结构前面的两个成员(cpu_val和cpu_mask),判断cpu_val是否等于(r9 & cpu_mask),r9是arch/arm/kernel/head.S
中调用__lookup_processor_type
时传入的CPU ID。如果比较相等,则表示当前proc_info_list
结构适用于这个CPU,直接返回这个结构的地址(存在r5中)。如果__proc_info_begin
、__proc_info_end
之间的所有proc_info_list
结构都不支持这个CPU,则返回0(r5)。
对于S3C2410、S3C2440开发板,它们的CPU ID都是0x41129200,而在arch/arm/mm/proc-arm920.S
中定义的__arm920_proc_info
结构中,cpu_val、cpu_mask等于0x41009200、0xff00fff0,刚好匹配。内核中要包含这个文件,在arch/arm/mm/Makefile
中可以看到下面这行,它表示需要配置CONFIG_CPU_ARM920T(配置菜单中System Type->Support ARM920T procrssor)。
1 | obj-$(CONFIG_CPU_ARM920T) += proc_arm920.o |
下面讲解__ lookup_machine_type
,它和__lookup_processor_type
函数代码相似。
内核中对于每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc
结构,它定义了开发板相关的一些属性和函数。比如机器类型ID、起始I/O物理地址,Bootloader传入的参数的地址、中断初始化函数、I/O映射函数等。比如对于SDMK2440开发板,在arch/arm/mach-s3c2440/mach-smdk2440.c
中定义如下:
1 | MACHINE_START(S3C2440, "SMDK2440") |
宏MACHINE_START
、MACHINE_END
在include/asm-arm/mach/arch.h
文件中定义
1 | /* |
所以上一段代码扩展开来就是:
1 | static const struct machine_desc __mach_desc_S3C2440 \ |
其中的MACH_TYPE_S3C2440在arch/arm/tools/mach-types
中定义,它最后会被换成一个头文件include/asm-arm/mach-types.h
供其他文件包含。machine_desc在include/asm-arm/mach/arch.h
文件中定义。所有的machine_desc结构都处于“.arch.info.init”段中,在连接内核时,它们被组织在一起,开始地址为__arch_info_begin
,结束地址为__arch_info_end
。这可以从连接脚本文件arch/arm/kernel/vmlinux.lds
中看出来:
1 | __arch_info_begin = .; //machine_desc结构的开始地址 |
不同的machine_desc结构用于不同的开发板,U-Boot调用内核时,会在r1寄存器中给出开发板的标记(机器类型ID)。__loockup_machine_type
函数将这个值与machine_desc中的nr成员比较,如果两者相等则表示找到匹配的machine_desc结构,于是返回它的地址(存在r5中)。如果__arch_info_begin
和__arch_info_end
之间所有的machine_desc结构的nr成员都不等于r1寄存器的值,则返回0(r5)。
对于本书所用的S3C2410、S3C2440开发板,U-Boot传入的机器类型ID为MACH_TYPE_SMDK2410、MACH_TYPE_SMDK2440。它们对应的machine_desc结构分别在arch/arm/mach-s3c2440/mach-smdk2440.c
和arch/arm/mach-s3c2410/mach-smdk2410.c
中定义,所以两个文件要编进内核。在配置菜单中,选中下面两个开发板即可。
1 | System Type --> S3C2410 Machines --> SMDK2410/A9M2410 |
__lookup_machine_type
函数的代码如下(/arch/arm/kernel/head-common.S
):
1 | 3: .long . |
如果__lookup_processor_tyep
、__lookup_machine_type
函数都返回成功,则后续引导程序将继续执行下去。其中的__create_page_tables
函数用来创建一级页表以建立虚拟地址到物理地址的映射关系,它用到__lookup_processor_type
函数返回的proc_info_list结构。在引导的最后,调用start_kernel
函数进入内核启动的第二阶段。__lookup_machine_type
函数确定的machine_desc结构将在第二阶段多次使用。
start_kernel 函数部分代码分析
进入start_kernel
函数(init/main.c
)之后,如果串口上没有看到内核的启动信息,一般而言有两个原因:Bootloader传入的命令行参数不对,或者setup_arch
函数(arch/arm/kernel/setup.c
)针对开发板的设置不正确。
在调用setup_arch
函数之前就已经调用“printk(linux_banner)”
了,但是这个时候printk函数只是将打印信息放在缓存区中,并没有打印到控制台上(串口、LCD屏等),因为这个时候控制台还未初始化。printk打印的内容在console_init
函数注册、初始化控制台之后才真正输出。
移植U-Boot时,U-Boot传给内核的参数有两类:预先存在某个地址的tag列表和调用内核时在r1寄存器中指定的机器类型ID。后者在引导阶段的__lookup_machine_type
函数已经用到。而tag列表将在setup_arch
函数中进行初步处理。本节将重点介绍setup_arch
函数、console_init
函数、以tag列表的处理(内存tag、命令行tag)、串口控制台的初始化为主线。
setup_arch 函数分析
setup_arch
函数在arch/arm/kernel/setup.c
中定义,其部分代码及流程图如下:
1 | void __init setup_arch(char **cmdline_p) |
首先,第[4]行的setup_processor
函数被用来进行处理器相关的一些设置,它会调用引导阶段的lookup_processor_type
函数以获得该处理器的proc_info_list结构。
接下来,第[5]行的setup_machine
函数被用来获得开发板的machine_desc结构,这通过调用引导阶段lookup_machine_type
函数来实现。以后就会根据开发板的machine_desc结构来进行一些开发板的相关操作,
第[7]-[8]行用来确定Bootloader传入的启动参数的地址,它在开发板的machine_desc结构中指定,第[8]行将它转换为虚拟地址。比如对于S3C2440开发板,在arch/arm/mach-s3c2440/mach-smdk2440.c
中有如下定义。启动参数的地址就是(S3C2410_SDRAM_PA + 0x100),即0x30000100。
1 | MACHINE_START(S3C2440,"SMDK2440") |
第[13]行处理每个tag。文件arch/arm/kernel/setup.c
对每种tag都定义了相应的处理函数,比如对于内存tag、命令行tag,使用如下两行代码指定了它们的处理函数为parse_tag_mem32、parse_tag_cmdline
。
1 | __tagtable(ATAG_MEM,parse_tag_mem32); |
parse_tag_mem32
函数根据内存tag定义的内存起始地址、长度,在全局结构变量meminfo中增加内存的描述信息。以后内核就可以通过meminfo结构了解开发板的的内存信息。parse_tag_cmdline
只是简单的将命令行tag的内容复制到字符串default_command_line中保存下来,后面才进一步处理。
第[18]行扫描命令行参数,对其中的一些参数进行先期的处理。这些参数使用“__early_param”来定义,比如arch/arm/kernel/setup.c
中下面的一行代码,它表示如果命令行中有“mem=…”的字样,就调用early_mem
(在include/asm-arm/setup.h
中定义)对它进行处理:
1 | __early_param("mem=",early_mem); |
“mem=…”用来强制限制Linux系统所能使用的内存总量,比如“mem=60M”使得系统只能使用60MB的内存,即使内存tag中指明了共有64MB内存。类似的参数还有“initrd=”等。
此时命令行的处理还没有结束,在setup_arch函数之外还会进行一系列后续处理,比如start_kernel
函数中调用如下代码:
1 | setup_command_line(command_line); |
比如对于命令行中的“console=ttySAC0”,它的处理过程就是第[4]行的parse_args
函数调用第[6]行传入的unknown_bootoption
函数,最后调用下面代码指定的处理函数console_setup
(在kernel/printk.c
中定义)。
1 | __setup("console=",console_setup); |
命令行参数“console=…”用来指定要使用的控制台的名称、序号、参数。比如对于“console=ttySAC0,115200”,表示要使用的控制台名称为ttySAC,序号为0(第一个串口),波特率为115200。经过console_setup
处理后,会在全局结构变量console_cmdline中保存这些信息,在后面console_init
函数初始化控制台时会根据这些信息选择要使用的控制台。setup_arch
函数后面会调用paging_init
函数,这也是一个开发板相关的函数。
paging_init函数分析
这个函数在setup_arch
函数中的调用形式如下:
1 | paging_init(&meminfo,mdesc); |
meminfo中存放内存的信息,前面解释内存tag时确定了构建这个全局结构。
mdesc就是前面lookup_machine_type
函数返回的machine_desc结构。对于S3C2440开发板,这个结构在arch/arm/mach-s3c2440/mach-smdk2440.c
中有如下定义:
1 | MACHINE_START(S3C2440, "SMDK2440") |
上面几行代码是移植Linux必须关注的数据结构。对于S3C2410开发板,它在arch/arm/mach-s3c2410/mach-smdk2410.c
paging_init
函数在arch/arm/mm/mmu.c
中定义,根据我们的移植目的–让内核可以在S3C2440上运行。关注如下流程:
1 | paging_init -> devicemaps_init -> mdesc -> map_io() |
对于S3C2440开发板,就是调用smdk2410_map_io
函数,它也是arch/arm/mach-s3c2440/mach-s3c2440.c
中定义。
1 | static void __init smdk2440_map_io(void) |
上述三个函数所实现的功能,从名字就可以看出,第四行中参数值表示开发板晶振的频率。当前开发板所使用的晶振频率是12MHz,不是16934400,这就是S3C2440开发板上启动uImage时串口输出乱码的原因,将它改为12000000即可。
console_init 函数分析
虽然上面已经找到内核无法正常输出信息的原因,但我们不该止步于此。在2.4的内核中,命令行参数常用“console=ttyS0”来指定控制台为串口0,在2.6版本的内核中改为“console=ssySAC0”。分析console_init
函数的功能就可以了解这点。console_init
函数被start_kernel
函数调用,它在drivers/char/tty_io.c
文件中定义
1 | void __init console_init(void) |
它调用地址范围__con_initcall_start
至__con_initcall_end
之间的定义的每个函数,这些函数使用console_initcall
宏来指定。比如drivers/serial/s3c2410.c
中:
1 | console_initcall(s3c24xx_serial_initconsole); |
s3c24xx_serial_initconsole
函数也是在drivers/serial/s3c2410.c
中定义,它初始化S3C24xx类SoC的串口控制台,部分代码如下:
1 | static int s3c24xx_serial_initconsole(void) |
s3c24xx_serial_console结构在drivers/serial/s3c2410.c
中定义如下:
1 | static struct console s3c24xx_serial_console = |
register_console(&s3c24xx_serial_console);
在内核中注册控制台,就是把s3c24xx_serial_console结构链入一个全局链表console_drivers中(它在kernel/printf.c
中定义)。并且使用其中的name和index与前面的“console=…”指定的控制台相比较,如果相符,则以后的printk信息从这个控制台输出。
对于本书的情况,“console=ttySAC0”,而s3c24xx_serial_console结构中名字为“ttySAC”,序号为-1(可取任意值),所以两者匹配,printk信息将从串口0输出。
现在总结一下上面分析的内核启动第二阶段的函数调用过程
,相同的缩进表示它们是在同一个函数中被调用:
1 | start_kernel -> |
修改内核
在arch/arm/mach-s3c2440/mach-smdk2440.c
中做如下修改:
1 | s3c24xx_init_clocks(16934400); |
然后执行“make uImage”生成uImage。
对于S3C2410、S3C2440开发板,上面生成的uImage都可以使用了。
1 | tftp 0x32000000 uImage 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage |
可以看到内核的启动信息,最后出现panic信息(这需要修改mtd分区、增加对yaffs文件系统的支持)。
修改 MTD 分区
MTD(Memory Technology Device),即内存技术设备,是Linux中对ROM、NOR Flash、NAND Flash等存储设备抽象出来的设备层。它向上提供统一的访问接口:读、写、擦除等;屏蔽了底层硬件的操作、各类存储设备的差别。得益于MTD设备的作用,重新划分NAND Flash的分区很简单。
驱动对设备的识别过程
驱动程序识别设备时,有以下两种方法。
- 驱动程序本身带有设备的信息,比如开始地址、中断号等;加载驱动程序时,就可以根据这些信息来识别设备。
- 驱动程序本身没有设备的信息,但是内核中已经根据其他方式确定了很多设备的信息;加载驱动程序时,将驱动程序与这些设备逐个比较,确定两者是否匹配,如果成功匹配,那么就可以通过驱动程序操作这个设备了。
内核常使用第二种方法来识别设备,这可以将各种设备集中在一个文件中管理,当开发板的配置改变时,便于修改代码。在内核文件include/linux/paltform/device.h
中,定义了两个数据结构来表示这些设备和驱动程序:paltform_device结构用来描述设备的名称、ID、所占用的资源(内存地址/大小、中断号)等;platform_driver结构用来描述各种操作函数,比如枚举函数、移除设备函数、驱动的名称等。
内核启动后,首先构造链表将描述设备的platform_device结构组织起来,得到一个设备的列表;当加载某个驱动程序的platform_driver结构时,使用一些匹配函数来检查驱动程序能否支持这些设备,常用的检察方式很简单:比较驱动程序和设备的名称。
以S3C2440开发板为例,在arch/arm/mach-s3c2440/mach-smdk2440.c
中定义了如下设备:在1
2
3
4
5
6
7static struct platform_device *smdk2440_devices[] __initdata = {
&s3c_device_usb,
&s3c_device_lcd,
&s3c_device_wdt,
&s3c_device_i2c,
&s3c_device_iis,
};arch/arm/plat-s3c24xx/common-smdk.c
中定义了如下设备:这些设备在1
2
3
4
5
6
7static struct platform_device __initdata *smdk_devs[] = {
&s3c_device_nand,
&smdk_led4,
&smdk_led5,
&smdk_led6,
&smdk_led7,
};smdk2410_init
函数或smdk2440_init
函数中,通过platform_add_devices
函数注册进内核中。
NAND Flash设备s3c_device_nand在arch/arm/plat-s3c24xx/devs.c
中的定义如下:对于S3C2440开发板,s3c_device_nand结构的名字会在s3c24xx_map_io函数中修改为“s3c2440-nand”,这个函数在1
2
3
4
5
6struct platform_device s3c_device_nand = {
.name = "s3c2410-nand",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_nand_resource),
.resource = s3c_nand_resource,
};arch/arm/plat-s3c24xx/s3c244x.c
中的定义如下:有了NAND Flash设备,还要有NAND Flash驱动程序,内核针对S3C2410、S3C2412、S3C2440定义了3个驱动。它们在1
2
3
4
5
6
7void __init s3c244x_map_io(struct map_desc *mach_desc, int size)
{
...
s3c_device_i2c.name = "s3c2440-i2c";
s3c_device_nand.name = "s3c2440-nand";
s3c_device_usbgadget.name = "s3c2440-usbgadget";
}drivers/mtd/s3c2410.c
中的s3c2410_nand_init
函数中注册进内核,如下所示:其中的s3c2440_nand_driver结构也是在相同的文件中定义,如下所示:1
2
3
4
5
6
7
8static int __init s3c2410_nand_init(void)
{
printk("S3C24XX NAND Driver, (c) 2004 Simtec Electronics\n");
platform_driver_register(&s3c2412_nand_driver);
platform_driver_register(&s3c2440_nand_driver);
return platform_driver_register(&s3c2410_nand_driver);
}可见,s3c_device_nand结构和s3c2440_nand_driver结构中的name成员相同,都是“s3c2440-nand”。1
2
3
4
5
6
7
8
9
10static struct platform_driver s3c2440_nand_driver = {
.probe = s3c2440_nand_probe,
.remove = s3c2410_nand_remove,
.suspend = s3c24xx_nand_suspend,
.resume = s3c24xx_nand_resume,
.driver = {
.name = "s3c2440-nand",
.owner = THIS_MODULE,
},
};platform_driver_register
函数就是根据这点确定它们是匹配的,所以调用s3c2440_nand_probe
函数来枚举NAND Flash设备s3c_device_nand。
从s3c2440_nand_probe
函数开始,可以一直找到对NAND Flash分区的识别,如下所示:这些函数都在1
2
3
4
5s3c2440_nand_probe(&s3c_device_nand) -> //这个参数是为了便于理解加上的
s3c24xx_nand_probe(&s3c_device_nand, TYPE_S3C2440) -> //
struct s3c2410_platform_nand *plat = to_nand_plat(pdev) -> //plat = &smdk_nand_info
s3c2410_nand_add_partition(info, nmtd, sets); -> //sets就是smdk_nand_info
add_mtd_partitions //实际的参数为smdk_default_nand_partdrivers/mtd/nand/s3c2410.c
中定义,最后的add_mtd_partitions
函数根据smdk_default_nand_part结构来确定分区。这个结构在arch/arm/plat-s3c24xx/common-smdk.c
中定义,要改变分区时修改它即可。
修改 MTD 分区
如上所述,要改变分区时,修改arch/arm/plat-s3c24xx/common-smdk.c
文件中的smdk_default_nand_part结构即可。本章节将NAND Flash划分为3个分区,前2MB用于存放内核,接下来的8MB用于存放JFFS2文件系统,剩下的用来存放YAFFS文件系统。
smdk_default_nand_part结构修改如下:
1 | static struct mtd_partition smdk_default_nand_part[] = { |
其中的MTDPART_OFS_APPEND表示当前分区紧接着上一个分区,MTDPART_SIZ_FULL表示当前分区的大小为剩余的Flash空间。
执行“make uImage”重新生成内核映像,重新启动后可以看到内核打印出如下分区信息。
1 | Creating 3 MTD partitions on "NAND 64MiB 3,3V 8-bit" |
由于目标板没有写入文件系统映像,也没有设置命令行使用网络文件系统(nfs),内核启动后还是会出现panic信息。
移植 YAFFS 文件系统
YAFFS 文件系统介绍
YAFFS(yet another flash file system)是一种类似于JFFS/JFFS2、专门为NAND Flash 设计的嵌入式文件系统,适用于大容量的存储设备。它是日志结构的文件系统,提供了损耗平衡和掉电保护,可以有效避免意外掉电对文件系统一致性和完整性的影响。与JFFS相比,它减少了一些功能,因此速度更快,占用内存更少。
YAFFS充分考虑了NAND Flash的特点,根据NAND Flash以页面为单位存取的特点,将文件组织成固定大小的数据段。利用NAND Flash提供的每个页面16字节的OOB空间来存储ECC(Error Correction Code)和文件系统的组织信息,不仅能实现错误检测和坏块处理,也能够提高文件系统的加载速度。YAFFS采用了一种多策略混合的垃圾回收算法,结合了贪心策略的高效性和随机选择的平均性,达到了兼顾系统开销和损耗平衡的目的。
YAFFS文件系统具有很好的移植性,可以在Linux、WindowsCE、pSOS、ThreadX、DSP-BIOS等多种操作系统上工作。为NAND Flash提供了一种可靠的操作系统,并且适合于对能耗要求高的嵌入式系统。
YAFFS文件系统目前已经发展到第二版本:YAFFS2,它向前兼容YAFFS1,主要特点是支持每页容量大于512字节的NAND Flash。
比较 | YAFFS2 | YAFFS1 | |
---|---|---|---|
写操作 | 快1-3倍 | 1.5MB/s-4.5MB/s | 1.5MB/s |
读操作 | 快1-2倍 | 7.6MB/s-16.7MB/s | 7.6MB/s |
删除操作 | 快4-34倍 | 7.8MB/s-62.5MB/s | 1.8MB/s |
垃圾回收 | 快2-7倍 | 2.1MB/s-7.7MB/s | 1.1MB/s |
内存消耗 | 减少25%-50% | - | - |
一般而言,在NOR Flash上使用JFFS2文件系统,在NAND Flash上使用YAFFS文件系统。JFFS2与YAFFS的性能比较如下。
| 性能 | JFFS2 | YAFFS |
| 内存消耗 | 每个节点(node)占用16字节
128MB的Flash将占用4MB内存 | 每页占用2字节
128MB的Flash将占用512KB内存 |
| 第一次启动时的扫描时间 | 128MB的Flash上时间为25S | 只需要读取OOB,时间为3S |
| 是否压缩 | 压缩 | 不压缩 |
| 代码复杂度 | 复杂 | 简单 |
| 使用的操作系统 | Linux、eCos | 很多,容易移植 |
| 启动时间 | Flash容量为4MB(or 8MB)时为4S | Flash容量为30MB时为7S |
YAFFS 文件系统移植
从http://www.aleph1.co.uk/cgi-bin/viewcvs.cgi/
获取源代码文件root.tar.gz。解压得到Development目录,里面有两个子目录:yaffs和yaffs2。yaffs目录不在维护,yaffs2兼容yaffs。
将yaffs2代码加入内核
可以通过yaffs2目录下的脚本文件patch.ker.sh来给内核打补丁,用法如下:
1 | usage: ./patch-ker.sh c/l kernelpath |
这表明,如果“c/l”为“c”,则yaffs2的代码会被复制到内核目录下;如果是“1”,则仅仅是在内核目录下创建一些连接文件。
假设下载解压后的yaffs2源码目录为/work/system/Development/yaffs2
,内核源码目录为/work/system/linux-2.6.22.6
,执行以下命令打补丁:
1 | cd /work/system/Development/yaffs2 |
上述命令完成以下3件事情。
- 修改内核fs/Kconfig文件,增加以下两行
1
2# Patched by YAFFS
source "fs/yaffs/Kconfig" - 修改内核fs/Makefile文件,增加以下两行
1
2# Patched by YAFFS
obj-$(CONFIG_YAFFS_FS) += yaffs2/ - 在内核
fs/
目录下创建yaffs2子目录,然后复制如下文件。
将yaffs2源码目录下得Makefile.kernel文件复制为内核fs/yaffs2/Makefile
文件。
将yaffs2源码目录下的Kconfig文件复制到内核fs/yaffs2
目录下。
将yaffs2源码目录下的*.c、*.h(不包括子目录下的文件)复制到内核fs/yaffs2
目录下。
配置、编译内核
阅读fs/yaffs2/Kconfig
文件可以了解各个配置选项的作用。
- CONFIG_YAFFS_FS:支持YAFFS文件系统
- CONFIG_YAFFS_YAFFS1:支持YAFFS1文件系统
对于每页大小为512字节的NAND Flash,要选上这个配置项 - CONFIG_YAFFS_YAFFS1:支持YAFFS2文件系统
对于每页大小为2048字节的NAND Flash,要选上这个配置项。本书所用的NAND Flash每页为512字节,这个配置项可以不选。 - CONFIG_YAFFS_AUTO_YAFFS2:自动选择YAFFS2格式。
如果不设置这个配置项,必须使用“yaffs2”字样来表示YAFFS2文件系统格式;如果设置了这个配置项,则可以使用“yaffs”字样来统一表示YAFFS、YAFFS2文件系统格式,驱动程序会根据NAND Flash页的大小自动分辨是YAFFS还是YAFFS2。 - CONFIG_YAFFS_9BYTE_TAGS
老的YAFFS1文件系统中,使用oob区中9个字节作为文件系统的标记(tag),比新的YAFFS1多了1个字节—“pageStatus”,它用来表示页的状态。
如果要使用老的YAFFS1,这个配置项要选上,另外要修改MTD设备层以使用老的oob layout结构。oob layout就是内核文件drivers/mtd/nand/nand_base.c
中的nand_oob_16结构。
Linux-2.6.22.6内核使用新的oob layout,格式如下。它表示ECC码存放的位置是oob区中0、1、…、7这8个字节;剩下的空间就称为可用空间,供文件系统使用,代码中将这些数据称为标记(tag):以前的内核使用老的oob layout,格式如下,ECC码的位置不一样,标记的位置也不一样。1
2
3
4
5
6
7
8
9
10static struct nand_ecclayout nand_oob_16 = {
.eccbytes = 6,
.eccpos = {0,1,2,3,6,7},
.oobfree = {
{
.offset = 8,
.length = 8
}
}
};如果要使用老格式的YAFFS1映像文件,定义CONFIG_YAFFS_9BYTE_TAGS配置项,并且修改nand_oob_16结构为老的格式。1
2
3
4
5
6static struct nand_ecclayout nand_oob_16 = {
.eccbytes = 6,
.eccpos = {8,9,10,13,14,15},
.oobavail = 9,
.oobfree = {{0,4},{6,2},{11,2},{4,1}}
}; - CONFIG_YAFFS_DOES_ECC:使用YAFFS本身的ECC校验参数。
一般使用MTD设备层的ECC校验函数,这个配置项不用设置。
了解各配置项的意义后,就可以配置内核,选上对YAFFS的支持了。在内核配置界面选中“YAFFS2 file system support”即可,其他配置选项使用默认值。最后执行“make uImage”编译内核。1
2
3File systems --->
Miscellaneous filesystems --->
<*> YAFFS2 file system support
编译、烧写、启动内核
到目前为止,内核已经同时支持了S3C2410和S3C2440,修改了NAND Flash的分区,增加了对YAFFS文件系统的支持。另外,内核原来已经支持JFFS2文件系统。现在的内核。已经基本可用,可以将它烧入NAND Flash中。
编译内核
1 | make uImage |
烧写内核
1 | tftp 0x32000000 uImage 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage |
启动内核
1 | nboot 0x32000000 0 0 |
要想开发板上电自动启动内核,可以设置bootcmd环境变量
1 | set bootcmd 'nboot 0x32000000 0 0; bootm 0x32000000' |