欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux 设备驱动程序开发-全部-...

最编程 2024-05-19 21:02:39
...

Linux 设备驱动开发(全)

原文:zh.annas-archive.org/md5/1581478CA24960976F4232EF07514A3E

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Linux 内核是一款复杂、可移植、模块化且广泛使用的软件,约 80%的服务器和超过一半的全球嵌入式系统都在运行该软件。设备驱动程序在 Linux 系统性能方面起着至关重要的作用。随着 Linux 成为最受欢迎的操作系统之一,对于开发个人设备驱动程序的兴趣也在稳步增长。

设备驱动程序是用户空间和设备之间的链接,通过内核。

本书将从两章开始,帮助您了解驱动程序的基础知识,并为您在 Linux 内核中的漫长旅程做好准备。本书还将涵盖基于 Linux 子系统的驱动程序开发,如内存管理、PWM、RTC、IIO、GPIO、中断请求管理。本书还将涵盖直接内存访问和网络设备驱动程序的实际方法。

本书中的源代码已在 x86 PC 和基于 NXP 的 ARM i.MX6 的 SECO UDOO Quad 上进行了测试,具有足够的功能和连接,可以覆盖本书中讨论的所有测试。还提供了一些驱动程序用于测试廉价组件,如 MCP23016 和 24LC512,它们分别是 I2C GPIO 控制器和 EEPROM 存储器。

通过本书的学习,您将能够熟悉设备驱动程序开发的概念,并能够使用最新的内核版本(写作时为 v4.13)从头开始编写任何设备驱动程序。

本书涵盖的内容

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

第二章,设备驱动程序基础,通过内核模块介绍了 Linux 的模块化,并描述了它们的加载/卸载。还描述了驱动程序架构和一些基本概念以及一些内核最佳实践。

第三章,内核设施和辅助函数,介绍了经常使用的内核函数和机制,如工作队列、等待队列、互斥锁、自旋锁,以及其他对于改进驱动程序可靠性有用的设施。

第四章,字符设备驱动程序,侧重于通过字符设备将设备功能导出到用户空间,并使用 IOCTL 接口支持自定义命令。

第五章,平台设备驱动程序,解释了什么是平台设备,并介绍了伪平台总线的概念,以及设备和总线匹配机制。本章以一般方式描述了平台驱动程序架构,以及如何处理平台数据。

第六章,设备树的概念,讨论了向内核提供设备描述的机制。本章解释了设备寻址、资源处理、设备树中支持的每种数据类型及其内核 API。

第七章,I2C 客户端驱动程序,深入探讨了 I2C 设备驱动程序架构、数据结构以及总线上的设备寻址和访问方法。

第八章,SPI 设备驱动程序,描述了基于 SPI 的设备驱动程序架构,以及涉及的数据结构。本章讨论了每个设备的访问方法和具体特性,以及应该避免的陷阱。还讨论了 SPI DT 绑定。

第九章,Regmap API - 寄存器映射抽象,概述了 regmap API 以及它如何抽象底层的 SPI 和 I2C 事务。本章描述了通用 API 以及专用 API。

第十章,IIO 框架,介绍了内核数据采集和测量框架,用于处理数字模拟转换器(DAC)和模拟数字转换器(ADC)。本章介绍了 IIO API,涉及触发缓冲区和连续数据捕获,并介绍了通过 sysfs 接口进行单通道采集。

第十一章,内核内存管理,首先介绍了虚拟内存的概念,以描述整个内核内存布局。本章介绍了内核内存管理子系统,讨论了内存分配和映射,它们的 API 以及涉及这些机制的所有设备,以及内核缓存机制。

第十二章,DMA - 直接内存访问,介绍了 DMA 及其新的内核 API:DMA 引擎 API。本章将讨论不同的 DMA 映射,并描述如何解决缓存一致性问题。此外,本章还总结了基于 NXP 的 i.MX6 SoC 的使用案例中使用的所有概念。

第十三章,Linux 设备模型,概述了 Linux 的核心,描述了内核中对象的表示方式,以及 Linux 是如何设计的,从 kobject 到设备,通过总线、类和设备驱动程序。本章还突出了用户空间中不为人知的一面,即 sysfs 中的内核对象层次结构。

