时间轴

2025-11-09

init


Linux驱动抽象

Linux驱动框架演进

Linux2.4的驱动架构

Linux2.6引入平台总线架构

Linux设备树

Linux 将存储器和外设分为 3 个基础大类:字符设备驱动块设备驱动网络设备驱动

Linux源码目录结构

目录 说明
arch 架构相关目录,存放多种 CPU 架构(如 arm、X86、MIPS 等)的适配代码
block 块设备相关代码目录,Linux 中以块设备形式管理硬盘、SD 卡等存储设备
crypto 加密算法目录,存放各类加密相关的实现代码
Documentation 官方 Linux 内核文档目录,包含内核功能、接口等详细说明
drivers 驱动目录,存放 Linux 系统支持的各类硬件设备驱动代码
firmware 固件目录,存放硬件设备所需的固件文件
fs 文件系统目录,存放 ext2、ext3、fat 等文件系统的实现代码
include 公共头文件目录,提供内核各模块共用的头文件
init 内核启动初始化目录,存放 Linux 内核启动阶段的初始化代码
ipc 进程间通信目录,存放管道、消息队列、共享内存等 IPC 机制的实现代码
kernel 内核核心目录,存放内核本身的核心功能代码
lib 库函数目录,存放内核使用的各类库函数
mm 内存管理目录(mm 为 memory management 缩写),负责内核内存管理功能
net 网络相关目录,存放 TCP/IP 协议栈等网络功能的实现代码
scripts 脚本目录,存放内核编译、测试等流程中使用的脚本文件
security 安全相关目录,存放内核安全机制的实现代码
sound 音频相关目录,存放音频设备驱动及音频处理的代码
tools 工具目录,存放 Linux 内核开发、调试用到的工具程序
usr 与 Linux 内核启动相关的代码目录
virt 内核虚拟机相关目录,存放内核级虚拟化功能的实现代码

最简单的 Linux 驱动结构解析

  • 组成部分
    1. 头文件(必须):驱动需包含内核相关头文件,其中<linux/module.h><linux/init.h>是必备的。
    2. 驱动加载函数(必须):加载驱动时,该函数会被内核自动执行。
    3. 驱动卸载函数(必须):卸载驱动时,该函数会被内核自动执行。
    4. 许可证声明(必须):因 Linux 内核遵循 GPL 协议,驱动加载时也需遵守相关协议,常见的有 GPL v2 等多种许可证类型。
    5. 模块参数(可选):是模块加载时传递给内核模块的值。
    6. 作者和版本信息(可选):用于声明驱动的作者及代码版本信息。
  • 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>



static int __init hello_world_init(void)
{
printk(KERN_INFO "Hello World: Module loaded\n");
return 0;
}

static void __exit hello_world_exit(void)
{
printk(KERN_INFO "Hello World: Module unloaded\n");
}

module_init(hello_world_init);
module_exit(hello_world_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("Hello World Kernel Module");

编译Linux驱动

  • 将驱动放在Linux内核里面,然后编译Linux内核。将驱动编译到Linux内核里面。
  • 将驱动编译成内核模块,独立于Linux内核之外
    • 内核模块是Linux系统中的一个特殊的机制,可以将一些使用频率很少或者暂时不用的功能编译成内核模块,在需要的时候再动态加载到内核里面。
    • 使用内核模块可以减小内核体积,加快启动速度。并且可以在系统运行时插入或者卸载驱动,无需重启系统。内核模块的后缀是.ko

编译成内核模块

1
2
3
4
5
6
7
8
9
10
11
12
obj-m += hello_world.o
KERNEL_SRC:=/home/zhaohang/repository/linux/linux-5.10.246
PWD ?=$(shell pwd)
ARCH = arm64
CROSS_COMPILE = aarch64-linux-gnu-

all:
$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_SRC) M=$(PWD) modules

clean:
$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_SRC) M=$(PWD) modules clean
rm -rf *.ko *.o *.mod.o *.mod.c *.symvers *.order

