《Linux设备驱动开发详解 A》一一3.4 Linux内核的编译及加载

简介:

本节书摘来华章计算机出版社《Linux设备驱动开发详解 A》一书中的第3章,第3.4节,作者:宋宝华 更多章节内容可以访问云栖社区“华章计算机”公众号查看。1

3.4 Linux内核的编译及加载

3.4.1 Linux内核的编译
Linux驱动开发者需要牢固地掌握Linux内核的编译方法以为嵌入式系统构建可运行的Linux操作系统映像。在编译内核时,需要配置内核,可以使用下面命令中的一个:
make conf?ig(基于文本的最为传统的配置界面,不推荐使用)
make menuconf?ig(基于文本菜单的配置界面)
make xconf?ig(要求QT被安装)
make gconf?ig(要求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所示。

image

内核配置包含的条目相当多,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所示的分层结构。
3.4.2 Kconfig和Makefile
在Linux内核中增加程序需要完成以下3项工作。
将编写的源代码复制到Linux内核源代码的相应目录中。
在目录的Kconfig文件中增加关于新源代码对应项目的编译配置选项。
在目录的Makefile文件中增加对新源代码的编译条目。
1.?实例引导:TTY_PRINTK字符设备
在讲解Kconfig和Makefile的语法之前,我们先利用两个简单的实例引导读者对其建立对具初步的认识。
首先,在drivers/char目录中包含了TTY_PRINTK设备驱动的源代码drivers/char/ttyprintk.c。而在该目录的Kconfig文件中包含关于TTY_PRINTK的配置项:

conf?ig 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”。

image

图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脚本,具体执行完全仿照上述过程即可。
2.?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后缀的形式来定义模块的组成文件,如下:

#

# Makef?ile for the linux ext2-f?ilesystem routines.
#

obj-$(CONF?iG_EXT2_FS) += ext2.o
ext2-y := balloc.o dir.o f?ile.o fsync.o ialloc.o inode.o \
       ioctl.o namei.o super.o symlink.o
ext2-$(CONF?iG_EXT2_FS_XATTR)     += xattr.o xattr_user.o xattr_trusted.o
ext2-$(CONF?iG_EXT2_FS_POSIX_ACL) += acl.o
ext2-$(CONF?iG_EXT2_FS_SECURITY)  += xattr_security.o
ext2-$(CONF?iG_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-$(CONF?iG_EXT2_FS) += ext2/

当CONFIG_EXT2_FS的值为y或m时,kbuild将会把ext2目录列入向下迭代的目
标中。
3.?Kconfig
内核配置脚本文件的语法也比较简单,主要包括如下几个方面。
(1)配置选项
大多数内核配置选项都对应Kconfig中的一个配置选项(config):

conf?ig 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没有被选中的时候,才可能出现这个SHDMA_R8A73A4。
conf?ig 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
conf?ig NETDEVICES

endmenu
所有处于“menu”和“endmenu”之间的配置选项都会成为“Network device support”的子菜单,而且,所有子菜单(config)选项都会继承父菜单(menu)的依赖关系,比如,“Network device support”对“NET”的依赖会被加到配置选项NETDEVICES的依赖列表中。
注意:menu后面跟的“Network device support”项仅仅是1个菜单,没有对应真实的配置选项,也不具备3种不同的状态。这是它和config的区别。
另一种方式是通过分析依赖关系生成菜单结构。如果菜单项在一定程度上依赖于前面的选项,它就能成为该选项的子菜单。如果父选项为“n”,子选项不可见;如果父选项可见,子选项才可见。例如:

conf?ig MODULES
   bool "Enable loadable module support"

conf?ig 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:

choice
depends on FB_S3C_VGA
prompt "Select VGA Resolution for S3C Framebuffer"
default FB_S3C_VGA_1024_768
conf?ig FB_S3C_VGA_1024_768
     bool "1024*768@60Hz"
     ---help---
     TBA 
conf?ig FB_S3C_VGA_640_480
     bool "640*480@60Hz"
     ---help---
     TBA 
endchoice

上述例子中,prompt配合choice起到提示作用。
用Kconfig配置脚本和Makefile脚本编写的更详细信息,可以分别参见内核文档Documentation目录内的kbuild子目录下的Kconfig-language.txt和Makefiles.txt文件。
4.?应用实例:在内核中新增驱动代码目录和子目录
下面来看一个综合实例,假设我们要在内核源代码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 conf?iguration
#
menu "TEST Driver "
comment " TEST Driver"

conf?ig CONF?iG_TEST
    bool "TEST support "

conf?ig CONF?iG_TEST_USER
    tristate "TEST user-space interface"
    depends on CONF?iG_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/Makef?ile

Makef?ile for the TEST.

obj-$(CONF?iG_TEST) += test.o test_queue.o test_client.o
obj-$(CONF?iG_TEST_USER) += test_ioctl.o
obj-$(CONF?iG_PROC_FS) += test_proc.o

obj-$(CONF?iG_TEST_CPU) += cpu/
该脚本根据配置变量的取值,构建obj-*列表。由于test目录中包含一个子目录cpu,因此当CONFIG_TEST_CPU=y时,需要将cpu目录加入列表中。
test目录中的cpu子目录也需包含如下Makefile:

drivers/test/test/Makef?ile

Makef?ile for the TEST CPU

obj-$(CONF?iG_TEST_CPU) += cpu.o
为了使得编译命令作用到能够整个test目录,test目录的父目录中Makefile也需新增如下脚本:
obj-$(CONF?iG_TEST) += test/
在drivers/Makefile中加入obj-$(CONFIG_TEST) += test/,使得用户在进行内核编译时能够进入test目录。
增加了Kconfig和Makefile之后的新test树形目录为:

|--test
   |-- cpu
       | -- cpu.c
       | -- Makef?ile
   |-- test.c
   |-- test_client.c
   |-- test_ioctl.c
   |-- test_proc.c
   |-- test_queue.c
   |-- Makef?ile
   |-- Kconf?ig

3.4.3 Linux内核的引导
引导Linux系统的过程包括很多阶段,这里将以引导ARM Linux为例来进行讲解(见image

图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共担这些负载,进行负载均衡。
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
相关文章
|
21天前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
21天前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
22天前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
22天前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
24天前
|
负载均衡 算法 Linux
深入探索Linux内核调度机制:公平与效率的平衡####
本文旨在剖析Linux操作系统内核中的进程调度机制,特别是其如何通过CFS(完全公平调度器)算法实现多任务环境下资源分配的公平性与系统响应速度之间的微妙平衡。不同于传统摘要的概览性质,本文摘要将直接聚焦于CFS的核心原理、设计目标及面临的挑战,为读者揭开Linux高效调度的秘密。 ####
34 3
|
27天前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
40 6
|
26天前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。
|
1月前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
77 4
|
28天前
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
30 2
|
28天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
45 1