第十四章,引脚控制和 GPIO 子系统,描述了内核引脚控制 API 和 GPIOLIB,这是处理 GPIO 的内核 API。本章还讨论了旧的和已弃用的基于整数的 GPIO 接口,以及基于描述符的接口,这是新的接口,最后讨论了它们如何在设备树中进行配置。

第十五章,GPIO 控制器驱动程序 - gpio_chip,编写此类设备驱动程序所需的必要元素。也就是说,它的主要数据结构是 struct gpio_chip。本章详细解释了这个结构,以及书籍源代码中提供的完整可用的驱动程序。

第十六章,高级中断请求(IRQ)管理,揭开了 Linux IRQ 核心的神秘面纱。本章介绍了 Linux IRQ 管理,从系统中断传播开始,移动到中断控制器驱动程序,因此解释了 IRQ 多路复用的概念,使用 Linux IRQ 域 API。

第十七章,输入设备驱动程序,提供了输入子系统的全局视图,处理基于 IRQ 和轮询的输入设备,并介绍了两种 API。本章解释并展示了用户空间代码如何处理这些设备。

第十八章,RTC 驱动程序,深入讲解了 RTC 子系统及其 API。本章还详细解释了如何在 RTC 驱动程序中处理闹钟。

第十九章,PWM 驱动程序,全面描述了 PWM 框架,讨论了控制器端 API 和消费者端 API。本章最后一节讨论了来自用户空间的 PWM 管理。

第二十章,调节器框架,突出了电源管理的重要性。本章的第一部分涉及电源管理 IC(PMIC),并解释了其驱动程序设计和 API。第二部分侧重于消费者方面,讨论了请求和使用调节器。

第二十一章,帧缓冲驱动程序,解释了帧缓冲的概念及其工作原理。它还展示了如何设计帧缓冲驱动程序,介绍了其 API,并讨论了加速和非加速方法。本章展示了驱动程序如何公开帧缓冲内存,以便用户空间可以在其中写入,而不必担心底层任务。

第二十二章,网络接口卡驱动程序,介绍了 NIC 驱动程序的架构及其数据结构,从而向您展示如何处理设备配置、数据传输和套接字缓冲区。

本书所需的内容

本书假定读者对 Linux 操作系统有中等水平的理解,对 C 编程有基本的知识(至少要能处理指针)。就是这样。如果某一章需要额外的技能,文档中会提供链接,帮助读者快速学习这些技能。

Linux 内核编译是一个相当长而繁重的任务。最低硬件或虚拟要求如下:

  • CPU:4 核

  • 内存:4 GB RAM

  • 免费磁盘空间:5 GB(足够大)

在本书中,您将需要以下软件清单:

  • Linux 操作系统:最好是基于 Debian 的发行版,例如本书中使用的 Ubuntu 16.04

  • 至少需要 gcc 和 gcc-arm-linux 的 5 版本(在书中使用)

其他必要的软件包在书中的专用章节中有描述。需要互联网连接以下载内核源代码。

本书适合对象

为了充分利用本书的内容,需要具备基本的 C 编程和基本的 Linux 命令知识。本书涵盖了广泛使用的嵌入式设备的 Linux 驱动程序开发,使用内核版本 v4.1,并覆盖了撰写本书时的最新版本的更改(v4.13)。本书主要面向嵌入式工程师、Linux 系统管理员、开发人员和内核黑客。无论您是软件开发人员、系统架构师还是愿意深入研究 Linux 驱动程序开发的制造商,本书都适合您。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“.name字段必须与您在特定文件中注册设备时给出的设备名称相同”。

代码块设置如下:

#include <linux/of.h> 
#include <linux/of_device.h> 

任何命令行输入或输出都以以下方式编写:

 sudo apt-get update

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

新术语重要单词以粗体显示。

警告或重要说明显示如下。

提示和技巧显示如下。

读者反馈

我们始终欢迎读者的反馈。让我们知道您对本书的看法-您喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出您真正能充分利用的标题。要向我们发送一般反馈,只需发送电子邮件至feedback@packtpub.com,并在主题中提及书名。如果您在某个主题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的自豪所有者,我们有很多东西可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”选项卡上。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名。

  5. 选择您要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 单击“代码下载”。

下载文件后,请确保使用最新版本解压文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Linux-Device-Drivers-Development。我们还有其他丰富的图书和视频代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载本书的彩色图片

