Linux设备驱动开发

978-7-115-55555-7
作者: 约翰 • 马迪厄(John Madieu)
译者: 袁鹏飞刘寿永
编辑: 陈聪聪

图书目录:

详情

本书讲解了Linux驱动开发的基础知识以及所用到的开发环境,全书分为22章,其内容涵盖了各种Linux子系统,包含内存管理、PWM、RTC、IIO和IRQ管理等,还讲解了直接内存访问和网络设备驱动程序的实用方法。在学完本书之后,读者将掌握设备驱动开发环境的概念,并可以从零开始为任何硬件设备编写驱动程序。 阅读本书需要具备基本的C语言程序设计能力,且熟悉Linux基本命令。本书主要是为嵌入式工程师、Linux系统管理员、开发人员和内核黑客而设计的。无论是软件开发人员,还是系统架构师或制造商,只要愿意深入研究Linux驱动程序开发,阅读本书后都将有所收获。

图书摘要

为嵌入式Linux开发定制的驱动程序

Linux设备驱动开发
Linux Device Drivers Development

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

袁鹏飞 刘寿永 译

人民邮电出版社

北京

图书在版编目(CIP)数据

Linux设备驱动开发 / (法)约翰·马迪厄(John Madieu) 著;袁鹏飞,刘寿永译. --北京:人民邮电出版社,2021.3

ISBN 978-7-115-55555-7

Ⅰ.①L… Ⅱ.①约…②袁…③刘… Ⅲ.①Linux操作系统-驱动程序-程序设计 Ⅳ.①TP316-85

中国版本图书馆CIP数据核字(2020)第247368号


版权声明

Copyright © Packt Publishing 2018. First published in the English language under the title Linux Device Drivers Development.

All Rights Reserved.

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

版权所有,侵权必究。


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

   译 袁鹏飞 刘寿永

责任编辑 陈聪聪

责任印制 王 郁 彭志环

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

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

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

北京鑫正大印刷有限公司印刷

◆ 开本:800×1000   1/16

印张:30.75

字数:570千字  2021年3月第1版

印数:1–2 000册  2021年3月北京第1次印刷

著作权合同登记号 图字:01-2017-8619号

定价:149.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京东市监广登字20170147号

内容提要

本书讲解了Linux驱动开发的基础知识以及所用到的开发环境,全书分为22章,其内容涵盖了各种Linux子系统,包含内存管理、PWM、RTC、IIO和IRQ管理等,还讲解了直接内存访问和网络设备驱动程序的实用方法。在学完本书之后,读者将掌握设备驱动开发环境的概念,并可以从零开始为任何硬件设备编写驱动程序。

阅读本书需要具备基本的C语言程序设计能力,且熟悉Linux基本命令。本书主要是为嵌入式工程师、Linux系统管理员、开发人员和内核黑客而设计的。无论是软件开发人员,还是系统架构师或制造商,只要愿意深入研究Linux驱动程序开发,阅读本书后都将有所收获。

关于作者

约翰·马迪厄(John Madieu)是嵌入式Linux和内核研发工程师,居住在法国巴黎。他主要为自动化、运输、医疗、能源等领域的公司开发驱动程序并提供开发板支持包(Board Support Package,BSP)。他目前就职于法国公司EXPEMB,该公司专注于模块化计算机的电子开发板设计和嵌入式Linux解决方案。同时,他还是一位开源和嵌入式系统爱好者,坚信通过知识分享能够学到更多的知识。

他爱好拳击,接受过6年的专业训练,并开始提供培训课程。

致谢

我非常感谢德维卡·巴蒂克(Devika Battike)、格宾·乔治(Gebin George)和Packt团队为按时出版本书所做的努力。没有他们这本书很可能无法面市,与他们合作非常愉快。

感谢多年来帮助过我,并仍在陪伴着我的所有良师益友:西普里恩·帕肯·恩格法特克(Cyprien Pacôme Nguefack),多年来我一直向他学习编程技巧;杰罗姆·普里耶(Jérôme Pouillier)和克里斯托夫·诺维奇(Christophe Nowicki),他们向我介绍了Buildroot,并把我带入内核编程领域;让·克里斯蒂安·雷拉(Jean-Christian Rerat)和EXPEMB的让·菲利普·杜泰尔(Jean-Philippe DU-Teil),对我职业生涯提供了指导和陪伴;对于所有帮助过我,但在本文中没有提到的人,我仍然要感谢他们提供的素材和信息,我会通过本书将这些信息传播给读者。

关于审校者

杰罗姆·普里耶(Jérôme Pouillier)是一位极客,对理解事物如何运作着迷。

他是Linux的早期使用者。他发现Linux是不受任何限制的系统,其中没有什么是不可修改的。Linux是一个出色的平台。

杰罗姆·普里耶毕业于法国高等信息工程师学院(Ecole Pour l’Informatique et les Technologies Avancées,EPITA),主修机器学习。除此之外,他还自学了电子学。但他很快把注意力从所有高级系统转移到操作系统。现在操作系统是他最喜欢的科目之一。

多年来,杰罗姆·普里耶为多个行业(多媒体、医疗、核能)设计(和调试)Linux固件。

除从事咨询活动外,杰罗姆·普里耶还是法国国立应用科学学院(Institut National des SciencesAppliquées,INSA)的操作系统专业教授。他撰写了许多关于系统编程、操作系统设计、实时系统等方面的课程资料。

前言

Linux内核是一种复杂、轻便、模块化并被广泛使用的软件。大约80%的服务器和全世界一半以上设备的嵌入式系统上运行着Linux内核。设备驱动程序在整个Linux系统中起着至关重要的作用。由于Linux已成为非常流行的操作系统,因此本人对开发基于Linux的设备驱动程序的兴趣也在逐步提升。

设备驱动程序通过内核在用户空间和设备之间建立连接。

本书前两章介绍驱动程序的基础知识,为Linux内核的学习做准备。之后介绍基于Linux各个子系统的驱动程序开发,如内存管理、PWM、RTC、IIO、GPIO、IRQ管理等。本书还介绍了直接内存访问和网络设备驱动程序的实用方法。

