第1章 内核开发简介
Linux起源于芬兰的莱纳斯·托瓦尔兹(Linus Torvalds)在1991年凭个人爱好开创的一个项目。这个项目不断发展,至今全球有1000多名贡献者。现在,Linux已经成为嵌入式系统和服务器的必选。内核作为操作系统的核心,其开发不是一件容易的事。
和其他操作系统相比,Linux拥有更多的优点。
·免费。
·丰富的文档和社区支持。
·跨平台移植。
·源代码开放。
·许多免费的开源软件。
本书尽可能做到通用,但是仍然有些特殊的模块,比如设备树,目前在x86上没有完整实现。那么话题将专门针对ARM处理器,以及所有完全支持设备树的处理器。为什么选这两种架构?因为它们在桌面和服务器(x86)以及嵌入式系统(ARM)上得到广泛应用。
本章涉及以下主题。
·开发环境设置。
·获取、配置和构建内核源码。
·内核源代码组织。
·内核编码风格简介。
1.1 环境设置
在开始任何开发之前,都需要设置开发环境。用于Linux开发的环境相当简单,至少在基于Debian的系统上是这样:
$ sudo apt-get update $ sudo apt-get install gawk wget git diffstat unzip texinfo \ gcc-multilib build-essential chrpath socat libsdl1.2-dev \ xterm ncurses-dev lzop
本书部分代码与ARM片上系统(System on Chip,SoC)兼容,应该安装gcc-arm:
sudo apt-get install gcc-arm-linux-gnueabihf
我在华硕RoG上运行Ubuntu 16.04,使用的是Intel Core i7(8个物理内核),16 GB内存,256 GB固态硬盘和1 TB磁盘驱动器。我最爱用的编辑器是Vim,读者可以使用任意一款自己熟悉的编辑器。
1.1.1 获取源代码
在早期内核(2003年前)中,使用奇偶数对版本进行编号:奇数是稳定版,偶数是不稳定版。随着2.6版本的发布,版本编号方案切换为X.Y.Z格式。
·X:代表实际的内核版本,也被称为主版本号,当有向后不兼容的API更改时,它会递增。
·Y:代表修订版本号,也被称作次版本号,在向后兼容的基础上增加新的功能后,它会递增。
·Z:代表补丁,表示与错误修订相关的版本。
这就是所谓的语义版本编号方案,这种方案一直持续到2.6.39版本;当Linus Torvalds决定将版本升级到3.0时,意味着语义版本编号在2011年正式结束,然后采用的是X.Y版本编号方案。
升级到3.20版时,Linus认为不能再增加Y,决定改用随意版本编号方案:当Y值增加到手脚并用也数不过来时就递增X。这就是版本直接从3.20变化到4.0的原因。
现在内核使用的X.Y随意版本编号方案,这与语义版本编号无关。
源代码的组织
为了本书的需要,必须使用Linus Torvald的Github仓库。
git clone https://github.com/torvalds/linux git checkout v4.1 ls
·arch/:Linux内核是一个快速增长的工程,支持越来越多的体系结构。这意味着,内核尽可能通用。与体系结构相关的代码被分离出来,并放入此目录中。该目录包含与处理器相关的子目录,例如alpha/、arm/、mips/、blackfin/等。
·block/:该目录包含块存储设备代码,实际上也就是调度算法。
·crypto/:该目录包含密码API和加密算法代码。
·Documentation/:这应该是最受欢迎的目录。它包含不同内核框架和子系统所使用API的描述。在论坛发起提问之前,应该先看这里。
·drivers/:这是最重的目录,不断增加的设备驱动程序都被合并到这个目录,不同的子目录中包含不同的设备驱动程序。
·fs/:该目录包含内核支持的不同文件系统的实现,诸如NTFS、FAT、ETX{2,3,4}、sysfs、procfs、NFS等。
·include/:该目录包含内核头文件。
·init/:该目录包含初始化和启动代码。
·ipc/:该目录包含进程间通信(IPC)机制的实现,如消息队列、信号量和共享内存。
·kernel/:该目录包含基本内核中与体系架构无关的部分。
·lib/:该目录包含库函数和一些辅助函数,分别是通用内核对象(kobject)处理程序和循环冗余码(CRC)计算函数等。
·mm/:该目录包含内存管理相关代码。
·net/:该目录包含网络(无论什么类型的网络)协议相关代码。
·scripts/:该目录包含在内核开发过程中使用的脚本和工具,还有其他有用的工具。
·security/:该目录包含安全框架相关代码。
·sound/:该目录包含音频子系统代码。
·usr/:该目录目前包含了initramfs的实现。
内核必须保持它的可移植性。任何体系结构特定的代码都应该位于arch目录中。当然,与用户空间API相关的内核代码不会改变(系统调用、/proc、/sys),因为它会破坏现有的程序。
本书使用的内核版本是4.1。因此,v4.11版本之前所做的任何更改都会涉及,至少会涉及框架和子系统。
1.1.2 内核配置
Linux内核是一个基于makefile的工程,有1000多个选项和驱动程序。配置内核可以使用基于ncurse的接口命令make menuconfig,也可以使用基于X的接口命令make xconfig。一旦选择,所有选项会被存储到源代码树根目录下的.config文件中。
大多情况下不需要从头开始配置。每个arch目录下面都有默认的配置文件可用,可以把它们用作配置起点:
ls arch/<you_arch>/configs/
对于基于ARM的CPU,这些配置文件位于arch/arm/configs/;对于i.MX6处理器,默认的配置文件位于arch/arm/configs/imx_v6_v7_defconfig;类似地,对于x86处理器,可以在arch/x86/configs/找到配置文件,仅有两个默认配置文件:i386_defconfig和x86_64_defconfig,它们分别对应于32位和64位版本。对x86系统,内核配置非常简单:
make x86_64_defconfig make zImage -j16 make modules makeINSTALL_MOD_PATH </where/to/install> modules_install
对于基于i.MX6的主板,可以先执行ARCH=arm make imx_v6_v7_defconfig,然后执行ARCH=arm make menuconfig。前一个命令把默认的内核选项存储到.config文件中;后一个命令则根据需求来更新、增加或者删除选项。
在执行make xconfig时,可能会遇到与Qt4相关的错误,这种情况下,应该执行下列命令安装相关的软件包:
sudo apt-get install qt4-dev-tools qt4-qmake
1.1.3 构建自己的内核
构建自己的内核需要指定相关的体系结构和编译器。这也意味着,不一定是本地构建:
ARCH=arm make imx_v6_v7_defconfig ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make zImage -j16
输出如下:
[...] LZO arch/arm/boot/compressed/piggy_data CC arch/arm/boot/compressed/misc.o CC arch/arm/boot/compressed/decompress.o CC arch/arm/boot/compressed/string.o SHIPPED arch/arm/boot/compressed/hyp-stub.S SHIPPED arch/arm/boot/compressed/lib1funcs.S SHIPPED arch/arm/boot/compressed/ashldi3.S SHIPPED arch/arm/boot/compressed/bswapsdi2.S AS arch/arm/boot/compressed/hyp-stub.o AS arch/arm/boot/compressed/lib1funcs.o AS arch/arm/boot/compressed/ashldi3.o AS arch/arm/boot/compressed/bswapsdi2.o AS arch/arm/boot/compressed/piggy.o LD arch/arm/boot/compressed/vmlinux OBJCOPY arch/arm/boot/zImage Kernel: arch/arm/boot/zImage is ready
内核构建完成后,会在arch/arm/boot/下生成一个单独的二进制映像文件。使用下列命令构建模块:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules
可以通过下列命令安装编译好的模块:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules_install
modules_install目标需要指定一个环境变量INSTALL_MOD_PATH,指出模块安装的目录。如果没有设置,则所有的模块将会被安装到/lib/modules/ $ (KERNELRELEASE)/kernel/目录下,具体细节将会在第2章讨论。
i.MX6处理器支持设备树,设备树是一些文件,可以用来描述硬件(相关细节会在第6章介绍)。无论如何,运行下列命令可以编译所有ARCH设备树:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs
然而,dtbs选项不一定适用于所有支持设备树的平台。要构建一个单独的DTB,应该执行下列命令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6d- sabrelite.dtb
1.2 内核约定
在内核代码的演化过程中应该遵守标准规则,这里只做简单介绍,后面会专门讨论。第3章~第13章会更全面地介绍内核开发的过程和要点。
1.2.1 编码风格
深入学习本节之前应该先参考一下内核编码风格手册,它位于内核源代码树的Documentation/CodingStyle目录下。编码风格是应该遵循的一套规则,如果想要内核开发人员接受其补丁就应该遵守这一规则。其中一些规则涉及缩进、程序流程、命名约定等。
常见的规则如下。
·始终使用8个字符的制表符缩进,每一行不能超过80个字符。如果缩进妨碍函数书写,那只能说明嵌套层次太多了。使用内核源代码scripts/cleanfile中的脚本可以设置制表符的大小和行长度:
scripts/cleanfile my_module.c
·可以使用intent工具正确缩进代码:
sudo apt-get install indent scripts/Lindent my_module.c
·每一个不被导出的函数或变量都必须声明为静态的。
·在带括号表达式的内部两端不要添加空格。s = size of (struct file);是可以接受的,而s = size of(struct file);是不被接受的。
·禁止使用typedef。
·请使用/* this */注释风格,不要使用// this。
·坏:// 请不要用这个。
·好:/* 内核开发人员这样用注释 */。
·宏定义应该大写,但函数宏可以小写。
·不要试图用注释去解释一段难以阅读的代码。应该重写代码,而不是添加注释。
1.2.2 内核结构分配和初始化
内核总是为其数据结构和函数提供两种可能的分配机制。
下面是其中的一些数据结构。
·工作队列。
·列表。
·等待队列。
·Tasklet。
·定时器。
·完成量。
·互斥锁。
·自旋锁。
动态初始化器是通过宏定义实现的,因此全用大写:INIT_LIST_HEAD()、DECLARE_WAIT_QUEUE_HEAD()、DECLARE_TASKLET()等。
这些将在第3章详细讨论。因此,表示框架设备的数据结构总是动态分配的,每个都有其自己的分配和释放API。框架设备类型如下。
·网络设备。
·输入设备。
·字符设备。
·IIO设备。
·类设备。
·帧缓冲。
·调节器。
·PWM设备。
·RTC。
静态对象在整个驱动程序范围内都是可见的,并且通过该驱动程序管理的每个设备也是可见的。而动态分配对象则只对实际使用该模块特定实例的设备可见。
1.2.3 类、对象、面向对象的编程
内核通过类和设备实现面向对象的编程。内核子系统被抽象成类,有多少子系统,/sys/class/下几乎就有多少个目录。struct kobj ect结构是整个实现的核心,它包含一个引用计数器,以便于内核统计有多少用户使用了这个对象。每个对象都有一个父对象,在sysfs(加载之后)中会有一项。
属于给定子系统的每个设备都有一个指向operations(ops)结构的指针,该结构提供一组可以在此设备上执行的操作。
1.3 总结
本章简要介绍了如何下载Linux源代码、构建第一个内核版本,以及一些常见概念。也就是说,本章很简短,不够详细,但是这只是一个简介,第2章将更详细地介绍内核构建过程、怎样实际编译外部或内核驱动程序,以及在启动内核开发之旅之前应该学习的一些基础知识。