我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。彩色图片将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/LinuxDeviceDriversDevelopment_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误是难免的。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书标题的勘误部分下的任何现有勘误列表中。要查看以前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将出现在勘误部分下。

盗版

互联网上盗版受版权保护的材料是一个持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。请通过copyright@packtpub.com与我们联系,并附上涉嫌盗版材料的链接。感谢您帮助我们保护我们的作者和我们为您提供有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章:内核开发简介

Linux 是 1991 年芬兰学生 Linus Torvalds 的一个业余项目。该项目逐渐增长,现在仍在增长,全球大约有 1000 名贡献者。如今,Linux 在嵌入式系统和服务器上都是必不可少的。内核是操作系统的核心部分,它的开发并不那么明显。

Linux 相对于其他操作系统有很多优势:

  • 免费

  • 有着完善的文档和庞大的社区

  • 在不同平台上可移植

  • 提供对源代码的访问

  • 大量免费开源软件

这本书试图尽可能通用。有一个特殊的主题,设备树,它还不是完全的 x86 特性。这个主题将专门用于 ARM 处理器,以及所有完全支持设备树的处理器。为什么选择这些架构?因为它们在台式机和服务器(对于 x86)以及嵌入式系统(ARM)上最常用。

本章主要涉及以下内容:

  • 开发环境设置

  • 获取、配置和构建内核源代码

  • 内核源代码组织

  • 内核编码风格简介

环境设置

在开始任何开发之前,你需要设置一个环境。至少在基于 Debian 的系统上,专门用于 Linux 开发的环境是相当简单的:

 $ 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系统芯片SoC)兼容。你也应该安装gcc-arm

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

我正在一台 ASUS RoG 上运行 Ubuntu 16.04,配备英特尔 i7 处理器(8 个物理核心),16GB 内存,256GB 固态硬盘和 1TB 磁性硬盘。我的最爱编辑器是 Vim,但你可以*选择你最熟悉的编辑器。

获取源代码

在早期的内核版本(直到 2003 年),使用了奇数-偶数版本样式;奇数版本是稳定的,偶数版本是不稳定的。当 2.6 版本发布时,版本方案切换为 X.Y.Z,其中:

  • X:这是实际内核的版本,也称为主要版本,当有不兼容的 API 更改时会增加。

  • Y:这是次要修订版本,当以向后兼容的方式添加功能时增加。

  • Z:这也被称为 PATCH,表示与错误修复相关的版本

这被称为语义版本控制,一直使用到 2.6.39 版本;当 Linus Torvalds 决定将版本号提升到 3.0 时,这也意味着 2011 年语义版本控制的结束,然后采用了 X.Y 方案。

当到了 3.20 版本时,Linus 认为他不能再增加 Y 了,并决定切换到任意的版本方案,当 Y 变得足够大以至于他数不过来时,就增加 X。这就是为什么版本从 3.20 直接变成了 4.0 的原因。请看:plus.google.com/+LinusTorvalds/posts/jmtzzLiiejc

现在内核使用任意的 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 版本都会被覆盖,至少可以这样说关于框架和子系统。

内核配置

Linux 内核是一个基于 makefile 的项目,具有数千个选项和驱动程序。要配置内核,可以使用make menuconfig进行基于 ncurse 的界面,或者使用make xconfig进行基于 X 的界面。一旦选择,选项将存储在源树的根目录中的.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_defconfigx86_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文件中,使用后一个命令,您可以根据需要更新添加/删除选项。

在使用xconfig时可能会遇到 Qt4 错误。在这种情况下,应该使用以下命令:

sudo apt-get install  qt4-dev-tools qt4-qmake

构建您的内核

构建内核需要您指定为其构建的体系结构,以及编译器。也就是说,对于本地构建并非必需。

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/中。这在第二章 设备驱动程序基础中讨论过。

i.MX6 处理器支持设备树,这是用来描述硬件的文件(这在第六章中详细讨论),但是,要编译每个ARCH设备树,可以运行以下命令:

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

但是,并非所有支持设备树的平台都支持dtbs选项。要构建一个独立的 DTB,您应该使用:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6d-    sabrelite.dtb

内核习惯

内核代码试图遵循标准规则。在本章中,我们只是介绍它们。它们都在专门的章节中讨论,从第三章开始,内核设施和辅助函数,我们可以更好地了解内核开发过程和技巧,直到第十三章Linux 设备模型

编码风格