本地的Linux代码在/lib/modules/$(uname -r)/kernel下

将驱动编译到内核

drivers/char(以字符驱动为例)创建文件夹helloworld,然后将驱动源代码放入,然后创建Kconfig文件

1
2
3
4
5
config helloworld
bool "helloworld support"
default y
help
helloworld

更改drivers

1
2
3
emacs ../Kconfig
# 添加
source "drivers/char/helloworld/Kconfig"

在驱动源码里创建Makefile

1
obj-$(CONFIG_helloworld) += helloworld.o

然后在上一级的Makefile中添加:

1
2
3
emacs ../Makefile
# 添加
obj-y += helloworld/

模块相关命令

模块加载命令

  • insmod
    • 功能:载入Linux内核模块
    • 语法:insmod 模块名
    • 例子:insmod hello_world.ko
  • modprobe
    • 功能:加载内核模块,同时这个模块所依赖的模块也同时被加载
    • 语法:modprobe 模块名
    • 举例:modprobe hello_world.ko

模块卸载命令

  • rmmod
    • 功能:移除已经载入Linux的内核模块
    • 语法:rmmod模块名
    • 举例:rmmod hello_world.ko

查看模块信息命令

  • lsmod命令
    • 功能:列出已经载入Linux的内核模块
    • 也可以使用命令cat /proc/modules来查看模块是否加载成功
  • modinfo命令
    • 功能:查看内核模块信息
    • 语法:modinfo模块名
    • 举例:modinfo hello_world.ko

配置文件

menuconfig配置驱动选项状态操作:

驱动状态:

  1. 把驱动编译成内核模块,用M来表示
  2. 把驱动编译到内核里面,用*来表示
  3. 不编译

使用空格来切换这三种不同状态。

选项的状态有:

  • [] : 表示有两种状态只能设置成选中或者不选中
  • <> : 表示有三种状态,可以设置成选中,不选中,和编译成模块
  • () : 表示用来存放字符串或者16进制数

Kconfig文件

Kconfig文件是图形化配置界面的源文件,图形化配置界面中的选项由Kconfig文件决定。当我们执行命令make menuconfig命令的时候,内核的配置工具会读取内核源码目录下的arch/xxx/Kconfig。xxx是ARCH的值,比如arm64,然后生成对应的配置界面供开发者使用。

config文件与.config文件

config文件和.config文件都是Linux内核的配置文件。

  • config文件位于Linux内核源码的arch/$(ARCH)/configs目录下,是Linux系统默认的配置文件。.
  • config文件位于Linux内核源码的顶层目录下,编译linux内核时会使用.config文件里面的配置来编译内核镜像。

若.config存在,make menuconfig界面的默认配置即当前.config文件的配置,若修改了图形化配置界面的设置并保存,则.config文件会被更新。

若.config文件不存在,make menuconfig界面的默认配置则为Kconfig文件中的默认配置。

使用命令make xxx_defconfig命令会根据arch/$(ARCH)/configs目录下默认文件生成.config文件。

Kconfig语法

参考:

主菜单

mainmenu用来设置主菜单的标题

举例:mainmenu "Linux/\$(ARCH) $(KERNELVERSION) Kernel Configuration"

上述名字设置的菜单名字为Linux/\$(ARCH) $(KERNELVERSION) Kernel Configuration

菜单结构

可以用menu/endmenu来生成菜单,menu是菜单开始的标志,endmenu是菜单结束的标志。这两个是成对出现的。

如下描述的是一个名字为:”Network device support”的菜单

1
2
3
4
menu "Network device support"
config NETDEVICE
...
endmenu

配置选项

使用关键字config来定义一个新的选项。每个选项都必须指定类型,类型包括bool, tristate, string, hex, int。最常见的是bool, tristate, string这三个。

  • bool类型有两种值:y和n;

  • trisate有三种值:y, m和n;

  • string为字符串类型。

