时间轴

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

当且仅当 module_init 中的 init 函数返回大于等于 0 的值模块才会被加载成功

模块信息

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
$ objdump -h hello_world.ko

hello_world.ko: file format elf64-little

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .init.text 00000034 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .exit.text 00000024 0000000000000000 0000000000000000 00000074 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
3 .note.gnu.property 00000020 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .note.gnu.build-id 00000024 0000000000000000 0000000000000000 000000b8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .note.Linux 00000018 0000000000000000 0000000000000000 000000dc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .rodata.str1.8 0000002b 0000000000000000 0000000000000000 000000f8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .modinfo 000000b2 0000000000000000 0000000000000000 00000123 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 __versions 00000080 0000000000000000 0000000000000000 000001d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 __patchable_function_entries 00000008 0000000000000080 0000000000000080 00000258 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
10 .data 00000000 0000000000000000 0000000000000000 00000260 2**0
CONTENTS, ALLOC, LOAD, DATA
11 .gnu.linkonce.this_module 000003c0 0000000000000000 0000000000000000 00000260 2**6
CONTENTS, ALLOC, LOAD, RELOC, DATA, LINK_ONCE_DISCARD
12 .plt 00000001 0000000000000000 0000000000000000 00000620 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .init.plt 00000001 0000000000000000 0000000000000000 00000621 2**0
ALLOC, READONLY
14 .text.ftrace_trampoline 00000001 0000000000000000 0000000000000000 00000621 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .bss 00000000 0000000000000000 0000000000000000 00000622 2**0
ALLOC
16 .comment 00000026 0000000000000000 0000000000000000 00000622 2**0
CONTENTS, READONLY
17 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000648 2**0
CONTENTS, READONLY

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

1
MODULE_INFO(my_field_name, "What eeasy value");

.modeinfo部分的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ readelf -x .modinfo hello_world.ko

Hex dump of section '.modinfo':
0x00000000 64657363 72697074 696f6e3d 68656c6c description=hell
0x00000010 6f20776f 726c6421 00617574 686f723d o world!.author=
0x00000020 6576656e 36323900 6c696365 6e73653d even629.license=
0x00000030 47504c00 73726376 65727369 6f6e3d41 GPL.srcversion=A
0x00000040 34303030 33444636 33303137 39423643 40003DF630179B6C
0x00000050 46314430 34350064 6570656e 64733d00 F1D045.depends=.
0x00000060 6e616d65 3d68656c 6c6f5f77 6f726c64 name=hello_world
0x00000070 00766572 6d616769 633d352e 31302e31 .vermagic=5.10.1
0x00000080 31302d76 382b2053 4d502070 7265656d 10-v8+ SMP preem
0x00000090 7074206d 6f645f75 6e6c6f61 64206d6f pt mod_unload mo
0x000000a0 64766572 73696f6e 73206161 72636836 dversions aarch6
0x000000b0 3400 4.

也可以使用modinfo查看

1
2
3
4
5
6
7
8
9
$ modinfo hello_world.ko
filename: /home/zhaohang/repository/linux/linux_driver_learning/01_hello_world/hello_world.ko
description: hello world!
author: even629
license: GPL
srcversion: A40003DF630179B6CF1D045
depends:
name: hello_world
vermagic: 5.10.110-v8+ SMP preempt mod_unload modversions aarch64

编译 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

系统管理员或在生产系统中则常用 modprobe。modprobe 更智能,它在加载指定的模块之前解析文件 modules.dep,以便首先加载依赖关系。它会自动处理模块依赖关系,就像包管理器所做的那样。

但是**modprobe 不能直接加载任意路径下的 .ko 文件** —— 它只从标准内核模块目录(如 /lib/modules/$(uname -r)/)中查找模块,并依赖 modules.dep 索引。

模块卸载命令

  • rmmod

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

    • 它不仅会尝试卸载 mymodule,还会自动检查并卸载那些只被 mymodule 依赖、且当前没有其他模块/进程在使用的依赖模块
    • 语法:modeprobe -r mymodule