在深入研究本节之前,您应始终参考内核编码风格手册,位于内核源树中的Documentation/CodingStyle。这种编码风格是一组规则,您至少应该遵守这些规则,如果需要内核开发人员接受其补丁。其中一些规则涉及缩进、程序流程、命名约定等。

最流行的是:

  • 始终使用 8 个字符的制表符缩进,并且每行应为 80 列长。如果缩进阻止您编写函数,那是因为该函数的嵌套级别太多。可以使用内核源代码中的scripts/cleanfile脚本调整制表符大小并验证行大小:
scripts/cleanfile my_module.c 
  • 您还可以使用indent工具正确缩进代码:
      sudo apt-get install indent

 scripts/Lindent my_module.c

  • 每个未导出的函数/变量都应声明为静态的。

  • 在括号表达式(内部)周围不应添加空格。s = size of (struct file);是可以接受的,而s = size of( struct file );是不可以接受的。

  • 禁止使用typdefs

  • 始终使用/* this */注释样式,而不是// this

    • 不好:// 请不要使用这个
  • 好的:/* 内核开发人员喜欢这样 */

  • 宏应该大写,但功能宏可以小写。

  • 注释不应该替换不可读的代码。最好重写代码,而不是添加注释。

内核结构分配/初始化

内核始终为其数据结构和设施提供两种可能的分配机制。

其中一些结构包括:

  • 工作队列

  • 列表

  • 等待队列

  • Tasklet

  • 定时器

  • 完成

  • 互斥锁

  • 自旋锁

动态初始化器都是宏,这意味着它们始终大写:INIT_LIST_HEAD()DECLARE_WAIT_QUEUE_HEAD()DECLARE_TASKLET()等等。

说到这一点,所有这些都在第三章中讨论,内核设施和辅助函数。因此,代表框架设备的数据结构始终是动态分配的,每个数据结构都有自己的分配和释放 API。这些框架设备类型包括:

  • 网络

  • 输入设备

  • 字符设备

  • IIO 设备

  • 帧缓冲

  • 调节器

  • PWM 设备

  • RTC

静态对象的作用域在整个驱动程序中可见,并且由此驱动程序管理的每个设备都可见。动态分配的对象仅由实际使用给定模块实例的设备可见。

类、对象和 OOP

内核通过设备和类来实现 OOP。内核子系统通过类进行抽象。几乎每个子系统都有一个/sys/class/下的目录。struct kobject结构是这种实现的核心。它甚至带有一个引用计数器,以便内核可以知道实际使用对象的用户数量。每个对象都有一个父对象,并且在sysfs中有一个条目(如果已挂载)。

每个属于特定子系统的设备都有一个指向操作ops)结构的指针,该结构公开了可以在此设备上执行的操作。

摘要

本章以非常简短和简单的方式解释了如何下载 Linux 源代码并进行第一次构建。它还涉及一些常见概念。也就是说,这一章非常简短,可能不够,但没关系,这只是一个介绍。这就是为什么下一章会更深入地介绍内核构建过程,如何实际编译驱动程序,无论是作为外部模块还是作为内核的一部分,以及在开始内核开发这段漫长旅程之前应该学习的一些基础知识。

第二章:设备驱动程序基础

驱动程序是一种旨在控制和管理特定硬件设备的软件。因此得名设备驱动程序。从操作系统的角度来看,它可以在内核空间(以特权模式运行)或用户空间(权限较低)中。本书只涉及内核空间驱动程序,特别是 Linux 内核驱动程序。我们的定义是设备驱动程序向用户程序公开硬件的功能。

这本书的目的不是教你如何成为 Linux 大师——我自己也不是——但在编写设备驱动程序之前,你应该了解一些概念。C 编程技能是必需的;你至少应该熟悉指针。你还应该熟悉一些操作函数。还需要一些硬件技能。因此,本章主要讨论:

  • 模块构建过程,以及它们的加载和卸载

  • 驱动程序骨架和调试消息管理

  • 驱动程序中的错误处理

用户空间和内核空间

内核空间和用户空间的概念有点抽象。这一切都与内存和访问权限有关。人们可能认为内核是特权的,而用户应用程序是受限制的。这是现代 CPU 的一个特性,允许它在特权或非特权模式下运行。这个概念在第十一章 内核内存管理中会更清楚。

用户空间和内核空间

