《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
相关文章
|
23小时前
|
缓存 运维 算法
深入理解Linux内核的虚拟内存管理
【5月更文挑战第6天】 在现代操作系统中,尤其是类Unix系统如Linux中,虚拟内存管理是一项核心功能,它不仅支持了多任务环境,还提供了内存保护和抽象。本文将深入探讨Linux操作系统的虚拟内存子系统,包括分页机制、虚拟地址空间布局、页面置换算法以及内存分配策略。通过对这些概念的剖析,我们旨在为读者揭示Linux如何有效地管理和优化物理内存资源,并确保系统的稳定运行与高效性能。
|
23小时前
|
Linux 调度 开发者
探索Linux内核调度:公平与效率的平衡艺术
【5月更文挑战第6天】 随着多核处理器的普及,操作系统的进程调度策略对系统性能的影响愈加显著。Linux作为广泛应用的开源操作系统,其内核调度器的设计哲学和实现细节一直是系统研究领域的热点。本文将深入分析Linux内核调度器的工作原理,探讨如何在保证公平性和效率之间取得平衡,并考察最新的调度器CFS(Completely Fair Scheduler)如何适应现代硬件架构的需求。
|
1天前
|
安全 Linux Android开发
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
该文介绍了如何在Linux服务器上交叉编译Android的FFmpeg库以支持HTTPS视频播放。首先,从GitHub下载openssl源码,解压后通过编译脚本`build_openssl.sh`生成64位静态库。接着,更新环境变量加载openssl,并编辑FFmpeg配置脚本`config_ffmpeg_openssl.sh`启用openssl支持。然后,编译安装FFmpeg。最后,将编译好的库文件导入App工程的相应目录,修改视频链接为HTTPS,App即可播放HTTPS在线视频。
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
|
6天前
|
运维 监控 Linux
提升系统稳定性:Linux内核参数调优实战
【5月更文挑战第1天】 在运维领域,保障服务器的高效稳定运行是核心任务之一。Linux操作系统因其开源、可靠和灵活的特点被广泛应用于服务器中。本文将深入探讨通过调整Linux内核参数来优化系统性能,提升服务器的稳定性和响应能力。文章首先介绍了内核参数调优的必要性和基本原则,然后详细阐述了调优过程中的关键步骤,包括如何监控当前系统状态,确定性能瓶颈,选择合适的参数进行调优,以及调优后的测试与验证。最后,文中提供了一些常见问题的解决策略和调优的最佳实践。
28 5
|
6天前
|
算法 大数据 Linux
深入理解Linux内核的进程调度机制
【4月更文挑战第30天】操作系统的核心职能之一是有效地管理和调度进程,确保系统资源的合理分配和高效利用。在众多操作系统中,Linux因其开源和高度可定制的特点,在进程调度机制上展现出独特优势。本文将深入探讨Linux内核中的进程调度器——完全公平调度器(CFS),分析其设计理念、实现原理及面临的挑战,并探索未来可能的改进方向。
|
7天前
|
前端开发 Linux iOS开发
【Flutter前端技术开发专栏】Flutter在桌面应用(Windows/macOS/Linux)的开发实践
【4月更文挑战第30天】Flutter扩展至桌面应用开发,允许开发者用同一代码库构建Windows、macOS和Linux应用,提高效率并保持平台一致性。创建桌面应用需指定目标平台,如`flutter create -t windows my_desktop_app`。开发中注意UI适配、性能优化、系统交互及测试部署。UI适配利用布局组件和`MediaQuery`,性能优化借助`PerformanceLogging`、`Isolate`和`compute`。
【Flutter前端技术开发专栏】Flutter在桌面应用(Windows/macOS/Linux)的开发实践
|
7天前
|
算法 Linux 调度
探索Linux内核:进程调度的奥秘
【4月更文挑战第30天】 在多任务操作系统中,进程调度是核心功能之一,它决定了处理器资源的分配。本文深入分析了Linux操作系统的进程调度机制,从调度器的基本原理到复杂的调度策略,以及它们如何影响系统性能和用户体验。通过剖析进程优先级、时间片分配以及实时性要求等方面,揭示了Linux如何在众多运行着的进程中做出快速而公平的决策,确保系统的高效与稳定运行。
|
7天前
|
算法 安全 Linux
深度解析:Linux内核内存管理机制
【4月更文挑战第30天】 在操作系统领域,内存管理是核心功能之一,尤其对于多任务操作系统来说更是如此。本文将深入探讨Linux操作系统的内核内存管理机制,包括物理内存的分配与回收、虚拟内存的映射以及页面替换算法等关键技术。通过对这些技术的详细剖析,我们不仅能够理解操作系统如何高效地利用有限的硬件资源,还能领会到系统设计中的性能与复杂度之间的权衡。
|
8天前
|
弹性计算 网络协议 Shell
自动优化Linux 内核参数
【4月更文挑战第29天】
6 1
|
8天前
|
弹性计算 网络协议 Linux
自动优化 Linux 内核参数
【4月更文挑战第28天】
18 0