Linux设备驱动开发(第2版)

978-7-115-66385-6
作者: 约翰·马迪厄(John Madieu)
译者: 陈莉君谢瑞莲
编辑: 武少波

图书目录:

详情

本书讲解了Linux设备驱动开发的基础知识以及所用到的开发环境。全书分为17章,内容涵盖了各种Linux子系统、内存管理、RTC、IIO和IRQ管理等,还讲解了DMA和部分设备驱动程序的使用方法。在学完本书之后,读者将掌握Linux设备驱动开发过程中涉及的各种概念,并可以从零开始为嵌入式设备编写驱动程序。 阅读本书需要具备基本的C语言编程能力,且熟悉Linux基本命令。

图书摘要

版权信息

书名:Linux设备驱动开发(第2版)

ISBN:978-7-115-66385-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

版  权

著    [法]约翰·马迪厄(John Madieu)

译    陈莉君 谢瑞莲

责任编辑 武少波

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

版权声明

Copyright © Packt Publishing 2022. First published in the English language under the title Linux Device Driver Development, Second Edition(9781803240060).

All Rights Reserved.

本书由英国Packt Publishing公司授权人民邮电出版社有限公司出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。

内容提要

本书讲解了Linux设备驱动开发的基础知识以及所用到的开发环境。全书分为17章,内容涵盖了各种Linux子系统、内存管理、RTC、IIO和IRQ管理等,还讲解了DMA和部分设备驱动程序的使用方法。在学完本书之后,读者将掌握Linux设备驱动开发过程中涉及的各种概念,并可以从零开始为嵌入式设备编写驱动程序。

阅读本书需要具备基本的C语言编程能力,且熟悉Linux基本命令。

译者序

着手翻译这本书时,我不由得想起自己20多年的“Linux内核之旅”。从带学生撰写第一本书《Linux操作系统内核分析》,到翻译《深入理解Linux内核(第3版)》和《Linux内核设计与实现》,再到撰写博客、录制慕课等,这一路走来,我似乎有一种天然的推动力。不知不觉间,一本本书、一篇篇文章、一个个视频悄然诞生,它们不再属于我,而是飞向广阔的开源社区。受益者薪火相传,将新知播向更遥远的地方。正如Linux本身那样经历“诞生于学生之手,成长于Internet,壮大于自由而开放的文化”的迢迢长路,一个个积跬步以至千里的Linux爱好者也从开源中获益,年复一年地成长、发展,在内核的星辰大海中得以飞扬游弋。我内心亦与有荣焉。

Linux内核这片汪洋大海漫无边际,总有热情懵懂的初学者加入社区,希望开启新的征程;那些曾经的书是过往的旧地图,随着时代发展,总该有一张新地图引领大家,标识出风云变幻间前人探索出的新风景。于是,我注意到了John Madieu撰写的这本《Linux设备驱动开发(第2版)》。他站在当时最新Linux内核版本5.10的门口,开启了一扇驱动硬件设备的崭新大门。

John Madieu不仅是一位嵌入式Linux及内核工程师,他还对拳击充满热情。或许得益于此,他的行文兼具工程人的细致与武人的凝练。Madieu撰写的内容不但详细而且硬核,囊括了内核开发简介、Linux内核模块的基本概念、内核的核心辅助函数、字符设备驱动程序、设备树、平台设备和驱动程序、I2C设备驱动程序、SPI设备驱动程序、Linux内核内存分配、DMA支持、内存访问抽象化——Regmap API简介、内核IRQ框架、LDM简介、IIO框架、引脚控制器和GPIO子系统以及Linux内核输入子系统等方面。

我们翻译本书时,ChatGPT正如日中天。因此,我们在翻译过程中也合理运用了ChatGPT的检索功能。在一字一句的校对过程中,念及那些在茫茫书海中按图索骥的学习者,我们常常有加上注释的冲动,但为了尽可能遵循原书的结构和风格,许多地方仍然保留了原书的语言风格和特点。我们在探索AI的应用性过程中或有疏漏,恳请读者给予指正。

感谢谢瑞莲老师,她与我共同承担了本书的翻译工作。感谢刘雪艺和赵梦田,她们非常仔细认真地翻译和校对了部分章节,感谢我们Linux内核之旅的研究生团队,尤其感谢团队成员刘冰、张子恒、徐东、南帅波、贠可盈、张小航、廉洋洋、杨宁柯、张帆、乔哲、白宇宣、王越、石泉、董旭、杨骏青、张玉哲、孙张品、张子攀、张纪庆和张翔哲。

阅读本书需要一份耐心,更需要一份执着。当你闯过一道道难关,阅读到本书的最后一章时,你会蓦然发现,书仅仅是地图,而渡你走向内核大海深处的方舟则是你的那份坚韧,以及那份不断突破自我认知的力量。

译者 陈莉君

致  谢