前面的图介绍了内核空间和用户空间之间的分离,并强调了系统调用代表它们之间的桥梁(我们稍后在本章讨论这一点)。可以描述每个空间如下:

  • 内核空间:这是内核托管和运行的一组地址。内核内存(或内核空间)是一段内存范围,由内核拥有,受到访问标志的保护,防止任何用户应用程序无意中干扰内核。另一方面,内核可以访问整个系统内存,因为它以更高的优先级在系统上运行。在内核模式下,CPU 可以访问整个内存(包括内核空间和用户空间)。

  • 用户空间:这是正常程序(如 gedit 等)受限制运行的一组地址(位置)。你可以把它看作是一个沙盒或*,这样用户程序就不能干扰其他程序拥有的内存或其他资源。在用户模式下,CPU 只能访问带有用户空间访问权限标记的内存。用户应用程序运行的优先级较低。当进程执行系统调用时,会向内核发送软件中断,内核会打开特权模式,以便进程可以在内核空间中运行。当系统调用返回时,内核关闭特权模式,进程再次被限制。

模块的概念

模块对于 Linux 内核来说就像插件(Firefox 就是一个例子)对于用户软件一样。它动态扩展了内核的功能,甚至不需要重新启动计算机。大多数情况下,内核模块都是即插即用的。一旦插入,它们就可以被使用。为了支持模块,内核必须已经使用以下选项构建:

CONFIG_MODULES=y 

模块依赖

在 Linux 中,模块可以提供函数或变量,并使用EXPORT_SYMBOL宏导出它们,使它们对其他模块可用。这些被称为符号。模块 B 对模块 A 的依赖是,模块 B 使用了模块 A 导出的符号之一。

depmod 实用程序

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

模块加载和卸载

要使模块运行,应该将其加载到内核中,可以使用insmod给定模块路径作为参数来实现,这是开发过程中首选的方法,也可以使用modprobe,这是一个聪明的命令,但在生产系统中更受欢迎。

手动加载

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

modprobe 和 insmod

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

insmod /path/to/mydrv.ko

这是一种低级形式的模块加载,它构成了其他模块加载方法的基础,也是本书中我们将使用的方法。另一方面,有modprobe,主要由系统管理员或在生产系统中使用。modprobe是一个聪明的命令,它解析文件modules.dep以便先加载依赖项,然后再加载给定的模块。它自动处理模块依赖关系,就像软件包管理器一样:

modprobe mydrv

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

/etc/modules-load.d/.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 

自动加载

depmod实用程序不仅构建modules.depmodules.dep.bin文件。它做的不仅仅是这些。当内核开发人员实际编写驱动程序时,他们确切地知道驱动程序将支持哪些硬件。然后他们负责为驱动程序提供所有受支持设备的产品和供应商 ID。depmod还处理模块文件以提取和收集这些信息,并生成一个modules.alias文件,位于/lib/modules/<kernel_release>/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、class、device class、device subclass、interface 以及可能标识设备的所有其他信息)来通知,这些信息发送到热插拔守护程序,它再调用modprobe来处理这些信息。modprobe然后解析modules.alias文件以匹配与设备关联的驱动程序。在加载模块之前,modprobe将在module.dep中查找它的依赖项。如果找到任何依赖项,那么在加载相关模块之前将加载依赖项;否则,模块将直接加载。

模块卸载

卸载模块的常用命令是rmmod。应该优先使用此命令来卸载使用insmod命令加载的模块。应该将模块名称作为参数给出。模块卸载是一个内核功能,可以根据CONFIG_MODULE_UNLOAD配置选项的值来启用或禁用。如果没有此选项,将无法卸载任何模块。让我们启用模块卸载支持:

CONFIG_MODULE_UNLOAD=y 

在运行时,内核将阻止卸载可能破坏事物的模块,即使有人要求这样做。这是因为内核保持对模块使用的引用计数,以便它知道模块是否实际上正在使用。如果内核认为移除模块是不安全的,它就不会这样做。显然,人们可以改变这种行为:

MODULE_FORCE_UNLOAD=y 

为了强制模块卸载,应该在内核配置中设置前述选项:

rmmod -f mymodule

另一方面,以智能方式卸载模块的更高级命令是modeprobe -r,它会自动卸载未使用的依赖项:

modeprobe -r mymodule