本书中的源代码已经在x86 PC和SECO的UDOO Quad上进行了测试,其中UDOO Quad的主芯片是恩智浦的ARM i.MX6,它具有丰富的功能和外部接口,可以覆盖本书中讨论的所有测试。它还提供了一些驱动程序来测试价格较低的组件,如MCP23016和24LC512,它们分别是I2C GPIO控制器和电可擦可编程只读存储器。

读完本书,读者能深刻理解设备驱动程序开发的概念,并且可以使用内核(本书写作时Linux内核版本为v4.13)从零开始编写任何设备驱动程序。

本书内容

第1章介绍Linux内核开发过程,主要讨论内核下载、配置和编译步骤,适用于x86系统和基于ARM的系统。

第2章介绍如何用内核模块来实现Linux的模块化,以及模块的加/卸载。本章还介绍了驱动程序的架构一些基本概念和内核最佳实践。

第3章介绍常用的内核功能和机制,如工作队列、等待队列、互斥锁、自旋锁,以及其他用于提高驱动程序可靠性的功能。

第4章重点介绍通过字符设备把设备功能输出到用户空间,以及IOCTL接口支持的自定义命令。

第5章解释什么是平台设备,介绍伪平台总线的概念,以及设备和总线的匹配机制。本章描述平台设备驱动的总体体系结构,以及处理平台数据的方法。

第6章讨论向内核提供设备描述的机制,解释设备寻址、资源处理、DT及其内核API支持的各种数据类型。

第7章深入讨论I2C设备驱动体系架构、数据结构和该总线上的设备寻址及访问方法。

第8章介绍基于SPI的设备驱动体系架构及其涉及的数据结构,还讨论了各种设备的访问方法和特性、应该避免的陷阱,以及SPI DT绑定。

第9章概述Regmap API及其对底层SPI和I2C处理的抽象方法,这些方法对通用API和专用API都适用。

第10章介绍内核数据采集和测量框架,以处理数模转换(DAC)和模数转换(ADC),重点涉及IIO API、触发缓冲区和连续数据捕获的处理方法,以及通过sysfs接口的单通道数据采集方法。

第11章先介绍虚拟内存的概念,以描述内核内存的总体布局。然后介绍内核内存管理子系统,讨论内存分配和映射,以及内核缓存机制。

第12章介绍DMA及其新的内核API:DMA引擎API。除了讨论各种DMA映射方法,介绍与缓存寻址相关的问题,还总结了基于恩智浦i.MX6 SoC的整体使用方法。

第13章介绍Linux的核心内容,描述内核中对象的表示方法,Linux的内部设计方法,从kobject到设备,再到各种总线、类和设备驱动程序。此外,还强调用户空间中鲜为人知的一面——sysfs中的内核对象层次结构。

第14章介绍内核引脚控制API和gpiolib,它们是处理GPIO的内核API。本章还讨论原来基于整数的GPIO接口和基于描述符的新接口。最后,介绍在DT内配置它们的方法。

第15章主要介绍GPIO控制器,它是编写这些设备驱动程序必需的元素。其主要数据结构是gpio_chip结构,本章将详细介绍该结构,并在本书配套的源代码中提供了一个完整可用的驱动程序。

第16章深入浅出地解释Linux IRQ核心内容:全面介绍Linux IRQ管理,先从中断在系统中的传播、中断控制器驱动程序开始,进而解释IRQ复用的概念,以及Linux IRQ域API的使用。

第17章全面介绍输入子系统——处理基于IRQ的输入设备和轮询输入设备,并引入二者的API。此外,还解释和展示了用户空间代码对这些设备的处理方式。

第18章介绍RTC子系统及其API,详细解释RTC驱动程序怎样处理闹钟。

第19章全面描述PWM框架,讨论控制器端API和消费端API,最后还讨论了用户空间的PWM管理。

第20章强调电源管理的重要性,先介绍电源管理IC(Power Management IC,PMIC),解释其驱动程序设计和API。接着,重点介绍消费端,讨论电源调节器的请求和使用。

第21章解释帧缓冲概念及其工作方式,介绍帧缓冲驱动程序的设计及其API,讨论加速和非加速方法,说明驱动程序怎样公开帧缓冲内存,从而使用户空间能够写入,而不必顾及底层任务。

第22章介绍NIC驱动程序体系结构及其数据结构,说明怎样处理设备配置、数据传输和套接字缓冲区。

阅读本书所需知识

阅读本书需要读者对Linux操作系统具有一定的了解,具备基本的C语言程序设计能力(至少需要理解指针处理)。如果某些章节需要其他知识辅助理解,书中会提供参考文档链接,便于读者快速学习。

Linux内核编译是一项费时又艰辛的工作,其最低硬件或虚拟机要求如下。

·CPU:4核。

·内存:4 GB。

·磁盘可用空间:5 GB。

本书还需要以下软件。

·Linux操作系统:最好是基于Debian的版本,本书示例使用的是Ubuntu 16.04。

·gcc和gcc-arm-linux至少应是第5版(本书使用该版本)。

·本书用到的其他软件包将在具体章节中介绍。内核源代码的下载需要网络连接。

读者对象

阅读本书需要读者具备基本的C语言程序设计能力,熟悉Linux基本命令。本书介绍的Linux驱动程序开发广泛用于嵌入式设备,使用的内核版本是v4.1,本书写作时的Linux内核最新版本是v4.13。本书主要是为嵌入式工程师、Linux系统管理员、开发人员和内核黑客设计的。无论是软件开发人员,还是系统架构师或制造商,只要愿意深入研究Linux驱动程序开发,本书就适合您阅读。

资源与支持

本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

配套资源

本书提供如下资源:

本书源代码。

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

提交勘误

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

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

扫码关注本书

扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

与我们联系

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

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

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

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

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

关于异步社区和异步图书

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