help表示帮助信息,当我们在图形化界面按下h按键,弹出来的就是help的内容。

举例:

1
2
3
4
5
config helloworld
bool "hello world support"
default y
help
hello world

依赖关系

Kconfig中的依赖关系可以用depends on和select

depends on 表示直接依赖关系

1
2
config A
depends on B

表示选项A依赖选项B,只有选项B被选中时,A选项才可以被选中

select表示反向依赖关系:

1
2
config A
select B

在A选项被选中的情况下,B选项自动被选中

可选选项

使用choice和endchoice定义可选择项

1
2
3
4
5
6
7
8
choice
bool "a"
config b
boot b1
config c
boot c1
...
endchoice

注释

在图形化配置界面显示一个注释

1
2
3
4
5
6
config TEST_CONFIG
bool "test"
default y
help
just test
comment "just for test"

souce

source用于读取另一个Kconfig文件,如source “init/Kconfig” 就是读取init目录下的Kconfig文件

驱动模块传参

驱动传参的意义:

优势:

  1. 通过驱动传参,可以让驱动程序更加灵活,兼容性更强。
  2. 可以通过驱动传参,设置安全校验,防止驱动被盗用

不足

  1. 使驱动代码变得复杂化
  2. 增加了驱动的资源占用

驱动可以传递的参数类型

C语言常用的数据类型内核大部分都支持驱动传参。这里将内核支持的驱动传递参数类型分为三类:

  • 基本类型:char, bool, int, long, short, byte, ushort, uint
  • 数组:array
  • 字符串:string

给 Linux 驱动传递参数的方式

  • 参数类型与对应函数

    | 参数类型 | 对应函数 | 功能 |
    | ————— | ——————————- | ————————— |
    | 基本类型 | module_param | 传递基本类型参数 |
    | 数组类型 | module_param_array | 传递数组类型参数 |
    | 字符串类型 | module_param_string | 传递字符串类型参数 |

  • 函数定义位置:这三个函数在 Linux 内核源码的include/linux/moduleparam.h中定义。

1
2
3
4
5
6
7
module_param(name, type, perm);
module_param_array(name, type, nump, perm);
module_param_string(name, string, len, perm);

module_param_named(name, variable, type, perm);

MODULE_PARM_DESC(name, "help description");

MODULE_PARM_DESC

函数功能:描述模块参数的信息。在include/linux/moduleparam.h定义

函数原型:MODULE_PARM_DESC(_parm, desc)

函数参数: _parm:要描述的参数的参数名称。desc:描述信息