正如你可能已经猜到的,这对开发人员来说是一个非常有帮助的选项。最后,可以使用以下命令检查模块是否已加载:

lsmod

驱动程序骨架

让我们考虑以下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"); 

模块入口和出口点

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

我们都记得main()函数,它是每个以 C/C++编写的用户空间程序的入口点,当该函数返回时程序退出。对于内核模块,情况有所不同。入口点可以有任何你想要的名称,而不像用户空间程序在main()返回时退出,出口点是在另一个函数中定义的。你需要做的就是告诉内核哪些函数应该作为入口或出口点执行。实际的函数hellowolrd_inithellowolrd_exit可以被赋予任何名称。实际上,唯一强制的是将它们标识为相应的加载和卸载函数,并将它们作为参数传递给module_init()module_exit()宏。

总之,module_init()用于声明在加载模块(使用insmodmodprobe)时应调用的函数。初始化函数中所做的事情将定义模块的行为。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函数完成后被释放。这仅适用于内置驱动程序,而不适用于可加载模块。内核将在其引导序列期间首次运行驱动程序的初始化函数。

由于驱动程序无法卸载,其初始化函数直到下次重启之前都不会再次被调用。不再需要保留对其初始化函数的引用。对于__exit关键字也是一样,当模块被静态编译到内核中时,或者未启用模块卸载支持时,其对应的代码将被省略,因为在这两种情况下,exit函数永远不会被调用。__exit对可加载模块没有影响。

让我们花更多时间了解这些属性是如何工作的。这一切都关于名为可执行和可链接格式ELF)的对象文件。一个 ELF 对象文件由各种命名的部分组成。其中一些是强制性的,并且构成了 ELF 标准的基础,但人们可以创造任何想要的部分,并让特殊程序使用它。这就是内核的做法。可以运行objdump -h module.ko来打印出构成给定module.ko内核模块的不同部分:

helloworld-params.ko 模块的部分列表

在标题中的部分中,只有少数是标准的 ELF 部分:

  • .text,也称为代码,其中包含程序代码

  • .data,其中包含初始化数据,也称为数据段

  • .rodata,用于只读数据

  • .评论

  • 未初始化数据段,也称为 由符号开始的块bss

其他部分是根据内核目的的需求添加的。对于本章来说,最重要的是 .modeinfo 部分,它存储有关模块的信息,以及 .init.text 部分,它存储以 __init 宏为前缀的代码。

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

总之,__init__exit 是 Linux 指令(实际上是宏),它们包装了用于符号放置的 C 编译器属性。它们指示编译器将它们分别放置在 .init.text.exit.text 部分,即使内核可以访问不同的对象部分。

模块信息

即使不必阅读其代码,人们也应该能够收集有关给定模块的一些信息(例如作者、参数描述、许可证)。内核模块使用其 .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 命令在给定模块上转储 .modeinfo 部分的内容:

helloworld-params.ko 模块的 .modeinfo 部分的内容

modinfo 部分可以被视为模块的数据表。实际上以格式化的方式打印信息的用户空间工具是 modinfo

modinfo 输出

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

许可

许可在给定模块中由 MODULE_LICENSE() 宏定义:

MODULE_LICENSE ("GPL"); 

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

/* 
 * The following license idents are currently accepted as indicating free 
 * software modules 
 * 
 * "GPL"                   [GNU Public License v2 or later] 
 * "GPL v2"                [GNU Public License v2] 
 * "GPL and additional rights"   [GNU Public License v2 rights and more] 
 * "Dual BSD/GPL"                [GNU Public License v2 
 *                          or BSD license choice] 
 * "Dual MIT/GPL"                [GNU Public License v2 
 *                          or MIT license choice] 
 * "Dual MPL/GPL"                [GNU Public License v2 
 *                          or Mozilla license choice] 
 * 
 * The following other idents are available 
 * 
 * "Proprietary"                 [Non free products] 
 * 
 * There are dual licensed components, but when running with Linux it is the 
 * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL 
 * is a GPL combined work. 
 * 
 * This exists for several reasons 
 * 1\.    So modinfo can show license info for users wanting to vet their setup 
 * is free 
 * 2\.    So the community can ignore bug reports including proprietary modules 
 * 3\.    So vendors can do likewise based on their own policies 
 */ 

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

模块作者

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>"); 

模块描述

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

MODULE_DESCRIPTION("Hello, world! Module"); 

