第三章 嵌入式Linux内核裁剪和制定的讲解及实例
3.1 Linux 内核开发概述
本文所说的内核开发其实是指结合实际开发需求对嵌入式linux的产品的内核和驱动进行相关的修改等开发工作,以此来满足项目的需求,这个含义与Linux团队的内核开发有很大差异。
本文所说的是对嵌入式linux内核进行二次开发,但仍然需要相关的研发人员需要拥有以下的一些技能点,才能够很好的胜任内核二次开发的工作:
(1)熟悉操作系统的基础知识,能够理解操作系统原理;
(2)熟练掌握C语言的使用,因为在Linux内核中绝大部分是使用C语言来完成的。
(3)对Linux内核源码的结构布局要有一个大概的了解;
(4)常用的设备驱动开发工程师们还需要对外围设备的工作原理以及驱动的编写方法需要有一定了解。
3.2 内核源码概述
3.2.1 内核源码目录组成
本文讲解的Linux内核源码以及后续的程序代码部分是主要针对Linux3.4.39的,当前的Linux操作系统一般由进程管理、内存管理、文件系统、驱动程序以及网络协议栈等组成的,如下为该内核源码的主要目录构成。
(1)arch目录:该文件夹下主要是包含了与硬件和平台相关的代码,其中每种平台对应着一个相应的目录,常见的平台有arm、x86和i386等。
(2)block目录:主要是包含了与块设备驱动程序I/O调度相关的代码。
(3)crypto目录:主要是包含一些常规的算法方面的代码,例如加密、散列、压缩以及CRC校验的算法代码等。
(4)Documentation目录:主要是包含内核中各个通用代码或模块的注释讲解等文件。
(5)drivers目录:这个目录下文件主要是各种类型的设备驱动,这个目录对于驱动开发工程师也是需要经常查阅的,同时里面的文档也是对初学者具有很大的实用学习价值,其中里面包含了char、block、net、spi和i2c等驱动的代码。
(6)fs目录:主要包含不同类型的文件系统,如常用的EXT、FAT以及JFFS2等类型文件系统。
(7)include目录:该目录如它名字一样的含义,即是包含了系统中代码的头文件,其中系统相关的头文件主要存放在include/linux的各个子目录下。
(8)init目录:主要包含内核系统的初始化代码。
(9)ipc目录:主要是包含进程间通信的代码,通过对这类文件的阅读可以让读者能够对内核的进程及多任务的运行原理有一个更加深入的理解。
(10)kernel目录:这个目录下代码是整个linux内核代码中最为重要的部分,包括进程调度和相关定时器的代码等,但是对于与平台相关的部分代码则存放在arch/*/kernel的目录下。
(11)net目录:主要包含网络管理相关的代码,用于实现各个网络协议的功能。
(12)mm目录:主要是包含内存管理相关的代码。
(13)scripts目录:主要包含用于内核配置的相关脚本文件。
(14)sound目录:主要包含音频相关的代码,如ALSA等的驱动代码等。
3.2.2 内核组成部分
从上一小结中的目录构成可以看出,Linux内核主要构成部分由进程调度、内存管理、虚拟文件系统、网络接口以及进程间通信这五个子系统构成的。如下图3.1所示为内核组成部分的相关依赖关系示意图。
图3.1 Linux各个子系统相关依赖关系
Figure 3.1 Dependencies Linux each subsystem
1.进程调度子系统
对于Linux内核中的五个子系统中,进程调度子系统处于它们的核心位置,因为进行调度子系统需要对内核系统中的多个进程进行控制,来实现进程间能够并行的执行,而且其他各个子系统都需依赖于进程调度子系统来完成它们的挂起和恢复进程的任务,这也是进程调度的主要工作。
2.内存管理子系统
内存管理子系统主要作用是可以控制多个进程能够安全合法的共享主内存中的区域。由于随着实际需求的难度的增加,单单依赖原本的CPU内存大小不足以满足需求,此时在Linux内存管理中具备实际内存与虚拟内存转换能力,Linux的内存管理能够为每个进程进行虚拟内存与物理内存之间的转换,这部分也在驱动开发中我们需要设置申请内存资源及将其与虚拟内存进行转换步骤中有体现。
3.虚拟文件子系统
在Linux的世界中常有个思想是“一切皆文件”,在Linux系统中不仅仅是普通文本文件和目录被当作文件处理,而且连字符设备、块设备等等都是当作文件来进行处理的,在它的背后就存在了一个就存在了一个软件抽象层(虚拟文件系统)来向用户空间提供文件系统的接口,与此同时还提供了一个抽象的功能,来实现不同的类型文件系统能够协同运行,如下图3.2所示为虚拟内存在内核中关系结构图。
图3.2 虚拟文件系统关系结构图
Figure 3.2 Virtual File System Relationship Structure
4.网络管理子系统
在我们使用socket编程来完成以太网通信的过程中,是否有想过它是如何实现的呢?没错,在调用socket的相关的函数时是通过内核的网络管理子系统完成的,该子系统主要分为四个主要组成部分,采用了面向对象的抽象编程思想来实现的,主要包括:网络协议、套接字、网络设备接口以及网络缓存区,通过它们实现了对各种网络标的存取以及各种网络设备硬件的支持。
5.进程间通信子系统
进程间通信子系统主要完成的任务是能够让Linux系统支持进程间多种不同通信机制,例如信号量、共享内存及管道等,让它们能够实现协同多个进程、不同资源的互斥访问、进程间进行同步以及传递消息等功能
3.3 Makefie文件介绍
在Linux开发中无论是应用开发或者是内核驱动开发,开发工程师都需要对Makefile文件有一定的认识和理解,因为通过Makefile来完成程序的编译与链接功能。本文主要是讲述的是内核源代码中各个子目录的kbuild的Makefile,这个部分的Makefile也是开发工程师最常接触到的。Makefile一般包含如下部分:
(1)目标定义
目标定义其实是用来定义哪些文件需要用来作为模块编译,哪些需要进行编译和链接进内核之中。如下代码为“\kernel\drivers\video”中的Makefile其中的一行编译代码,表示为由fb_notify.c或者fb_notify.s文件编译得到的fb_notify.o并将其并入到内核中,对于“obj-m”则表示将目标文件作为模块编译出来。
obj-y += fb_notify.o
以下的方法是更为常见,就是根据.config文件中的CONFIG_变量来确定文件的编译方式,可以的值为“y或者m”,例如以下:
obj-$(CONFIG_FB_CFB_FILLRECT) += cfbfillrect.o obj-$(CONFIG_FB_CFB_COPYAREA) += cfbcopyarea.o
(2)多个文件模块的定义
对于简单的Makefile编译可以使用上面(1)的方法一句的形式就足够满足要求了,但我们实际的项目中往往会存在是需要将多个文件编译为一个模块的情况,这个时候有两种方法可以完成该任务,可以通过模块名加-y或者-objs后缀形式来定义模块的组成文件。
a)使用-y的例子如下:
obj-$(CONFIG_FOO_FS) += foo.o foo-y := balloc.o dir.o file.o fsync.o ialloc.o inode.o \ ioctl.o namei.o super.o symlink.o foo-$(CONFIG_FOO_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o foo-$(CONFIG_FOO_FS_POSIX_ACL) += acl.o foo-$(CONFIG_FOO_FS_SECURITY) += xattr_security.o foo-$(CONFIG_FOO_FS_XIP) += xip.o
模块的名字为foo,它是由indoe.o、ialloc.o等多个目标文件链接生成的foo.o,最终生成foo.ko驱动模块文件,对于acl.o和xip.o等目标文件是否链接进foo.o中则取决于配置文件的配置情况。
b)使用-objs来进行编译的例子如下:
obj-m +=sahuLB.o sahuLB-objs:=simpLB.o sahu_lb_tools.o all: make -C /lib/modules/`uname -r`/build M=`pwd` clean: make -C /lib/modules/`uname -r`/build M=`pwd` clean install: /sbin/insmod sahuLB.ko remove: /sbin/rmmod sahuLB
上述例子是使用-objs来将simpLB.o和sahu_lb_tools.o链接进sahuLB.o中,最终编译出sahuLB.ko模块文件。
(3)目录层次的迭代
为了利于文件分类管理,我们编译生成出来的文件往往需要迭代到指定的目录中,如下例子为当将CONFIG_FOO_FS的值设置为y或者m时,kbuild将把foo目录列入向下迭代的目标中。
obj-$(CONFIG_FOO_FS) += foo/
3.4 Kconfig文件介绍
在上面讲述完Makefile文件之后,必不可少的需要讲解Kconfig文件,因这个两个文件是阅读内核文件时会起到地图导航的作用,在编译器编译内核时也是根据这个两个文件来确定需要编译哪些模块以及知道模块的依赖关系等。
通常情况下的Kconfig配置文件的语法是比较通俗易懂的,主要有如下部分:
(1)菜单入口
一般情况下内核配置的选项与Kconfig对应一个菜单的入口,例如:
config PCI_DEBUG bool "PCI Debugging" depends on PCI && DEBUG_KERNEL help Say Y here if you want the PCI core to produce a bunch of debug messages to the system log. Select this if you are having a problem with PCI support and want to see more of what is going on.
“config”表示新的配置选项,后面几行的是该配置选项的相关属性,其中该属性包括类型、数据范围、输入提示等信息。
每个配置选项都需要指定类型,其中常规的类型有:bool、tristate、string、hex和int等,类型定义后可以加入输入提示方法,该两中方法是等价的。
bool "PCI Debugging"
或者
bool Prompt "PCI Debugging"
a)输入提示一般格式
prompt <prompt> [if <ezpr>]
可通过可选的if来提示依赖的关系。
b)默认值的格式
depends of
c)选择关系格式
select <symbol> [if <expr>]
A如果选择了B,则在A被选择的情况下,B将会被自动的选上。
d)help帮助信息格式
help 开始 ....... 结束
(2)菜单结构
菜单入口在菜单数结构中的位置可以由两种方法的确定。
(a)使用”menu”和”endmenu”方式
menu "Graphics support" depends on HAS_IOMEM config HAVE_FB_ATMEL ....... endmenu
所有在”menu”和“endmenu”之间的菜单入口将会变成“Graphics support”的子菜单,同时所有的子菜单将会继承父菜单的依赖关系,例如“Graphics support”对“HAS_IOMEM”的依赖会被加到了配置选项HAVE_FB_ATMEL的依赖列表中。
值得注意的是,使用这个方法的menu后面跟的“Graphics support”项目只是一个菜单,没有对应真实的配置选项,也没有3种不同的状态值,这也是和“config”的主要区别。
(b)通过分析依赖关系来生成菜单结构
若菜单选项中存在一定的依赖选项,它就可以成为该选项的子菜单,当父选项为“N”时,子选项则不可见;反之父选项选上了,子选项可见。例子如下:
# # PCI configuration # config ARCH_SUPPORTS_MSI bool default n config PCI_MSI bool "Message Signaled Interrupts (MSI and MSI-X)" depends on PCI depends on ARCH_SUPPORTS_MSI help This allows device drivers to enable MSI (Message Signaled Interrupts). Message Signaled Interrupts enable a device to generate an interrupt using an inbound Memory Write on its PCI bus instead of asserting a device IRQ pin. Use of PCI MSI interrupts can be disabled at kernel boot time by using the 'pci=nomsi' option. This disables MSI for the entire system. If you don't know what to do here, say Y. config PCI_DEBUG bool "PCI Debugging" depends on PCI && DEBUG_KERNEL help Say Y here if you want the PCI core to produce a bunch of debug messages to the system log. Select this if you are having a problem with PCI support and want to see more of what is going on. When in doubt, say N.
PCI_MSI直接依赖于ARCH_SUPPORTS_MSI,只有当ARCH_SUPPORTS_MSI不为“n”时,该选项才可以看见。
对于Kconfig配置脚本和Makefile脚本的编写详细内容,可以分别参考Documentation目录的 kbuild 子目录下的 Kconfig-language.txt 和 Makefiles.txt 文件。
3.5 配置和编译内核
在上述讲解完Makefile和Kconfig的文件及其作用后,我们接下来就是需要通过配置和编译内核来生成一个.config文件,最后内核根据这个.config来编译生成一个zImage或者uImage等镜像文件。对内核的配置需要正确配置后,才能够进下一步的编译,否则往往由于配置不当会在编译或者运行时出现错误。
3.5.1 快速配置内核
进入Linux内核源代码的顶部目录中,可以输入命令行make menuconfig,可以进入如下图3.3所示的内核配置界面,对于该菜单配置的操作方法有如下介绍:
(1)可通过键盘的方向键移动光标,选中子菜单;
(2)可通过TAB键实现在菜单区和功能区的切换;
(3)在子菜单或者选项高亮,将光标移动到功能区选中
3.5.2 内核配置选项说明
配置Linux内核是一件比较复杂的工作,因为它的内核配置菜单比较复杂,接下来是对一些比较常规的选项进行说明,由于不同的Linux内核版本会有一定的差异,所以实际看到的内核配置界面也会跟下表3.1有所差异。
表 3.1 内核配置菜单选项说明
Table 3.1 Kernel Configuration Menu Options
3.5.3 内核编译
在上述内核配置完成的基础上,输入“make”命令即可开始进行编译内核,若如果内核中的Makefile文件并没有指定ARCH和CROSS_COMPILE参数值的话,此时输入命令时需要进行指定:
Make ARCH=arm CROSS_COMPILE=arm-none-linux-guneabi-
在编译的过程中可能会出现错误,此时需要我们实际根据情况来进行错误的解决直至编译完成,最后编译成功后将会生成vmlinux、Image或者zImage等文件。
3.6 内核裁剪示例讲解
一般情况下我们在进行内核配置和裁剪时,都不是从零开始配置的,往往是在原厂商提供的默认配置下进行修改调整的,通过二次裁剪来满足实际项目的需求。
在裁剪和配置内核常需要遵循的原则如下:
(1)基于某一款比较接近型号/性能的板子配置进行修改;
(2)选中必须或能确定的选项;
(3)对于不确定的选项不要改变其原来的配置;
(4)对于可选或者不选的选项,需要根据help信息确定或者不选;
(5)在进行一次修改时的改动不要太多,尽量采用渐进式来修改和验证;
(6)养成备份配置文件的习惯,以防突发事件出现时可以恢复配置。
接下来将给出一个常见功能配置的方案以供学习或参考,很多相关的功能需要跟据实际的板子和内核版本进行确定。
3.6.1 GPIO子系统配置
对于Linux2.6之后的版本中引入了GPIO子系统,它将全部的GPIO接口都通过“/sys/class/gpio/”的目录进行导出,给用户带来的便捷。
输入如下命令进行菜单配置:
$ make ARCH=arm menuconfig
在最顶层菜单中选择设备驱动选项“Device Drivers”,此选项也是驱动开发工程师经常需要关注的:
[*] Networking support ---> Device Drivers ---> File systems ---> Kernel hacking --->
进入“Device Drivers”子菜单后,选中“GPIO Suppport”
[*] SPI support ---> PPS support ---> PTP clock support -*- GPIO Support ---> <*> PWM Support --->
在“GPIO Support”子菜单中选中“/sys/class/gpio..”选项:
--- GPIO Support [*] /sys/class/gpio/... (sysfs interface) *** Memory mapped GPIO drivers: *** …
配置完成后,重新编译内核,使用刚才新生成的内核系统可以通过“/sys/class/gpio/”访问系统的GPIO接口了。
3.6.2 串口配置
对于串口的配置是嵌入式Linux系统必不可少的,默认控制台通常就是串口,因此必须在内核中使能串口和串口控制台支持。如下为相关的配置:
在“Device Drivers”子菜单中选中“Character devices”
Input device support ---> Character devices ---> -*- I2C support --->
然后进入“Character devices”子菜单中,然后选择“Serial drivers”:
[*] /dev/kmem virtual device support Serial drivers ---> [ ] ARM JTAG DCC console
进入到“Serial drivers”子菜单后,进行串口控制器的配置。默认的Linux控制台是串口,所以还需要使能串口控制台配置,串口控制器是与具体的处理器相关的,需要根据实际硬件进行选中,如下为根据实际项目的配置,选用的是三星的芯片,同时支持串口控制器以及它的DMA传输功能。
<*>Nwxell s3c Soc Serial port support (4)Available UART ports [*] Support for console on Nexell S3C serial port [*] Serial port 0 [*] Use DMA [*] Serial port 1 [*] Use DMA
3.6.2 USB Host 驱动配置
正常情况下USB可以外接很多种设备的,不同的设备它的驱动配置也是不一样的,接下来以U盘为例进行讲解其配置。
在Linux系统中将U盘看做是一种SCSI设备,因此在内核中的配置时必须使其支持SCSI。在主菜单中选择“Device Drivers”,进入子菜单后再选择“SCSI device support”
[*] Block devices ---> [*] Misc devices ---> SCSI device support ---> < > Serial ATA and Parallel ATA drivers --->
然后进入“SCSI device support”子菜单,进行如下配置:
< > RAID Transport Class <*> SCSI device support < > SCSI target support [*] legacy /proc/scsi/ support *** SCSI support type (disk, tape, CD-ROM) *** <*> SCSI disk support < > SCSI tape support < > SCSI OnStream SC-x0 tape support
接下来将配置驱动中的USB控制器驱动配置,进入“Device Drivers”,进入“USB support”子菜单:
< > Sound card support ---> [ ] HID Devices ---> [*] USB support ---> <*> MMC/SD/SDIO card support --->
进入子菜单后再进行如下配置:
-- USB support <*> Support for Host-side USB [*] USB device filesystem (DEPRECATED) [*] USB device class-devices (DEPRECATED) [*] USB runtime power management (suspend/resume and wakeup) <*> EHCI HCD (USB 2.0) support [*] Support for Freescale controller [*] Support for Host1 port on Freescale controller [*] Support for DR host port on Freescale controller [*] Root Hub Transaction Translators
使用U盘时,一般是需要支持USB大容量的,因此需要选择“USB Mass Storage support”:
… <*> USB Mass Storage support [ ] USB Mass Storage verbose debug
一般情况下还需要配置驱动能够支持FAT格式支持,以下是菜单的配置:
File systems ---> DOS/FAT/NT Filesystems ---> <*> MSDOS fs support <*> VFAT (Windows-95) fs support (437) Default codepage for FAT (iso8859-1) Default iocharset for FAT < > NTFS file system support
在上述配置配置保存后,需要重新编译内核,在新的内核系统上就可以使用U盘了。
3.6.3 网卡驱动配置
对于配置网卡时,首先是需要使能网络,就是需要选中“Networking support”:
Power management options ---> [*] Networking support ---> Device Drivers ---> File systems --->
进入子菜单后,需要配置TCP/IP才能够正常使用以太网:
[*] Networking support ---> Networking options ---> <*> Packet socket <*> Unix domain sockets < > PF_KEY sockets [*] TCP/IP networking [*] IP: multicasting [ ] IP: advanced router [*] IP: kernel level autoconfiguration [*] IP: DHCP support [*] IP: BOOTP support [*] IP: RARP support
只有开启了“Networking support”的支持后才能够在“Device Drivers”菜单中进入“Network device support”子菜单:
Device Drivers ---> [ ] Multiple devices driver support (RAID and LVM) ---> < > Generic Target Core Mod (TCM) and ConfigFS Infrastructure --->53 [*] Network device support ---> [ ] ISDN support ---> < > Telephony support ---> Input device support --->
在这个子菜单下进入“Network device support”子菜单,并根据实际情况在“Ethernet driver support”中选择配置物理网卡,如下配置是根据实际项目需求进行的配置。
[*] Nexell devices <*> Nexell devices <*> Nexell 10/100/1000 Ethernet driver [*] NXPMAC Platform bus support
3.6.4 小结
由于内核配置的选项非常多,本节仅针对了U盘、GPIO配置以及网络配置进行讲解,其他的配置方式也是一样的,不同的是需要根据实际的项目需求结合实际板子进行配置。
3.7 本章总结
本章主要针对Linux的内核裁剪及制定进行了讲解,主要分别对内核源码、内核的配置和编译进行了详细的讲解,最后通过GPIO、串口配置等示例来讲解如何对内核进行裁剪,让读者对内核裁剪有一个更加直观的认识,也为后续的驱动讲解奠定基础。