我要感谢Jerôme Grard以审慎的眼光提出的改进建议。我还要感谢Pacôme Cyprien Nguefack、Claudia ATK、Elsie Zeufack、Loïca,以及所有直接或间接参与本书写作的人,感谢他们的倾力相助,陪伴我走过这段创作旅程。

感谢我的CEO(创始伙伴)Kyle在我花时间审阅本书时给予的理解,也感谢整个明斯克市所提供的干净、和平、舒适的学习、工作与生活环境。

关于审校者

Robertino Beniš在嵌入式领域拥有超过15年的经验,曾参与过智能家居、移动设备(全球出货量超过1000万台)与车载信息娱乐系统等多个项目。他曾在嵌入式Linux以及许多实时操作系统,包括裸机(还有人记得高通Brew吗?)领域工作过。

目前,他正筹划在加州建立NFTee区块链工程公司,同时于白俄罗斯共和国的明斯克国立语言大学学习俄语。

前  言

Linux内核是一个复杂、可移植、模块化的软件,广泛运行在全球约80%的服务器上和过半数设备的嵌入式系统中。设备驱动程序对Linux系统的运行效果具有决定性影响。随着Linux成为最流行的操作系统之一,人们对开发个人设备驱动程序的兴趣也在稳步增长。

设备驱动程序是连接用户空间和硬件设备的桥梁,是通过Linux内核实现的。

本书的前两章会帮助你理解驱动程序的基础知识,为学习Linux内核的漫长旅途做准备。之后本书将会涉及Linux子系统基础上的驱动开发,比如内存管理、工业输入/输出(Industrial Input/Output,IIO)、通用输入/输出(General-Purpose Input/Output,GPIO)、中断请求(Interrupt Request,IRQ)管理以及集成电路总线(Inter-Integrated Circuit,I2C)和串行外设接口(Serial Peripheral Interface,SPI)。

本书还将介绍直接存储器访问(Direct Memory Access,DMA)和寄存器映射抽象的实用方法。

本书中的源代码已经在x86 PC和SECO公司的UDOO QUAD开发板上进行了测试。本书还提供了一些驱动程序来测试廉价组件,如MCP23016和24LC512,它们分别是I2C的GPIO控制器和电擦除可编程只读存储器(Electrically-Erasable Programmable Read-Only Memory,EEPROM)。

读完本书以后,你将熟悉设备驱动程序开发的概念,并能够使用Linux最新的稳定内核分支(写作本书时为v5.10.y)自主开发各类设备驱动程序。

目标读者

为了理解本书的内容,读者需要具备基本的C语言编程能力和Linux命令相关知识。本书涵盖了被广泛使用的嵌入式设备的Linux驱动程序开发知识,使用的内核版本是v5.10。本书主要面向嵌入式工程师、Linux系统管理员和开发人员。

无论你是系统开发人员、系统架构师或制造商,还是愿意深入研究Linux设备驱动程序开发的其他人员,本书都值得你阅读。

主要内容

第1章“内核开发简介”介绍Linux内核的开发过程,讨论针对x86和基于ARM的内核的下载、配置和编译步骤。

第2章“Linux内核模块的基本概念”讨论Linux内核模块的参数处理以及模块的加载/卸载。

第3章“处理内核的核心辅助函数”介绍常用的内核函数和机制,如工作队列、等待队列、互斥锁、自旋锁,以及其他一切有助于提高驱动程序可靠性的机制。

第4章“编写字符设备驱动程序”重点介绍如何通过字符设备将数据导出到用户空间,以及利用ioctl方法向设备发送特殊命令。

第5章“理解和利用设备树”讨论向内核描述设备的机制,解释设备寻址、资源处理,以及设备树(Device Tree,DT)支持的多种数据类型及其内核应用程序接口(Application Program Interface,API)。

第6章“设备、驱动程序和平台抽象简介”解释Linux内核平台抽象、伪平台总线的概念,以及设备与驱动程序匹配机制。

第7章“平台设备和驱动程序的概念”描述平台驱动程序的一般架构,以及如何处理平台数据。

第8章“编写I2C设备驱动程序”深入探讨I2C设备驱动程序的架构、数据结构,以及总线上驱动程序的初始化和注册方法。

第9章“编写SPI设备驱动程序”描述基于SPI的设备驱动程序架构以及相关数据结构,讨论部分设备的访问方法和特性,以及SPI 与设备树相关的内容。

第10章“深入理解Linux内核内存分配”首先介绍虚拟内存的概念,描述整个内核内存的布局;然后介绍内核内存管理子系统,讨论内存分配和映射、相关API以及涉及的设备。

第11章“实现DMA支持”介绍DMA及其新的内核API——DMA引擎API,讨论不同的DMA映射,并描述如何解决缓存一致性问题。本章还用一个通用的例子总结相关的概念。

第12章“内存访问抽象化——Regmap API简介:寄存器映射抽象化”对寄存器映射API进行概述,讲解它们如何抽象底层的SPI事务,并介绍相关API。