第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 = sizeof (struct file);是可以接受的,而s = sizeof(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章将更详细地介绍内核构建过程、怎样实际编译外部或内核驱动程序,以及在启动内核开发之旅之前应该学习的一些基础知识。

第2章 设备驱动程序基础

驱动程序是专用于控制和管理特定硬件设备的软件,因此也被称作设备驱动程序。从操作系统的角度来看,它可以位于内核空间(以特权模式运行),也可以位于用户空间(具有较低的权限)。本书仅涉及内核空间驱动程序,特别是Linux内核驱动程序。我们给出的定义是,设备驱动程序把硬件功能提供给用户程序。

本书的目的不是教读者怎样成为Linux专家(我根本不是专家),但是在编写设备驱动程序之前,应该了解一些概念。C语言编程技巧是必需的,至少应该熟悉指针,并熟悉一些处理函数和必要的硬件知识。

本章涉及以下主题。

·模块构建过程及其加载和卸载。

·驱动程序框架以及调试消息管理。

·驱动程序中的错误处理。

2.1 内核空间和用户空间

内核空间和用户空间的概念有点抽象,主要涉及内存和访问权限,如图2-1所示。可以这样认为:内核是有特权的,而用户应用程序则是受限制的。这是现代CPU的一项功能,它可以运行在特权模式或非特权模式。学习第11章之后,这个概念会更加清晰。

图2-1说明内核空间和用户空间的分离,并强调了系统调用代表它们之间的桥梁(将在本章后面讨论)。每个空间的描述如下。

·内核空间:内核驻留和运行的地址空间。内核内存(或内核空间)是由内核拥有的内存范围,受访问标志保护,防止任何用户应用程序有意或无意间与内核搞混。另一方面,内核可以访问整个系统内存,因为它在系统上以更高的优先级运行。在内核模式下,CPU可以访问整个内存(内核空间和用户空间)。

·用户空间:正常程序(如gedit等)被限制运行的地址(位置)空间。可以将其视为沙盒或监狱,以便用户程序不能混用其他程序拥有的内存或任何其他资源。在用户模式下,CPU只能访问标有用户空间访问权限的内存。用户应用程序运行到内核空间的唯一方法是通过系统调用,其中一些调用是read、write、open、close和mmap等。用户空间代码以较低的优先级运行。当进程执行系统调用时,软件中断被发送到内核,这将打开特权模式,以便该进程可以在内核空间中运行。系统调用返回时,内核关闭特权模式,进程再次受限。

2.1.1 模块的概念

模块之于Linux内核就像插件(组件)之于用户软件(如Firefox),模块动态扩展了内核功能,甚至不需要重新启动计算机就可以使用。大多数情况下,内核模块是即插即用的。一旦插入,就可以使用了。为了支持模块,构建内核时必须启用下面的选项:

CONFIG_MODULES=y

2.1.2 模块依赖

Linux内核中的模块可以提供函数或变量,用EXPORT_SYMBOL宏导出它们即可供其他模块使用,这些被称作符号。模块B对模块A的依赖是指模块B使用从模块A导出的符号。

在内核构建过程中运行depmod工具可以生成模块依赖文件。它读取/lib/modules/<kernel_release>/中的每个模块来确定它应该导出哪些符号以及它需要什么符号。该处理的结果写入文件modules.dep及其二进制版本modules.dep.bin。它是一种模块索引。

2.1.3 模块的加载和卸载

模块要运行,应该先把它加载到内核,可以用insmod或modprobe来实现,前者需要指定模块路径作为参数,这是开发期间的首选;后者更智能化,是生产系统中的首选。

1.手动加载

手动加载需要用户的干预,该用户应该拥有root访问权限。实现这一点的两种经典方法如下。

在开发过程中,通常使用insmod来加载模块,并且应该给出所加载模块的路径:

insmod /path/to/mydrv.ko

这种模块加载形式低级,但它是其他模块加载方法的基础,也是本书中将要使用的方法。相反,系统管理员或在生产系统中则常用modprobe。modprobe更智能,它在加载指定的模块之前解析文件modules.dep,以便首先加载依赖关系。它会自动处理模块依赖关系,就像包管理器所做的那样:

modprobe mydrv

能否使用modprobe取决于depmod是否知道模块的安装。

/etc/modules-load.d/<filename>.conf

如果要在启动的时候加载一些模块,则只需创建文件/etc/modules-load.d/<filename>.conf,并添加应该加载的模块名称(每行一个)。<filename>应该是有意义的名称,人们通常使用模块:/etc/modules-load.d/modules.conf。当然也可以根据需要创建多个.conf文件。

下面是一个/etc/modules-load.d/mymodules.conf文件中的内容:

#this line is a comment 
uio 
iwlwifi

2.自动加载

depmod实用程序的作用不只是构建modules.dep和modules.dep.bin文件。内核开发人员实际编写驱动程序时已经确切知道该驱动程序将要支持的硬件。他们把驱动程序支持的所有设备的产品和厂商ID提供给该驱动程序。depmod还处理模块文件以提取和收集该信息,并在/lib/modules/<kernel_release>/modules.alias中生成modules.alias文件,该文件将设备映射到其对应的驱动程序。

下面的内容摘自modules.alias:

alias usb:v0403pFF1Cd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio 
alias usb:v0403pFF18d*dc*dsc*dp*ic*isc*ip*in* ftdi_sio 
alias usb:v0403pDAFFd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio 
alias usb:v0403pDAFEd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio 
alias usb:v0403pDAFDd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio 
alias usb:v0403pDAFCd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio 
alias usb:v0D8Cp0103d*dc*dsc*dp*ic*isc*ip*in* snd_usb_audio 
alias usb:v*p*d*dc*dsc*dp*ic01isc03ip*in* snd_usb_audio 
alias usb:v200Cp100Bd*dc*dsc*dp*ic*isc*ip*in* snd_usb_au

在这一步,需要一个用户空间热插拔代理(或设备管理器),通常是udev(或mdev),它将在内核中注册,以便在出现新设备时得到通知。

通知由内核发布,它将设备描述(pid、vid、类、设备类、设备子类、接口以及可标识设备的所有其他信息)发送到热插拔守护进程,守护进程再调用modprobe,并向其传递设备描述信息。接下来,modprobe解析modules.alias文件,匹配与该设备相关的驱动程序。在加载模块之前,modprobe会在module.dep中查找与其有依赖关系的模块。如果发现,则在相关模块加载之前先加载所有依赖模块;否则,直接加载该模块。

3.模块卸载

常用的模块卸载命令是rmmod,人们更喜欢用这个来卸载insmod命令加载的模块。使用该命令时,应该把要卸载的模块名作为参数向其传递。模块卸载是内核的一项功能,该功能的启用或禁用由CONFIG_MODULE_UNLOAD配置选项的值决定。没有这个选项,就不能卸载任何模块。以下设置将启用模块卸载功能:

CONFIG_MODULE_UNLOAD=y

在运行时,如果模块卸载会导致其他不良影响,则即使有人要求卸载,内核也将阻止这样做。这是因为内核通过引用计数记录模块的使用次数,这样它就知道模块是否在用。如果内核认为删除一个模块是不安全的,就不会删除它。然而,以下设置可以改变这种行为:

MODULE_FORCE_UNLOAD=y

上面的选项应该在内核配置中设置,以强制卸载模块:

rmmod -f mymodule

而另一个更高级的模块卸载命令是modprobe -r,它会自动卸载未使用的相关依赖模块:

modprobe -r mymodule

这对于开发者来说是一个非常有用的选择。用下列命令可以检查模块是否已加载:

lsmod

2.2 驱动程序框架

请看模块helloworld,它将成为本章其余部分工作的基础:

helloworld.c

#include <linux/init.h> 
#include <linux/module.h> 
#include <linux/kernel.h> 
 
static int __init helloworld_init(void) { 
    pr_info("Hello world!\n"); 
    return 0; 
}  
 
static void __exit helloworld_exit(void) { 
    pr_info("End of the world\n"); 
}  
 
module_init(helloworld_init); 
module_exit(helloworld_exit); 
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>"); 
MODULE_LICENSE("GPL");

2.2.1 模块的入点和出点

内核驱动程序都有入点和出点:前者对应于模块加载时调用的函数(modprobe和insmod),后者是模块卸载时执行的函数(在执行rmmod或modprobe -r时)。

main()函数是用C/C++编写的每个用户空间程序的入点,当这个函数返回时,程序将退出。而对于内核模块,情况就不一样了:入点可以随意命名,它也不像用户空间程序那样在main()返回时退出,其出点在另一个函数中定义。开发人员要做的就是通知内核把哪些函数作为入点或出点来执行。实际函数helloworld_init和helloworld_exit可以被命名成任何名字。实际上,唯一必须要做的是把它们作为参数提供给module_init()和module_exit()宏,将它们标识为相应的加载和删除函数。

综上所述,module_init()用于声明模块加载(使用insmod或modprobe)时应该调用的函数。初始化函数中要完成的操作是定义模块的行为。module_exit()用于声明模块卸载(使用rmmod)时应该调用的函数。

在模块加载或卸载后,init函数或exit函数立即运行一次。

__init和__exit属性

__init和__exit实际上是在include/linux/init.h中定义的内核宏,如下所示:

#define __init__section(.init.text) 
#define __exit__section(.exit.text)

__init关键字告诉链接器将该代码放在内核对象文件的专用部分。这部分事先为内核所知,它在模块加载和init函数执行后被释放。这仅适用于内置驱动程序,而不适用于可加载模块。内核在启动过程中第一次运行驱动程序的初始化函数。

由于驱动程序不能卸载,因此在下次重启之前不会再调用其init函数,没有必要在init函数内记录引用次数。对于__exit关键字也是如此,在将模块静态编译到内核或未启用模块卸载功能时,其相应的代码会被忽略,因为在这两种情况下都不会调用exit函数。__exit对可加载模块没有影响。

我们花一点时间进一步了解这些属性的工作方式,这涉及被称作可执行和可链接格式(ELF)的目标文件。ELF目标文件由不同的命名部分组成,其中一些部分是必需的,它们成为ELF标准的基础,但也可以根据自己的需要构建任一部分,并由特殊程序使用。内核就是这样做。执行objdump -h module.ko即可打印出指定内核模块module.ko的不同组成部分。

图2-2中只有少部分属于ELF标准。

·.text:包含程序代码,也称为代码。

·.data:包含初始化数据,也称为数据段。

·.rodata:用于只读数据。

·.comment:注释。

·未初始化的数据段,也称为由符号开始的块(block started by symbol,bss)。

其他部分是根据内核的需要添加的。本章较重要的部分是.modinfo和.init.text,前者存储有关模块的信息,后者存储以__init宏为前缀的代码。

链接器(Linux系统上的ld)是binutils的一部分,负责将符号(数据、代码等)放置到生成的二进制文件中的适当部分,以便在程序执行时可以被加载器处理。二进制文件中的这些部分可以自定义、更改它们的默认位置,甚至可以通过提供链接器脚本[称为链接器定义文件(LDF)或链接器定义脚本(LDS)]来添加其他部分。要实现这些操作只需通过编译器指令把符号的位置告知链接器即可,GNU C编译器为此提供了一些属性。Linux内核提供了一个自定义LDS文件,它位于arch/<arch> /kernel/vmlinux.lds.S中。对于要放置在内核LDS文件所映射的专用部分中的符号,使用__init和__exit进行标记。

总之,__init和__exit是Linux指令(实际上是宏),它们使用C编译器属性指定符号的位置。这些指令指示编译器将以它们为前缀的代码分别放在.init.text和.exit.text部分,虽然内核可以访问不同的对象部分。

2.2.2 模块信息

即使不读代码,也应该能够收集到关于给定模块的一些信息(如作者、参数描述、许可)。内核模块使用其.modinfo部分来存储关于模块的信息,所有MODULE_*宏都用参数传递的值更新这部分的内容。其中一些宏是MODULE_DESCRIPTION()、MODULE_AUTHOR()和MODULE_LICENSE()。内核提供的在模块信息部分添加条目的真正底层宏是MODULE_INFO(tag,info),它添加的一般信息形式是tag=info。这意味着驱动程序作者可以自由添加其想要的任何形式信息,例如:

MODULE_INFO(my_field_name, "What eeasy value");

在给定模块上执行objdump -d -j .modinfo命令可以转储内核模块.modinfo部分的内容,如图2-3所示。

modinfo部分可以看作模块的数据表。实际格式化打印信息的用户空间工具是modinfo,如图2-4所示。

除自定义信息之外,还应该提供标准信息,内核为这些信息提供了宏,包括许可、模块作者、参数描述、模块版本和模块描述。

1.许可

模块的许可由MODULE_LICENSE()宏定义:

MODULE_LICENSE ("GPL");

应该如何与其他开发人员共享(或不共享)许可定义源代码。MODULE_LICENSE()告诉内核模块采用何种许可。它对模块行为有影响,因为与GPL不兼容的许可将导致模块不能通过EXPORT_SYMBOL_GPL()宏看到/使用内核导出的服务/函数,这个宏只对GPL兼容模块显示符号,这与EXPORT_SYMBOL()相反,后者为具有任何许可的模块导出函数。加载GPL不兼容模块也会导致内核被污染,这意味着已经加载非开源或不可信代码,可能没有社区支持。请记住,没有MODULE_LICENSE()的模块被认为是非开源的,也会污染内核。以下摘自include/linux/module.h,描述了内核支持的许可:

/* 
 * 下列许可标识符目前被接受为指示自由软件模块 
 * 
 * "GPL"                   [GNU公共许可证v2或更高版本] 
 * "GPL v2"                [GNU公共许可证v2] 
 * "GPL and additional rights"   [GNU公共许可证v2和附加权利等] 
 * "Dual BSD/GPL"                [GNU公共许可证v2式BCD许可证选择] 
 *                               [GNU公共许可证证MIT许可证选择] 
 * "Dual MIT/GPL"                [GNU公共许可证v2] 
 *                               or MIT license choice] 
 * "Dual MPL/GPL"                [GNU公共许可证v2式mozilla许可证选择] 
 * 
 * 
 * 以下其他标识是可用的 
 * 
 * "Proprietary"                 [非免费产品] 
 * 
 * 有双重许可组件,但是与Linux一起运行时,因为GPL是相关的,所以这不是 
 * 问题。同样,与GPL链接的LGPL是GPL的组合 
 *  
 * 这种情况的存在有几个原因 
 * 1.   modinfo可以为想要审查其设置的用户免费显示许可信息 
 * 2.   社区可以忽略包括专有模块在内的Bug报告 
 * 3.   供应商也可以根据自己的策略进行同样的操作 
*/

模块至少必须与GPL兼容,才能享受完整的内核服务。

2.模块作者

MODULE_AUTHOR()声明模块的作者:

MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");

作者可能有多个,在这种情况下,每个作者必须用MODULE_AUTHOR()声明:

MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>"); 
MODULE_AUTHOR("Lorem Ipsum <l.ipsum@foobar.com>");

3.模块描述

MODULE_DESCRIPTION()简要描述模块的功能:

MODULE_DESCRIPTION("Hello, world! Module");

2.3 错误和消息打印

错误代码由内核或用户空间应用程序(通过errno变量)解释。错误处理在软件开发中非常重要,而不仅仅是在内核开发中。幸运的是,内核提供的几种错误,几乎涵盖了可能会遇到的所有错误,有时需要把它们打印出来以帮助进行调试。

2.3.1 错误处理

为给定的错误返回错误的错误码会导致内核或用户空间应用产生不必要的行为,从而做出错误的决定。为了保持清楚,内核树中预定义的错误几乎涵盖了我们可能遇到的所有情况。一些错误(及其含义)在include/uapi/asm-generic/errno-base.h中定义,列表的其余错误可以在include/uapi/asm- generic/errno.h中找到。以下是从include/uapi/asm- generic/errno-base.h中摘录的错误列表:

#define  EPERM      1   /* 操作不允许*/ 
#define  ENOENT         2   /* 没有这样的文件或目录 */ 
#define  ESRCH      3   /* 没有这样的进程 */ 
#define  EINTR      4   /* 中断系统调用 */ 
#define  EIO        5   /* I/O错误*/ 
#define  ENXIO      6   /* 没有这样的设备或地址 */ 
#define  E2BIG      7   /* 参数列表太长 */ 
#define  ENOEXEC        8   /* Exec格式错误 */ 
#define  EBADF      9   /* 错误的文件数量*/ 
#define  ECHILD         10  /* 没有子进程 */ 
#define  EAGAIN         11  /* 再试一次 */ 
#define  ENOMEM         12  /* 内存不足 */ 
#define  EACCES         13  /* 没有权限 */ 
#define  EFAULT         14  /* 错误的地址 */ 
#define  ENOTBLK        15  /* 块设备要求*/ 
#define  EBUSY     16   /* 设备或资源忙 */ 
#define  EEXIST         17  /* 文件已存在 */ 
#define  EXDEV     18   /* 跨设备的链接 */ 
#define  ENODEV         19  /* 没有这样的设备 */ 
#define  ENOTDIR        20  /* 不是目录 */ 
#define  EISDIR         21  /* 是目录 */ 
#define  EINVAL         22  /* 无效参数 */ 
#define  ENFILE         23  /* 文件表溢出*/ 
#define  EMFILE         24  /* 打开的文件太多 */ 
#define  ENOTTY         25  /* 不是打字机 */ 
#define  ETXTBSY        26  /* 文本文件忙 */ 
#define  EFBIG     27   /* 文件太大 */ 
#define  ENOSPC         28  /* 设备上没有空间了 */ 
#define  ESPIPE         29  /* 非法寻求 */ 
#define  EROFS     30   /* 只读文件系统 */ 
#define  EMLINK         31  /* 链接太多 */ 
#define  EPIPE     32   /* 破坏的管道 */ 
#define  EDOM      33   /* 函数域外的数学参数 */ 
#define  ERANGE         34  /* 数学结果无法表示 */

大多情况下,经典的返回错误方式是这种形式:return -ERROR,特别是在响应系统调用时。例如,对于I/O错误,错误代码是EIO,应该执行的语句是return -EIO:

dev = init(&ptr); 
if(!dev) 
return -EIO

错误有时会跨越内核空间,传播到用户空间。如果返回的错误是对系统调用(open、read、ioctl、mmap)的响应,则该值将自动赋给用户空间errno全局变量,在该变量上调用strerror(errno)可以将错误转换为可读字符串:

#include <errno.h>  /* 访问errno全局变量 */ 
#include <string.h> 
[...] 
if(wite(fd, buf, 1) < 0) { 
    printf("something gone wrong! %s\n", strerror(errno)); 
} 
[...]

当遇到错误时,必须撤销在这个错误发生之前的所有设置。通常的做法是使用goto语句:

ptr = kmalloc(sizeof (device_t)); 
if(!ptr) { 
        ret = -ENOMEM 
        goto err_alloc; 
} 
dev = init(&ptr); 
if(dev) { 
        ret = -EIO 
        goto err_init; 
} 
return 0; 
 
err_init: 
        free(ptr); 
err_alloc: 
        return ret;

使用goto语句的原因很简单。当处理错误时,假设错误出现在第5步,则必须清除以前的操作(步骤1~步骤4)。而不是像下面这样执行大量的嵌套检查操作:

if (ops1() != ERR) { 
    if (ops2() != ERR) { 
        if ( ops3() != ERR) { 
            if (ops4() != ERR) {

这可能会令人困惑,并可能导致缩进问题。像下面这样用goto语句会使控制流程显得更直观,这种方法更受欢迎:

if (ops1() == ERR) // | 
    goto error1;   // | 
if (ops2() == ERR) // | 
    goto error2;   // | 
if (ops3() == ERR) // | 
    goto error3;   // | 
if (ops4() == ERR) // V 
    goto error4; 
error5: 
[...] 
error4: 
[...] 
error3: 
[...] 
error2: 
[...] 
error1: 
[...]

这就是说应该只在函数中使用goto跳转。

2.3.2 处理空指针错误

当返回指针的函数返回错误时,通常返回的是NULL指针。而去检查为什么会返回空指针是没有任何意义的,因为无法准确了解为什么会返回空指针。为此,内核提供了3个函数ERR_PTR、IS_ERR和PTR_ERR:

void *ERR_PTR(long error); 
long IS_ERR(const void *ptr); 
long PTR_ERR(const void *ptr);

第一个函数实际上把错误值作为指针返回。假若函数在内存申请失败后要执行语句return -ENOMEM,则必须改为这样的语句:return ERR_PTR (-ENOMEM);。第二个函数用于检查返回值是否是指针错误:if (IS_ERR(foo))。最后一个函数返回实际错误代码:return PTR_ERR(foo);。以下是一个例子,说明如何使用ERR_PTR、IS_ERR和PTR_ERR:

static struct iio_dev *indiodev_setup(){ 
    [...] 
    struct iio_dev *indio_dev; 
    indio_dev = devm_iio_device_alloc(&data->client->dev, sizeof(data)); 
    if (!indio_dev) 
        return ERR_PTR(-ENOMEM); 
    [...] 
    return indio_dev; 
} 
 
static int foo_probe([...]){ 
    [...] 
    struct iio_dev *my_indio_dev = indiodev_setup(); 
    if (IS_ERR(my_indio_dev)) 
        return PTR_ERR(data->acc_indio_dev); 
    [...] 
}

关于错误处理补充一点,摘录自内核编码风格部分:如果函数名称是动作或命令式命令,则函数返回的错误代码应该是整数;如果函数名称是一个谓词,则该函数应返回布尔值succeeded(成功的)。例如,add work是一个命令,add_work()函数返回0表示成功,返回-EBUSY表示失败。同样,PCI device present是谓词,pci_dev_present()函数如果成功找到匹配设备,则返回1;否则返回0。

2.3.3 消息打印——printk()

printk()是在内核空间使用的,其作用和在用户空间使用printf()一样,执行dmesg命令可以显示printk()写入的行。根据所打印消息的重要性不同,可以选用include/linux/kern_levels.h中定义的八个级别的日志消息,下面将介绍它们的含义。

下面列出的是内核日志级别,每个级别对应一个字符串格式的数字,其优先级与该数字的值成反比。例如,0具有较高的优先级:

#define KERN_SOH     "\001"          /* ASCII头开始 */ 
#define KERN_SOH_ASCII     '\001' 
#define KERN_EMERG   KERN_SOH "0"    /* 系统不可用 */ 
#define KERN_ALERT   KERN_SOH "1"    /* 必须立即采取行动*/ 
#define KERN_CRIT    KERN_SOH "2"    /* 重要条件 */ 
#define KERN_ERR     KERN_SOH "3"    /* 错误条件 */ 
#define KERN_WARNING KERN_SOH "4"    /* 警报条件 */ 
#define KERN_NOTICE  KERN_SOH "5"    /* 正常但重要的情况*/ 
#define KERN_INFO    KERN_SOH "6"    /* 信息 */ 
#define KERN_DEBUG   KERN_SOH "7"    /* 调试级别消息 */

以下代码显示如何打印内核消息和日志级别:

printk(KERN_ERR "This is an error\n");

如果省略调试级别(printk("This is an error\n")),则内核将根据CONFIG_DEFAULT_MESSAGE_LOGLEVEL配置选项(这是默认的内核日志级别)向该函数提供一个调试级别。实际上可以使用以下宏,其名称更有意义,它们是对前面所定义内容的包装——pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_notice、pr_info和pr_debug:

pr_err("This is the same error\n");

对于新开发的驱动程序,建议使用这些包装。printk()的实现是这样的:调用它时,内核会将消息日志级别与当前控制台的日志级别进行比较;如果前者比后者更高(值更低),则消息会立即打印到控制台。可以这样检查日志级别参数:

cat /proc/sys/kernel/printk  
4 4 1 7

上面的输出中,第一个值是当前日志级别(4),第二个值是按照CONFIG_DEFAULT_MESSAGE_LOGLEVEL选项设置的默认值。其他值与本章内容无关,可以忽略。

内核日志级别列表如下:

/* integer equivalents of KERN_<LEVEL> */ 
#define LOGLEVEL_SCHED          -2    /* 计划代码中的延迟消息设置为此特殊级别 */ 
#define LOGLEVEL_DEFAULT   -1   /*默认(或最新)日志级别*/ 
#define LOGLEVEL_EMERG          0    /* 系统不可用 */ 
#define LOGLEVEL_ALERT          1    /* 必须立即采取行动*/ 
#define LOGLEVEL_CRIT           2    /* 重要条件 */ 
#define LOGLEVEL_ERR        3   /* 错误条件 */ 
#define LOGLEVEL_WARNING    4   /* 警报条件 */ 
#define LOGLEVEL_NOTICE         5    /*  正常但重要的情况*/ 
#define LOGLEVEL_INFO           6    /*  信息 */ 
#define LOGLEVEL_DEBUG          7    /*  调试级别消息 */

当前日志级别可以这样更改:

# echo <level> > /proc/sys/kernel/printk

printk()永远不会阻塞,即使在原子上下文中调用也足够安全。它会尝试锁定控制台打印消息。如果锁定失败,输出则被写入缓冲区,函数返回,永不阻塞。然后通知当前控制台占有者有新的消息,在它释放控制台之前打印它们。

内核也支持其他调试方法:动态调试或者在文件的顶部使用#define DEBUG。对这种调试方式感兴趣的人可以参考以下内核文档:Documentation/dynamic-debug-howto.txt。

2.4 模块参数

像用户程序一样,内核模块也可以接受命令行参数。这样能够根据给定的参数动态地改变模块的行为,开发者不必在测试/调试期间无限期地修改/编译模块。为了对此进行设置,首先应该声明用于保存命令行参数值的变量,并在每个变量上使用module_param()宏。该宏在include/linux/moduleparam.h(这也应该包含在代码中:#include <linux/moduleparam.h>)中这样定义:

module_param(name, type, perm); 

该宏包含以下元素。

·name:用作参数的变量的名称。

·type:参数的类型(bool、charp、byte、short、ushort、int、uint、long、ulong),其中charp代表字符指针。

·perm:代表/sys/module/<module>/parameters/<param>文件的权限,其中包括S_IWUSR、S_IRUSR、S_IXUSR、S_IRGRP、S_WGRP和S_IRUGO。

·S_I:只是一个前缀。

·R:读。W:写。X:执行。

·USR:用户。GRP:组。UGO:用户、组和其他。

可以使用|(或操作)设置多个权限。如果perm为0,则不会创建sysfs中的文件参数。强烈推荐使用S_IRUGO只读参数;使用| |(OR)与其他属性可以获得细粒度的属性。

当使用模块参数时,应该用MODULE_PARM_DESC描述每个参数。这个宏将把每个参数的描述填充到模块信息部分。以下例子摘自本书代码库提供的helloworldparams.c源文件:

#include <linux/moduleparam.h> 
[...] 
 
static char *mystr = "hello"; 
static int myint = 1; 
static int myarr[3] = {0, 1, 2}; 
 
module_param(myint, int, S_IRUGO); 
module_param(mystr, charp, S_IRUGO); 
module_param_array(myarr, int,NULL, S_IWUSR|S_IRUSR); /*  */ 
 
MODULE_PARM_DESC(myint,"this is my int variable"); 
MODULE_PARM_DESC(mystr,"this is my char pointer variable"); 
MODULE_PARM_DESC(myarr,"this is my array of int"); 
 
static int foo() 
{ 
    pr_info("mystring is a string: %s\n", mystr); 
    pr_info("Array elements: %d\t%d\t%d", myarr[0], myarr[1], myarr[2]); 
    return myint; 
}

要在加载该模块时提供参数,请执行以下操作:

# insmod hellomodule-params.ko mystring="packtpub" myint=15 myArray=1,2,3

在加载模块之前,执行modinfo可以显示该模块支持的参数说明:

$ modinfo ./helloworld-params.ko 
filename: /home/jma/work/tutos/sources/helloworld/./helloworld-params.ko 
license: GPL 
author: John Madieu <john.madieu@gmail.com> 
srcversion: BBF43E098EAB5D2E2DD78C0 
depends: 
vermagic: 4.4.0-93-generic SMP mod_unload modversions 
parm: myint:this is my int variable (int) 
parm: mystr:this is my char pointer variable (charp) 
parm: myarr:this is my array of int (array of int)

2.5 构建第一个模块

可以在两个地方构建模块,这取决于是否希望用户能够自己使用内核配置界面启用该模块。

2.5.1 模块的makefile

makefile是用来执行一组操作的特殊文件,其中最重要的操作是程序的编译。专用工具make用于解析makefile。在说明整个make文件之前,先介绍一下obj- <X>kbuild变量。

几乎在每个内核的makefile中都至少会有obj <-X>变量的一个实例。这实际上对应于obj- <X>模式,其中<X>应该是y、m、空白或n。总的来说,位于内核构建系统头部的makefile使用它。这些行定义要构建的文件、所有特殊的编译选项,以及要递归进入的任何子目录。一个简单的例子如下:

obj-y += mymodule.o

这告诉kbuild在当前目录中有一个名为mymodule.o的对象。mymodule.o将从mymodule.c或mymodule.S构建。如何以及是否构建或链接mymodule.o取决于<X>的值。

·如果<X>设置为m,则使用变量obj-m,并将mymodule.o构建为模块。

·如果<X>设置为y,则使用变量obj-y,mymodule.o将构建为内核的一部分。也可以说它是一个内置模块。

·如果<X>设置为n,则使用变量obj-n,不会构建mymodule.o。

因此,经常用到obj-$(CONFIG_XXX)模式(其中CONFIG_XXX是内核配置选项),在内核配置过程中可以设置或者不设置它。下面是一个例子:

obj-$(CONFIG_MYMODULE) += mymodule.o

$(CONFIG_MYMODULE)根据内核配置期间的值计算为y或m(请记住make menuconfig)。如果CONFIG_MYMODULE既不是y也不是m,则文件不会被编译或链接。y表示内置(在内核配置过程中代表yes),m代表模块。$(CONFIG_MYMODULE)从正常的配置过程中获取正确的设置,这将在2.5.2节中解释。

最后一个用例如下:

obj-<X> += somedir/

这意味着kbuild应该进入somedir目录,查找其中所有的makefile并处理它们,以决定应该构建哪些对象。

回到makefile,下面是makefile的内容,我们将用它构建本书中介绍的每个模块:

obj-m := helloworld.o 
 
KERNELDIR ?= /lib/modules/$(shell uname -r)/build 
 
all default: modules 
install: modules_install 
 
modules modules_install help clean: 
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@

·obj-m := helloworld.o:obj-m列出要构建的模块。对于每一个<filename> .o,进行系统构建时会查找<filename> .c。obj-m用于构建模块,而obj-y将构建内置对象。

·KERNELDIR := /lib/modules/$(shell uname -r)/build:KERNELDIR是预构建的内核源码的位置。正如前面所说,构建任何模块都需要预构建内核。如果已经从源代码构建了内核,则应该把这个变量设置为内核构建源代码目录的绝对路径。-C要求实用程序make在读取makefile或执行其他任何操作之前先更改到指定的目录。

·M=$(shell pwd):这与内核构建系统相关。内核makefile使用这个变量来定位要构建的外部模块的目录。.c文件应该被放置在这里。

·all default: modules:此行指示实用程序make执行modules目标,在构建用户应用程序时,无论all还是default都是传统目标。换句话说,make default、make all或者简单的make命令将被翻译为make modules来执行。

·modules modules_install help clean::这行代表makefile中列出的目标有效。

·$(MAKE) -C $(KERNELDIR ) M=$(shell pwd) $@:为上面列举的每个目标所执行的规则。$ @将被替换为引起规则运行的目标名称。换句话说,如果调用make modules,则$@将被替换为modules,规则将变为$(MAKE)-C $(KERNELDIR ) M=$(shell pwd) module。

2.5.2 内核树内

在内核树中构建驱动程序之前,应该先确定驱动程序中的哪个目录用于存放.c文件。假若文件名是mychardev.c,它包含特殊字符驱动程序的源代码,则应该把它放在内核源码的drivers/char目录中。驱动程序中的每个子目录都有makefile和kconfig文件。将以下内容添加到该目录的kconfig中:

config PACKT_MYCDEV 
  tristate "Our packtpub special Character driver" 
  default m 
  help 
    Say Y here if you want to support the /dev/mycdev device. 
    The /dev/mycdev device is used to access packtpub.

在同一个目录的makefile中添加:

obj-$(CONFIG_PACKT_MYCDEV)   += mychardev.o

更新makefile时要小心,.o文件名称必须与.c文件名完全一致。如果源文件是foobar.c,则需在makefile中使用foobar.o。要把驱动程序构建为模块,请在arch/arm/configs目录下开发板的defconfig中添加下面一行内容:

CONFIG_PACKT_MYCDEV=m

也可以运行make menuconfig来从UI中选择它,然后运行make,构建内核,再运行make modules构建模块(包括自己的模块)。为了将驱动程序编译到内核中,只需用y替换m:

CONFIG_PACKT_MYCDEV=m

这里所介绍的一切都是嵌入式开发板制造商所做的,它们为开发板提供开发板支持包(Board Support Package,BSP),以及包含自定驱动程序的内核,如图2-5所示。

配置完成后,可以分别使用make和make modules构建内核和模块。

内核源码树中包含的模块安装在/lib/modules/$ (KERNELRELEASE)/kernel/中。在Linux系统上,它是/lib/modules/$(uname -r)/kernel/。运行以下命令安装模块:

CONFIG_PACKT_MYCDEV=m

2.5.3 内核树外

在构建外部模块之前,需要有一个完整的、预编译的内核源代码树。内核源码树版本必须与将加载和使用模块的内核相同。有两种方法可以获得预构建的内核版本。

·自己构建(前面讨论过)。

·从发行版本库安装linux-headers- *包。

sudo apt-get update 
sudo apt-get install linux-headers-$(uname -r)

这将只安装头文件,而不是整个源代码树。然后,头文件将被安装在/usr/src/linux-headers-$(uname -r)下。在我的计算机上,它位于/usr/src/linux-headers-4.4.0-79-generic/。有一个符号链接/lib/modules/$(uname-r)/build,指向前面安装的头文件,这应该是在makefile中指定为内核目录的路径。这就是需要为预构建的内核所做的一切。

2.5.4 构建模块

处理完makefile后,只需要切换到源码目录并运行make命令或者make modules:

jma@jma:~/work/tutos/sources/helloworld$ make 
make -C /lib/modules/4.4.0-79-generic/build \ 
    M=/media/jma/DATA/work/tutos/sources/helloworld modules 
make[1]: Entering directory '/usr/src/linux-headers-4.4.0-79-generic' 
    CC [M] 
/media/jma/DATA/work/tutos/sources/helloworld/helloworld.o 
    Building modules, stage 2. 
    MODPOST 1 modules 
    CC 
/media/jma/DATA/work/tutos/sources/helloworld/helloworld.mod.o 
    LD [M] 
/media/jma/DATA/work/tutos/sources/helloworld/helloworld.ko 
    make[1]: Leaving directory '/usr/src/linux-headers-4.4.0-79- generic' 
    jma@jma:~/work/tutos/sources/helloworld$ ls 
    helloworld.c  helloworld.ko  helloworld.mod.c  helloworld.mod.o 
helloworld.o  Makefile  modules.order  Module.symvers 
    jma@jma:~/work/tutos/sources/helloworld$ sudo insmod  helloworld.ko 
    jma@jma:~/work/tutos/sources/helloworld$ sudo rmmod helloworld 
    jma@jma:~/work/tutos/sources/helloworld$ dmesg 
    [...] 
    [308342.285157] Hello world! 
    [308372.084288] End of the world

上面的例子使用的是本地构建,在x86机器上为x86机器编译。交叉编译怎么实现?这个过程是在机器A(称为宿主机)上编译,该代码要运行在机器B(称为目标机)上;宿主机和目标机具有不同的体系结构。经典用例是在x86机器上构建的代码要运行在ARM架构上,交叉编辑就是这样。

交叉编译内核模块时,内核makefile实际上需要了解两个变量:ARCH和CROSS_COMPILE,它们分别表示目标体系结构和编译器的前缀名称。因此内核模块本地编译和交叉编译之间的差别是make命令。下面这条命令是为ARM构建:

make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-

2.6 总结

本章介绍了驱动程序开发的基础知识,解释了模块/内置设备的概念以及它们的加载和卸载。即使不熟悉用户空间,读者也可以编写完整的驱动程序,打印格式化消息,理解init/exit的概念。第3章将介绍字符设备,这样能够定位增强功能,编写可从用户空间访问的代码,并对系统产生重大影响。

相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程