参数名 含义
name 参数名(可在命令行传入)
type 参数类型(如:intboolcharp
perm /sys/module/<modname>/parameters/ 中的权限,如 0644
nump (仅数组)保存数组元素数量的变量地址
len (仅字符串)缓冲区长度
string 指向字符串缓冲的指针

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int myint = 1;
static char *mystring = "default";
static int myarray[3] = {0, 1, 2};
static int arr_argc = 3;

module_param(myint, int, 0644);
MODULE_PARM_DESC(myint, "An integer parameter");

module_param(mystring, charp, 0444);
MODULE_PARM_DESC(mystring, "A string parameter");

module_param_array(myarray, int, &arr_argc, 0644);
MODULE_PARM_DESC(myarray, "An integer array parameter");

static int __init hello_init(void)
{
int i;
pr_info("Hello, world!\n");
pr_info("myint = %d\n", myint);
pr_info("mystring = %s\n", mystring);
for (i = 0; i < arr_argc; i++)
pr_info("myarray[%d] = %d\n", i, myarray[i]);
return 0;
}

static void __exit hello_exit(void)
{
pr_info("Goodbye!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

加载模块时可以传参

1
insmod hello_world.ko myint=42 mystring="hi" myarray=9,8,7

运行时候查看参数

1
2
3
cd /sys/module/hello_world/parameters/
cat myint
echo 99 > myint

权限定义位置:读写权限在include/linux/stat.hinclude/uapi/linux/stat.h中定义。

  • include/linux/stat.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_STAT_H
#define _LINUX_STAT_H


#include <asm/stat.h>
#include <uapi/linux/stat.h>

#define S_IRWXUGO (S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
#define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO (S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO (S_IXUSR|S_IXGRP|S_IXOTH)


#endif
  • include/uapi/linux/stat.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _UAPI_LINUX_STAT_H
#define _UAPI_LINUX_STAT_H

#include <linux/types.h>

#if defined(__KERNEL__) || !defined(__GLIBC__) || (__GLIBC__ < 2)

#define S_IFMT 00170000
#define S_IFSOCK 0140000
#define S_IFLNK 0120000
#define S_IFREG 0100000
#define S_IFBLK 0060000
#define S_IFDIR 0040000
#define S_IFCHR 0020000
#define S_IFIFO 0010000
#define S_ISUID 0004000
#define S_ISGID 0002000
#define S_ISVTX 0001000

#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)

#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100

#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010

#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002
#define S_IXOTH 00001

#endif

#endif /* _UAPI_LINUX_STAT_H */

相关的权限主要是文件访问权限宏(File Permission Bits)

宏名 八进制值 含义
S_IRWXU 00700 所有者(U)读、写、执行权限(RWX)
S_IRUSR 00400 所有者(USR)读权限(R)
S_IWUSR 00200 所有者(USR)写权限(W)
S_IXUSR 00100 所有者(USR)执行权限(X)
S_IRWXG 00070 所属组(G)读、写、执行权限(RWX)
S_IRGRP 00040 所属组(GRP)读权限(R)
S_IWGRP 00020 所属组(GRP)写权限(W)
S_IXGRP 00010 所属组(GRP)执行权限(X)
S_IRWXO 00007 其他用户(O)读、写、执行权限(RWX)
S_IROTH 00004 其他用户(OTH)读权限(R)
S_IWOTH 00002 其他用户(OTH)写权限(W)
S_IXOTH 00001 其他用户(OTH)执行权限(X)

还有组合权限

S_IRWXUGO

  • 定义(S_IRWXU | S_IRWXG | S_IRWXO)
  • 含义(展开后)00700 | 00070 | 00007 = 00777
  • 实际意义:允许 所有者、组、其他用户 (UGO) 拥有 读、写、执行 权限
  • 举例用途:设置所有人可读写执行,如临时目录 /tmp

S_IALLUGO

  • 定义(S_ISUID | S_ISGID | S_ISVTX | S_IRWXUGO)
  • 含义(展开后)0004000 | 0002000 | 0001000 | 00777 = 01777
  • 实际意义:包含特殊位(SUID、SGID、Sticky)以及所有人读写执行权限
  • 举例用途:常见权限:drwxrwxrwt(如 /tmp

S_IRUGO

  • 定义(S_IRUSR | S_IRGRP | S_IROTH)
  • 含义(展开后)00400 | 00040 | 00004 = 00444
  • 实际意义:所有人 权限
  • 举例用途:常用于只读文件

S_IWUGO

  • 定义(S_IWUSR | S_IWGRP | S_IWOTH)
  • 含义(展开后)00200 | 00020 | 00002 = 00222
  • 实际意义:所有人 权限
  • 举例用途:少用,一般仅限特定目录

S_IXUGO

  • 定义(S_IXUSR | S_IXGRP | S_IXOTH)
  • 含义(展开后)00100 | 00010 | 00001 = 00111
  • 实际意义:所有人 执行 权限
  • 举例用途:使所有人可执行某脚本或程序

内核符号表导入导出

驱动程序可以编译成内核模块,也就是KO文件。每个KO文件是相互独立的,也就是说模块之间无法相互访问。但是在某些使用场景下要相互访问,如B模块要用A模块中的函数。(B模块依赖于A模块)

符号表

”符号“就是内核中的函数名,全局变量名等。符号表就是用来记录这些”符号“的文件。

内核符号表导出

导出宏

宏名称 适用场景
EXPORT_SYMBOL 导出符号到内核符号表
EXPORT_SYMBOL_GPL 仅适用于包含 GPL 许可的模块

导出的符号可被其他模块使用,使用前只需声明即可。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <linux/module.h>
#include <linux/init.h>

int add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add); // 导出add函数到内核符号表

static int __init helloworld_init(void)
{
printk("helloworld!\n");
return 0;
}

static void __exit helloworld_exit(void)
{
printk("helloworld bye\n");
}

module_init(helloworld_init); // 指定模块初始化函数
module_exit(helloworld_exit); // 指定模块退出函数

MODULE_LICENSE("GPL"); // 声明许可证

导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <linux/module.h>
#include <linux/init.h>

/* 声明外部模块导出的函数 */
extern int add(int a, int b);

static int __init use_add_init(void)
{
int result = add(3, 5);
pr_info("Result of add(3,5) = %d\n", result);
return 0;
}

static void __exit use_add_exit(void)
{
pr_info("use_add module unloaded\n");
}

module_init(use_add_init);
module_exit(use_add_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhao Hang");
MODULE_DESCRIPTION("Module that calls add() from helloworld");

注意:加载时要先加载导出的那个模块,卸载时要先卸载导入的那个模块。因为此时两个模块已经有了依赖关系,modprobe会自动处理这些依赖关系,不需要显式声明。

使用makefile中定义的宏

核心思路:想要让它被 C 代码可见,必须通过 编译器命令行传入宏定义

1
cc -D宏名=值 source.c

例子:

1
2
3
4
5
6
7
obj-m += mydriver.o

# 定义一个宏
MY_DRIVER_VER := 0x10

# 把它传给编译器
KBUILD_CFLAGS_MODULE += -DMY_DRIVER_VER=$(MY_DRIVER_VER)

其他变量名:

变量名 作用范围 典型用途
ccflags-y 当前模块的所有 .c 文件 普通 C 编译选项
asflags-y 汇编文件 汇编参数
subdir-ccflags-y 当前目录及子目录 全局作用
KBUILD_CFLAGS 全局(由内核顶层 Makefile 设置) 平台级 CFLAGS
KBUILD_CFLAGS_MODULE 内核模块
EXTRA_CFLAGS 已废弃(旧版用法) 临时附加选项

然后在driver中可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <linux/module.h>
#include <linux/kernel.h>

static int __init mydriver_init(void)
{
pr_info("mydriver version: 0x%x\n", MY_DRIVER_VER);
return 0;
}

static void __exit mydriver_exit(void)
{
pr_info("mydriver exit\n");
}

module_init(mydriver_init);
module_exit(mydriver_exit);

MODULE_LICENSE("GPL");

Linux驱动笔记

目录 链接
1. Linux 驱动框架 https://even629.com/posts/2511093/
2. Linux 驱动加载逻辑 https://even629.com/posts/2511100/
3. 字符设备基础 https://even629.com/posts/2511113/
4. 并发与竞争 https://even629.com/posts/2511123/
5. 高级字符设备进阶 https://even629.com/posts/2511133/
6. 中断 https://even629.com/posts/2511143/
7. 平台总线 https://even629.com/posts/2511153/
8. 设备树 https://even629.com/posts/2511300/
9. 设备模型 https://even629.com/posts/2512013/
10. 热插拔 https://even629.com/posts/2512023/
11. pinctrl子系统 https://even629.com/posts/2512160/
12. gpio子系统 https://even629.com/posts/2512173/
13. 输入子系统 https://even629.com/posts/2512183/
14. 单总线 https://even629.com/posts/2512233/
15. I2C TODO
16. SPI TODO
17. 串口 TODO
18. PWM TODO
19. RTC TODO
20. 看门狗 TODO
21. CAN TODO
22. 网络设备 TODO
23. ADC TODO
24. IIO TODO
25. USB TODO
26. LCD TODO
27. PCIe TODO