第13章“揭秘内核IRQ框架”全面介绍Linux IRQ管理,从中断在整个系统中的传播开始,到中断控制器驱动程序,从而使用Linux IRQ域API解释中断多路复用的概念。

第14章“LDM简介”概述Linux的核心设计,描述对象如何在内核中表示,以及Linux底层通用设计原理——从kobject到设备,再到总线、类和设备驱动程序。

第15章“深入了解IIO框架”介绍内核数据采集和测量框架,以便处理数模转换器(Digital-to-Analog Converter,DAC)和模数转换器(Analog-to-Digital Converter,ADC)。本章从内核空间和用户空间介绍IIO API(得益于libiio),这些API用于处理触发的缓冲区和连续的数据捕获。

第16章“充分利用引脚控制器和GPIO子系统”描述内核引脚控制基础设施和API,以及GPIO芯片驱动程序和gpiolib(处理GPIO的内核API)。本章将介绍旧的和已弃用的基于整数的GPIO接口,以及新引入的基于描述符的GPIO接口,讨论在设备树中配置它们的方式。本章还会介绍libgpiod,这是用于在用户空间中处理GPIO的官方库。

第17章“利用Linux内核输入子系统”提供Linux内核输入子系统的全局视图,涉及基于IRQ的及轮询的输入设备,并介绍这两种API。本章将解释并展示用户空间如何处理此类设备。

阅读本书所需的知识

本书假设读者对Linux操作系统有中等程度的了解,并具备基本的C语言编程知识(至少了解数据结构、指针处理和内存分配)。所有代码示例都已在Linux内核v5.10中测试通过。

本书涉及的软/硬件

操作系统要求

一台计算机,要求具有良好的网络带宽,以及足够的磁盘空间和RAM(Random Access Memory,随机存储器),以便下载和构建Linux内核

最好是基于Debian的Linux发行版

市面上的任何Cortex-A嵌入式板(如UDOO QUAD、 Jetson Nano、Raspberry Pi和BeagleBone)

Yocto/Buildroot发行版,或者任何嵌入式或特定的操作系统(如树莓派的Raspbian操作系统)

其他必要的软件包在本书的专门章节中都有描述。下载内核源代码需要网络连接。

如果使用的是本书的电子版,建议读者自行编写代码,或者从本书的随书资源中获取代码。这样做能帮助你避免因复制和粘贴代码带来的潜在错误。

资源与支持

资源获取

本书提供如下资源:

本书源代码;

本书思维导图;

异步社区7天VIP会员。

要获得以上资源,您可以扫描下方二维码,根据指引领取。

提交勘误信息