卸载内核模块功能的启用或禁用由 CONFIG_MODULE_UNLOAD 配置选项的值决定。没有这个选项,就不能卸载任何模块。

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

设置启动时加载模块

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

1
2
3
4
5
emacs /etc/modules-load.d/mymodule.conf
# 下面时mymodule.conf的内容
uio
iwlwifi
i2c-dev

查看模块信息命令

  • 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 文件到当前 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中定义。

module_param

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
/**
* module_param - typesafe helper for a module/cmdline parameter
* @name: the variable to alter, and exposed parameter name.
* @type: the type of the parameter
* @perm: visibility in sysfs.
*
* @name becomes the module parameter, or (prefixed by KBUILD_MODNAME and a
* ".") the kernel commandline parameter. Note that - is changed to _, so
* the user can use "foo-bar=1" even for variable "foo_bar".
*
* @perm is 0 if the variable is not to appear in sysfs, or 0444
* for world-readable, 0644 for root-writable, etc. Note that if it
* is writable, you may need to use kernel_param_lock() around
* accesses (esp. charp, which can be kfreed when it changes).
*
* The @type is simply pasted to refer to a param_ops_##type and a
* param_check_##type: for convenience many standard types are provided but
* you can create your own by defining those variables.
*
* Standard types are:
* byte, hexint, short, ushort, int, uint, long, ulong
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
*/
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)

module_param_array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/**
* module_param_array - a parameter which is an array of some type
* @name: the name of the array variable
* @type: the type, as per module_param()
* @nump: optional pointer filled in with the number written
* @perm: visibility in sysfs
*
* Input and output are as comma-separated values. Commas inside values
* don't work properly (eg. an array of charp).
*
* ARRAY_SIZE(@name) is used to determine the number of elements in the
* array, so the definition must be visible.
*/
#define module_param_array(name, type, nump, perm) \
module_param_array_named(name, name, type, nump, perm)

