参考书籍:
Linux版本编号规则为:
VERSION.PATCHLEVEL.SUBLEVEL[-EXTRAVERSION]
其中:
Linux将设备分为了如下三个大类:
字符设备是指必须以字节流顺序访问的设备,如字符终端( /dev/console
)、串口( /dev/tty*
)、触摸屏、磁带驱动器、鼠标等。
open
、 close
、 read
、 write
系统调用。/dev
下)。open()
、 read()
、 write()
等方式进行访问。块设备允许按照任意顺序进行访问,以块为单位进行操作,如硬盘、eMMC等。
dd
的命令操作设备的块FAT
、 EXT4
等文件系统后,使用文件系统的方式进行访问。/dev
下)。open()
、 read()
、 write()
等方式进行访问。网络设备面向数据包的发送和接收设计,内核与网络设备之间的通信主要使用Socket。
loopback
)。eth0
。read()
、 write()
等。当前的通用处理器主要采用了冯诺依曼架构和哈佛架构两种架构形式,其对应的常见的CPU系列有:
arch | char | short | int | long | ptr | long long | u8 | u16 | u32 | u64 |
---|---|---|---|---|---|---|---|---|---|---|
i386 | 1 | 2 | 4 | 4 | 4 | 8 | 1 | 2 | 4 | 8 |
alpha | 1 | 2 | 4 | 8 | 8 | 8 | 1 | 2 | 4 | 8 |
armv4l | 1 | 2 | 4 | 4 | 4 | 8 | 1 | 2 | 4 | 8 |
ia64 | 1 | 2 | 4 | 8 | 8 | 8 | 1 | 2 | 4 | 8 |
m68k | 1 | 2 | 4 | 4 | 4 | 8 | 1 | 2 | 4 | 8 |
mips | 1 | 2 | 4 | 4 | 4 | 8 | 1 | 2 | 4 | 8 |
ppc | 1 | 2 | 4 | 4 | 4 | 8 | 1 | 2 | 4 | 8 |
sparc | 1 | 2 | 4 | 4 | 4 | 8 | 1 | 2 | 4 | 8 |
sparc64 | 1 | 2 | 4 | 8 | 8 | 8 | 1 | 2 | 4 | 8 |
x86_64 | 1 | 2 | 4 | 8 | 8 | 8 | 1 | 2 | 4 | 8 |
常用存储器分类可见常用存储器 > 常用存储器。
可见Kconfig基础语法。
Linux的组件可以选择编译进Linux内核或者编译为Linux内核模块,编译为Linux内核模块的好处有:
Linux内核模块的加载、卸载以及查看基本信息等操作:
insmod
或 modprobe
命令,其中后者可以在加载目标模块的同时把依赖的模块也加载了。rmmod
命令modinfo
命令lsmod
命令由于Linux内核模块是直接以内核模式运行的,因此可以出于安全考虑,在编译Linux内核时,禁用加载模块功能。
相比于普通的应用程序开发,Linux内核的开发需要额外注意以下几点:
一个Linux内核模块应当具有且实现以下几个组成部分:
static int __init init_func(void)
{
// 具体实现...
// 初始化成功时应当返回0
// 执行失败时应当选择 <linux/errno.h> 中的错误代码
}
// 指定初始化函数
module_init(init_func);
注意:
goto
语句实现,也可以编写统一的 cleanup
函数检查资源是否被成功分配并释放资源(推荐后者)。一般 cleanup
函数不会被注册为卸载函数,因此不能被标记为 __exit
。<linux/errno.h>
中的错误码,这样方便用户使用 perror
之类的函数将错误转换为有意义的字符串。__init
标识符的用途可见章节4.3.6 其他常用特性中的解释。static void __exit exit_function(void)
{
// ...模块卸载,释放资源
}
// 指定模块卸载函数
module_exit(exit_function);
注意:
__exit
标识符的用途可见章节4.3.6 其他常用特性中的解释。模块在加载时可以传入一些命令行参数,其主要通过如下方式进行定义与加载:
#include "linux/moduleparam.h"
// 相当于缺省参数
static int port = 8080;
/**
* 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).
*/
module_param(port, int, S_IRUGO);
在 module_param
函数中, perm
为访问许可配置,其被定义与 include/linux/sta.h
中,其基本定义与用户态的权限定义一致。具体如下:
S_IRWXU
:文件拥有者具有可读、可写、可执行权限S_IRUSR
:文件拥有者具有可读权限S_IWUSR
:文件拥有者具有可写权限S_IXUSR
:文件拥有者具有可执行权限S_IRWXG
:同组用户具有可读、可写、可执行权限S_IRGRP
:同组用户具有可读权限S_IWGRP
:同组用户具有可写权限S_IXGRP
:同组用户具有可执行权限S_IRWXO
:其他用户具有可读、可写、可执行权限S_IROTH
:其他用户具有可读权限S_IWOTH
:其他用户具有可写权限S_IXOTH
:其他用户具有可执行权限S_IRWXUGO
:所有用户具有可读、可写、可执行权限S_IALLUGO
:所有用户具有全部权限S_IRUGO
:所有用户具有可读权限S_IWUGO
:所有用户具有可写权限S_IXUGO
:所有用户具有可执行权限module_param
函数中,通常使用如下的权限符:S_IRUGO
表示任何人都可以读取该参数(最常用)S_IRUGO|S_IWUSR
表示root用户可以修改该参数。但是当参数发生修改时,内核不会通知内核模块参数被修改,因此通常不使用。在加载时可以通过如下命令指定模块参数:
insmod xxx.ko port=value #例如:port=80
内核模块支持的加载参数列表如下:
bool
invbool
:关联int型,反转bool值,输入为 true
则传入为 false
。charp
:关联char*,内核会为用户提供的字符串分配内存并设置指针。int
long
short
uint
:关联unsigned int,u表示无符号。ulong
ushort
#include "linux/moduleparam.h"
static int udp_port = 8081;
// 相当于缺省参数
static int tcp_port = 8080;
/**
* module_param_named - typesafe helper for a renamed module/cmdline parameter
* @name: a valid C identifier which is the parameter name.
* @value: the actual lvalue to alter.
* @type: the type of the parameter
* @perm: visibility in sysfs.
*
* Usually it's a good idea to have variable names and user-exposed names the
* same, but that's harder if the variable must be non-static or is inside a
* structure. This allows exposure under a different name.
*/
module_param_named(port, tcp_port, int, S_IRUGO);
其中:
name
:在加载模块时使用的参数名。要求必须符合C语言标识符规范。value
:在源码中实际存储时使用的参数。随后在加载内核时使用:
insmod xxx.ko port=value #例如:port=80
进行加载。此时参数名为 name
中指定的参数而非 value
的名称。
在本质上,module_param(name, type, perm)
等价于 module_param_named(name, name, type, perm)
,在内核中定义如下:
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)
普通数组的传递方式如下:
#include "linux/moduleparam.h"
static int arr[10] = { 0 };
static int arr_size = 0;
/**
* 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.
*/
// module_param_array(name, type, nump, perm)
module_param_array(arr, int, &arr_size, S_IRUGO);
其中:
name
:存放数组的变量,并且为参数名type
:数组中元素的类型nump
:为存放实际接收元素数量的变量地址perm
:权限符上述示例调用如下:
insmod xxx.ko arr=value1,value2,...
注意:
#include "linux/moduleparam.h"
/**
* module_param_array_named - renamed parameter which is an array of some type
* @name: a valid C identifier which is the parameter name
* @array: 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
*
* This exposes a different name than the actual variable name. See
* module_param_named() for why this might be necessary.
*/
module_param_array_named(name, array, type, nump, perm)
其中:
name
:加载模块时使用的参数名array
:实际存放的数组位置type
:数组中元素的类型#include "linux/moduleparam.h"
/**
* 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).
*/
module_param_string(name, string, len, perm)
其中:
name
:加载模块时使用的参数名string
:实际存放的字符串len
:为最大的可存放的字符串大小,sizeof(string) - 1
。module_param
配合 charp
的方式,这样不用手动管理内存,也不用提前为字符串分配一个足够大的内存区域,且不需要在模块退出时手动释放这部分内存。使用:
#include "linux/moduleparam.h"
MODULE_PARM_DESC(_parm, desc);
进行描述,其中:
_parm
为要描述的参数名称desc
为参数信息随后使用 modinfo
即可查看模块参数提示信息。
int add_int(int a, int b)
{
return a + b;
}
// 导出符号
EXPORT_SYMBOL(add_int);
int add_int_gpl(int a, int b)
{
return a + b;
}
// 导出包含GPL许可证的符号
EXPORT_SYMBOL_GPL(add_int);
导出符号后的函数可以被其他内核模块使用,使用前只需要声明一下函数定义即可(通常是用引用头文件方式)。
注:
EXPORT_SYMBOL_GPL
导出的符号不可以被非GPL模块引用。cat /proc/kallsyms
即可查看导出符号表。模块许可证声明:
MODULE_LICENSE("GPL v2");
模块作者信息:
MODULE_AUTHOR("xxtech");
模块描述:
MODULE_DESCRIPTION("...");
模块版本:
MODULE_VERSION("V1.0");
模块别名:
MODULE_ALIAS("...");
static int var_name __initdata = 0;
上述 __initdata
和模块加载函数的 __init
都标识在内核被加载完毕后,其所定义的变量或函数会被从内存中扔掉。
2. 与上一条相似的是,内核中也有 __exitdata
与 __exit
。上述四个特性均会被放置到特殊ELF端。
在为内核编写模块时,一定要选择与目标系统相匹配的内核源码,否则生成的模块无法使用。
TODO: 更新dkms等无需原码的编译方式
TODO
Ubuntu可以使用 apt
和 git
两种方式获取源码,也可以使用当前系统配合主线版本内核进行编译。
使用apt获取源码:
直接搜索linux内核,并找出当前使用的内核包名
sudo apt search linux-image
返回结果为:
Sorting... Done
Full Text Search... Done
linux-image-generic-hwe-24.04/noble,now 6.8.0-31.31 amd64 [installed,automatic]
Generic Linux kernel image
则直接使用apt下载上述内核包的源码即可。
apt-get install linux-source-6.8.0
该内核源码会被下载到 /usr/src
目录,复制并解压即可。
使用git获取源码:
在获取源码之前,需要获取当前系统信息:
uname -a
Linux h13-VMware-Virtual-Platform 6.8.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 20 00:40:06 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
上述结果指明目前使用的内核版本为 6.8.0-31-generic
。
Ubuntu所使用的内核源码仓库页面为:https://kernel.ubuntu.com/git/
从上述页面中:
git clone https://git.launchpad.net/~canonical-kernel/ubuntu/+source/linux-intel/+git/noble
git checkout 7fdb45c9bbbc95a3300b4d8de3f751f4c05c98e2
# 先创建目录
mkdir noble
# 初始化git仓库
git init
# 连接到git仓库
git remote add origin https://git.launchpad.net/~canonical-kernel/ubuntu/+source/linux-intel/+git/noble
# 只clone目标版本
git fetch --depth 1 origin 7fdb45c9bbbc95a3300b4d8de3f751f4c05c98e2
# 将工作目录切换到目标commit的内容
git checkout FETCH_HEAD
只做内核驱动/模块开发建议使用后者,完整clone速度过慢。
以上述命令为例,截止2024年05月30日:
提取当前内核配置并使用主线内核代码:
主线代码获取方式略。在获取到主线内核代码后,可以使用如下方式获取当前系统的config:
cp -v /boot/config-$(uname -r) .config
本实例为一个基础普通模块的示例。
Linux已经在 lib/test_module.c
放置了一个基础的Hello World模块,其主要代码如下:
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
static int __init test_module_init(void)
{
pr_warn("Hello, world\n");
return 0;
}
module_init(test_module_init);
static void __exit test_module_exit(void)
{
pr_warn("Goodbye\n");
}
module_exit(test_module_exit);
MODULE_AUTHOR("Kees Cook <[email protected]>");
MODULE_LICENSE("GPL");
并在 lib/Makefile
中已经添加了如下代码:
obj-$(CONFIG_TEST_LKM) += test_module.o
经过搜索发现, CONFIG_TEST_LKM
在如下几个文件中被定义为 m
:
tools/testing/selftests/kmod/config
tools/testing/selftests/splice/config
obj-m += test_module.o
因此当需要编译自己的内核模块时,可以直接在Makefile中添加如下代码:
obj-m += xxx.o
因此可以直接在 lib
目录下直接建立一个新文件 hello_module.c
,并编写如下代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
static int __init hello_module_init(void)
{
printk("Hello module inited.\n");
return 0;
}
static void __exit hello_module_exit(void)
{
printk("Hello module exited.\n");
}
module_init(hello_module_init);
module_exit(hello_module_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("h13");
MODULE_DESCRIPTION("hello word module.");
MODULE_VERSION("V1.0");
注意 printk
需要以 \n
结尾,否则不会及时刷新。
在 lib/Makefile
的末尾增加如下代码:
# Hello world module
obj-m += hello_module.o
回到linux代码根目录,执行:
make modules -j8 #8线程
即可在 lib
路径下得到 hello_module.ko
,使用
使用 modinfo hello_module.ko
查看模块信息:
# modinfo hello_module.ko
filename: .../lib/hello_module.ko
version: V1.0
description: hello word module.
author: h13
license: GPL v2
srcversion: C6A36BCF3E2045DD1DE8E30
depends:
retpoline: Y
intree: Y
name: hello_module
vermagic: 6.8.1+ SMP preempt mod_unload modversions
注:
pr_fmt(fmt)
:pr_fmt应用及原理解析在上述结果中可以明显地发现即使在下载时内核源码版本匹配,但是编译出来的模块的vermagic仍不匹配。
因此应当直接修改所下载的内核源码中 include/linux/vermagic.h
文件的 vermagic
定义。首先使用 lsmod
列出当前内核已加载的模块,并随便选取一个已加载的模块使用 modinfo
查看其模块信息:
vermagic: 6.8.0-31-generic SMP preempt mod_unload modversions
然后查看 include/linux/vermagic.h
中的相关定义。
/* Simply sanity version stamp for modules. */
#ifdef CONFIG_SMP
#define MODULE_VERMAGIC_SMP "SMP "
#else
#define MODULE_VERMAGIC_SMP ""
#endif
#ifdef CONFIG_PREEMPT_BUILD
#define MODULE_VERMAGIC_PREEMPT "preempt "
#elif defined(CONFIG_PREEMPT_RT)
#define MODULE_VERMAGIC_PREEMPT "preempt_rt "
#else
#define MODULE_VERMAGIC_PREEMPT ""
#endif
#ifdef CONFIG_MODULE_UNLOAD
#define MODULE_VERMAGIC_MODULE_UNLOAD "mod_unload "
#else
#define MODULE_VERMAGIC_MODULE_UNLOAD ""
#endif
#ifdef CONFIG_MODVERSIONS
#define MODULE_VERMAGIC_MODVERSIONS "modversions "
#else
#define MODULE_VERMAGIC_MODVERSIONS ""
#endif
#ifdef RANDSTRUCT
#include <generated/randstruct_hash.h>
#define MODULE_RANDSTRUCT "RANDSTRUCT_" RANDSTRUCT_HASHED_SEED
#else
#define MODULE_RANDSTRUCT
#endif
#define VERMAGIC_STRING \
UTS_RELEASE " " \
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \
MODULE_ARCH_VERMAGIC \
MODULE_RANDSTRUCT
发现是 UTS_RELEASE
字段不匹配,其定义与 generated/utsrelease.h
中,是自动生成的头文件。
回到linux内核源代码根目录的 Makefile
文件,定位到两段关键代码:
VERSION = 6
PATCHLEVEL = 8
SUBLEVEL = 1
EXTRAVERSION =
NAME = Hurr durr I'ma ninja sloth
# Read KERNELRELEASE from include/config/kernel.release (if it exists)
KERNELRELEASE = $(call read-file, include/config/kernel.release)
KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
可发现将修改第一段的内核版本号即可,修改后如下。
VERSION = 6
PATCHLEVEL = 8
SUBLEVEL = 0
EXTRAVERSION = -31-generic
NAME = Hurr durr I'ma ninja sloth
最后的 +
号出现条件为:
LOCALVERSION
LOCALVERSION
为空即可。make LOCALVERSION= include/config/kernel.release
综上,当官方提供的内核源代码版本与实际内核不符时,应当:
Makefile
中的版本号。LOCALVERSION
为空,或删除 scripts/setlocalversion
中的加号(建议)。vermagic: 6.8.0-31-generic SMP preempt mod_unload modversions
与系统中已加载的模块信息一致,但是此时加载仍然报错则证明内核源代码不匹,应当考虑编译安装当前有源代码的内核。
make modules
make modules-install
make install
随后重启,通常不建议卸载原内核,当内核配置出错无法启动后,可以在引导处切换内核。
使用 dmesg
抓取当前日志:
dmesg
在原终端加载模块:
# 使用ismod或modprobe均可
# insmod hello_module.ko
# 但是推荐使用modprobe,因为modprobe会自动安装目标模块所依赖的模块,而ismod不能
modprobe hello_module.ko
无报错,再使用 dmesg
抓取当前日志,会发现新增日志:
[ 85.532987] Hello module inited.
使用 lsmod
也可以正常获取模块信息
lsmod | grep hello
hello_module 12288 0
卸载mod也可以正常输出日志
rmmod hello_module
[ 411.289281] Hello module exited.
再次使用 lsmod | grep hello
则无结果。
若抓取日志,会发现一个警告,是由于没有签名造成的:
dmesg | grep hello
[ 85.531743] hello_module: module verification failed: signature and/or required key missing - tainting kernel
可以选择在 Makefile
文件中添加如下配置,关闭强制签名。
CONFIG_MODULE_SIG=n
注:
cat /proc/kmsg
无输出,则可以使用 lsof /proc/kmsg
检查是否有进程在不停获取该文件输出dmesg
也无输出,则应当检查内核编译时是否开启 CONFIG_PRINTK
当内核模块 test_module.ko
由多个源文件 file1.c
、 file2.c
、 ...
构成时,Makefile中可以如下编写:
obj-m += test_module.o
module-objs += file1.o file2.o ...
用户态驱动的优点:
libusb
等。同时可以先在用户态完成大部分驱动开发与调试,随后移植到内核态。用户态驱动的缺点:
vm86
。mmap
映射 /dev/mem
才可以直接访问物理内存,但是需要对应权限(不一定需要root,可以配置访问权限)。ioperm
或 ioctl
才可以访问IO端口,但是并非所有平台都有此支持,性能较差且需要对应权限。mlock
或许可以缓解,但是用户态驱动通常会链接多个库,需要占用多个page。本章节的字符设备驱动程序将以一个简易的进程间管道通信为例。
基本设定:
可以使用 cat /proc/devices
查看当前系统中的设备列表,例如:
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
14 sound/midi
14 sound/dmmidi
21 sg
29 fb
89 i2c
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
...
Block devices:
2 fd
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
...
此外还有使用 ls
命令查看设备的方法(但是该方法无法查看未注册文件节点的设备,以及子目录设备,故不推荐):
在 /dev
目录下执行 ls -l
,可以得到如下结果:
...
drwxrwxrwt 2 root root 40 Jun 4 01:31 shm/
crw------- 1 root root 10, 231 Jun 4 01:31 snapshot
drwxr-xr-x 3 root root 200 Jun 4 01:31 snd/
brw-rw----+ 1 root cdrom 11, 0 Jun 4 01:31 sr0
lrwxrwxrwx 1 root root 15 Jun 4 01:31 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Jun 4 01:31 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Jun 4 01:31 stdout -> /proc/self/fd/1
crw-rw-rw- 1 root tty 5, 0 Jun 4 01:35 tty
crw--w---- 1 root tty 4, 0 Jun 4 01:31 tty0
crw--w---- 1 root tty 4, 1 Jun 4 01:31 tty1
crw--w---- 1 root tty 4, 10 Jun 4 01:31 tty10
...
在上述结果的各列对应关系如下:
| 权限 | 文件数 | 所有者 | 用户组 | 文件大小或设备数 | 修改日期 | 文件名 |
drwxrwxrwt 2 root root 40 Jun 4 01:31 shm/
crw------- 1 root root 10, 231 Jun 4 01:31 snapshot
在上述栏目中,列 权限
第一个字母及其对应关系如下:
在上述栏目中,列 文件大小或设备数
在文件类型不同时其意义也不同:
主设备号, 次设备号
在Linux 2.6.0到最新的Linux 6.10中, dev_t
被定义为一个 u32
类型,其:
dev_t dev = MKDEV(major, minor);
int major = MAJOR(dev);
int minor = MINOR(dev);
在Linux 2.5系列的修改中,设备编号从原先的8+8=16位变成了如今的32位。
分配设备号主要有两种方式,分别为静态分配和动态分配。
静态分配需要指定主设备号,主设备号的选择可以查看 Documentation/admin-guide/devices.txt
中的示例。
静态分配设备号的前提是要知道哪些设备号可用,再去指定想要申请的设备号。
查询空闲设备号需要读取 /proc/devices
文件未使用的设备号。
其主要的API有如下两个
// 该函数会从from下的从设备号作为起始从设备号,向from所指的主设备下连续申请count个设备
int register_chrdev_region(dev_t from, unsigned count, const char *name)
// 该函数会在指定的主设备号下注册一个从设备,此时要求主设备号大于0。
// 设备信息为该函数的第二、三个参数。
// 在此种用法下,函数的成功返回值为0,失败为负值。
// 该函数也可以用于动态分配设备号,见下一章节。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
上述函数的 struct file_operations
用于建立文件操作和驱动程序操作的连接。具体可见绑定文件操作章节。
动态分配不需要指定主设备号,根据 Documentation/admin-guide/devices.txt
中的说明,对于字符设备而言,动态分配的主设备号会从254开始向下分配直到234,分配完毕后再从511开始向下分配直到384。
为了考虑驱动在多台设备上的兼容性,避免冲突,应当优先考虑动态分配设备号。
// 该函数会从baseminor开始,向dev所指主设备下连续注册count个设备
// baseminor通常为0
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
// 该函数会自动,此时要求主设备号等于0。
// 设备信息为该函数的第二、三个参数。
// 在此种用法下,函数的成功返回值为0,失败为负值。
// 该函数也可以用于动态分配设备号,见下一章节。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
对于上述的所有分配设备编号的方法,均可以使用如下的函数进行释放:
void unregister_chrdev_region(dev_t from, unsigned count)
使用上述的注册设备号的方法仅会在系统中注册一个字符设备,但是并不会在文件系统中注册对应的设备。
device_create
函数的声明如下:
#include <linux/device.h>
__printf(5, 6) struct device *
device_create(const struct class *cls, struct device *parent, dev_t devt,
void *drvdata, const char *fmt, ...);
其中:
const struct class *cls
:设备所属类型,该类型需要自行定义。struct device *parent
:父设备指针,如果没有则设置为 NULL
。dev_t devt
:需要创建设备节点的设备号void *drvdata
:设备私有数据指针,传入后可使用 dev_set_drvdata
和 dev_get_drvdata
进行读写。const char *fmt
及其可变参数:用于构造设备名称struct device
指针。出现错误时返回 NULL
。device_destory
函数的声明为:
#include <linux/device.h>
void device_destroy(const struct class *cls, dev_t devt);
其中:
const struct class *cls
:设备所属类型dev_t devt
:需要销毁的设备节点的设备号如上述章节所述, struct file_operations
用于建立文件操作和驱动程序操作的连接,该数据结构的定义如下(Linux 6.10版本):
struct file_operations {
struct module *owner;
fop_flags_t fop_flags;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
void (*splice_eof)(struct file *file);
int (*setlease)(struct file *, int, struct file_lease **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
unsigned int poll_flags);
} __randomize_layout;
其主要成员及用法为:
owner |
不可 | 常用 THIS_MODULE |
用于存储提供该文件操作的模块的指针,避免模块在使用时被卸载。 |
fop_flags |
|||
llseek |
用于修改当前文件的读写位置,并返回修改后的新位置。 | ||
read |
可为空1 | 用于从设备中读取数据,返回值为读取的字节数。 在该函数的实现中,不需要驱动程序使用 file->fmode 鉴别是否可读写,此工作内核已完成。 |
|
aio_read |
可为空1 | 用于从设备中异步读取数据,数据读取完毕后调用参数指定的回调函数。 | |
write |
可为空1 | 用于写入数据,返回值为写入的字节数。 | |
aio_write |
可为空1 | 用于从设备中异步写入数据,数据写入完毕后调用参数指定的回调函数。 | |
readdir |
可为空 | 读取目录操作。对于文件类型,该参数应当为NULL。 | |
iopoll |
可为空 | 非阻塞IO多路复用模型的后端实现。用于查询请求某个事件/操作是否会被阻塞。 为NULL时则意味告诉内核请求该设备任何事件/操作都不会发生阻塞。 |
|
ioctl |
可为空1 | 为内核/用户提供该设备的特殊命令操作。 | |
mmap |
可为空1 | 将设备内存映射到进程地址空间。 | |
open |
可为空 | 如果该函数为NULL,则打开该设备的请求无论如何都会成功,且驱动程序不会知道本设备被打开。 | |
flush |
可为空 | 含义同用户态的 flush 。如果该函数为NULL,则内核会忽略用户的 flush 请求。flush会被在用户态的每次close时都会被调用一次。 (但是用户态的每次close不一定会触发release,见下例) |
|
release |
可为空 | 注意是没有close函数的,也就是该函数并不对应open函数。 有时file文件在一次打开后会被共享(如使用了 folk 、 dup 等),此时内核中维护的计数器+1,并且当计数器归0时才会真正调用release函数,但是每次关闭都会调用一次上方的flush函数。 该函数也是可以被设置为NULL的,效果与open相似。 |
|
fsync |
可为空1 | fsync 的后端调用。 |
|
aio_fsync |
可为空1 | fsync 的异步版本。 |
|
lock |
常为空 | 用于实现文件锁定。 | |
注:
NULL
时,则会告诉内核该设备不支持对应操作,当用户调用时会抛出错误。注意本章节所述的 file
类型与C标准库的 FILE
类型无任何关联。file
是内核提供的一个数据结构,不会暴露给用户态程序。而 FILE
是C语言标准库中所定义的,不会出现在内核态的代码中。
在内核中,struct file
指针通常被称为 file
或 filep
。struct file
数据结构的定义如下(Linux 6.10版本):
struct file {
union {
/* fput() uses task work when closing and freeing file (default). */
struct callback_head f_task_work;
/* fput() must use workqueue (most kernel threads). */
struct llist_node f_llist;
unsigned int f_iocb_flags;
};
/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
fmode_t f_mode;
atomic_long_t f_count;
struct mutex f_pos_lock;
loff_t f_pos;
unsigned int f_flags;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
其主要成员及其用法为:
f_lock |
|||
f_mode |
标志位,表示该文件是否是否可读或可写,由内核配置。 | ||
f_pos |
当前的读写位置。除上一章节 llseek 函数以外不应当直接修改该成员。 |
||
f_flags |
标志位,表示用户请求的属性(如只读、阻塞等)。 会被用户请求改变该标志位的值,内核不需要提前配置该标志位的值,内核只负责读取该值。 |
||
f_op |
不可 | 存储上一章节中所定义的 struct file_operations 。该成员允许随时修改,从而在需要的时候重载各种需要的操作(例如 /dev/null 文件)。 |
|
private_data |
主要用于跨系统调用时保存或传递相关信息,或存储驱动自身所需要保存的信息。 该指针所分配的资源需要在release方法中手动释放。 |
||
f_dentry |
文件对应的目录项结构,大多数设备文件无需关心。 |
inode
即Linux的混合索引分配的节点,不过设备文件通常不需要关心混合索引分配的文件系统管理相关的内容(电科爱考),因此需要关注的成员主要有如下两个:
i_rdev |
包含了上述的 dev_t 可以用于存储设备号等。 |
||
i_cdev |
字符设备的 struct cdev 类型。 |
由于Linux在2.5系列中修改了 dev_t
的位数,因此出于可移植性考虑,建议使用如下的接口获取设备号(而不是先获取 dev_t
再获取设备号):
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
通常来说(但不绝对),注册一个字符设备需要完成申请结构体空间、配置结构体、注册设备、反注册设备、回收空间几个任务。注意错误处理和资源回收即可。字符设备需要使用 cdev_t
,其定义于 <linux/cdev.h>
中。
需要注意的是字符设备使用的 cdev_t
有两种内存分配方式:
cdev_t
结构体:
cdev_t
结构体cdev_init
初始化该结构体,同时指定 file_operations
owner
cdev_add
添加字符设备cdev_t*
指针cdev_alloc
分配内存空间,此时无需再使用 cdev_init
初始化cdev
的 file_operations
owner
cdev_add
添加字符设备cdev_del
即可解除注册并可能回收内存。但是需要额外注意以下几点:cdev_init
初始化静态分配的结构体时,结构体的 kobj_type
会被注册为静态分配类型,并指定release函数为 cdev_default_release
。cdev_alloc
为指针分配内存时, kobj_type
会被注册为动态分配类型,并指定release函数为 cdev_dynamic_release
。cdev_del
解除注册,且无需担心动态内存回收问题。cdev_del
解除注册后,设备不一定会被立即移除,内核会等待用户停止使用该设备后才会移除。但是使用 cdev_del
解除注册后,内核模块不应当再调用该模块。static int __init mpipe_init(void)
{
int ret = 0;
// allocate char dev region.
ret = alloc_chrdev_region(&dev, 0, 1, "mpipe");
if(ret)
goto failed;
// allocate cdev
if((_cdev = cdev_alloc()) == NULL)
goto cdev_alloc_failed;
_cdev->ops = &fops;
_cdev->owner = THIS_MODULE;
// register a character device.
ret = cdev_add(_cdev, dev, 1);
if(ret)
goto cdev_add_failed;
cdev_add_failed:
cdev_del(_cdev);
cdev_alloc_failed:
unregister_chrdev_region(dev, 1);
failed:
return ret;
}
static void __exit mpipe_exit(void)
{
cdev_del(_cdev);
unregister_chrdev_region(dev, 1);
}
open应当完成如下任务:
file_operations
中的指针filep
中填写应当存放在 private_data
中的数据对应地,release应当完成如下任务:
filep
中寄存的数据需要注意的是,一旦用户态对某个文件发起close操作(即使close并未完成),该文件的其他所有操作均会被返回为错误信息。
read和write方法的函数指针如下所示:
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
需要注意的是:
char __user *
是用户空间的指针,在内核代码中永远不要直接操作它们,原因如下:
loff_t *
是长偏移类型的偏移量,是在文件中的偏移量,因此与内存中常用的 ssize_t
不同。*offp
所表示的文件位置。内核和用户之间的copy方法如下:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
上述两个函数在Linux 6.10中均由汇编实现。
read方法的返回值应符合如下几种情况:
count
。
count
时表示读取成功。count
时,在部分情况下程序可能会重复执行read直到凑够 count
,例如使用标准库的 fread
读取设备文件就会自动执行此流程。<linux/errno.h>
write和read方法的返回值及其注意事项相似。
在Linux中已对用户态提供了readv和writev两个批量读写的api。内核驱动可以自行选择是否实现批量版本的读写方法。若内核驱动不实现该方法,则readv和writev会自动在用户态多次调用对应的read和write方法进行实现。随之带来的问题就是执行一次readv或writev会多次切换CPU状态。
而对于用户而言,使用readv、writev而不是read、write的主要原因是前者可以批量复制位于不连续内存空间上的数据。
在Linux 2.6.18的 struct file_operations
中还保留有 readv
和 writev
的定义:
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
上述支持在Linux 2.6.19中已经删除。
内核中集成了很多可选的调试技术支持。这些调试技术或多或少的都会对内核的运行速度有所影响,因此通常发行版的Linux一般不会开启这些支持,这也是推荐使用自行编译的内核而非发行版内核的一个原因。
内核中的调试选项都在 menuconfig
下的 Kernel hacking
中,可选的调试选项如下:
TODO,用到再说
Linux内核驱动的多个函数在实际运行中均非独占运行,一个 open
或 read
函数均可能会被多个线程的多个不同的上下文同时调用,因此要做好并发环境下的临界区管理。
在内核程序中通常也会依靠信号量或互斥锁进行临界区访问管理,但是需要注意的是,内核态程序也会随时被切换为休眠状态,即使此时还处于内核的临界区内。在执行一些操作时被切换为休眠状态是一个非常危险的事情。而Linux内核也给出了允许进入休眠态的互斥锁和不允许进入休眠态的互斥锁。
内核程序是这样的,用户态的程序临界区管理只需要上互斥锁就可以,而内核程序需要考虑的就很多了,内核程序在占用互斥锁后退出临界区之前会不会随时被切换为休眠状态、在使用禁止休眠的锁机制的临界区内被调用的其他内核组件会不会被切换为休眠状态等,都是需要考虑的事情。
Linux内核所使用的信号量和互斥锁应当使用头文件 <asm/semaphore.h>
,其相关用法见如下几个子章节所述。
普通信号量的动态初始化可以用如下的函数进行:
void sema_init(struct semaphore *sem, int val);
该函数中的 val
为该信号量的初始值。
互斥锁可以按照如下的方式使用静态定义:
#include <asm/semaphore.h>
// 定义初始值为1的互斥量,即mutex
DECLARE_MUTEX(name);
// 定义初始值为0的互斥量,在获取该互斥量之前必须先显式的解锁
DECLARE_MUTEX_LOCK(name);
由于该宏的实际用途为定义,但命名却使用了 DECLARE
,因此在Linux 2.6.36之后就删除了该接口。
同样的,Linux也提供了互斥锁动态初始化的方法:
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
不过这两个函数也在之后的版本被移除。
互斥锁本质就是信号量的增加与减少。互斥锁的减少可以使用如下的函数:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
函数 down
会阻塞等待直至成功获取到对应的互斥量,且该操作不允许被用户空间中断。
函数 down_interruptible
也会互斥等待,但是允许等待该信号量的操作被用户空间程序中断。在其因用户空间的程序中断的时候该函数的返回值为非零值,且此时内核程序并未获得该互斥量。
函数 down_trylock
则永远不会休眠,成功获得互斥资源时返回值为0,其他情况为非0。
互斥资源操作时,在非必要情况下应当使用允许中断的版本。
注意上述的中断指的是可否被用户中断,而不是可否被阻塞休眠。不可休眠的是自旋锁。
互斥锁的增加直接使用如下函数即可:
void up(struct semaphore *sem);
读写锁与普通的信号量有些不同,因此其需要使用头文件 <linux/rwsem.h>
,其初始化方法与操作方法如下:
void init_rwsem(struct rw_semaphore *sem);
// 读者API
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
// 写者API
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
其中, downgrade_write
是将写者降级为读者。
在内核编程时常用的一个设计方法是在别的线程初始化某个活动,原线程会等待该活动结束后才会继续执行后续任务。尽管使用信号量就可以解决这个需求,但是内核也提供了更好的解决方案,即completion接口,头文件为 <linux/completion.h>
,用法如下:
// 静态初始化
DECLARE_COMPLETION(my_completion);
// 动态初始化
struct completion my_completion;
init_completion(&my_completion);
随后可以如下使用:
void wait_for_completion(struct completion *c);
void complete(struct completion *c);
void complete_all(struct completion *c);
这里将自旋锁与普通的互斥锁分开讨论的主要原因是由于自旋锁和互斥锁的特性(不可休眠)、实现、应用场景及约定均有所不同,因此在后续的讨论中会将轮询实现的自旋锁和信号量分开讨论。
在用户态,自旋锁主要用于避免快速上锁/释放锁的场景下线程进入阻塞状态后,因调度算法导致进程再次获得处理机所需要的较长时间。而无论内核态还是用户态,自旋锁是都用于避免被阻塞休眠的。在内核中,有些工作不能进入阻塞状态,例如中断处理程序等。
自旋锁的头文件位于 <linux/spinlock.h>
,用法如下:
// 静态初始化
spinlock_t lock = SPIN_LOCK_UNLOCKED;
// 动态初始化
void spin_lock_init(spinlock_t *lock);
自旋锁只是在申请和等待锁时不会被休眠,但是自旋锁并不负责保护临界区代码不会被休眠。并且在使用自旋锁时应当避免代码被休眠,否则极有可能引发死锁并引起内核崩溃,例如在中断服务函数中使用了一个自旋锁,但是该锁被其他进程持有,且该进程被中断服务抢占CPU时间,导致中断永远无法结束,进程也永远无法获得CPU时间从而导致死锁。
但是避免休眠很难做到,例如:
copy_form_user
kmalloc
因此自旋锁的操作除了如下的两个基础API:
void spin_lock(spinlock_t *lock);
void spin_unlock(spinlock_t *lock);
以外,还有相较高级一些的API:
// 获得自旋锁的函数
void spin_lock_irqsave(spinlock_t *lock, unsigned long long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
// 释放自旋锁的函数
void spin_unlock_irqsave(spinlock_t *lock, unsigned long long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
其中:
spin_lock_irqsave
会在获得自旋锁之前禁止中断,先前的状态会被保存于 flags
中。该函数是宏函数,因此 flags
会被修改。其本质调用为 flags = _raw_spin_lock_irqsave(lock);
spin_lock_irq
和上述函数的实际调用一致,只是忽略了需要保存的 flags
。spin_lock_bh
会关闭所有软件中断,但不会关闭硬中断。spin_unlock_irqsave
可以将由 spin_lock_irqsave
保存的 flags
恢复。还有非阻塞式的函数:
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
上述函数在成功获得自旋锁时会返回0,其他情况为非0。
自旋读写锁的头文件依旧位于 <linux/spinlock.h>
,其常用方法如下:
// 静态初始化
rwlock_t lock = RE_LOCK_UNLOCKED;
// 动态初始化
rwlock_t lock;
rwlock_init(&lock);
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqsave(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqsave(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
并发管理向来不是一个轻松的任务,并发及互斥锁的常见问题有如下几个章节:
在编写代码时,很容易会写出一个已经持有互斥锁的函数去调用另外一个需要持有互斥锁才能运行的函数,这种通常称为递归调用。而在上述给出的并发管理工具中并未给出类似于POSIX中的递归锁或可重入锁,因此在内核编程中应更加着重注意此类问题。通常的做法是在所有暴露给内核的API中做互斥管理,然后在这些API中所调用的内部实现均假设该互斥资源已被占有。
一个API需要同时获取多个互斥资源是一个很常见的事情,且非常容易触发操作系统中的类似于哲学家吃饭问题的死锁问题,通常的处理方式是一个内核程序统一按照一个有序的方式申请资源(即资源的有序分配法)。
并且,需要注意的是,在同时应当获得一个信号量和一个自旋锁时,必须优先获取信号量,随后再去申请自旋锁。因为持有自旋锁的进程被信号量阻塞是一件严重错误且非常危险的事情。
当然,最好的处理此类需要获取多个互斥资源的方法就是尽量避免这种现象。
关于互斥资源管理的粒度问题,可以直接以Linux对SMP的支持进行举例。
在第一个支持多处理器的Linux2.0,整个内核有且仅有一个巨大的临界区(BKL,Big Kernel Lock)。当需要操作内核临界区时都需要申请该互斥锁。而在后来的Linux2.2中,互斥锁管理的粒度逐渐细化,例如I/O子系统共用一个互斥锁,网络系统共用一个互斥锁等。到了现在的版本,Linux内核中已经拥有了上千个互斥锁。
互斥资源管理粒度的细化无疑会提升CPU资源的利用率,但是在实际的工程中,不应当过早的细化互斥资源的管理,因为项目最终的性能瓶颈往往不会出现在互斥资源的管理上。而混乱的互斥资源管理反而会导致更多严重问题的出现。
例如若要实现一个如下图所示的循环缓冲区,可以使用读写锁,仅使用原子变量表示读写index,并使用该原子index做到Berstein条件即可。
而原子操作对应的头文件为 <asm/atomic.h>
,常见的操作方法如下:
// 设置初始值
void atomic_set(atomic_t *v, int i);
// 可用于但不仅用于静态初始化
atomic_t v = ATOMIC_INIT(0);
// 读取
int atomic_read(atomic_t *v);
// 加减运算
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
// 自增自减
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
// 带判定是否为0的减法及自增自减方法,返回值仅有0或1,与 atomic_*_and_return 方法不同
// (注意不存在加法函数,即不存在 `atomic_add_and_test` 方法)
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
// 执行加法并判定是否为负,返回值仅有0或1
int atomic_add_negative(int i, atomic_t *v);
// 执行自增自减或加减并返回结果,与 atomic_*_and_test 不同
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
此外,内核还提供了一组原子位操作,可以用于操作非原子变量。这些原子位操作速度非常快,在架构支持的情况下仅需要一个机器指令就可以完成,则这些平台进行此类操作的时候就可以不关闭中断。但是其实现也高度依赖于具体平台,用于表述目标位的 nr
参数通常为 int
,但是有时会被定义为 unsigned long
;要修改的元素通常用 unsigned long *
来做指针,但在一些平台中会被定义为 void*
。其主要的API如下:
void set_bit(nr, void* addr);
void clear_bit(nr, void* addr);
void change_bit(nr, void* addr);
test_bit(nr, void* addr);
int test_and_set_bit(nr, void* addr);
int test_and_clear_bit(nr, void* addr);
int test_and_change_bit(nr, void* addr);
通常来说并不建议使用原子位操作,而建议改用自旋锁进行操作。
seqlock与信号量不同,其不需要任何阻塞操作。其通常用于处理读者写者问题,但是只能解决仅有一个写者的情况。
seqlock的使用与原理如下:
int seq = 0;
void reader()
{
int value = 0;
// 等待进入临界区,进入临界区条件为seq为偶数
while(seq % 2);
// 进入临界区
do {
if(seq_old != seq)
seq_old = seq;
value = Read();
} while(seq_old != seq);
}
void writer()
{
seq ++;
write();
seq ++;
}
在上述原理中,写者:
注意:不要用seqlock保护一个指针,因为指针在writer里面很可能会被修改为NULL等一旦访问就会触发错误的值。
seqlock的头文件位于 <linux/seqlock.h>
,使用方法如下:
初始化:
// 静态初始化
seqlock_t lock = SEQLOCK_UNLOCKED;
// 动态初始化
seqlock_t lock;
seqlock_init(&lock);
执行读取任务:
int reader()
{
unsigned int seq = 0;
do {
// 下一句可能会进行自旋操作,直到写者退出
seq = read_seqbegin(&lock);
// Do sth...
read();
} while read_seqretry(&lock, seq);
}
上述代码中的 read_seqbegin
、 read_seqretry
分别可以换成如下的任意版本:
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned long flags);
执行写入任务直接使用如下API即可
void write_seqlock(seqlock_t *lock);
void write_sequnlock(seqlock_t *lock);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);
void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
RCU方法是一个高级的互斥机制,尽管它很少的被用于驱动程序设计中。不过在路由表和Starmode的射频IP驱动仍使用了该方法。
RCU是一个支持一个写者和多个读者同时进行操作的一个互斥机制(但不意味着至多只有一个写者)。RCU适用于需要频繁的读取数据,而修改数据并不多的情景。例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。
RCU的基本流程:
读者:
写者:
资源回收:
注意,Linux的RCU并不依赖引用计数器,因为引用计数器本身的互斥保护也是一个麻烦的事情。在Linux中,RCU的基本规则是所有读者对该数据的访问均以原子操作进行,从而保证在进行读者的原子操作时不会被抢占。随后,当所有处理器都完成一次任务调度后(此时保证了所有读者完成对资源的读取)即可进行资源的清除工作。RCU的性能表现比自旋锁要好。
资源的清除工作通过预先设置的回调函数实现,通常该函数的工作内容就是调用 kfree
。
void call_rcu(struct rcu_head* head, void(*func)(void *arg), void *arg);
per-CPU变量如字面意思一样,是每个CPU都有一份实例的变量,per-CPU确保这些变量在每个CPU上独立且只能自己可见。若需要进行数据同步操作则需要额外的互斥等机制进行实现。
考虑如下场景:
那么per-CPU则有如下特性:
per-CPU变量的声明与定义:
#include "linux/percpu-defs.h"
// 声明未初始化的变量
DECLARE_PER_CPU(int, my_counter);
// 声明并初始化
DEFINE_PER_CPU(int, my_counter) = 0;
// 启用缓存行对齐避免伪共享(本例中直接定义了数组)
DEFINE_PER_CPU(int[4], my_array) __cacheline_aligned = { 0 };
#include "linux/percpu-defs.h"
int __percpu *dyn_counter = alloc_percpu(int);
// 操作per-CPU变量
*per_cpu_ptr(dyn_counter, smp_processor_id()) = 42;
free_percpu(dyn_counter);
在读写per-CPU变量时需要禁用抢占,关闭抢占后当前任务不会被其他任务抢占,确保当前任务只会在同一个CPU上运行,例如:
int cpu = get_cpu(); // 获取当前CPU ID并禁用抢占
per_cpu(my_counter, cpu)++; // 操作当前CPU的变量
put_cpu(); // 启用抢占
或者直接用隐含启停抢占的形式:
get_cpu_var(my_counter)++; // 操作当前CPU变量(自动禁用抢占)
put_cpu_var(my_counter); // 释放
上述接口的定义如下:
#define get_cpu() ({ preempt_disable(); __smp_processor_id(); })
#define put_cpu() preempt_enable()
/*
* Must be an lvalue. Since @var must be a simple identifier,
* we force a syntax error here if it isn't.
*/
#define get_cpu_var(var) \
(*({ \
preempt_disable(); \
this_cpu_ptr(&var); \
}))
/*
* The weird & is necessary because sparse considers (void)(var) to be
* a direct dereference of percpu variable (var).
*/
#define put_cpu_var(var) \
do { \
(void)&(var); \
preempt_enable(); \
} while (0)
其主要依靠 preempt_disable()
和 preempt_enable()
进行管理CPU抢占。
per-CPU变量需要注意如下的注意点:
在第5章讲解了如何编写或实现用户对设备的读取或写入请求,但是在真实的设备中往往还有例如弹出设备、修改波特率等非普通读写操作。而这些方法往往使用ioctl作为操作接口。
在用户空间中, ioctl
的函数原型如下:
int ioctl(int fd, unsigned long cmd, ...);
尽管在用户空间,该API的参数被定义为可变参数,但是在内核中并非如此。
查阅资料并分析可知,用户态的 ioctl
参数要求为:
ioctl
至多支持3个参数,因此可变参数的引用并没有引入更多的参数支持ioctl
支持2个参数的调用,这也是使用可变参数的原因在Linux 2.6.35及以前,内核的 ioctl
接口同时被定义为如下的接口:
int (*ioctl)(struct inode *inode, struct file *filep, unsigned int cmd, unsigned long arg);
long (*unlocked_ioctl) (struct file *, unsigned int cmd, unsigned long arg);
long (*compat_ioctl) (struct file *, unsigned int cmd, unsigned long arg);
在Linux 2.6.36及之后,内核的ioctl仅保留了如下的接口:
long (*unlocked_ioctl) (struct file *, unsigned int cmd, unsigned long arg);
long (*compat_ioctl) (struct file *, unsigned int cmd, unsigned long arg);
而在内核态,需要注意的是:
ioctl
至多支持3个参数,这样也和内核态的接口定义做到了匹配虽然老的 ioctl
接口已不再使用,但是还是要先从老的版本开始讲起。
如上文所述, ioctl
的接口定义如下:
int (*ioctl)(struct inode *inode, struct file *filep, unsigned int cmd, unsigned long arg);
上述接口中的参数:
struct inode *
和 struct file *
的定义与用法和Linux驱动开发笔记 > 5 8 open和release方法中的一致,内核负责将用户态的 ioctl
中指定的 fd
转化为这两个数据结构。cmd
则和用户态中传入的对应参数完全保持一致,而在用户态和内核态对该参数含义的约定也应当保持一致,通常用对应的头文件来实现。而内核态对 cmd
参数的处理通常可以使用 switch
方法。至于 cmd
参数的命令编号原则可见章节Linux驱动开发笔记 > 8.1.3 cmd命令编号。而返回值也应当从 <linux/errno.h>
中选值返回,常用的值如下:
-ENOTTY
:POSIX标准规定的指定的命令非法的返回值注意:老的ioctl会在获得大内核锁(BKL)的条件下运行,所以应当尽快处理并返回。
unsigned long
数据大小不同的情况:unsigned long
就是4Byte的整数类型,此时再传递给内核态驱动程序就可能出现错误。此外,更大的问题在于老的ioctl方法是由内核的大内核锁(BKL)实现并发控制的,驱动在ioctl中较长的占用处理器则会导致无关的进程会被该ioctl产生较长时间的系统调用延迟()。为了解决此问题(The new way of ioctl()),Linux在2.6.11开始引入了 unlocked_ioctl
和 compat_ioctl
,并在Linux 2.6.36彻底删除了 ioctl
接口。
正如上文所述,在64位系统上运行32位的用户态程序会导致传输传递时字节大小不匹配的问题。因此无论在32位还是64位的系统上都需要分别实现这两个函数。其调用情况为:
用户和内核位数关系 | ioct实际调用接口 | 理应的内核实现 |
---|---|---|
32位用户态调用32位内核驱动 | int32_t (*unlocked_ioctl) (struct file *, unsigned int cmd, uint32_t arg); |
|
64位用户态调用32位内核驱动 | 不存在此种情况 | |
64位用户态调用64位内核驱动 | unlocked_ioctl |
int64_t (*unlocked_ioctl) (struct file *, unsigned int cmd, uint64_t arg); |
32位用户态调用64位内核驱动 | compat_ioctl |
int32_t (*compat_ioctl) (struct file *, unsigned int cmd, uint32_t arg); |
几个注意点:
unlocked_ioctl
只是位宽不一样。compat_ioctl
意味"兼容性的ioctl"unlocked_ioctl
意味"解锁的ioctl",此函数不会为内核施加大内核锁,且此时需要驱动程序管理自己的互斥锁。按照规则,ioctl的cmd并不能随意编号,一个基本原则是命令号应当在整个系统范围内唯一。该原则的设定主要有如下考虑:
-EINVAL
错误,从而导致错误未被有效暴露。Linux的cmd命令编号原则应参考 Documentation/userspace-api/ioctl/ioctl-number.rst
(翻译可见ioctl-number.rst)。在定义cmd编号时,应当在头文件中参考上述规则使用 _IO(type,nr)
、 _IOR(type,nr,size)
、 _IOWR(type,nr,size)
等宏进行定义。
上述参数中:
type
应当填写该设备所拥有的类型序号,可能多个设备共用一个 type
,并按照 nr
的范围划分,可见ioctl-number.rstnr
应当在该设备所拥有的 type
的 nr
子区间内顺序递增size
不应当填写具体的size值,直接填写类型即可使用示例如下(cifs_ioctl.h
):
#define CIFS_IOCTL_MAGIC 0xCF
#define CIFS_IOC_COPYCHUNK_FILE _IOW(CIFS_IOCTL_MAGIC, 3, int)
#define CIFS_IOC_SET_INTEGRITY _IO(CIFS_IOCTL_MAGIC, 4)
#define CIFS_IOC_GET_MNT_INFO _IOR(CIFS_IOCTL_MAGIC, 5, struct smb_mnt_fs_info)
#define CIFS_ENUMERATE_SNAPSHOTS _IOR(CIFS_IOCTL_MAGIC, 6, struct smb_snapshot_array)
#define CIFS_QUERY_INFO _IOWR(CIFS_IOCTL_MAGIC, 7, struct smb_query_info)
#define CIFS_DUMP_KEY _IOWR(CIFS_IOCTL_MAGIC, 8, struct smb3_key_debug_info)
#define CIFS_IOC_NOTIFY _IOW(CIFS_IOCTL_MAGIC, 9, struct smb3_notify)
#define CIFS_DUMP_FULL_KEY _IOWR(CIFS_IOCTL_MAGIC, 10, struct smb3_full_key_debug_info)
#define CIFS_IOC_NOTIFY_INFO _IOWR(CIFS_IOCTL_MAGIC, 11, struct smb3_notify_info)
#define CIFS_IOC_GET_TCON_INFO _IOR(CIFS_IOCTL_MAGIC, 12, struct smb_mnt_tcon_info)
#define CIFS_IOC_SHUTDOWN _IOR('X', 125, __u32)
而对于上述的"编码宏",Linux也提供了对应的"解码宏":
_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)
除了自己在ioctl的接受函数里预定义的命令以外,Linux还内置了一些通用的预定义命令。这些命令会被Linux系统截断,并不会传递到内核驱动中。且应当注意自己的cmd命令编号不应当与系统的预定义命令冲突或重复。
预定义命令主要可以分为如下三组:
type
都是 T
。这些命令主要有:FIOCLEX
:设备执行时关闭标志,File ioctl close on exec。设置了这个标志后,当进程退出(即执行 exec()
系统调用)时,自动关闭该文件。FIONCLEX
:清除设备执行时关闭标志,主要用于撤销上述命令。FIOASYNC
:使能或关闭socket的异步模式。FIOQSIZE
:该命令在操作普通文件时会返回文件或目录的大小,但是用于操作设备时会返回 -ENOTTY
错误。FIONBIO
:允许或禁止一个文件(通常是Socket)的非阻塞模式。不过修改该标志位的方法通常是用 fnctl
使用 F_SETFL
命令完成。在ioctl传参时,应当注意传递的是实际参数还是指针。传递普通参数时正常实现即可,当传递指针时应当注意该用户空间的指针是否合法,其方法主要分为如下几类:
copy_from_user
或 copy_to_user
即可。int access_ok(int type, const void *addr, unsigned long size);
其中, type
可以选择 VERIFY_READ
或 VERIFY_WRITE
,参数 addr
指用户空间地址,参数 size
指要传输的字节数。注意, VERIFY_WRITE
是 VERIFY_READ
的超集。 access_ok
将返回0或1。
随后即可使用 <asm/uaccess.h>
中的无需检查的宏函数进行传输(预处理时展开):
__put_user(x, ptr)
: x
为内核态数据、 ptr
为用户指针__get_user(x, ptr)
: x
为内核态数据、 ptr
为用户指针access_ok
只负责检验该内存地址是否位于进程对应有权限访问的区域内access_ok
后可以使用Linux为常用的1、2、4、8字节类型提供的,尽管大部分代码并不需要使用 access_ok
进行访问。access_ok
)的宏函数(预处理时展开):
put_user(x, ptr)
: x
为内核态数据、 ptr
为用户指针get_user(x, ptr)
: x
为内核态数据、 ptr
为用户指针注意:2和3仅可用于sizeof为1、2、4、8字节大小的参数传递,其他类型无法使用。
宏函数内置的sizeof是对用户空间的指针所指对象大小进行计算(即 sizeof(*(ptr))
)
在驱动程序中,对设备的操作也应当分为不同的权限等级,例如读取硬盘、写入硬盘以及格式化硬盘本就不应当是同一组权限。而在Linux中也内置了一些权限,例如在 <linux/capability.h>
中预先定义了如下一些可能常用的权限:
CAP_DAC_OVERRIDE
:CAP_NET_ADMIN
:执行网络管理任务的权限,例如配置网络接口CAP_SYS_MODULE
:载入或者卸载内核模块的权限CAP_SYS_RAWIO
:执行裸IO的权限,例如直接使用I2C或者USB通信CAP_SYS_ADMIN
:CAP_SYS_TTY_CONFIG
:执行tty配置任务的权限<sys/sched.h>
中的如下API检查是否有对应权限:int capable(int capability);
对于不满足权限的请求可以返回 -EPREM
。
并非所有的设备都有必要使用ioctl这种控制方式,例如想要实现一个PWM控制的舵机驱动程序,大可不必使用ioctl实现这类控制,直接使用字符控制即可。例如若实现:
echo "90" > /dev/servo0
则比使用 ioctl
外加特定的头文件定义 ioctl
的 cmd
要方便且可移植。类似的例子还有使用 AT
指令配置调制解调器等设备。
当设备无法立即完成某请求时,常用的做法之一是实现一个阻塞型IO。
阻塞和休眠通常都是由等待一个事件或资源引起的,因此其等待的时间通常具有不可预测的特性。在进入休眠时,务必注意下列注意事项:
关于阻塞IO和非阻塞IO的相关定义与基础知识可以详见:IO模型 > 3 IO模型。
由于无论是否IO为阻塞型IO,read、write的行为必须和poll的行为保持同步,因此此处不详细说明阻塞IO模型下的read与write语义。
阻塞型IO的read与write语义应详见:
在Linux中的头文件 <linux/wait.h>
中提供了阻塞与休眠相关的API,其较为重要的使用方法如下:
// 静态定义
DECLARE_WAIT_QUEUE_HEAD(name);
// 动态定义
wait_queue_head_t q_head;
init_waitqueue_head(&q_head);
// 休眠API(注意都是宏函数)
/**
* @brief: 休眠直到condition为true
* @note: 每次wq_head被唤醒都会被执行并判定condition
*/
wait_event(wq_head, condition)
/**
* @brief: 休眠直到condition为true,支持系统被挂起期间冻结
*/
wait_event_freezable(wq_head, condition)
/**
* @brief: 休眠直到condition为true,或者超时(以jiffy表示)时中断休眠。
* @return:
* - 当超时后condition为true返回0
* - 当超时后condition为false返回1
* - 超时前condition为true则返回剩余jiffy数(大于等于1)
*/
wait_event_timeout(wq_head, condition, timeout)
/**
* @brief: 休眠直到condition为true。在执行等待前和后,cmd1和cmd2会分别被执行
* @return:
* - 当其被信号打断时返回值为非零
*/
wait_event_cmd(wq_head, condition, cmd1, cmd2)
/**
* @brief: 休眠直到condition为true,或者被信号中断休眠。当其被信号打断时返回值为非零。
* @return:
* - 当其被信号打断时返回值为非零
*/
wait_event_interruptible(wq_head, condition)
注:
condition
是一个不带副作用的 bool
表达式,该表达式可能会被多次求值。condition
的另一注意点见下方关键注意点。// wake_up会唤醒所有等在queue上的非独占休眠线程
wake_up(x)
// wake_up_interruptible只会唤醒可中断休眠的线程
wake_up_interruptible(x)
关键注意点:
wake_up
后,阻塞队列上的(非独占等待节点之前的)所有简单休眠的线程均会被唤醒并判定休眠条件 condition
。在简单休眠中,队列并没有实际的先后作用,队列的作用发生于Linux驱动开发笔记 > 独占等待中。wake_up
可能会唤醒多个简单休眠对象,因此 condition
的操作也必须是原子的。wake_up_interruptible()
唤醒所有普通休眠的线程,自然就包括了被poll类函数阻塞的线程。在上一章节的简单休眠中所使用的 wait_queue_head_t
的定义如下:
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
其是由一个自旋锁和链表组成。
而线程的休眠与恢复会影响 task_struct
中的 __state
标志位,该标志位有如下几个状态:
TASK_RUNNING
TASK_INTERRUPTIBLE
TASK_UNINTERRUPTIBLE
__TASK_STOPPED
__TASK_TRACED
linux/sched.h
,从而影响任务的调度。而以简单休眠章节中的最简单的休眠函数 wait_event
为例,来分析 wait_event
(宏)函数的具体定义:
/*
* The below macro ___wait_event() has an explicit shadow of the __ret
* variable when used from the wait_event_*() macros.
*
* This is so that both can use the ___wait_cond_timeout() construct
* to wrap the condition.
*
* The type inconsistency of the wait_event_*() __ret variable is also
* on purpose; we use long where we can return timeout values and int
* otherwise.
*/
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
for (;;) { \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
if (condition) \
break; \
\
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
cmd; \
} \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
#define __wait_event(wq_head, condition) \
(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
schedule())
/**
* wait_event - sleep until a condition gets true
* @wq_head: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
* @condition evaluates to true. The @condition is checked each time
* the waitqueue @wq_head is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*/
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0)
其定义和使用了三个宏,按照调用顺序依次为:
wait_event(wq_head, condition)
:
might_sleep
宏函数,被设计为一个注解,该函数会先后调用如下两个函数:
__might_sleep(__FILE__, __LINE__);
,在源码中定义为空函数。might_resched()
,即 int __cond_resched(void)
函数。__wait_event(wq_head, condition);
。__wait_event(wq_head, condition)
:
___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())
,即:
0
___wait_event
定义中的 cmd
部分)___wait_event(wq_head, condition, state, exclusive, ret, cmd)
:
wait_queue_entry
prepare_to_wait_event
将等待队列入口添加到等待队列中,同时设置现成的状态。cmd
中调用 shedule()
选择下一个要运行的进程,并进行上下文切换。正如章节简单休眠中所述,线程请求简单休眠后,一次 wake_up
可能会唤醒多个简单休眠对象,每一个被唤醒的多个简单休眠对象又会重新竞争 condition
资源,竞争不到的休眠对象又会被 schedule
重新切换到别的进程上,在一定程度上浪费了资源。
因此linux设计了一种一次只会唤醒一个休眠对象的等待,称为独占等待。
独占等待的特性:
wake_up
被调用时,其会一直唤醒第一个独占休眠以及之前的队列上的等待进程。独占等待的相关api为:
/**
* @brief: 独占等待直到condition为true。在执行等待前和后,cmd1和cmd2会分别被执行
* @return:
* - 当其被信号打断时返回值为非零
* @note:
* - 独占等待相关注意事项见上述段落
*/
wait_event_exclusive_cmd(wq_head, condition, cmd1, cmd2)
手工休眠主要工作是将内核提供的宏函数展开并按照需要的逻辑实现。
#TODO
/**
* @brief: 唤醒队列上的第一个独占等待的进程(如果存在)和所有非独占等待的进程
* @return:
* @note:
*/
wake_up(x)
/**
* @brief: 唤醒队列上的nr个独占等待的进程(如果存在)和所有非独占等待的进程
* @return:
* @note:
* - 当nr为0时是唤醒所有独占等待的进程,而非0个
*/
wake_up_nr(x, nr)
/**
* @brief: 唤醒队列上的所有进程
* @return:
* @note:
* - 本质上就是 wake_up_nr(x, 0)
*/
wake_up_all(x)
/**
* @brief:
* @return:
* @note:
*/
wake_up_locked(x)
wake_up_all_locked(x)
/**
* @brief:
* - 唤醒队列上的第一个独占等待的进程(如果存在)和所有非独占等待的进程,
* 并跳过所有不可中断休眠的进程
* @return:
* @note:
*/
wake_up_interruptible(x)
// 下面两个均为跳过所有不可中断休眠进程的版本。
wake_up_interruptible_nr(x, nr)
wake_up_interruptible_all(x)
/**
* @brief:
* - 同步版本的唤醒更倾向于在唤醒者进入休眠后,被唤醒者才会被执行;
* 而非普通版本的立即执行。
* - 在并发上通常用于调度者不希望自己立即被调度处理器时使用。
* - 性能上的优势在于控制了被唤醒者的执行顺序,减少了上下文切换的次数。
* @return: void
*/
wake_up_interruptible_sync(x)
学习本章前,务必学习IO模型。
除了阻塞IO之外,Linux支持用户程序以非阻塞IO打开/操作设备。
选择是否为非阻塞IO需要且仅能在 f_open
阶段进行设置。
// 只读、非阻塞
fd = open("path", O_RDONLY | O_NONBLOCK)
随后在内核模块中的后续操作时就应当根据是否定义了该非阻塞标志实现不同的行为。例如当应用程序请求的操作无法执行时,通常返回 -EAGAIN
(try it again)。
只有 read
、 write
和 open
文件操作会收非阻塞标志的影响。
Linux为IO多路复用模型提供了 select
、 poll
、 epoll
三种操作接口。但是这三种操作接口本质都是调用 file_operations
结构体中的 poll
函数指针实现的。
设备驱动程序中的 poll
函数声明为:
__poll_t poll(struct file *filep, struct poll_table_struct *wait);
尽管IO多路复用的实现机制较为复杂,但是在上述函数中只需要实现如下的基本语义:
0. 自己定义并管理一个等待队列 wait_queue_head_t
。
poll_wait
将当前线程塞回上述等待队列。wake_up_interruptible()
唤醒对应队列上的所有普通休眠进程。示例如下:
struct mpipe_dev {
// 读/写等待队列
wait_queue_head_t read_queue;
wait_queue_head_t write_queue;
// 读/写环形缓冲区
struct kfifo read_fifo;
struct kfifo write_fifo;
struct cdev cdev;
spinlock_t lock;
// ...
}
static int __init mpipe_init(void) { ... }
static void __exit mpipe_exit(void) { ... }
static __poll_t poll(struct file *filep, struct poll_table_struct *wait)
{
__poll_t mask = 0;
struct mpipe_dev *dev = file->private_data;
// 1. 不需要管具体的系统实现,只需要将等待队列插入到
poll_wait(filep, &dev->read_queue, wait);
poll_wait(filep, &dev->write_queue, wait);
// 2. 不用管用户具体在等待哪个,将设备是否可读、可写等全检查一遍并返回
if(!kfifo_is_empty(&dev->read_fifo)) {
// 读缓冲区非空, 可读
mask |= POLLIN | POLLRDNORM;
}
if(!kfifo_is_full(&dev->read_fifo)) {
// 写缓冲区非满, 可写
mask |= POLLOUT | POLLWRNORM;
}
return mask;
}
static int mpipe_write(struct file *filep, const char __user *, size_t, loff_t *)
{
struct mpipe_dev *dev = filep->private_data;
// ...
// 3. 当写入事件成功完成时, 如果缓冲区非空, 通知读队列开读。
if (!kfifo_is_empty(&dev->read_fifo))
wake_up_interruptible(&dev->read_queue);
return ...;
}
static int mpipe_read(struct file *filep, char __user *, size_t, loff_t *)
{
struct mpipe_dev *dev = filep->private_data;
// ...
// 3. 当读取事件成功完成时, 如果缓冲区非满, 通知写队列开写。
if (!kfifo_is_full(&dev->write_fifo))
wake_up_interruptible(&dev->write_queue);
return ...;
}
至于具体的 select
、 poll
、 epoll
的底层原理可参见Linux驱动开发笔记 > 8 7 2 select、poll、epoll的底层原理和数据结构。
简单的机制总结可见:Linux内核原理及其开发/应试笔记与八股 > ^uhjg4c。
上述的poll函数,除了用于为 select
、 poll
、 epoll
三种操作接口提供后端外,还需要保证在文件的不同状态时与 read
、 write
函数保持对应匹配的行为(即使当前文件被使用阻塞IO打开),因为对应用程序而言,poll等函数(select、epoll,下同)的返回结果必须保证能够明确接下来的read和write函数是否会遭到阻塞。
具体可见后续章节。
从设备读取数据时可以分为如下三种情况进行讨论:
-EAGAIN
。POLLHUP
。向设备写入数据时可以分为如下三种情况进行讨论:
-EAGAIN
。-ENOSPC
(无剩余空间),无论设备是否为阻塞IO。此外,还需要注意:
fsync
方法,fsync方法会阻塞直到缓冲区数据被写入设备,无论该IO是否被设置为阻塞IO。fsync
方法。fsync
函数的声明为:
int fsync(struct file *filep, loff_t start, loff_t end, int datasync);
其中:
struct file *filep
为指向文件的指针。loff_t start
:同步操作的起始位置,是一个64位的偏移量。loff_t end
:同步操作的结束位置,同样是一个64位的偏移量。int datasync
:用来指示同步操作的类型,如果 datasync
非零,则仅同步文件的数据部分,而不同步文件的元数据(如最后修改时间等)。如果为零,则同时同步数据和元数据。本章节所讲述的IO模型为信号驱动型IO。
用户态APP启用异步通知的基本步骤为:
sigaction
或 signal
注册需要监听的信号及其回调函数。通常推荐使用前者。// 1. 注册监听信号及其回调函数
// 这里使用signal只是因为使用起来比较简单和简洁,实际使用中应使用sigaction。
signal(SIGIO, &callback);
// 2. 设置文件所有者为当前进程
fcntl(fd, F_SETOWN, getpid());
// 3. 启用异步通知
// 获取该文件原先的文件状态标签,添加异步通知功能,再设置回去
f_flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_ASYNC);
// 4. 定义回调函数, 略
...
通常来说,用户应假设只有socket和terminal具有异步通知能力。
上述用户态操作对应的内核驱动程序的操作分别为:
为文件设置回调函数 | 内核会将 filep->f_owner 设置为目标PID。 |
|
为文件设置所有者 | ||
启用异步通知 | 只要当FASYNC标记被改变, 驱动程序的 fasync 操作被调用。 |
|
调用 close 操作 |
调用驱动程序的 fasync ,将该 struct file* 移除通知队列 |
fasync
函数的声明为:
int fasync(int fd, struct file *filp, int on);
内核中为了方便该函数的实现,提供了如下的辅助函数:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
:
fasync_struct
中添加或移除节点。当 on == 0
时移除, on == 1
时添加。fasync
函数中传来的一致,第四个参数为驱动程序所管理的 fasync_struct
链表。void kill_fasync(struct fasync_struct **fp, int sig, int band)
:
struct fasync_struct **fp
:驱动程序所管理的 fasync_struct
链表int sig
:驱动程序要发出的信号int band
:系统调用 sys_open
的内部处理详见:基础IO打开关闭(open.c) > ^qyilk5。
POSIX的用户态 close
语义可见:IEEE Std 1003.1™-2017 学习笔记 > ^s5pcbs。
系统调用 sys_close
的内部处理详见:基础IO打开关闭(open.c) > ^4b5xl4。
fd
文件号,从 fd table
中找到对应的内核中的文件对象 struct file
。系统调用 sys_read
的内部处理详见:基础IO读写(read_write.c) > ^4c1l78。
该Demo为一个简易的IPC管道( mpipe
),其具备如下基本特性:
/dev/mpipe
路径。mpipe
均有一个唯一的 mpipe_id
。mpipe
进行连接之前,所有的读写操作均不可正常进行。mpipe
连接时,双方必须知晓对方的 mpipe_id
。mpipe_id
的操作为非阻塞操作,只要 mpipe_id
合法即成功。mpipe_id
后,应当使用 ioctl
轮询连接状态。mpipe_id
均匹配,管道才会被成功连接。open
close
read
write
poll
类操作signal
、 sigaction
struct mpipe_info
)在内核驱动程序设计中,通常会使用 filep->private_data
使得驱动程序可以存储与用户态 fd
相绑定的数据信息。
在本驱动程序设计中,主要用于存储与单端管道相关的数据结构。
struct mpipe_list
)管道被禁用 | mpipe_info.target_mpipe_id == MPIPE_STATUS_DISABLED |
管道等待连接 | mpipe_info.target_mpipe_id == MPIPE_STATUS_DISCONNECTED |
管道已连接 | mpipe_info.partner_pipe != NULL |
针对上述结构体的操作可以考虑如下两种并发管理:
/drivers/char/ppdev.c
)在操作系统的学习中我们知道了死锁产生的四个必要条件以及死锁预防的方式:
而在上述的并发互斥管理中,需要在如下的情况下考虑死锁预防:
list_spin
)和各节点锁( mpipe_info.info_spin
)之间的循环等待。即保证先对链表锁加锁,才能对各节点锁加锁。mpipe_info.info_spin
)之间的循环等待。即保证现对ID小的基础信息结构体加锁,再对ID大的基础信息结构体加锁。如上述管道的基本特性,两个管道之间要建立连接需要其目标 mpipe_id
均为对方的 mpipe_id
。则需要考虑如下的几个场景:
mpipe_id
,则设置该目标id,同时检测对方的目标id是否为当前管道。
由于本子章节并不针对于上述假象设备 mpipe
,因此本字章节的设备名均命名为 mdev
。
在内核中的驱动正在响应用户的操作请求的过程中, f_op->release
被调用,那么仍在处理的操作请求如何完成?
f_op->release
中释放 struct mdev_info
将无法保证正在执行的操作会被顺利完成,例如下方代码中,在完成 mdev_read
中的 do_operation1
后被下处理器,并完成 mdev_release
函数,随后 do_operation2
就会触发错误:static ssize_t mdev_read(struct file *filep, char __user *buf, size_t count, loff_t *ppos)
{
// 1. 获取info数据结构
struct mdev_info *mdev_info_p = filep->private_data;
// 2. 执行操作1
do_operation1(mdev_info_p);
// 3. 执行操作2
do_operation2(mdev_info_p);
return ...;
}
static int mdev_release(struct inode *node, struct file *filep)
{
// [错误示范] 直接释放相关数据结构
// 1. 获取info数据结构
struct mdev_info *mdev_info_p = filep->private_data;
// 2. 回收数据结构
kfree(mdev_info_p);
return 0;
}
free
操作:struct mdev_info {
// ...
// ref counter for mdev_info, used to complete incomplete requests.
atomic_t ref_counts;
};
/**
* @brief: add references to mdev_info.
*/
static void mdev_info_get(struct mdev_info *info)
{
atomic_inc(&info->ref_counts);
}
/**
* @brief: remove the reference to mpipe_info, andelease the memory space
* when the last reference is released.
*/
static void mdev_info_put(struct mdev_info *info)
{
if(atomic_dec_and_test(&info->ref_counts))
{
vfree(info);
}
}
static int mdev_open(struct inode *node, struct file *filep)
{
//...
// success.
// 增加对该结构体的引用计数
mdev_info_get(mdev_info);
return 0;
}
static ssize_t mdev_read(struct file *filep, char __user *buf, size_t count, loff_t *ppos)
{
// 1. 获取info数据结构
struct mdev_info *mdev_info_p = filep->private_data;
// 2. 增加引用计数器
mdev_info_get(mdev_info);
// 3. 执行操作1
do_operation1(mdev_info_p);
// 4. 执行操作2
do_operation2(mdev_info_p);
// 5. 解除引用奇数
mdev_info_put(mdev_info);
return ...;
}
static int mdev_release(struct inode *node, struct file *filep)
{
// [错误示范] 普通计数器
// 1. 获取info数据结构
struct mdev_info *mdev_info_p = filep->private_data;
// 2. 解除驱动对该info的引用计数
mdev_info_put(mdev_info_p);
return 0;
}
mdev_read
的 struct mdev_info *mdev_info_p = filep->private_data;
处被下处理器,此时引用计数器并未增加。mdev_release
的执行。此时引用计数器被成功归0, struct mdev_info
被成功释放。mdev_read
继续执行,此时计数器为-1,且需要访问的 struct mdev_info
已被清空。错误发生。针对这个问题,则可以考虑使用引用计数器管理 mdev_info
解决,详见相关API。
则最终Demo程序如下:
其用户态测试程序为:
内核中会定义一个硬件的时钟中断,该中断频率通常为1000hz(软件仿真器中是24hz),该值不为固定值,可以在编译内核时设置,因此进行内核开发时不应当指定固定值,而应当通过API来获取。且在修改内核中断频率后,必须重新编译并使用新的内核模块。
该值可在内核态代码中访问,其被定义在 <linux/param.h>
中的 HZ
中。
内核中的变量 jiffies_64
在系统启动时会被置0,并当上述时钟中断发生时计数器会+1。该计数器在32位和64位系统中均为64位。
在实际开发中,应当使用 jiffies
变量,其为 unsigned long
类型,其具体位数可见2.2.1.1 不同架构处理器下的数据类型大小。在64位处理器上这两个变量其实是同一个,对该变量的访问也是原子的。在32位处理器上,该值是 jiffies_64
的低32位,且访问非原子。因此:
jiffies_64
应使用统一的接口:#include <linux/jiffies.h>
u64 get_jiffies_64(void);
jiffies
时可以直接访问。在32位平台下,1000hz中断时, jiffies
大约50天会溢出一次。但是内核态代码仍然应当谨慎处理该问题。
而想只用一个32位的计数器完全从根本上解决溢出问题是不现实的(例如32位的 jiffies_a
于数百天以前记录,此时和当前 jiffies
比较大小或者计算时间差),因此其被如下设计:
jiffies
应当至少可以满足比较至多数秒或数天间隔的时间戳。jiffies_64
。#include <linux/jiffies.h>
// 宏函数, 当a比b靠后时返回真, 表达式为((long)(b) - (long)(a) < 0))
int time_after(unsigned long a, unsigned long b);
// 宏函数, 当a比b靠前时返回真, 表达式为((long)(a) - (long)(b) < 0))
int time_before(unsigned long a, unsigned long b);
// 宏函数, 当a比b靠后或相等时返回真, 表达式为((long)(b) - (long)(a) <= 0))
int time_after_eq(unsigned long a, unsigned long b);
// 宏函数, 当a比b靠前或相等时返回真, 表达式为((long)(a) - (long)(b) <= 0))
int time_before_eq(unsigned long a, unsigned long b);
其原理为将 a
和 b
强制转换为 signed long
后进行比较,本质依赖负数补码原则,以char为例,其无符号和有符号之间的强制转换关系如下:
unsigned long |
signed long |
---|---|
0 | 0 |
1 | 1 |
2 | 2 |
3 | 3 |
... | ... |
127 | 127 |
128 | -128 |
129 | -127 |
130 | -126 |
... | ... |
254 | -2 |
255 | -1 |
0 | 0 |
1 | 1 |
例如:
a = 254
,随后计数器自增溢出到 1
时b取值,此时运算 (char)(b) - (char)(a) = 1 - (-2) = 3
,可以正常运算和判定先后。a
、 b
时差超过半个计数周期并发生溢出时,运算就不再准确:
a = 254
b = 128
(char)(b) - (char)(a) = -128 - (-2) = -126
判定不再准确#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
linux内核为所有平台都提供了访问CPU时钟周期数的API,在支持的平台上会返回正确宽度的 cycles_t
,但是在不支持的平台上返回结果恒为0。
#include <linux/timex.h>
cycles_t get_cycles(void);
在Pentium后的所有x86和x86_64的CPU上均会有一个TSC计数器(Timestamp counter),该计数器会记录CPU的时钟周期数,可以通过如下方式访问:
#include <asm/msr.h>
// 宏, 将64位的计数器原子地读取到两个32位的变量中
rdtsc(low32, high32);
// 宏, 只原子地读取低32位
rdtscl(low32);
// 宏, 将64位计数器原子地读取到一个64位变量中
rdtscll(var64);
在大多数情况下,内核只需要处理 jiffies
就可以满足需求,并且通常会将年月日时分秒的处理留给用户空间进行处理,但是内核依旧提供了一些绝对时间的处理方法。
#include <linux/time.h>
unsigned long mktime(unsigned int year, unsigned int mon,
unsigned int day, unsigned int hour,
unsigned int min, unsigned int sec);
#include <linux/time.h>
// struct timeval可以记录微秒(ms)级别的时间
// 且在许多平台上该API也有微秒级别的精度
void do_gettimeofday(struct timeval *tv);
#include <linux/time.h>
// struct timespec可以记录纳秒(ns)级别的时间
// 但是实际上也只有时钟滴答的精度(jiffies)
struct timespec current_kernel_time(void);
在考虑使用延迟执行时,需要根据延迟时间分别考虑使用的API:
jiffies
),且可以以一个时钟滴答为单位(通常为若干毫秒)进行延迟时,此时应优先考虑使用 jiffies
等待。直接使用:
while(time_before(jiffies, target))
cpu_relax();
即可,需要注意的是:
jiffies
永远无法得到更新,直接陷入死机。cpu_relax()
会做出不同的行为(例如在支持超线程的CPU上会让出处理器给其他线程),但是行为具体做了什么不重要,因为不推荐使用此方式。while(time_before(jiffies, target))
shedule();
该方法在大多数情况下并没有什么问题,但依旧不是最好的解决方案。当系统中有别的可运行进程时,该代码可以正常让出处理器;但是当系统中仅剩这一个可运行进程或低负荷情况下, shedule()
并不能有效地让出处理器,此时又变成忙等状态。
直接使用休眠章节提到的 wait_event_*timeout
函数即可,API如下:
#include <linux/wait.h>
wait_queue_head_t wait;
/**
* @brief: 休眠直到condition为true,或者超时(以jiffy表示)时中断休眠。
* @return:
* - 当超时后condition为true返回0
* - 当超时后condition为false返回1
* - 超时前condition为true则返回剩余jiffy数(大于等于1)
*/
wait_event_timeout(wq_head, condition, timeout)
/**
* @brief: 休眠直到condition为true,或者被信号中断休眠,或者超时(以jiffy表示)时中断休眠。当其被信号打断时返回值为非零。
* @return:
* - 当超时后condition为true返回0
* - 当超时后condition为false返回1
* - 超时前condition为true则返回剩余jiffy数(大于等于1)
*/
wait_event_interruptible_timeout(wq_head, condition, timeout)
在使用上述API进行等待时只需要把 condition
填写为 0
并等待超时即可。
本方法与上述让出处理器的方法的区别仅在于本方法的API还额外设置了任务状态( TASK_INTERRUPTIBLE
/ TASK_UNINTERRUPTIBLE
)。
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述函数的实际实现包含在 <asm/delay.h>
中,且除了udelay以外的两个函数可能并未被定义。而 udelay
一定会被提供并且其最终的延迟时间一定可以达到目标延迟时间或更长。
此外,内核还未 udelay
和 ndelay
中能接收的参数值添加了上限,当值过大时会提示 __bad_udelay
。
注意:上述三个函数均为忙等待函数,非忙等待函数为如下几个函数:
#include <linux/delay.h>
// 可以被中断的休眠,当进程被提前唤醒时,其返回值为提前唤醒的毫秒数。
unsigned long msleep_interruptible(unsigned int millisecs);
// 非忙等,但是也不可中断休眠
void ssleep(unsigned int seconds);
内核定时器通常有如下的典型应用场景(实际上并不局限于此):
fops->close
等操作时,使用内核定时器异步完成。此外内核还提供了一些API可以用于查询当前上下文的状态,API及不同上下文的注意事项可见当前上下文状态与注意事项。
内核定时器的几个重要特性:
timer_list
结构都会在运行之前从活动定时器链表中移走。内核定时器相关API如下:
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
`` - 其中,上述结构体中: -
expires表示期望定时器执行时的
jiffies值。 -
function为抵达
jiffies值时被调用的函数。 -
function被调用时的参数为该定时器的指针,即内核调用时为
timer.function(&timer)` 。
timer.h
):#define DEFINE_TIMER(_name, _function)
struct timer_list _name =
__TIMER_INITIALIZER(_function, 0)
`` - 在调用时,使用
DEFINE_TIMER定义定时器变量名并传入目标函数即可。 - 随后需要使用
add_timer` 将该定时器加入到内核中。
timer.h
):#define timer_setup_on_stack(timer, callback, flags)
__init_timer_on_stack((timer), (callback), (flags))
`` - 通常的定时器使用
DEFINE_TIMER和
timer_setup即可。 - 如需使用栈上定时器则需要使用
timer_setup_on_stack,且有如下的注意事项: - 栈上定时器的生命周期受限于当前函数栈帧,必须在函数返回前删除并释放定时器,仅适用于短期定时操作。 - 释放栈上定时器的操作为
destroy_timer_on_stack,在某些内核配置模式下,未调用该释放操作可能会导致内存泄漏。 - 上述两个函数的返回值为
void。 - 随后需要使用
add_timer` 将该定时器加入到内核中。
C void add_timer(struct timer_list *timer); void add_timer_on(struct timer_list *timer, int cpu);
add_timer*
函数将定时器加入到内核中,其中:add_timer
会将定时器加入到当前CPU中。add_timer_on
会将定时器加入到指定CPU中,可以减少跨CPU的中断和上下文切换开销(例如实现NUMA架构下的局部性优化)。C int mod_timer(struct timer_list *timer, unsigned long expires); int mod_timer_pending(struct timer_list *timer, unsigned long expires);
C int del_timer(struct timer_list *timer); int del_timer_sync(struct timer_list *timer);
del_timer_sync
可以确保该函数在返回时没有任何CPU在运行定时器的回调函数,在SMP系统上可以用于避免竞态等。C int timer_pending(const struct timer_list * timer);
1 if the timer is pending, 0 if not.
tasklet机制即小任务机制,其特性有:
tasklet其最根本的功能是可以将部分或后续操作异步的进行处理,其常用使用流程:
tasklet_struct
的变量。tasklet_schedule
将回调函数中的操作放入异步处理。经典例子:
void my_tasklet_handler(unsigned long data) {
// 在中断外完成中断处理的后半部分...
}
// 某中断处理函数
static irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
// 快速读取设备状态寄存器(关键操作)
device_status = readl(DEVICE_STATUS_REG);
printk(KERN_INFO "Interrupt handled on CPU %d: Device status = 0x%x\n",
smp_processor_id(), device_status);
// 调度 tasklet 处理非关键操作...
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
tasklet相关API如下(头文件 linux/interrupt.h
):
C struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; bool use_callback; union { void (*func)(unsigned long data); void (*callback)(struct tasklet_struct *t); }; unsigned long data; };
C void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
C void tasklet_schedule(struct tasklet_struct *t);
C void tasklet_disable(struct tasklet_struct *t); void tasklet_disable_nosync(struct tasklet_struct *t);
tasklet_disable
在该tasklet被执行时调用会忙等直到tasklet退出。tasklet_disable_nosync
在该tasklet被执行时调用操作无效,并且不会忙等。C void tasklet_enable(struct tasklet_struct *t);
workqueue和tasklet都是内核的一种异步执行机制,其区别如下表所示。
运行环境 | "内核线程"的上下文 | 中断上下文 |
环境限制 | 无,允许休眠,可以不原子化。 但由于其在"内核线程"的上下文中,故不可访问用户空间。 |
要遵守中断限制,如禁止休眠等。 |
多次执行 | 可以多次执行,每次执行之前调用 schedule_work() |
可以多次执行,每次执行之前调用 tasklet_schedule |
抢占 | 执行时可以抢占,可以长时间占用 | 不可抢占,尽快退出 |
并发性 | 使用多线程workqueue时可以在多个CPU上并行执行 | 同一个tasklet不会在多个CPU上同时执行 |
延迟 | 延迟高 | 延迟很低,实时性高 |
延迟控制 | 可以做到指定时间的延迟 | 不可指定延迟 |
如上表所示,workqueue是在"内核线程"中执行的,具体来说其可在如下两种工作队列中执行:
workqueue在创建时,可以选择:
queue_work
)使用的CPU。需要注意:// 静态定义
#define DECLARE_WORK(name, void (*function)(void*))
// 动态定义
#define INIT_WORK(_work, _func)
#include <linux/workqueue.h>
// 下方两个函数已被宏定义到alloc_workqueue。下方为宏替换后的函数原型,并非实际定义
// 为该队列在每个CPU上都创建一个专属的内核线程
struct workqueue_struct *create_workqueue(const char *name);
// 仅创建一个内核线程
struct workqueue_struct *create_singlethread_workqueue(const char *name);
// 提交并指定延迟,延迟单位为jiffies
bool queue_delayed_work(struct workqueue_struct wq,
struct delayed_work dwork,
unsigned long delay);
`` - 其中: - <span style="background:#fff88f"><font color="#c00000">返回值</font></span>: - 当任务成功加入队列时返回
true - 如果工作项已经在队列中(即重复提交时)返回
false - 内存顺序保证: - 如果
queue_work 返回
true,则在
queue_work 调用之前的所有内存写操作(
stores )对执行
work 的CPU是可见的。 - 即在
work 执行时,可以安全地访问在
queue_work` 之前写入的数据。
C bool cancel_delayed_work(struct delayed_work *dwork);
true
flush_workqueue(wq);
C void destroy_workqueue(struct workqueue_struct *wq);
flush_workqueue
阻塞并等待队列中所有任务完成。flush_work
阻塞并等待某一个任务完成。并不是所有的内核模块都有独立管理一个等待队列的必要,因此内核提供了如下几种共享队列:
system_wq
:普通优先级的工作队列(默认)。system_highpri_wq
:高优先级的工作队列。system_long_wq
:适用于执行时间较长的任务。而对于共享队列来说,其有如下的特性:
create_workqueue
或 destroy_workqueue
管理队列。其拥有如下的API:
// 提交工作,返回值意义同上一章节
bool schedule_work(struct work_struct *work);
// 提交工作并指定运行的CPU
bool schedule_work_on(int cpu, struct work_struct *work);
// 提交工作并指定延迟,延迟单位为jiffies
bool schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay);
// 提交工作并指定延迟和所运行的CPU
bool schedule_delayed_work_on(int cpu, struct delayed_work *dwork,
unsigned long delay)
Linux内核的内存分配存在如下的层次结构:
alloc_pages
接口。Linux的内存区段划分取决于具体的硬件平台,可以使用如下命令查询:
cat /proc/buddyinfo
一个demo结果为:
root@VM-4-7-ubuntu:/proc# cat buddyinfo
Node 0, zone DMA 9 18 13 8 12 4 3 5 3 0 0
Node 0, zone DMA32 10175 2398 642 372 107 27 4 2 0 5 1
注:wsl中通常没有该文件。
在x86上,Linux内存区段被划分为如下三个区段:
ZONE_DMA
:物理地址 0x00000000
到 0x00FFFFFF
(0~16 MB),专供老式ISA设备使用。ZONE_NORMAL
:物理地址 0x01000000
到 0x07FFFFFF
(16 MB ~ 896 MB),内核可直接线性映射到虚拟地址空间的区域。ZONE_HIGHMEM
:物理地址 0x08000000
及以上(高于896 MB),供用户空间程序使用,需动态映射到内核空间。如上述章节,伙伴系统负责以page为单位对物理内存进行分割,则当内核需要分配大块的内存时,直接使用伙伴系统是最优选择。其主要提供了如下的API(均位于 linux/gfp.h
):
alloc_pages(gfp_t gfp_mask, unsigned int order)
:
struct page*
),可直接用于底层内存操作。__get_free_pages(gfp_t gfp_mask, unsigned int order)
:
alloc_pages
类似,但返回物理内存的起始虚拟地址(内核逻辑地址)。free_pages(unsigned long addr, unsigned int order)
:
get_zeroed_page(gfp_t gfp_mask)
:order
为要申请或施放的页面数的 gfp_mask
可见kmalloc接口开发调用。slab基于buddy system实现了如下的两种缓存:
struct task_struct
)频繁分配设计(通过 kmem_cache_create
创建)。kmalloc
通过它们实现内存分配。正如上述所属的两种缓存,当:
inode
对象)。kmalloc
分别可能造成如下问题:kmalloc
会比直接使用slab多造成一些花销在终端中输入如下命令即可查看slab的使用情况:
cat /proc/slabinfo
一个输出demo:
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
nf_conntrack 197 264 320 12 1 : tunables 0 0 0 : slabdata 22 22 0
au_finfo 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0
slab在开发中通常按照如下的方式进行调用:
kmem_cache_create
创建专用slab缓存,函数原型等价于(但不等于):// 新版本
void kmem_cache_create(const char name, size_t size, slab_flags_t flag);
上述函数实际使用宏函数实现,依赖于C11的 `_Generic()` 特性同时兼容新旧版本。 其中: - `name` :为缓存名称,用于 `/proc/slabinfo` 和调试信息中标识,命名应有唯一性。<font color="#c00000">该参数应当使用静态存储</font>,通常直接取字符串。 - `size` :每个slab对象的大小,应使用 `sizeof(obj)` 获取。 - `flag` :控制如何完成分配,可用值如下: - `SLAB_NO_REAP` :禁止内核在内存不足时主动回收(Reap)该Slab缓存的空闲内存页,<font color="#c00000">通常不用</font>。在SLUB实现中已经弃用,是早期Linux的设计。 - `SLAB_HWCACHE_ALIGN` :对齐到硬件缓存行大小,保证单个CPU一次只能读取一个对象,减少跨核心的缓存行无效化。在实际开发中应当使用一些工具验证该标志位的真实收益,避免盲目使用导致的内存开销。 - `SLAB_CACHE_DMA` :要求从DMA内存区段分配。 - `ctor` :对象的构造函数,初始化用,可为 `NULL` 。 2. 使用 `kmem_cache_alloc` 从高速缓存中分配内存:
C
void kmem_cache_alloc(struct kmem_cache cachep, gfp_t flags);
3. 使用 `kmem_cache_free` 回收内存:
C
void kmem_cache_free(struct kmem_cache s, void objp);
4. 使用 `kmem_cache_destroy` 销毁slab缓存:
C
void kmem_cache_destroy(struct kmem_cache *s);
```
特性:
kmalloc
申请的内存需要使用 kfree
。函数原型(但实际上并非如此):
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t gfp);
其中分配标志 gfp
主要分为"分配优先级"和"分配选项"两类,这两类之间可以使用或运算结合配置。
分配优先级有:
GFP_NOWAIT
:在内核空间中分配内存,不会引起休眠。GFP_ATOMIC
:原子地分配内存,不会引起休眠,可能会使用紧急内存池。通常在中断中使用。GFP_KERNEL
:在内核空间中分配内存,可能休眠。GFP_USER
:在内核中为用户空间相关操作分配内存(例如 mmap
),可能休眠,且该内存用户可见。GFP_HIGHUSER
:类似于 GFP_USER
,但是优先分配高端内存。GFP_NOIO
:类似于GFP_KERNEL
,但是禁止I/O代码初始化。GFP_NOFS
:类似于GFP_KERNEL
,但是禁止文件系统调用。__GFP_ZERO
:分配并清空内存空间。__GFP_DMA
:分配可DMA区段中的内存。__GFP_HIGHMEM
:分配高端内存,且可能使用紧急内存池。__GFP_COLD
:从冷页面页表中进行内存分配,这部分内存所在页面没有被频繁访问,甚至已经被swap到硬盘。可以用于分配不经常使用的内存。在不使用该标志时,会优先分配热缓存页面。__GFP_NOWARN
:避免在kmalloc中使用printk。__GFP_HIGH
:表示高优先级请求,在紧急情况下使用,允许使用内核预留的内存页面。__GFP_REPEAT
:进行有限次数的重试。__GFP_NOFAIL
:无限重试直到分配成功。慎重使用。__GFP_NORETRY
:若请求的内存不可得则应当立即返回,使用该标志位可以减少休眠。在终端中输入如下命令即可查询 kmalloc
的使用情况:
cat /proc/slabinfo | grep kmalloc
正如上文所述,kmalloc
是基于slab实现的,所以其使用的头文件也是 slab.h
。
在内核启动时,内核通过 kmalloc_caches
数组预先创建一系列通用Slab缓存,该系列缓存大小为8B、16B、32B、...、8KB,这些缓存使用上一子章节末尾的查询命令输出的slab名分别为kmalloc-8、kmalloc-16、...、kmalloc-8k。
当发生 kmalloc
调用时,其会将 kmalloc
中的 size
参数向上对齐到最小的缓存块,例如 200Byte -> 256Byte。随后根据GFP标志为其分配对象。
特性:
vmalloc
申请的内存需要使用 vfree
。特性 | slab-特定对象缓存 | slab-通用对象缓存(kmalloc) | vmalloc |
---|---|---|---|
虚拟地址连续性 | 连续 | 连续 | 连续 |
物理地址连续性 | 一定连续 | 一定连续 | 不一定连续 |
能否避免休眠 | 可以避免 | 可以避免 | 不能 |
所得内存使用效率 | 高 | 高 | 一般 |
#include "linux/vmalloc.h"
// 注:下方并非实际定义,仅为原型参考
void *vmalloc(unsigned long size);
本子章节的学习需要先完成内存屏障的学习。
编译器及CPU对内存访问的常见优化方式有使用高速缓存、指令重排等。而对于硬件来说,上述两种方式均不可接受。其对应的解决方案为:
Linux内核提供了如下的几种内存屏障的使用方式:
#include <linux/kernel.h>
// 通知编译器插入一个内存屏障,但并不影响CPU层面的乱序执行
void barrier(void);
#include <asm/system.h>
// 以下四种API均为在硬件层面插入内存屏障
// 在大多数架构中,下列四种API调用时均会隐含 barrier() 调用
// 读内存屏障,可以保证 rmb() 调用之前的所有读取操作均已完成
void rmb(void);
// 阻止部分读取操作的读内存屏障,除非明白与 rmb() 之间的差别,否则不建议调用
void read_barrier_depends(void);
// 写内存屏障,可以保证 wmb() 调用之前的所有写入操作均已完成
void wmb(void);
// 全内存屏障,可以保证 mb() 调用之前的所有读写操作均已完成
void mb(void);
#include <asm/system.h>
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
一个经典的内存屏障的使用形式如下:
// 写入寄存器进行硬件配置
writel(dev->regs.addr, xxx);
writel(dev->regs.size, xxx);
writel(dev->regs.ops, xxx);
// 插入写内存屏障,确保前面三哥寄存器操作均已完成
wmb();
// 执行硬件操作
writel(dev->regs.control, xxx);
Linux系统的包含串口、PCI bus、DMA等IO端口的分配均在 /proc/ioports
中可见,并会列出IO的地址范围,例如:
# cat /proc/ioports
0000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0064-0064 : keyboard
0070-0071 : rtc0
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
0170-0177 : 0000:00:01.1
0170-0177 : ata_piix
01f0-01f7 : 0000:00:01.1
01f0-01f7 : ata_piix
0376-0376 : 0000:00:01.1
0376-0376 : ata_piix
03f2-03f2 : floppy
03f4-03f5 : floppy
03f6-03f6 : 0000:00:01.1
03f6-03f6 : ata_piix
03f7-03f7 : floppy
03f8-03ff : serial
0600-063f : 0000:00:01.3
0600-0603 : ACPI PM1a_EVT_BLK
0604-0605 : ACPI PM1a_CNT_BLK
0608-060b : ACPI PM_TMR
0700-070f : 0000:00:01.3
0cf8-0cff : PCI conf1
0d00-ffff : PCI Bus 0000:00
afe0-afe3 : ACPI GPE0_BLK
c000-cfff : PCI Bus 0000:02
d000-dfff : PCI Bus 0000:01
e000-e03f : 0000:00:06.0
e040-e05f : 0000:00:01.2
e040-e05f : uhci_hcd
e060-e07f : 0000:00:05.0
e080-e09f : 0000:00:07.0
e0a0-e0af : 0000:00:01.1
e0a0-e0af : ata_piix
值得注意的是上述地址范围并非内存地址范围,上述地址是独立编址的,称作端口IO(Port IO)。
内核中为IO端口分配提供了如下的接口:
#include <linux/ioports.h>
struct resource *request_region(resource_size_t start, resource_size_t n, const char *name);
#include <linux/ioports.h>
void release_region(resource_size_t start, resource_size_t n);
#include <asm/io.h>
// 读写8位
u8 inb(unsigned long addr);
void outb(unsigned char x, unsigned long port);
// 读写16位
u16 inw(unsigned long addr);
void outw(unsigned short x, unsigned long port);
// 读写32位
u32 inl(unsigned long addr);
void outl(unsigned int x, unsigned long port);
#include <asm/io.h>
void insb(unsigned long port, void *dst, unsigned long count);
void outsb(unsigned long port, const void *src, unsigned long count);
void insw(unsigned long port, void *dst, unsigned long count);
void outsw(unsigned long port, const void *src, unsigned long count);
void insl(unsigned long port, void *dst, unsigned long count);
void outsl(unsigned long port, const void *src, unsigned long count);
_p
后缀。CPU硬件或操作系统通常会把硬件的寄存器或内存映射到内存空间当中去。与上一章节的端口IO(Port IO)的区别点在于该内存地址位于内存空间中,称作内存映射IO(Memory-Mapped IO,MMIO)。根据计算机体系结构和目标IO的不同,IO内存可能是、也可能不是经由页表进行的,但是在内核编程时使用的步骤是一致的。
使用IO内存的基本步骤为:
request_mem_region
),进行互斥和资源管理ioremap
),方便进行权限管理iounmap
)release_mem_region
)使用页表组织MMIO的优点有:
mmap
将MMIO映射到用户空间在使用IO内存时,基本流程和使用普通内存一致,均为申请-使用-释放的过程,只是使用的API和成功/失败对应的意义不同。
request_mem_region
的函数原型可参考(注意并非实际函数原型):
#include <linux/ioport.h>
struct resource * request_mem_region(resource_size_t start, resource_size_t n, const char* name);
需要注意:
*malloc
那样分配内存request_mem_region
进行标记,对应内存区域依旧可以被访问,但是不能确保安全,且无法使用该函数的 name
进行追踪。#include <asm/io.h>
void __iomem *ioremap(phys_addr_t offset, size_t size);
void __iomem *ioremap_prot(phys_addr_t phys_addr, size_t size, unsigned long prot)
需要注意:
vmalloc
区域中ioremap_prot
手动配置。#include <asm/io.h>
void iounmap(volatile void __iomem *addr);
在部分平台上允许使用普通IO操作来访问这些内存,但是该方法不具备可移植性。应当使用如下的方法族:
#include <asm/io.h>
// 读取单个
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
// 写入单个
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
// 读写序列
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16(void *addr, void *buf, unsigned long count);
void ioread32(void *addr, void *buf, unsigned long count);
void iowrite8(void *addr, const void *buf, unsigned long count);
void iowrite16(void *addr, const void *buf, unsigned long count);
void iowrite32(void *addr, const void *buf, unsigned long count);
// 内存操作
void memset_io(void *dst, int c, unsigned long count);
void memcpy_fromio(void *to, const void *from, unsigned long count);
void memcpy_toio(void *to, const void *from, unsigned long count);
该函数的参考原型如下(并非实际原型):
#include <linux/ioport.h>
void release_mem_region(resource_size_t start, resource_size_t n);
#include <asm/hardirq.h>
in_interrupt();
当当前上下文为软件或硬件中断时会返回非零值。
在中断上下文中时需要注意:
current
指针也无效。schedule
或 wait_event
等。也不能调用可能引起休眠的函数或信号量,例如 kmalloc(..., GFP_KERNEL)
。#include <asm/hardirq.h>
in_atpmic();
在原子上下文中时需要注意:
current
指针可用,但是不能访问用户空间。使用 cat /proc/interrupts
命令,有:
CPU0 CPU1
0: 170 0 IR-IO-APIC 2-edge timer
1: 10 0 IR-IO-APIC 1-edge i8042
8: 1 0 IR-IO-APIC 8-edge rtc0
9: 214 0 IR-IO-APIC 9-fasteoi acpi
11: 0 0 IO-APIC 11-fasteoi virtio2, uhci_hcd:usb1
12: 124 0 IR-IO-APIC 12-edge i8042
16: 3589 0 IR-IO-APIC 16-fasteoi ehci_hcd:usb1
24: 0 652 IR-PCI-MSI 32768-edge xhci_hcd
...
上述内容中:
需要补充的是:
此外,使用 cat /proc/stat
命令,在 intr
行也可以查看各中断的触发次数,例如:
...
intr 2586828620 0 9 0 0 24 0 3 0 0 0 0 0 15 0 34688478 0 0 0 0 0 0 0 0 0 50 539037977 688394395 0 284284934 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
...
上述 intr
行中:
在Linux内核中,注册(安装)中断例程需要使用如下的API:
#include <linux/interrupts.h>
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
其中:
unsigned int irq |
选用的中断号,选用原则见:中断号选用原则 |
irq_handler_t handler |
中断处理函数指针,原型为:(*irq_handler_t)(int irq, void *dev) 其中: - irq 为中断号,同参数1- dev 为指向私有数据区的指针,同参数5返回值 irq_handler_t 可选:- IRQ_NONE :中断未由本设备触发- IRQ_HANDLED :中断已被处理- IRQ_WAKE_THREAD :需要唤醒线程化处理(结合 IRQF_ONESHOT 使用) |
unsigned long flags |
中断管理掩码 |
const char *name |
中断号拥有者,会在 /proc/interrupts 中显示 |
void *dev |
可以用于指向私有数据区。 在非共享中断也可以设置为 NULL ,但不建议,最好保持全内核唯一性(所以一般指向 dev )。因为该值在内核中断管理的数据结构中起到了类似Key值的作用。 具体可见关于dev参数的说明。 |
常用的中断管理掩码有:
IRQF_TRIGGER_NONE
:不指定触发方式,沿用硬件或固件的当前配置。IRQF_TRIGGER_RISING
:上升沿触发IRQF_TRIGGER_FALLING
:下降沿触发IRQF_TRIGGER_HIGH
:高电平触发IRQF_TRIGGER_LOW
:低电平触发IRQF_TRIGGER_PROBE
:自动探测触发(慎用,需要二次判定触发条件是否正确,且整个内核原码中尚无使用)IRQF_SHARED
:共享中断。新平台中很少使用。
IRQF_PROBE_SHARED
:共享中断,允许驱动在共享中断线时,绕过触发方式的严格一致性检查。
IRQF_COND_ONESHOT
:表示该共享中断在执行完成后必须显式的重新启用。需要注意:
IRQF_SHARED
使用,否则无效。-EINVAL
。IRQF_ONESHOT
重复设置,但没有必要。IRQF_TIMER
:标记为定时器中断,优先处理以保障精度。IRQF_PERCPU
:标记为per-CPU中断,每个CPU核心独立注册并处理该中断,通常用于ARM的每个CPU的本地计时器,或高性能网卡的每CPU中断。IRQF_NOBALANCING
:禁止中断负载均衡,用于将中断绑定到一个特定的CPU上。IRQF_IRQPOLL
:中断用于轮询模式优化,用于需要频繁轮询的中断。IRQF_ONESHOT
:单次启动中断,表示中断在执行完成后必须显式的重新启用。IRQF_NO_THREAD
:禁止线程化中断,强制使用原子上下文处理程序,用于极低延迟或不可睡眠的中断处理。IRQF_NO_AUTOEN
:不自动启用中断,需手动调用 enable_irq()
,可以灵活控制中断启用时机,例如初始化阶段延迟启用。IRQF_NO_SUSPEND
:在系统挂起期间不关闭此中断,用于电源按钮、RTC等外部唤醒设备。
IRQF_FORCE_RESUME
:在系统唤醒后强制重新启用中断,修复某些休眠后中断未正确恢复的场景。IRQF_EARLY_RESUME
:在系统唤醒早期阶段恢复中断IRQF_COND_SUSPEND
:可以动态的决定是否可以被挂起。
enable_irq_wake(irq)
可以把该中断作为唤醒中断,从而在系统挂起期间不关闭此中断。disable_irq(irq)
可以禁用该中断。Linux内核中,并非所有的中断都可以自由选择中断号:
rk3588-base.dtsi
中有如下片段uart0: serial@fd890000 {
compatible = "rockchip,rk3588-uart", "snps,dw-apb-uart";
reg = <0x0 0xfd890000 0x0 0x100>;
interrupts = <GIC_SPI 331 IRQ_TYPE_LEVEL_HIGH 0>;
clocks = <&cru SCLK_UART0>, <&cru PCLK_UART0>;
clock-names = "baudclk", "apb_pclk";
dmas = <&dmac0 6>, <&dmac0 7>;
dma-names = "tx", "rx";
pinctrl-0 = <&uart0m1_xfer>;
pinctrl-names = "default";
reg-shift = <2>;
reg-io-width = <4>;
status = "disabled";
};
该片段将 uart0
的中断定义到了共享外设中断( GIC_SPI
)的331号中断中。
因此在进行驱动程序编写时,需要使用如下的API进行中断号获取:
#include <linux/platform_device.h>
int platform_get_irq(struct platform_device *dev, unsigned int n);
#include <linux/pci.h>
// 申请含有指定中断数量的中断向量
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs, unsigned int max_vecs, unsigned int flags);
// 获取中断向量中的第nr个中断号
int pci_irq_vector(struct pci_dev *dev, unsigned int nr);
中断取消应当使用如下的API:
#include <linux/interrupt.h>
const void *free_irq(unsigned int irq, void *dev_id);
关于 free_irq
函数的说明:
dev_id
指向的数据(如有)有效。关于 dev
参数的说明:
dev_id
参数必须和中断注册时的 dev
参数保持一致。dev_id
参数必须全内核中唯一,否则会冲突(因此通常取dev对象的指针)。dev_id
参数用于在内核的中断管理数据结构( desc
)中当作Key值。#include <linux/interrupts.h>
// 禁用并等待当前的中断例程完成,要避免死锁
void disable_irq(unsigned int irq);
// 禁用中断,但不等待例程完成,要注意竞态
void disable_irq_nosync(unsigned int irq);
bool disable_hardirq(unsigned int irq);
void disable_percpu_irq(unsigned int irq);
void enable_irq(unsigned int irq);
void enable_percpu_irq(unsigned int irq, unsigned int type);
在Linux内核内部维护了一个计数器,只有中断启用次数大于等于禁用次数时,中断才会被启动(当启用大于禁用时,会抛WARN日志)。而平台的中断启停实现则是修改可编程中断控制器指定中断的掩码,从而在所有的处理器上禁用或启用中断。
#include <linux/irqflags.h>
// 注意下列函数为宏函数,下列原型并非实际原型
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
与上一子章节不同的是,某个核心全部中断的开启和关闭并没有维护计数器。
如前文中断的注册章节所述,共享中断注册使用如下API时:
#include <linux/interrupts.h>
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
参数:
handle
:必须可以识别属于自己的中断flags
:
IRQF_SHARED
等中断共享控制掩码dev
:cat /proc/interrupts
中的体现如下方的中断号11,由 virtio2
和 USB1
共享:本章并无太多难点,更多的是使用规范类问题。
参考资料:
关于 u8
、 s8
与 uint8_t
、int8_t
:
Although it would only take a short amount of time for the eyes and brain to become accustomed to the standard types like
uint32_t
, some people object to their use anyway.Therefore, the Linux-specific
u8/u16/u32/u64
types and their signed equivalents which are identical to standard types are permitted -- although they are not mandatory in new code of your own.When editing existing code which already uses one or the other set of types, you should conform to the existing choices in that code.
即:
<linux/types.h>
中的 u8
、 s8
等,但也允许使用 <stdint.h>
中的 uint8_t
和 int8_t
基本索引:
直接参考Linux 内核代码风格 — The Linux Kernel documentation(中文版)即可。
PCI的总线拓扑结构如下图所示:
几个关键概念:
几个额外注意点:
回到Linux软件层面:
使用 lspci
指令