错误和消息打印

错误代码要么由内核解释,要么由用户空间应用程序(通过errno变量)解释。错误处理在软件开发中非常重要,比在内核开发中更重要。幸运的是,内核提供了几个几乎涵盖了你可能遇到的每个错误的错误,并且有时你需要打印它们以帮助你调试。

错误处理

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

#define  EPERM        1    /* Operation not permitted */ 
#define  ENOENT             2    /* No such file or directory */ 
#define  ESRCH        3    /* No such process */ 
#define  EINTR        4    /* Interrupted system call */ 
#define  EIO          5    /* I/O error */ 
#define  ENXIO        6    /* No such device or address */ 
#define  E2BIG        7    /* Argument list too long */ 
#define  ENOEXEC            8    /* Exec format error */ 
#define  EBADF        9    /* Bad file number */ 
#define  ECHILD            10    /* No child processes */ 
#define  EAGAIN            11    /* Try again */ 
#define  ENOMEM            12    /* Out of memory */ 
#define  EACCES            13    /* Permission denied */ 
#define  EFAULT            14    /* Bad address */ 
#define  ENOTBLK           15    /* Block device required */ 
#define  EBUSY       16    /* Device or resource busy */ 
#define  EEXIST            17    /* File exists */ 
#define  EXDEV       18    /* Cross-device link */ 
#define  ENODEV            19    /* No such device */ 
#define  ENOTDIR           20    /* Not a directory */ 
#define  EISDIR            21    /* Is a directory */ 
#define  EINVAL            22    /* Invalid argument */ 
#define  ENFILE            23    /* File table overflow */ 
#define  EMFILE            24    /* Too many open files */ 
#define  ENOTTY            25    /* Not a typewriter */ 
#define  ETXTBSY           26    /* Text file busy */ 
#define  EFBIG       27    /* File too large */ 
#define  ENOSPC            28    /* No space left on device */ 
#define  ESPIPE            29    /* Illegal seek */ 
#define  EROFS       30    /* Read-only file system */ 
#define  EMLINK            31    /* Too many links */ 
#define  EPIPE       32    /* Broken pipe */ 
#define  EDOM        33    /* Math argument out of domain of func */ 
#define  ERANGE            34    /* Math result not representable */ 

大多数时候,返回错误的经典方法是以return -ERROR的形式返回,特别是当涉及到回答系统调用时。例如,对于 I/O 错误,错误代码是EIO,应该return -EIO

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

错误有时会跨越内核空间并传播到用户空间。如果返回的错误是对系统调用(openreadioctlmmap)的回答,则该值将自动分配给用户空间的errno全局变量,可以使用strerror(errno)将错误转换为可读字符串:

#include <errno.h>  /* to access errno global variable */ 
#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 步,必须清理之前的操作(步骤 4、3、2、1)。而不是进行大量的嵌套检查操作,如下所示:

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 在函数中向前移动。

处理空指针错误

当涉及到从应该返回指针的函数返回错误时,函数经常返回NULL指针。这是一种有效但相当无意义的方法,因为人们并不确切知道为什么返回了这个空指针。为此,内核提供了三个函数,ERR_PTRIS_ERRPTR_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_PTRIS_ERRPTR_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

消息打印 - printk()

printk()对内核来说就像printf()对用户空间一样。由printk()编写的行可以通过dmesg命令显示。根据您需要打印的消息的重要性,您可以在include/linux/kern_levels.h中定义的八个日志级别消息之间进行选择,以及它们的含义:

以下是内核日志级别的列表。这些级别中的每一个都对应于字符串中的一个数字,其优先级与数字的值成反比。例如,0是更高的优先级:

#define KERN_SOH     "\001"            /* ASCII Start Of Header */ 
#define KERN_SOH_ASCII     '\001' 

#define KERN_EMERG   KERN_SOH "0"      /* system is unusable */ 
#define KERN_ALERT   KERN_SOH "1"      /* action must be taken immediately */ 
#define KERN_CRIT    KERN_SOH "2"      /* critical conditions */ 
#define KERN_ERR     KERN_SOH "3"      /* error conditions */ 
#define KERN_WARNING KERN_SOH "4"      /* warning conditions */ 
#define KERN_NOTICE  KERN_SOH "5"      /* normal but significant condition */ 
#define KERN_INFO    KERN_SOH "6"      /* informational */ 
#define KERN_DEBUG   KERN_SOH "7"      /* debug-level messages */ 

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

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