module_param_string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* module_param_string - a char array parameter
* @name: the name of the parameter
* @string: the string variable
* @len: the maximum length of the string, incl. terminator
* @perm: visibility in sysfs.
*
* This actually copies the string when it's set (unlike type charp).
* @len is usually just sizeof(string).
*/
#define module_param_string(name, string, len, perm) \
static const struct kparam_string __param_string_##name \
= { len, string }; \
__module_param_call(MODULE_PARAM_PREFIX, name, \
&param_ops_string, \
.str = &__param_string_##name, perm, -1, 0);\
__MODULE_PARM_TYPE(name, "string")

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 指向字符串缓冲的指针

权限定义

perm 是指 sysfs 中参数文件文件权限位

读写权限在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
  • 实际意义:所有人 执行 权限
  • 举例用途:使所有人可执行某脚本或程序

示例

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
48
49
50
#include <linux/init.h>
#include <linux/module.h>


static int myint = 0;
module_param(myint,int, 0644);
MODULE_PARM_DESC(myint, "A sample int parameter");

static char* mycharp = "hello";
module_param(mycharp, charp, 0644);
MODULE_PARM_DESC(mycharp, "A sample charp parameter");

static int myarr[3] = {1, 2, 3};
static int myarr_argc = ARRAY_SIZE(myarr);
module_param_array(myarr, int, &myarr_argc, 0644);
MODULE_PARM_DESC(myarr, "A sample array parameter");

static char mystring[] = "default_value";
module_param_string(mystr, mystring, ARRAY_SIZE(mystring), 0644);
MODULE_PARM_DESC(mystr, "A sample string parameter");


static void print_param(void){
int i;
pr_info("[myint]: %d\n", myint);
pr_info("[mycharp]: %s\n", mycharp);
pr_info("[myarr]: ");
for(i =0;i<myarr_argc;i++){
pr_info("%d ", myarr[i]);
}
pr_info("[mystr]: %s\n", mystring);
}

static int __init param_test_init(void){
printk("param test init\n");
print_param();
return 0;
}

static void __exit param_test_exit(void){
print_param();
printk("param test exit\n");
}

module_init(param_test_init);
module_exit(param_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("linux driver parameter test");

加载模块时可以传参

1
insmod parameter.ko myint=42 mycharp="hello world" myarr=9,8,7 mystr="hello"

运行时候查看参数

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

内核符号表导入导出

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

符号表

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

模块依赖关系

linux 内核中的模块可以提供函数或变量,用EXPORT_SYMBOL宏导出它们即可供其他模块使用,这些被称作符号。

模块 B 对模块 A 的依赖是指模块 B 使用从模块 A 导出的符号。

depmod 是用户空间工具(通常由 kmod 包提供),在内核安装后运行:

1
depmod -a <kernel_version>
  • 它扫描 /lib/modules/<kernel_release>/ 下所有 .ko 模块文件:
    • 使用 modinfo -F depends 或直接解析 ELF 符号表;
    • 硔定每个模块 需要哪些符号(imports)提供哪些符号(exports)
    • 构建模块间的依赖关系图。

生成的依赖文件

文件 作用
modules.dep 文本格式,每行格式: module.ko: dependent_module1.ko dependent_module2.ko ...
modules.dep.bin 二进制格式,供 modprobe 快速加载(避免每次解析文本)
modules.symbols / modules.symbols.bin 记录所有导出符号及其所属模块(用于反向查找)

depmod 还处理模块文件以提取和收集该信息,并在/lib/modules/<kernel_release>/modules.alias中生成 modules.alias 文件,该文件将设备映射到其对应的驱动程序。

modprobe 会解析 modules.alias 文件。

内核符号表导出

导出宏

宏名称 适用场景
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);

static int __init module_export_init(void){
pr_info("add init\n");
return 0;
}

static void __exit module_export_exit(void){
pr_info("add exit\n");
}

module_init(module_export_init);
module_exit(module_export_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@163.com>");
MODULE_DESCRIPTION("A sample for module export");

导入

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
#include <linux/module.h>
#include <linux/init.h>

static int a = 0;
module_param(a,int, 0644);
MODULE_PARM_DESC(a, "add test left num a, default 0");

static int b = 0;
module_param(b, int, 0644);
MODULE_PARM_DESC(b, "add test left num b, default 0");



extern int add(int a, int b);

static int __init module_export_init(void){
pr_info("hello init");
pr_info("a=%d, b=%d\n", a, b);
pr_info("a+b=%d\n", a+b);
return 0;
}

static void __exit module_export_exit(void){
pr_info("hello exit");
}

module_init(module_export_init);
module_exit(module_export_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<731005515@qq.com>");
MODULE_DESCRIPTION("A sample for module export");

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

使用 makefile 中定义的宏

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

1
cc -D宏名=值 source.c

例子:

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

# 定义一个宏
MY_DRIVER_VER := 0x10

# 把它传给编译器
KBUILD_CFLAGS_MODULE += -DMY_DRIVER_VER=$(MY_DRIVER_VER)
# 或者使用
ccflags-y += -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 | https://even629.com/posts/2512283/ |
| 16. SPI | https://even629.com/posts/2512303/ |
| 17. UART | https://even629.com/posts/2512313/ |
| 18. PWM | https://even629.com/posts/2601043/ |
| 19. RTC | https://even629.com/posts/2601053/ |
| 20. Watchdog | https://even629.com/posts/2601063/ |
| 21. CAN | https://even629.com/posts/2601093/ |
| 22. 网络设备 | https://even629.com/posts/2601133/ |
| 23. ADC | https://even629.com/posts/2601143/ |
| 24. IIO | TODO |
| 25. USB | TODO |
| 26. LCD | TODO |
| 27. PCIe | TODO |