Linux驱动开发者需要牢固地掌握Linux内核的编译方法以为嵌入式系统构建可运行的Linux操作系统映像。在编译内核时,需要配置内核,可以使用下面命令中的一个:
#make config(基于文本的最为传统的配置界面,不推荐使用) #make menuconfig(基于文本菜单的配置界面) #make xconfig(要求QT被安装) #make gconfig(要求GTK+被安装)
在配置Linux内核所使用的make config、make menuconfig、make xconfig和make gconfig这4种方式中,最值得推荐的是make menuconfig,它不依赖于QT或GTK+,且非常直观,对/home/baohua/develop/linux中的Linux 4.0-rc1内核运行make ARCH=arm menuconfig后的界面如图3.9所示。
图3.9 Linux内核编译配置
内核配置包含的条目相当多,arch/arm/configs/xxx_defconfig文件包含了许多电路板的默认配置。只需要运行make ARCH=arm xxx_defconfig就可以为xxx开发板配置内核。
编译内核和模块的方法是:
make ARCH=arm zImage make ARCH=arm modules
上述命令中,如果ARCH=arm已经作为环境变量导出,则不再需要在make命令后书写该选项。执行完上述命令后,在源代码的根目录下会得到未压缩的内核映像vmlinux和内核符号表文件System.map,在arch/arm/boot/目录下会得到压缩的内核映像zImage,在内核各对应目录内得到选中的内核模块。
Linux内核的配置系统由以下3个部分组成。
·Makefile:分布在Linux内核源代码中,定义Linux内核的编译规则。
·配置文件(Kconfig):给用户提供配置选择的功能。
·配置工具:包括配置命令解释器(对配置脚本中使用的配置命令进行解释)和配置用户界面(提供字符界面和图形界面)。这些配置工具使用的都是脚本语言,如用Tcl/TK、Perl等。
使用make config、make menuconfig等命令后,会生成一个.config配置文件,记录哪些部分被编译入内核、哪些部分被编译为内核模块。
运行make menuconfig等时,配置工具首先分析与体系结构对应的/arch/xxx/Kconfig文件(xxx即为传入的ARCH参数),/arch/xxx/Kconfig文件中除本身包含一些与体系结构相关的配置项和配置菜单以外,还通过source语句引入了一系列Kconfig文件,而这些Kconfig又可能再次通过source引入下一层的Kconfig,配置工具依据Kconfig包含的菜单和条目即可描绘出一个如图3.9所示的分层结构。
在Linux内核中增加程序需要完成以下3项工作。
·将编写的源代码复制到Linux内核源代码的相应目录中。
·在目录的Kconfig文件中增加关于新源代码对应项目的编译配置选项。
·在目录的Makefile文件中增加对新源代码的编译条目。
在讲解Kconfig和Makefile的语法之前,我们先利用两个简单的实例引导读者对其建立对具初步的认识。
首先,在drivers/char目录中包含了TTY_PRINTK设备驱动的源代码drivers/char/ttyprintk.c。而在该目录的Kconfig文件中包含关于TTY_PRINTK的配置项:
config TTY_PRINTK tristate "TTY driver to output user messages via printk" depends on EXPERT && TTY default n ---help--- If you say Y here, the support for writing user messages (i.e. console messages) via printk is available. The feature is useful to inline user messages with kernel messages. In order to use this feature, you should output user messages to /dev/ttyprintk or redirect console to this TTY. If unsure, say N.
上述Kconfig文件的这段脚本意味着只有在EXPERT和TTY被配置的情况下,才会出现TTY_PRINTK配置项,这个配置项为三态(可编译入内核,可不编译,也可编译为内核模块,选项分别为“Y”、“N”和“M”),菜单上显示的字符串为“TTY driver to output user messages via printk”,“help”后面的内容为帮助信息。图3.10显示了TTY_PRINTK菜单以及help在运行make menuconfig时的情况。
除了布尔(bool)配置项外,还存在一种布尔配置选项,它意味着要么编译入内核,要么不编译,选项为“Y”或“N”。
图3.10 Kconfig菜单项与help信息
在目录的Makefile中关于TTY_PRINTK的编译项为:
obj-$(CONF IG_TTY_PRINTK) += ttyprintk.o
上述脚本意味着如果TTY_PRINTK配置选项被选择为“Y”或“M”,即obj-$(CONFIG_TTY_PRINTK)等同于obj-y或obj-m,则编译ttyprintk.c,选“Y”时会直接将生成的目标代码连接到内核,选“M”时则会生成模块ttyprintk.ko;如果TTY_PRINTK配置选项被选择为“N”,即obj-$(CONFIG_TTY_PRINTK)等同于obj-n,则不编译ttyprintk.c。
一般而言,驱动开发者会在内核源代码的drivers目录内的相应子目录中增加新设备驱动的源代码或者在arch/arm/mach-xxx下新增加板级支持的代码,同时增加或修改Kconfig配置脚本和Makefile脚本,具体执行完全仿照上述过程即可。
这里主要对内核源代码各级子目录中的kbuild(内核的编译系统)Makefile进行简单介绍,这部分是内核模块或设备驱动开发者最常接触到的。
Makefile的语法包括如下几个方面。
(1)目标定义
目标定义就是用来定义哪些内容要作为模块编译,哪些要编译并链接进内核。
例如:
obj-y += foo.o
表示要由foo.c或者foo.s文件编译得到foo.o并链接进内核(无条件编译,所以不需要Kconfig配置选项),而obj-m则表示该文件要作为模块编译。obj-n形式的目标不会被编译。
更常见的做法是根据make menuconfig后生成的config文件的CONFIG_变量来决定文件的编译方式,如:
obj-$(CONF IG_ISDN) += isdn.o obj-$(CONF IG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o
除了具有obj-形式的目标以外,还有lib-y library库、hostprogs-y主机程序等目标,但是这两类基本都应用在特定的目录和场合下。
(2)多文件模块的定义。
最简单的Makefile仅需一行代码就够了。如果一个模块由多个文件组成,会稍微复杂一些,这时候应采用模块名加-y或-objs后缀的形式来定义模块的组成文件,如下:
# # Makefile for the linux ext2-filesystem routines. # obj-$(CONFIG_EXT2_FS) += ext2.o ext2-y := balloc.o dir.o file.o fsync.o ialloc.o inode.o \ ioctl.o namei.o super.o symlink.o ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o ext2-$(CONFIG_EXT2_FS_POSIX_ACL) += acl.o ext2-$(CONFIG_EXT2_FS_SECURITY) += xattr_security.o ext2-$(CONFIG_EXT2_FS_XIP) += xip.o
模块的名字为ext2,由balloc.o、dir.o、file.o等多个目标文件最终链接生成ext2.o直至ext2.ko文件,并且是否包括xattr.o、acl.o等则取决于内核配置文件的配置情况,例如,如果CONFIG_EXT2_FS_POSIX_ACL被选择,则编译acl.c得到acl.o并最终链接进ext2。
(3)目录层次的迭代
如下例:
obj-$(CONFIG_EXT2_FS) += ext2/
当CONFIG_EXT2_FS的值为y或m时,kbuild将会把ext2目录列入向下迭代的目标中。
内核配置脚本文件的语法也比较简单,主要包括如下几个方面。
(1)配置选项
大多数内核配置选项都对应Kconfig中的一个配置选项(config):
config MODVERSIONS bool "Module versioning support" help Usually, you have to use modules compiled with your kernel. Saying Y here makes it ...
“config”关键字定义新的配置选项,之后的几行代码定义了该配置选项的属性。配置选项的属性包括类型、数据范围、输入提示、依赖关系、选择关系及帮助信息、默认值等。
·每个配置选项都必须指定类型,类型包括bool、tristate、string、hex和int,其中tristate和string是两种基本类型,其他类型都基于这两种基本类型。类型定义后可以紧跟输入提示,下面两段脚本是等价的:
bool “Networking support”
和
bool prompt "Networking support"
·输入提示的一般格式为:
prompt <prompt> [if <expr>]
其中,可选的if用来表示该提示的依赖关系。
·默认值的格式为:
default <expr> [if <expr>]
如果用户不设置对应的选项,配置选项的值就是默认值。
·依赖关系的格式为:
depends on(或者requires) <expr>
如果定义了多重依赖关系,它们之间用“&&”间隔。依赖关系也可以应用到该菜单中所有的其他选项(同样接受if表达式)内,下面两段脚本是等价的:
bool "foo" if BAR default y if BAR
和
depends on BAR bool "foo" default y
·选择关系(也称为反向依赖关系)的格式为:
select <symbol> [if <expr>]
A如果选择了B,则在A被选中的情况下,B自动被选中。
·数据范围的格式为:
range <symbol> <symbol> [if <expr>]
Kconfig中的expr(表达式)定义为:
<expr> ::= <symbol> <symbol> '=' <symbol> <symbol> '!=' <symbol> '(' <expr> ')' '!' <expr> <expr> '&&' <expr> <expr> '||' <expr>
也就是说,expr是由symbol、两个symbol相等、两个symbol不等以及expr的赋值、非、与或运算构成。而symbol分为两类,一类是由菜单入口配置选项定义的非常数symbol,另一类是作为expr组成部分的常数symbol。比如下面代码中的SHDMA_R8A73A4是一个布尔配置选项,表达式“ARCH_R8A73A4&&SH_DMAE!=n”暗示只有当ARCH_R8A73A4被选中且SH_DMAE不为n(即要么被选中,要么作为模块)的时候,才可能出现这个SHDMA_R8A73A4。
config SHDMA_R8A73A4 def_bool y depends on ARCH_R8A73A4 && SH_DMAE != n
·为int和hex类型的选项设置可以接受的输入值范围,用户只能输入大于等于第一个symbol,且小于等于第二个symbol的值。
·帮助信息的格式为:
help(或---help---) 开始 … 结束
帮助信息完全靠文本缩进识别结束。“---help---”和“help”在作用上没有区别,设计“---help---”的初衷在于将文件中的配置逻辑与给开发人员的提示分开。
(2)菜单结构
配置选项在菜单树结构中的位置可由两种方法决定。第一种方式为:
menu "Network device support" depends on NET config NETDEVICES … endmenu
所有处于“menu”和“endmenu”之间的配置选项都会成为“Network device support”的子菜单,而且,所有子菜单(config)选项都会继承父菜单(menu)的依赖关系,比如,“Network device support”对“NET”的依赖会被加到配置选项NETDEVICES的依赖列表中。
注意: menu后面跟的“Network device support”项仅仅是1个菜单,没有对应真实的配置选项,也不具备3种不同的状态。这是它和config的区别。
另一种方式是通过分析依赖关系生成菜单结构。如果菜单项在一定程度上依赖于前面的选项,它就能成为该选项的子菜单。如果父选项为“n”,子选项不可见;如果父选项可见,子选项才可见。例如:
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,只有MODULES不为“n”时,该选项才可见。
除此之外,Kconfig中还可能使用“choices...endchoice”、“comment”、“if...endif”这样的语法结构。其中“choices...endchoice”的结构为:
choice <choice options> <choice block> endchoice"
它定义一个选择群,其接受的选项(choice options)可以是前面描述的任何属性,例如,LDD6410的VGA输出分辨率可以是1 024×768或者800×600,在drivers/video/samsung/Kconfig中就定义了如下choice:
depends on FB_S3C_VGA prompt "Select VGA Resolution for S3C Framebuffer" default FB_S3C_VGA_1024_768 config FB_S3C_VGA_1024_768 bool "1024*768@60Hz" ---help--- TBA config FB_S3C_VGA_640_480 bool "640*480@60Hz" ---help--- TBA endchoice
上述例子中,prompt配合choice起到提示作用。
用Kconfig配置脚本和Makefile脚本编写的更详细信息,可以分别参见内核文档Documentation目录内的kbuild子目录下的Kconfig-language.txt和Makefiles.txt文件。
下面来看一个综合实例,假设我们要在内核源代码drivers目录下为ARM体系结构新增如下用于test driver的树形目录:
|--test |-- cpu | -- cpu.c |-- test.c |-- test_client.c |-- test_ioctl.c |-- test_proc.c |-- test_queue.c
在内核中增加目录和子目录时,我们需为相应的新增目录创建Makefile和Kconfig文件,而新增目录的父目录中的Kconfig和Makefile也需修改,以便新增的Kconfig和Makefile能被引用。
在新增的test目录下,应该包含如下Kconfig文件:
# # TEST driver configuration # menu "TEST Driver " comment " TEST Driver" config TEST bool "TEST support " config TEST_USER tristate "TEST user-space interface" depends on TEST endmenu
由于test driver对于内核来说是新功能,所以需首先创建一个菜单TEST Driver。然后,显示“TEST support”,等待用户选择;接下来判断用户是否选择了TEST Driver,如果选择了(CONFIG_TEST=y),则进一步显示子功能:用户接口与CPU功能支持;由于用户接口功能可以被编译成内核模块,所以这里的询问语句使用了tristate。
为了使这个Kconfig能起作用,修改arch/arm/Kconfig文件,增加:
source "drivers/test/Kconf ig"
脚本中的source意味着引用新的Kconfig文件。
在新增的test目录下,应该包含如下Makefile文件:
# drivers/test/Makefile # # Makefile for the TEST. # obj-$(CONFIG_TEST) += test.o test_queue.o test_client.o obj-$(CONFIG_TEST_USER) += test_ioctl.o obj-$(CONFIG_PROC_FS) += test_proc.o obj-$(CONFIG_TEST_CPU) += cpu/
该脚本根据配置变量的取值,构建obj-*列表。由于test目录中包含一个子目录cpu,因此当CONFIG_TEST_CPU=y时,需要将cpu目录加入列表中。
test目录中的cpu子目录也需包含如下Makefile:
# drivers/test/test/Makefile # # Makefile for the TEST CPU # obj-$(CONFIG_TEST_CPU) += cpu.o
为了使得编译命令作用到能够整个test目录,test目录的父目录中Makefile也需新增如下脚本:
obj-$(CONFIG_TEST) += test/
在drivers/Makefile中加入obj-$(CONFIG_TEST)+=test/,使得用户在进行内核编译时能够进入test目录。
增加了Kconfig和Makefile之后的新test树形目录为:
|--test |-- cpu | -- cpu.c | -- Makefile |-- test.c |-- test_client.c |-- test_ioctl.c |-- test_proc.c |-- test_queue.c |-- Makefile |-- Kconfig
引导Linux系统的过程包括很多阶段,这里将以引导ARM Linux为例来进行讲解(见图3.11)。一般的SoC内嵌入了bootrom,上电时bootrom运行。对于CPU0而言,bootrom会去引导bootloader,而其他CPU则判断自己是不是CPU0,进入WFI的状态等待CPU0来唤醒它。CPU0引导bootloader,bootloader引导Linux内核,在内核启动阶段,CPU0会发中断唤醒CPU1,之后CPU0和CPU1都投入运行。CPU0导致用户空间的init程序被调用,init程序再派生其他进程,派生出来的进程再派生其他进程。CPU0和CPU1共担这些负载,进行负载均衡。
图3.11 ARM上的Linux引导流程
bootrom是各个SoC厂家根据自身情况编写的,目前的SoC一般都具有从SD、eMMC、NAND、USB等介质启动的能力,这证明这些bootrom内部的代码具备读SD、NAND等能力。
嵌入式Linux领域最著名的bootloader是U-Boot,其代码仓库位于http://git.denx.de/u-boot.git/。早前,bootloader需要将启动信息以ATAG的形式封装,并且把ATAG的地址填充在r2寄存器中,机型号填充在r1寄存器中,详见内核文档Documentation/arm/booting。在ARM Linux支持设备树(Device Tree)后,bootloader则需要把dtb的地址放入r2寄存器中。当然,ARM Linux也支持直接把dtb和zImage绑定在一起的模式(内核ARM_APPENDED_DTB选项“Use appended device tree blob to zImage”),这样r2寄存器就不再需要填充dtb地址了。
类似zImage的内核镜像实际上是由没有压缩的解压算法和被压缩的内核组成,所以在bootloader跳入zImage以后,它自身的解压缩逻辑就把内核的镜像解压缩出来了。关于内核启动,与我们关系比较大的部分是每个平台的设备回调函数和设备属性信息,它们通常包装在DT_MACHINE_START和MACHINE_END之间,包含reserve()、map_io()、init_machine()、init_late()、smp等回调函数或者属性。这些回调函数会在内核启动过程中被调用。后续章节会进一步介绍。
用户空间的init程序常用的有busybox init、SysVinit、systemd等,它们的职责类似,把整个系统启动,最后形成一个进程树,比如Ubuntu上运行的pstree:
init─┬─NetworkManager─┬─dhclient │ ├─2*[{NetworkManager}] ├─VBoxSVC─┬─VirtualBox───29*[{VirtualBox}] │ └─11*[{VBoxSVC}] ├─VBoxXPCOMIPCD ├─accounts-daemon───{accounts-daemon} ├─acpid ├─apache2───5*[apache2] ├─at-spi-bus-laun───2*[{at-spi-bus-laun}] ├─atd ├─avahi-daemon───avahi-daemon ├─bluetoothd ├─cgrulesengd ├─colord───2*[{colord}] ├─console-kit-dae───64*[{console-kit-dae}] ├─cpufreqd───{cpufreqd} ├─cron ├─cupsd ├─2*[dbus-daemon] ├─dbus-launch ├─dconf-service───2*[{dconf-service}] ├─dnsmasq