如果省略调试级别(printk("This is an error\n")),内核将根据CONFIG_DEFAULT_MESSAGE_LOGLEVEL配置选项为函数提供一个调试级别,这是默认的内核日志级别。实际上可以使用以下更有意义的宏之一,它们是对先前定义的宏的包装器:pr_emergpr_alertpr_critpr_errpr_warningpr_noticepr_infopr_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    /* Deferred messages from sched code 
                            * are set to this special level */ 
#define LOGLEVEL_DEFAULT   -1    /* default (or last) loglevel */ 
#define LOGLEVEL_EMERG           0     /* system is unusable */ 
#define LOGLEVEL_ALERT           1     /* action must be taken immediately */ 
#define LOGLEVEL_CRIT            2     /* critical conditions */ 
#define LOGLEVEL_ERR       3     /* error conditions */ 
#define LOGLEVEL_WARNING   4     /* warning conditions */ 
#define LOGLEVEL_NOTICE          5     /* normal but significant condition */ 
#define LOGLEVEL_INFO            6     /* informational */ 
#define LOGLEVEL_DEBUG           7     /* debug-level messages */ 

当前日志级别可以通过以下更改:

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

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

内核还支持其他调试方法,可以动态使用#define DEBUG或在文件顶部使用#define DEBUG。对此类调试风格感兴趣的人可以参考内核文档中的Documentation/dynamic-debug-howto.txt文件。

模块参数

与用户程序一样,内核模块可以从命令行接受参数。这允许根据给定的参数动态更改模块的行为,并且可以帮助开发人员在测试/调试会话期间不必无限制地更改/编译模块。为了设置这一点,首先应该声明将保存命令行参数值的变量,并对每个变量使用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代表 char 指针

  • perm:这表示/sys/module/<module>/parameters/<param>文件的权限。其中一些是S_IWUSRS_IRUSRS_IXUSRS_IRGRPS_WGRPS_IRUGO,其中:

  • S_I只是一个前缀

  • R:读取,W:写入,X:执行

  • USR:用户,GRP:组,UGO:用户,组,其他人

最终可以使用|(或操作)来设置多个权限。如果 perm 为0,则sysfs中的文件参数将不会被创建。您应该只使用S_IRUGO只读参数,我强烈建议;通过与其他属性进行|(或)运算,可以获得细粒度的属性。

在使用模块参数时,应该使用MODULE_PARM_DESC来描述每个参数。这个宏将在模块信息部分填充每个参数的描述。以下是一个示例,来自书籍的代码库中提供的helloworld-params.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)

构建您的第一个模块

有两个地方可以构建一个模块。这取决于您是否希望人们使用内核配置界面自行启用模块。

模块的 makefile

Makefile 是一个特殊的文件,用于执行一系列操作,其中最重要的是编译程序。有一个专门的工具来解析 makefile,叫做make。在跳转到整个 make 文件的描述之前,让我们介绍obj-<X> kbuild 变量。

在几乎每个内核 makefile 中,都会看到至少一个obj<-X>变量的实例。这实际上对应于obj-<X>模式,其中<X>应该是ym,留空,或n。这是由内核 makefile 从内核构建系统的头部以一般方式使用的。这些行定义要构建的文件、任何特殊的编译选项以及要递归进入的任何子目录。一个简单的例子是:

 obj-y += mymodule.o 

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

  • 如果<X>设置为m,则使用变量obj-mmymodule.o将作为一个模块构建。

  • 如果<X>设置为y,则使用变量obj-ymymodule.o将作为内核的一部分构建。然后说 foo 是一个内置模块。

  • 如果<X>设置为n,则使用变量obj-mmymodule.o将根本不会被构建。

因此,通常使用obj-$(CONFIG_XXX)模式,其中CONFIG_XXX是内核配置选项,在内核配置过程中设置或不设置。一个例子是:

obj-$(CONFIG_MYMODULE) += mymodule.o 

$(CONFIG_MYMODULE)根据内核配置过程中的值评估为ym。如果CONFIG_MYMODULE既不是y也不是m,则文件将不会被编译或链接。y表示内置(在内核配置过程中代表是),m代表模块。$(CONFIG_MYMODULE)从正常配置过程中获