作者、译者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区(https://www.epubit.com),按书名搜索,进入本书页面,单击“发表勘误”,输入勘误信息,单击“提交勘误”按钮即可(见下页图)。本书的作者、译者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

与我们联系

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。

如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作译者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

关于异步社区和异步图书

“异步社区”是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作译者与读者在线交流互动,以及传统出版与数字出版的融合发展。

“异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社在计算机图书领域四十余年的发展与积淀。异步图书面向各行业的信息技术用户。

第1篇 Linux内核开发基础

第1篇将帮助你迈出进入Linux内核开发的第一步。在第1篇中,我们将介绍Linux内核的基础设施、编译和设备驱动程序开发。作为必要的步骤,我们将介绍内核开发者必须知道的一些概念,如睡眠、锁、基本的工作调度和中断处理机制。最后,我们将介绍必不可少的字符设备驱动程序,以允许内核空间和用户空间通过标准的系统调用进行交互。

第1篇包含如下4章:

第1章“内核开发简介”;

第2章“理解Linux内核模块的基本概念”;

第3章“处理内核的核心辅助函数”;

第4章“编写字符设备驱动程序”。

第1章  内核开发简介

Linux始于1991年,是一名芬兰学生Linus Torvalds的业余爱好项目。该项目日臻成熟、不断发展,如今在世界各地约有1000名贡献者。现在,无论是在嵌入式系统中还是在服务器上,Linux都是必不可少的操作系统。

内核是操作系统的核心部分,其开发并不简单。与其他操作系统相比,Linux拥有许多优点:它是免费的,有良好的文档记录,还有一个大型社区,可以在不同的平台间移植,提供对源代码的访问,并且有许多免费的开源软件。

本书将尽可能让描述具有普适性。有一个特殊的主题,称为设备树,它还不是一个完整的x86特性。因此这个主题将专门讨论ARM处理器,特别是那些完全支持设备树的处理器。为什么采用这些架构?因为它们主要用于台式机和服务器(x86),以及嵌入式系统(ARM)。

本章将讨论以下主题:

设置开发环境;

配置和构建Linux内核。

1.1  设置开发环境

当你在嵌入式系统领域工作时,甚至在设置开发环境之前,你必须熟悉如下专业术语。

目标机:生成构建(build)所产生的二进制文件的目标机器。这台机器将运行二进制文件。

宿主机:执行构建过程的机器。

编译:也称为本地编译或本地构建。当目标机和宿主机相同时,就会发生这种情况。也就是说,当你在机器A(宿主机)上编译一个二进制文件时,它将在同一台机器A(目标机)或其他同类机器上执行。原生编译需要一个原生编译器,因此原生编译器的目标机和宿主机是相同的。

交叉编译:目标机和宿主机是不同的。在这里,你可以在机器A(宿主机)构建一个将在机器B(目标机)上执行的二进制文件。在这种情况下,宿主机(机器A)必须安装支持目标体系结构的交叉编译器。因此,交叉编译器是目标机和宿主机不同的编译器。

由于嵌入式计算机的资源(CPU、RAM、磁盘等)有限或较少,因此宿主机通常是强大得多的x86机器,有更多的资源来加速开发过程。然而,在过去几年,嵌入式计算机变得更加强大,它们越来越多地用于本地编译(因此用作宿主机)。一个典型的例子是树莓派4,它具有强大的四核CPU和高达8GB的RAM。

本章将使用x86机器作为宿主机,创建本地构建或进行交叉编译。因此,“本地构建” 一词将指代“x86本地构建”。

要快速检查本机信息,可以使用以下命令:

lsb_release -a
Distributor ID: Ubuntu 
Description:    Ubuntu 18.04.5 LTS 
Release:  18.04 
Codename: bionic

笔者的计算机是一台华硕RoG,它有一颗16核的AMD Ryzen CPU(你可以使用lscpu命令获取这些信息)、16GB的RAM,以及256GB的SSD(Solid State Disk,固态硬盘)和1TB的硬盘驱动器(你可以使用df -h命令获取这些信息)。可以说,一个四核CPU和4GB或8GB的RAM应该足够了,但会导致构建时间延长。笔者最喜欢的编辑器是Vim你也可以自由使用自己最喜欢的编辑器。如果你使用的是台式机,则可以使用目前正被广泛使用的Visual Studio Code(简称VS Code)。

现在我们已经熟悉了将要使用的与编译相关的关键字,下面可以开始设置宿主机了。

1.1.1  设置宿主机

在开始开发流程之前,需要设置开发环境。专用于Linux开发的环境非常简单——至少在基于Debian的系统中是这样。

在宿主机上,需要安装以下几个包:

$ sudo apt update
$ sudo apt install gawk wget git diffstat unzip \
    texinfo gcc-multilib build-essential chrpath socat \
    libsdl1.2-dev xterm ncurses-dev lzop libelf-dev make

在上面的代码中,我们安装了一些开发工具和一些必要的库,以便在配置Linux内核时拥有良好的用户界面。

现在,我们需要安装编译器和工具(链接器、汇编器等),以确保构建过程正常工作并为目标机生成可执行文件。这组工具称为Binutils,编译器加Binutils(如果有的话,再加上其他构建时依赖库)的组合称为工具链。因此,你需要理解“我需要(某个)体系结构的工具链”或类似句子的含义。

理解和安装工具链

在开始编译之前,我们需要安装必要的包和工具用于本地编译或ARM交叉编译,也就是工具链。GCC(GNU Compiler Collection,GNU编译器套件)是Linux内核支持的编译器。Linux内核中定义的许多宏都与GCC相关。因此,我们使用GCC作为(交叉)编译器。

可以使用以下工具链安装命令进行本地编译:

sudo apt install gcc binutils

当需要进行交叉编译时,必须识别并安装正确的工具链。与本机编译器相比,交叉编译器可执行文件的前缀是目标操作系统、体系结构和(有时候是)库的名称。因此,为了识别特定于体系结构的工具链,我们定义了如下命名约定:arch[-vendor][-os]-abi。下面看看这个命名约定中的字段是什么意思。

arch标识架构,即ARM、MIPS、x86、i686等。

vendor为工具链供应商(公司),即Bootlin、Linaro、none(如果没有工具链供应商的话)等,或者省略该字段。

os是目标操作系统,即Linux或none(裸机)。如果省略,则假定为裸机。

abi代表应用程序二进制接口。它指的是底层二进制文件的结构,包括函数调用约定、参数传递方式等。可能的约定包括eabi、gnueabi和gnueabihf。

eabi代表要编译的代码将运行在裸机ARM核心上。

gnueabi代表将编译用于Linux的代码。

gnueabihf与gnueabi相同,但末尾的hf表示硬浮点,这表明编译器及其底层库使用的是硬件浮点指令,而不是浮点指令的软件实现,如定点软件实现。如果没有可用的浮点硬件,指令将被捕获并由浮点仿真模块执行。使用软件模拟时,功能上唯一的实际区别是执行速度变慢。

下面是一些工具链的名称,读者可以通过它们来理解工具链的命名约定。

arm-none-eabi:一个针对ARM架构的工具链。它没有供应商,针对裸机系统(不针对操作系统),并遵循ARM EABI。

arm-none-linux-gnueabi或arm-linux-gnueabi:我们可以使用这个工具链提供的默认配置(ABI)为运行在Linux上的ARM体系结构生成对象。请注意,arm- none-linux-gnueabi与arm-linux-gnueabi是相同的,因为当未指定供应商时,我们假定没有供应商。支持硬件浮点的工具链变体是arm-linux-gnueabihf或arm-none-linux-gnueabihf。

现在我们已经熟悉了工具链的命名约定,从而可以确定哪些工具链能够用于目标架构进行交叉编译。

要在32位的ARM机器上进行交叉编译,可以使用以下命令安装工具链:

sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf

请注意,在Linux树和GCC中,64位的ARM后端/支持称为aarch64。因此,交叉编译器必须有类似gcc-aarch64-linux-gnu*的名称,而Binutils必须有类似binutils -aarch64-linux-gnu*的名称。因此,对于64位的ARM工具链,可以使用以下命令:

sudo apt install make gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu

注意

aarch64只支持/提供硬件浮点aarch64工具链,因此不需要在末尾指定hf。

请注意,并非所有版本的编译器都能编译给定的Linux内核版本。因此,考虑Linux内核版本和编译器(GCC)版本是很重要的。虽然前面的命令安装了你所使用的Linux发行版支持的最新版本,但也可以指定特定的版本。要实现这一点,可以使用gcc-<version>- <arch>-linux-gnu*。

例如,要安装aarch64的GCC 8,可以使用以下命令:

sudo apt install gcc-8-aarch64-linux-gnu

现在工具链已经安装完毕,我们可以查看分发包管理器选择的版本。例如,要检查安装了哪个版本的aarch64交叉编译器,可以使用以下命令:

$ aarch64-linux-gnu-gcc --version
aarch64-linux-gnu-gcc (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04)
7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
[...]

对于32位的ARM变体,可以使用以下命令:

$ arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-gcc (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04)
7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
[...]

最后,对于本地版本,可以使用以下命令:

$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.

现在已经设置好了开发环境,并确保使用了正确的工具版本,我们可以开始下载Linux内核源代码并深入研究它们了。

1.1.2  获取Linux内核源代码

在内核早期(直到2003年),Linux使用了奇偶版本控制风格,其中奇数表示稳定,偶数表示不稳定。当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版本控制方案,这与语义版本控制无关。

根据Linux内核发布模型,内核总有两个最新版本:稳定版本和长期支持(Long-Term Support,LTS)版本。所有的bug修复和新特性都由子系统维护者收集和准备,然后提交给Linus Torvalds,以便将它们包含到Linux树中。 Linux树又称为主线Linux树,或者称为master Git存储库。每个稳定的版本都起源于此。

每个新的内核版本在发布之前,都会通过发布候选标签提交给社区,以便开发人员测试和完善所有的新功能。根据这个周期内收到的反馈,Linus会决定最终版本是否可以发布。当Linus确信新内核已经准备就绪时,他就会发布最终版本。我们称这个版本为“稳定”版本,而不是“候选版本”:这些版本是vX.Y版本。

发布版本没有严格的时间表,但新的主线内核通常每两三个月发布一次。稳定的内核版本基于Linus版本,也就是主线树版本。

稳定的内核在由Linus发布之后,就会出现在linux-stable树中并成为一个分支,可以接收错误修复。之所以被称为稳定树(stable tree),是因为它用于跟踪先前发布的稳定内核。它由Greg Kroah-Hartman维护和策划,但所有修复都必须首先进入Linus的代码树(后文简称Linus树),即主线存储库。一旦在主线存储库中修复了bug,就可以将其应用于之前发布的内核,这些内核仍然由内核开发社区维护。所有已经向后移植到稳定版本的修复程序,在考虑移植之前都必须满足一组重要的标准,其中之一是它们“必须已经存在于Linus树中”。

注意

修正过错误的内核版本被认为是稳定的。

例如,在Linus发布4.9内核时,稳定内核是基于内核编号方案发布的,即4.9.1、4.9.2、4.9.3,依此类推。这样的版本称为bug修复内核版本(bugfix kernel release),其序列通常缩短为“4.9.Y”,表示其在稳定内核发布树中的分支。每个稳定的内核发布树都由其内核开发者维护,该内核开发者将负责为发布选择必要的补丁,并进行审查/发布。通常,在下一个主线内核可用之前,只会发布几个修正过错误的内核版本,除非它被指定为长期维护内核。

在每个子系统和内核维护者存储库中,我们可以找到Linus树或稳定树。在Linus树中,只有一个分支,也就是主分支。它的标签要么是稳定版本,要么是候选版本。在稳定树中,每个稳定内核版本都有一个分支(名为<A.B>.y,其中<A.B>是Linus树中的发布版本),而每个分支则包含其bug修复内核版本。

下载并组织源代码

在本书中,我们将使用Linus树,可以使用以下命令下载Linus树:

  git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
  --depth 1
  git checkout v5.10
  ls

在上面的命令中,我们使用--depth 1来避免下载历史记录(或者只选择上一次提交的历史记录),这可以显著降低下载文件的大小并节省时间。由于Git支持分支和标记,因此checkout命令允许你切换到特定的标记或分支。在这个例子中,我们切换到v5.10标签。

注意

在本书中,我们将使用Linux内核v5.10。

下面让我们来看看主线内核源代码目录的内容。

arch/:为了尽可能通用,特定于体系结构的代码会与其他代码分开。该目录包含特定于处理器的代码,这些代码组织在每个体系结构的子目录中,如alpha/、arm/、mips/、arm64/等。

block/:该目录包含块存储设备的代码。

crypto/:该目录包含加密API和加密算法的代码。

certs/:该目录包含证书和签名文件,用于启用模块签名,以便内核加载签名过的模块。

documentation/:该目录包含用于不同内核框架和子系统的API的描述。在公共论坛上提问之前,你应该先查看该目录。

drivers/:这是内容最繁多的目录,因为随着设备驱动程序的合并,它会不断增长。该目录包含每个设备的驱动程序,它们被组织在不同的子目录中。

fs/:该目录包含内核支持的不同文件系统的实现,如NTFS、FAT、ETX{2,3,4}、sysfs、procfs、NFS等。

include/:该目录包含内核头文件。

init/:该目录包含初始化和启动代码。

ipc/:该目录包含进程间通信(Inter-Process Communication,IPC)机制的实现,如消息队列、信号量和共享内存。

kernel/:该目录包含基本内核中与体系结构无关的部分。

lib/:该目录包含库例程和一些辅助函数,比如通用的内核对象(kobject)处理程序和循环冗余码(Cyclic Redundancy Code,CRC)计算函数。

mm/:该目录包含内存管理代码。

net/:该目录包含网络协议代码(无论网络类型是什么)。

samples/:该目录包含各个子系统的设备驱动程序示例。

scripts/:该目录包含内核使用的脚本和工具,此外还包含其他一些有用的工具。

security/:该目录包含安全框架代码。

sound/:该目录包含音频子系统代码。

tools/:该目录包含Linux内核开发和各种子系统的测试工具,如USB、虚拟宿主机测试模块、GPIO、IIO和SPI等。

usr/:该目录当前包含initramfs实现。

virt/:这是虚拟化目录,其中包含用于hypervisor的内核虚拟机(Kernel-based Virtual Machine,KVM)模块。

为确保可移植性,任何特定于体系结构的代码都应该放在arch目录下。此外,与用户空间API相关的内核代码(系统调用、/proc、/sys等)不应更改,因为更改会破坏现有的程序。

在本节中,我们熟悉了Linux内核的源代码。在浏览了Linux内核的所有源代码之后,将它们配置得能够编译内核似乎是很自然的事情。

1.2  配置和构建Linux内核

在Linux内核源代码中,有许多可用的驱动程序/特性和构建选项。配置过程包括选择哪些特性/驱动程序,选择的这些选项将成为编译过程的一部分。根据我们是执行本地编译还是交叉编译,有些环境变量必须在配置过程开始之前定义。

1.2.1  指定编译选项

内核的Makefile调用的编译器是$(CROSS_COMPILE)gcc。也就是说,CROSS_ COMPILE是交叉编译工具(gcc、as、ld、objcopy等)的前缀,必须在调用make时指定,或者必须在执行任何make命令之前导出。只有gcc以及与之相关的Binutils可执行文件的前缀是$(CROSS_COMPILE)。

请注意,这里做了各种假设,Linux内核构建基础设施会根据目标机体系结构启用各种选项/特性/标志。为此,除了交叉编译器前缀,还必须指定目标机的体系结构,这可以通过ARCH环境变量来完成。

因此,典型的Linux配置或构建命令如下:

 ARCH=<XXXX> CROSS_COMPILE=<YYYY> make menuconfig

也可以使用如下命令:

 ARCH=<XXXX> CROSS_COMPILE=<YYYY> make <make-target>

如果启动命令时不希望指定这些环境变量,则可以将它们导出到当前shell中,示例如下:

 export CROSS_COMPILE=aarch64-linux-gnu
 export ARCH=aarch64

请记住,如果没有指定这些环境变量,本地宿主机将成为目标机。也就是说,如果省略CROSS_COMPILE或未设置CROSS_COMPILE, $(CROSS_COMPILE)gcc将得到gcc,对于调用的其他工具亦如此(例如,$(CROSS_COMPILE)ld将得到ld)。

以同样的方式,如果ARCH(目标体系结构)被省略或没有设置,则默认执行make的宿主机,默认为$(uname -m)。

因此,要想使用gcc为宿主机体系结构本地编译内核,就应该保持CROSS_COMPILE和ARCH未定义。

1.2.2  理解内核配置过程

Linux内核是一个基于Makefile的项目,其中包含数千个选项和驱动程序。每个启用的选项都可以使另一个选项可用,或者可以将特定的代码拉入构建。要配置内核,可以对基于ncurses的接口使用make menuconfig命令,或对基于X的接口使用make xconfig命令。基于ncurses的接口配置如图1.1所示。

对于大多数选项,你有3个选择。但在配置Linux内核时,我们可以列出5种类型的选项,具体如下。

布尔选项,有两个选择。

(blank),它禁用此功能。当这个选项在配置菜单中突出显示时,你可以按N来省略功能。它等同于false,当它被禁用时,生成的配置选项会在配置文件中被注释掉。

(*),它在内核中静态编译。这意味着当内核第一次被加载时,它将始终存在。它等价于true,你可以在配置菜单中选中一个功能并按Y。得到的选项将在配置文件中显示为CONFIG_<OPTION >=y,例如CONFIG_INPUT_ EVDEV=y。

图1.1  内核配置界面

三态选项,即除了可以接受布尔值状态,还可以接受第3种状态,它在配置窗口中被标记为(M)。该选项在配置文件中的结果为CONFIG_<OPTION>=m,例如CONFIG_ INPUT_ EVDEV=m。要生成一个可加载的模块(如果这个选项允许的话),你可以按下M选择这个特性。

字符串选项,接受字符串值,例如CONFIG_CMDLINE="noinitrd console= ttymxc0, 115200"。

Hex选项,接受十六进制值,例如CONFIG_PAGE_ OFFSET=0x80000000。

Int选项,接受整数值,例如CONFIG_CONSOLE_ LOGLEVEL_DEFAULT=7。

选中的选项将存储在源代码树的根目录下的.config文件中。

想要知道哪种配置在你的平台上可以有效运行是很难的。大多数情况下,不需要从头开始配置。每个arch目录下都有默认配置文件和功能配置文件,你可以将它们作为起点(重要的是要从已经可用的配置开始):

ls arch/<your_arch>/configs/

对于基于ARM的32位CPU,这些配置文件可以在arch/arm/configs/目录中找到。在这种体系结构中,每个CPU系列通常只有一个默认配置。例如,对于i.MX6-7处理器,默认的配置文件是arch/arm/configs/imx_v6_v7_defconfig。但是,在基于ARM的 64位CPU上,只有一个大的默认配置可以定制。它位于arch/arm64/configs/目录中,称为defconfig。类似地,对于x86处理器,我们可以在arch/x86/configs/目录中找到相关文件。这里有两个默认的配置文件—— i386_defconfig和x86_64_defconfig,分别用于32位和64位的x86体系结构。

给定一个默认配置文件,内核配置命令如下:

 make <foo_defconfig>

运行该命令将在主目录(根目录)中生成一个新的.config文件,而旧的.config文件将被重命名为.config.old。这可以方便地恢复以前的配置更改。然后,可以使用以下命令自定义配置:

 make menuconfig

保存更改将更新.config文件。虽然你可以与协作者共享此配置,但你最好创建一个与Linux内核源代码中提供的配置文件相同的最小格式的默认配置文件。为此,可以使用以下命令:

 make savedefconfig

该命令将创建一个最小的配置文件(因为不存储非默认设置)。生成的默认配置文件称为defconfig,存储在源代码树的根目录中。你可以使用以下命令将其存储到另一个位置:

 mv defconfig arch/<arch>/configs/myown_defconfig

这样你就可以在Linux内核源代码中共享同一个参考配置,其他开发者现在可以通过执行以下命令得到与你相同的.config文件:

 make myown_defconfig

请注意,对于交叉编译,必须在执行任何make命令之前设置ARCH和CROSS_COMPILE,即使对内核配置也是如此;否则,你的配置可能会出现意外更改。

根据目标系统的不同,可以使用以下配置命令。

要在64位的x86处理器上进行本地编译,命令非常简单(可以省略编译选项):

 make x86_64_defconfig
 make menuconfig

给定一个基于ARM i.MX6的32位板,可以执行以下命令:

 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx_v6_v7_defconfig
 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make menuconfig

使用第一组命令,你可以将默认选项存储在.config文件中;而使用第二组命令,你可以根据需要更新(添加/删除)各种选项。

对于64位ARM板,可以执行以下命令:

 ARCH=aarch64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig
 ARCH=aarch64 CROSS_COMPILE=aarch64-linux-gnu- make menuconfig

你可能会在使用xconfig时遇到Qt4错误。在这种情况下,应该直接使用以下命令来安装缺少的包:

 sudo apt install qt4-dev-tools qt4-qmake

注意

你可能正在从旧内核切换到新内核。给定旧的.config文件,你可以将其复制到新的内核源代码树中,然后执行make oldconfig命令。如果新内核中有新选项,系统将提示你是否将这些选项包括进去。但是,你可能希望对这些选项使用默认值。在这种情况下,应执行make olddefconfig命令。最后,如果要拒绝所有新选项,应执行make oldnoconfig命令。

要找到初始配置文件,也可以有更好的选择,尤其当你的计算机已经在运行时。Linux发行版Debian和Ubuntu将.config文件保存在/boot目录下,所以可以使用以下命令复制此配置文件:

cp /boot/config-`uname -r` .config

其他Linux发行版可能不会这样做。因此,建议你始终启用IKCONFIG和IKCONFIG_ PROC内核配置选项,这将允许你通过/proc/config.gz访问.config文件。这是一个标准方法,适用于嵌入式发行版。

一些有用的内核配置特性

现在可以配置内核了,让我们列举一些有用的内核配置特性,这些内核配置特性可能值得你在内核中启用。

IKCONFIG和IKCONFIG_PROC:笔者认为它们最重要。它们使内核配置在运行时可用,你可以在其他系统中重用此配置,或者只是查找特定功能的启用状态。例如:

    # zcat /proc/config.gz | grep CONFIG_SOUND
    CONFIG_SOUND=y
    CONFIG_SOUND_OSS_CORE=y
    CONFIG_SOUND_OSS_CORE_PRECLAIM=y
    # CONFIG_SOUNDWIRE is not set
    #

CMDLINE_EXTEND和CMDLINE:前者是一个布尔值,它允许你从配置中扩展内核命令行;后者是一个包含实际命令行扩展值的字符串,例如CMDLINE= "noinitrd usbcore.authorized_default=0 "。

CONFIG_KALLSYMS:这是一个布尔选项,用于访问/proc/kallsyms目录中的内核符号表(其中包含了符号与其地址之间的映射)。这对于跟踪器和其他需要将内核符号映射到地址的工具非常有用。该选项在输出oops消息时使用;否则代码清单将产生十六进制输出,会变得很难解读。

CONFIG_PRINTK_TIME:当输出来自内核的消息时,该选项显示时间信息。这对于运行时发生的事件进行时间戳标记会有所帮助。

CONFIG_INPUT_EVBUG:允许你调试输入设备。

CONFIG_MAGIC_SYSRQ:允许你对系统进行一些控制(例如重新启动、转储一些状态信息等),即使在系统崩溃之后,只需要使用一些组合键即可执行。

DEBUG_FS:允许你启用对调试文件系统的支持,可以在其中调试GPIO、CLOCK、DMA、REGMAP、IRQ和许多其他子系统。

FTRACE和DYNAMIC_FTRACE:允许你启用功能强大的ftrace跟踪程序,以跟踪整个系统。启用ftrace跟踪程序后,它的一些枚举选项也可以启用,具体如下。

FUNCTION_TRACER:允许你跟踪内核中的任何非内联函数。

FUNCTION_GRAPH_TRACER:与前一个枚举选项的功能相同,但它显示了一个调用图(调用函数和被调用函数)。

IRQSOFF_TRACER:该枚举选项使你能够跟踪内核中IRQ的周期。

PREEMPT_TRACER:允许你测量抢占关闭的延迟。

SCHED_TRACER:允许你调度延迟跟踪。

现在内核已经配置好了,之后必须构建以生成一个可运行内核。在1.2.3节中,我们将描述内核的构建过程,以及预期的构建组件。

1.2.3  构建Linux内核

这一步需要你使用与配置步骤相同的shell,否则就必须重新定义ARCH和CROSS_COMPILE环境变量。

Linux内核是一个基于Makefile的项目。构建这样的项目需要使用make工具并执行make命令。对于Linux内核,make命令必须以普通用户身份在主内核源目录下执行。

默认情况下,如果没有指定,则make目标是所有的文件。在Linux内核源代码中,对于x86体系结构,它指向(或依赖)vmlinux bzImage modules目标;对于ARM或aarch64体系结构,它指向vmlinux zImage modules dtbs目标。

在这些目标中,bzImage是一个特定于x86体系架构的make目标,它会生成具有相同名称bzImage的二进制文件。Vmlinux也是一个make目标,用于生成一个名为vmlinux的Linux映像。zImage和dtbs都是ARM和aarch64体系架构专用的make目标:前者生成具有相同名称的Linux映像,后者则为目标机CPU构建设备树源。Modules作为一个make目标,将构建所有选定的模块(在配置中用m标记)。

相关图书

操作系统基础与实践——基于openEuler平台
操作系统基础与实践——基于openEuler平台
Linux常用命令自学手册
Linux常用命令自学手册
eBPF开发指南从原理到应用
eBPF开发指南从原理到应用
Linux后端开发工程实践
Linux后端开发工程实践
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux

相关文章

相关课程