Linux 内核系列文章
Linux 内核设计与实现
深入理解 Linux 内核
Linux 设备驱动程序(一)
Linux 设备驱动程序(二)
Linux 设备驱动程序(三)
Linux 设备驱动程序(四)
Linux设备驱动开发详解
深入理解Linux虚拟内存管理
文章目录
- Linux 内核系列文章
- 前言
- 一、设备驱动程序简介
- 二、构造和运行模块
- 1、vscode 编写
- 2、hello 模块
- 3、模块参数
- 4、在用户空间编写驱动程序
- 5、快速参考
- 三、字符设备驱动程序
- 1、 主设备号和次设备号
- (1)分配和释放设备编号
- (2)scull_load 脚本
- 2、 一些重要的数据结构
- (1)文件操作
- (2)file 结构
- (3)inode 结构
- 3、 字符设备的注册
- 4、快速参考
- 四、调试技术
- 1、 内核中的调试支持
- 2、通过打印调试
- 3、 通过查询调试
- (1)使用 /proc 文件系统
- 五、并发和竞态
- 1、快速参考
- 六、高级字符驱动程序操作
- 1、ioctl
- (1)用户空间
- (2)系统实现
- 2、阻塞型 I/O
- (1)休眠的简单介绍
- (2)简单休眠
- 3、快速参考
- 七、时间、延迟及延缓操作
- 1、度量时间差
- (1)使用 jiffies 计数器
- (2)获取当前时间
- 2、延迟执行
- (1)长延迟
- (a)忙等待
- (b)让出处理器
- (c)超时
- (2)短延迟
- 3、内核定时器
- (1)定时器 API
- (2)内核定时器的实现
- 4、tasklet
- 5、工作队列
- (1)共享队列
- 6、快速参考
- (1)计时
- (2)延迟
- (3)内核定时器
- (4)tasklet
- (5)工作队列
- 八、分配内存
- 1、 kmalloc函数的内幕
- (1)flags 参数
- 2、 后备高速缓存
- (1)基于 slab 高速缓存的 scull:scullc
- (2)内存池
- 3、get_free_page 和相关函数
- (1)alloc_pages 接口
- 4、 vmalloc及其辅助函数
- 5、 per-CPU变量
- 6、获取大的缓冲区
- (1)在引导时获得专用缓冲区
- 7、快速参考
- 九、与硬件通信
- 1、 I/O 端口和 I/O 内存
- 2、 使用I/O端口
- (1)I/O 端口分配
- (2)操作 I/O 端口
- (3)在用户空间访问 I/O 端口
- (4)串操作
- 3、 使用I/O内存
- (1)I/O 内存分配和映射
- (2)访问 I/O 内存
- (3)像 I/O 内存一样使用端口
- 4、 快速参考
前言
本文主要用来摘录《Linux 设备驱动程序第三版》一书中学习知识点,本书基于 Linux 2.6.10 版本,源代码摘录基于 Linux 2.6.34 ,两者之间可能有些出入。
驱动模块构建
【视频】 Linux之驱动开发篇
Linux内核模块分析(module_init宏)
Linux驱动编程中EXPORT_SYMBOL() 介绍
Linux EXPORT_SYMBOL宏详解
一、设备驱动程序简介
二、构造和运行模块
1、vscode 编写
vscode软件设置头文件路径的方法
.vscode/c_cpp_properties.json 中文件如下:
{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/usr/src/linux-headers-4.15.0-142/include/","/usr/src/linux-headers-4.15.0-142/arch/x86/include","/usr/src/linux-headers-4.15.0-142/"],"defines": [],"compilerPath": "/usr/bin/gcc","cStandard": "c11","cppStandard": "c++17","intelliSenseMode": "linux-gcc-x64"}],"version": 4
}
内核头文件路径为:
/lib/modules/`uname -r`/
# 其值为: /usr/src/linux-headers-4.15.0-142/
2、hello 模块
// hello.c
#include <linux/init.h>
#include <linux/module.h>MODULE_LICENSE("Dual BSD/GPL");static int hello_init(void) {printk(KERN_DEBUG "Hello, world\n");return 0;
}static void hello_exit(void) {printk(KERN_DEBUG "Goodbye, cruel world\n");
}module_init(hello_init);
module_exit(hello_exit);
# Makefile
obj-m := hello.o # 要生成的模块名
# hello-objs:= a.o b.o # 生成这个模块名所需要的目标文件KDIR := /lib/modules/`uname -r`/build
# KDIR := /home/liuqz/learnLinux/linux-4.15.18
PWD := $(shell pwd)default:make -C $(KDIR) M=$(PWD) modulesclean:rm -rf *.o *.o.cmd *.ko *.mod.c .tmp_versions modules.order Module.symversrm -rf .cache.mk .*ko.cmd .*.mod.o.cmd .*.o.cmd
3、模块参数
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
内核支持的模块参数类型如下:
-
bool
-
invbool
布尔值(取 true 或 false),关联的变量应该是 int 型。invbool 类型反转其值,也就是说,true 值变成 false,而 false 变成 true。 -
charp
字符指针值。内核会为用户提供的字符串分配内存,并相应设置指针。 -
int
-
long
-
short
-
uint
-
ulong
-
ushort
具有不同长度的基本整数值。以 u 开头的类型用于无符号值。
模块装载器也支持数组参数,在提供数组值时用逗号划分各数组成员。要声明数组参数,需要使用下面的宏:
module_param_array(name,type,num, perm);
其中,name 是数组的名称(也就是参数的名称),type 是数组元素的类型,num 是一个整数变量,而 perm 是常见的访问许可值。如果在装载时设置数组参数,则 num 会被设置为用户提供的值的个数。模块装载器会拒绝接受超过数组大小的值。
如果我们需要的类型不在上面所列出的清单中,模块代码中的钩子可让我们来定义这些类型。具体的细节请参阅 moduleparam.h 文件。所有的模块参数都应该给定一个默认值;
insmod 只会在用户明确设置了参数的值的情况下才会改变参数的值。模块可以根据默认值来判断是否是一个显式指定的参数。
module_param 中的最后一个成员是访问许可值,我们应使用 <linux/stat.h> 中存在的定义。这个值用来控制谁能够访问 sysfs 中对模块参数的表述。如果 perm 被设置为 0 ,就不会有对应的 sysfs 入口项;否则,模块参数会在 /sys/module(注 3)中出现,并设置为给定的访问许可。如果对参数使用 S_IRUGO ,则任何人均可读取该参数,但不能修改; S_IRUGO|S_IWUSR 允许 root 用户修改该参数。注意、如果一个参数通过sysfs 而被修改,则如同模块修改了这个参数的值一样,但是内核不会以任何方式通知模块。大多数情况下,我们不应该让模块参数是可写的,除非我们打算检测这种修改并作出相应的动作。
加载 hello 模块后,在 /sys/module 下有如下文件:
ls /sys/module/hello/ -l
total 0
-r--r--r-- 1 root root 4096 5月 8 14:48 coresize
drwxr-xr-x 2 root root 0 5月 8 14:48 holders
-r--r--r-- 1 root root 4096 5月 8 14:48 initsize
-r--r--r-- 1 root root 4096 5月 8 14:48 initstate
drwxr-xr-x 2 root root 0 5月 8 14:48 notes
-r--r--r-- 1 root root 4096 5月 8 14:48 refcnt
drwxr-xr-x 2 root root 0 5月 8 14:48 sections
-r--r--r-- 1 root root 4096 5月 8 14:48 srcversion
-r--r--r-- 1 root root 4096 5月 8 14:48 taint
--w------- 1 root root 4096 5月 8 14:48 uevent
4、在用户空间编写驱动程序
首次接触内核的 Unix 程序员可能对编写模块比较紧张,然而编写用户空间程序来直接对设备端口进行读写就容易多了。
相对于内核空间编程,用户空间编程具有自己的一些优点。有时候编写一个所谓的用户空间驱动程序是替代内核空间驱动程序的一个不错的方法。在这一小节,我们将讨论编写用户空间驱动程序的几个理由。但本书主要讲述内核空间的驱动程序,因此除了这里的讨论之外,我们不会进一步深入讨论这个话题。
用户空间驱动程序的优点可以归纳如下:
-
可以和整个 C 库链接。驱动程序不用借助外部程序(即前面提到的和驱动程序一起发行的用于提供策略的用户程序)就可以完成许多非常规任务。
-
可以使用通常的调试器调试驱动程序代码,而不用费力地调试正在运行的内核。
-
如果用户空间驱动程序被挂起,则简单地杀掉它就行了。驱动程序带来的问题不会挂起整个系统,除非所驱动的硬件已经发生严重故障。
-
和内核内存不同,用户内存可以换出。如果驱动程序很大但是不经常使用,则除了正在使用的情况之外,不会占用太多内存。
-
良好设计的驱动程序仍然支持对设备的并发访问。
-
如果读者必须编写封闭源码的驱动程序,则用户空间驱动程序可更加容易地避免因为修改内核接口而导致的不明确的许可问题。
例如,USB 驱动程序可在用户空间编写;具体可参阅 libusb 项目(libusb.sourceforge.net ,该项目还比较"年轻"),以及内核源代码中的 “gadgetfs” 。X 服务器是用户空间驱动程序的另一个例子。它十分清楚硬件可以做什么、不可以做什么,并且为所有的 X 客户提供图形资源。然而,值得注意的是目前基于帧缓冲区(frame-buffer)的图形环境正在慢慢成为发展趋势。这种环境下对于实际的图形操作,X 服务器仅仅是一个基于真正内核空间驱动程序的服务器。
通常,用户空间的驱动程序被实现为一个服务器进程,其任务是替代内核作为硬件控制的唯一代理。客户应用程序可连接到该服务器并和设备执行实际的通信;这样,好的驱动程序进程可允许对设备的并发访问。其实这就是 X 服务器的本质。
除了具备上述优点外,用户空间驱动程序也有很多缺点,下面列出其中最重要的几点:
- 中断在用户空间中不可用。对该限制,在某些平台上也有相应的解决办法,比如 IA32 架构上的 vm86 系统调用。
- 只有通过 mmap 映射 /dev/mem 才能直接访问内存,但只有特权用户才可以执行这个操作。
- 只有在调用 ioperm 或 iopl 后才可以访问 I/O 端口。然而并不是所有平台都支持这两个系统调用,并且访问 /dev/port 可能非常慢,因而并非十分有效。同样只有特权用户才能引用这些系统调用和访问设备文件。
- 响应时间很慢。这是因为在客户端和硬件之间传递数据和动作需要上下文切换。
- 更严重的是,如果驱动程序被换出到磁盘,响应时间将令人难以忍受。使用 mlock 系统调用或许可以缓解这一问题,但由于用户空间程序一般需要链接多个库,因此通常需要占用多个内存页。同样,mlock 也只有特权用户才能引用。
- 用户空间中不能处理一些非常重要的设备,包括(但不限于)网络接口和块设备等。
如上所述,我们看到用户空间驱动程序毕竞做不了太多的工作。然而依然存在一些有意义的应用,例如对 SCSI 扫描设备(由包 SANE 实现)和 CD 刻录设备(由 cdrecord 和其他工具实现)的支持。这两种情况下,用户空间驱动动程序都依赖内核空间驱动程序 “SCSI generic”,它导出底层通用的 SCSI 功能到用户空间程序,然后再由用户空间驱动程序驱动自己的硬件。
有一种情况适合在用户空间处理,这就是当我们准备处理一种新的、不常见的硬件时。在用户空间中我们可以研究如何管理这个硬件而不用担心挂起整个系统。一旦完成,就很容易将户空间驱动程序封装到内核模块中。
5、快速参考
本节将总结本章中提到的内核函数、变量、宏以及 /proc 文件,可以作为对这些内容的一个参考。每一项都会在相关头文件之后列出。从本章开始,以后每一章里都会有类似的一节来总结引入的新符号。本节中出现的条目会以它们在文中出现的顺序列出:
insmod
modprobe
rmmod
用来装载模块到正运行的内核和移除模块的用户空间工具。
#include<linux/init.h>
module_init(init_function);
module_exit(cleanup_function);// 用于指定模块的初始化和清除函数的宏。
__init
__initdata
__exit
__exitdata
仅用于模块初始化或清除阶段的函数(__init和__exit)和数据(__initdata 和__exitdata)标记。标记为初始化的项目会在初始化结束后丢弃;而退出项目在内核未被配置为可卸载模块的情况下被丢弃。内核通过将相应的目标对象放置在可执行文件的特殊 ELF 段中而让这些标记起作用。
#include <linux/sched.h>// 最重要的头文件之一。该文件包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。
current->pid
current->comm
当前进程的进程 ID 和命令名。
struct task_struct *current;
当前进程。
obj-m
由内核构造系统使用的 makefile 符号,用来确定在当前目录中应构造哪些模块。
/sys/module
/proc/modules
/sys/module 是 sysfs 目录层次结构中包含当前已装载模块信息的目录。/proc/modules 是早期用法,
只在单个文件中包括这些信息,其中包含了模块名称、每个模块使用的内存总量以及使用计数等。
每一行之后还追加有额外的字符串,用来指定模块的当前活动标志。
vermagic.o
内核源代码目录中的一个目标文件,它描述了模块的构造环境。
#include <linux/module.h>
// 必需的头文件,它必须包含在模块源代码中。#include <linux/version.h>
// 包含所构造内核版本信息的头文件。
LINUX_VERSION_CODE
整数宏,在处理版本依赖的预处理条件语句中非常有用。
EXPORT_SYMBOL (symbol);
EXPORT_SYMBOL_GPL (symbol);
用来导出单个符号到内核的宏。第二个宏将导出符号的使用限于 GPL 许可证下的模块。
MODULE_AUTHOR (author);
MODULE_DESCRIPTION (description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
在目标文件中添加关于模块的文档信息。
module_init(init_function);
module_exit(exit_function);
// 用来声明模块初始化和清除函数的宏。#include <linux/moduleparam.h>
module_param(variable, type, perm);
// 用来创建模块参数的宏,用户可在装载模块时(或者对内建代码引导时)调整这些参数的值。
// 其中的类型可以是bool、charp、int、invbool、long、short、ushort、uint、ulong 或者 intarray.#include <linux/kernel.h>
int printk(const char * fmt, ...);
// 函数 printf 的内核代码。
三、字符设备驱动程序
1、 主设备号和次设备号
(1)分配和释放设备编号
在建立一个字符设备之前,我们的驱动程序首先要做的事情就是获得一个或者多个设备编号。完成该工作的必要函数是 register_chrdev_region,该函数在 <linux/fs.h> 中声明:
int register_chrdev_region(dev_t first, unsigned count, const char *name)
其中,first 是要分配的设备编号范围的起始值。first 的次设备号经常被置为 0,但对该函数来讲并不是必需的。count 是所请求的连续设备编号的个数。注意,如果 count 非常大,则所请求的范围可能和下一个主设备号重叠,但只要我们所请求的编号范围是可用的,则不会带来任何问题。最后,name 是和该编号范围关联的设备名称,它将出现在 /proc/devices 和 sysfs 中。
和大部分内核函数一样,register_chrdev_region 的返回值在分配成功时为 0。在错误情况下,将返回一个负的错误码,并且不能使用所请求的编号区域。
如果我们提前明确知道所需要的设备编号,则 register_chrdev_region 会工作得很好。但是,我们经常不知道设备将要使用哪些主设备号;因此,Linux 内核开发社区一直在努力转向设备编号的动态分配。在运行过程中使用下面的函数,内核将会为我们恰当分配所需要的主设备号:
int alloc_chrdev_region(dev_t *dev, unsigned firstminor, unsigned count,const char *name)
在上面这个函数中,dev 是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor 应该是要使用的被请求的第一个次设备号,它通常是 0。 count 和 name 参数与 register_chrdev_region 函数是一样的。
不论采用哪种方法分配设备编号,都应该在不再使用它们时释放这些设备编号。设备编号的释放需要使用下面的函数:
void unregister_chrdev_region(dev_t first, unsigned int count);
通常,我们在模块的清除函数中调用 unregister_chrdev_region 函数。
上面的函数为驱动程序的使用分配设备编号,但是它们并没有告诉内核关于拿来这些编号要做什么工作。在用户空间程序可访问上述设备编号之前,驱动程序需要将设备编号和内部函数连接起来,这些内部函数用来实现设备的操作。我们会马上讨论如何实现这种连接,但在此之前还需要进一步讨论有关设备号的内容。
(2)scull_load 脚本
#!/bin/sh
module="scull"
device="scull"
mode="664"# 使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定模块位置,
# 这是因为新的modutils默认不会在当前目录中查找模块。
/sbin/insmod ./$module.ko $* || exit 1# 删除原有节点
rm -f /dev/${device}[0-3]major=$(awk "\$2= =\"$module\"{print \$1}" /proc/devices)
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3# 给定适当的组属性及许可,并修改属组。
# 并非所有的发行版都具有 staff 组, 有些有 wheel 组。
group="staff"
grep -q '^staff:' /etc/group || group="wheel"chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
2、 一些重要的数据结构
(1)文件操作
// include/linux/fs.h
/** NOTE:* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl* can be called without the big kernel lock held in all filesystems.*/
struct file_operations {/* * 第一个 file_operations 字段并不是一个操作;相反,它是指向"拥有"该结* 构的模块的指针。内核使用这个字段以避免在模块的操作正在被使用时卸载该模* 块。几乎在所有的情况下,该成员都会被初始化为 THIS_MODULE,它是定义在* <linux/module.h> 中的一个宏。*/struct module *owner;/* * 方法 llseek 用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回。* 参数 loff_t 一个"长偏移量",即使在 32 位平台上也至少占用 64 位的数据宽度。* 出错时返回一个负的返回值。如果这个函数指针是NULL,对seek的调用将会以某* 种不可预期的方式修改 file结构(在"file 结构"一节中有描述)中的位置计数器.*/loff_t (*llseek) (struct file *, loff_t, int);/* * 用来从设备中读取数据。该函数指针被赋为NULL值时,将导致read 系统调用出错* 并返回 -EINVAL("Invalid argument,非法参数")。函数返回非负值表示成功读* 取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数* 类型)。*/ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);/* * 向设备发送数据。如果没有这个函数,write系统调用会向程序返回一个-EINVAL。* 如果返回值非负,则表示成功写入的字节数。*/ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);/* * 初始化一个异步的读取操作--- 即在函数返回之前可能不会完成的读取操作。如* 果该方法为 NULL,所有的操作将通过 read(同步)处理。*/ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);/* * 初始化设备上的异步写入操作。*/ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);/* * 对于设备文件来说,这个字段应该为NULL。它仅用干读取目录,只对文件系统有用。*/int (*readdir) (struct file *, void *, filldir_t);/* * poll方法是 poll、epoll和 select 这三个系统调用的后端实现。这三个系统调用可用* 来查询某个或多个文件描述符上的读取或写入是否会被阻塞。poll方法应该返回一* 个位掩码,用来指出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进* 程置于休眠状态直到 I/O 变为可能时的信息。如果驱动程序将 poll 方法定义为* NULL,则设备会被认为既可读也可写,并且不会被阻塞。*/unsigned int (*poll) (struct file *, struct poll_table_struct *);/* * 系统调用ioctl提供了一种执行设备特定命令的方法(如格式化软盘的某个磁道,这* 既不是读操作也不是写操作)。另外,内核还能识别一部分ioctl命令,而不必调用* fops表中的ioctl。如果设备不提供ioctl入口点,则对于任何内核未预先定义的请* 求,ioctl系统调用将返回错误(-ENOTTY,"No such ioctl for device,该设备无* 此 ioctl 命令")。*/int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);/* * */long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);/* * */long (*compat_ioctl) (struct file *, unsigned int, unsigned long);/* * mmap用于请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,那* 么mmap系统调用将返回 -ENODEV。*/int (*mmap) (struct file *, struct vm_area_struct *);/* * 尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明* 一个相应的方法。如果这个入口为NULL,设备的打开操作永远成功,但系统不会* 通知驱动程序。*/int (*open) (struct inode *, struct file *);/* * 对 flush 操作的调用发生在进程关闭设备文件描述符副本的时候,它应该执行(并* 等待)设备上尚未完结的操作。请不要将它同用户程序使用的 fsync 操作相混淆。* 目前,flush 仅仅用于少数几个驱动程序,比如,SCSI 磁带驱动程序用它来确保设* 备被关闭之前所有的数据都被写入到磁带中。如果 flush 被置为 NULL,内核将简单* 地忽略用户应用程序的请求。*/int (*flush) (struct file *, fl_owner_t id);/* * 当 file 结构被释放时,将调用这个操作。与 open相仿,也可以将 release 设置为* NULL(注 5)。* * 注 5: 注意,release 并不是在进程每次调用 close 时都会被调用。只要 file 结构被共享(如* 在fork 或dup调用之后),release 就会等到所有的副本都关闭之后才会得到调用。如果* 需要在关闭任意一个副本时刷新那些特处理的数据,则应实现flush 方法。*/int (*release) (struct inode *, struct file *);/* * 该方法是fsync 系统调用的后端实现,用户调用它来刷新待处理的数据。如果驱动* 程序没有实现这一方法,fsync 系统调用返回 -EINVAL。*/int (*fsync) (struct file *, struct dentry *, int datasync);/* * 这是 fsync 方法的异步版本。*/int (*aio_fsync) (struct kiocb *, int datasync);/* * 这个操作用来通知设备其FASYNC 标志发生了变化。异步通知是比较高级的话题,* 将在第六章介绍。如果设备不支持异步通知,该字段可以是 NULL。*/int (*fasync) (int, struct file *, int);/* * lock方法用于实现文件锁定,锁定是常规文件不可缺少的特性、但设备驱动程序几* 乎从来不会实现这个方法。*/int (*lock) (struct file *, int, struct file_lock *);/* * sendpage是sendfile系统调用的另外一半,它由内核调用以将数据发送到对应的文* 件,每次一个数据页。设备驱动程序通常也不需要实现 sendpage。*/ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);/* * 该方法的目的是在进程的地址空间中找到一个合适的位置,以便将底层设备中的内* 存段映射到该位置。该任务通常由内存管理代码完成,但该方法的存在可允许驱动* 程序强制满足特定设备需要的任何对齐需求。大部分驱动程序可设置该方法为 NULL。*/unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);/* * 该方法允许模块检查传递给 fcntl(F_SETFL...)调用的标志。*/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);/* * */int (*setlease)(struct file *, long, struct file_lock **);
};
补充:其他版本的实现
/* * 这些方法用来实现分散/聚集型的读写操作。应用程序有时需要进行涉及多个内存* 区域的单次读或写操作,利用上面这些系统调用可完成这类工作,而不必强加额外* 的数据拷贝操作。如果这些函数指针被设置为 NULL,就会调用 read 和 write 方法* (可能是多次)。*/ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*writev) (struct file *,const struct iovec *, unsigned lang, loff_t *);/* * 这个方法实现 sendfile 系统调用的读取部分。sendfile 系统调用以最小的复制操作* 将数据从一个文件描述符移动到另一个。例如,Web服务器可以利用这个方法将某* 个文件的内容发送到网络连接。设备驱动程序通常将 sendfile 设置为 NULL。*/ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);/* * 当应用程序使用fcntl来请求目录改变通知时,该方法将被调用。该方法仅对文件* 系统有用、驱动程序不必实现 dir_notify。*/int (*dir_notify)(struct file *, unsigned long);
(2)file 结构
在 <linux/fs.h> 中定义的 struct file 是设备驱动程序所使用的第二个最重要的数据结构,注意、file 结构与用户空间程序中的 FILE 没有任何关联。FILE 在 C 库中定义且不会出现在内核代码中;而 struct file 是一个内核结构,它不会出现在用户程序中。
file 结构代表一个打开的文件(它并不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的 file 结构)。它由内核在 open 时创建,并传递给在该文件上进行操作的所有函数,直到最后的 close 函数。在文件的所有实例都被关闭之后,内核会释放这个数据结构。
在内核源码中,指向 struct file 的指针通常被称为 file 或 filp(“文件指针”)。为了不至于和这个结构本身相混淆,我们一致将该指针称为 filp。这样,file 指的是结构本身,filp 则是指向该结构的指针。
struct file 中最重要的成员罗列如下。与上节相似,这张清单在首次阅读时可以略过。在下一节中将看到一些真正的 C 代码,我们会详细讨论其中的某些字段。
// include/linux/fs.h
struct file {/** fu_list becomes invalid after file_free is called and queued via* fu_rcuhead for RCU freeing*/union {struct list_head fu_list;struct rcu_head fu_rcuhead;} f_u;struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt/** 与文件相关的操作。内核在执行 open操作时对这个指针赋值,以后需要处理这些* 操作时就读取这个指针。filp->f_op中的值决不会为方便引用而保存起来;也* 就是说,我们可以在任何需要的时候修改文件的关联操作,在返回给调用者之后,* 新的操作方法就会立即生效。例如,对应于主设备号1(/dev/null、/dev/zero等等)* 的open代码根据要打开的次设备号替换filp->f_op中的操作。这种技巧允许相* 同主设备号下的设备实现多种操作行为,而不会增加系统调用的负担。这种替换文* 件操作的能力在面向对象编程技术中称为"方法重载"。*/const struct file_operations *f_op;spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */atomic_long_t f_count;/** 文件标志,如 O_RDONLY、O_NONBLOCK和O_SYNC。为了检查用户请求的是否是非* 阻塞式的操作(我们将在第六章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O),* 驱动程序需要检查 O_NONBLOCK标志,而其他标志很少用到。注意,检查读 / 写权* 限应该查看 f_mode而不是 f_flags。所有这些标志都定义在<linux/fcntl.h>中。*/unsigned int f_flags;/** 文件模式。它通过 FMODE_READ 和 FMODE_WRITE 位来标识文件是否可读或可写(或可读写)。* 读者可能会认为要在自己的 open 或 ioctl函数中查看这个字段,以便检* 查是否拥有读 / 写访问权限,但由于内核在调用驱动程序的 read 和 write 前已经检* 查了访问权限,所以不必为这两个方法检查权限。在没有获得对应访问权限而打开* 文件的情况下,对文件的读写操作将被内核拒绝,驱动程序无需为此而作额外的判断。*/fmode_t f_mode;/** 当前的读/写位置。loff_t是一个 64位的数(用gcc的术语说就是 long long)。* 如果驱动程序需要知道文件中的当前位置,可以读取这个值,但不要去修改它。* read/write 会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对* filp->f_pos 进行操作。这一规则的一个例外是 llseek方法,该方法的目的本身* 就是为了修改文件位置。*/ loff_t f_pos;struct fown_struct f_owner;const struct cred *f_cred;struct file_ra_state f_ra;u64 f_version;
#ifdef CONFIG_SECURITYvoid *f_security;
#endif/* needed for tty driver, and maybe others *//** open 系统调用在调用驱动程序的 open 方法前将这个指针置为 NULL。驱动程序可* 以将这个字段用于任何目的或者忽略这个字段。驱动程序可以用这个字段指向已分* 配的数据,但是一定要在内核销毁 file 结构前在 release 方法中释放内存。* private_data是跨系统调用时保存状态信息的非常有用的资源,我们的大部分示* 例都使用了它。*/void *private_data;#ifdef CONFIG_EPOLL/* Used by fs/eventpoll.c to link all the hooks to this file */struct list_head f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNTunsigned long f_mnt_write_state;
#endif
};
更多:
/** 文件对应的目录项(dentry)结构。除了用filp->f_dentry->d_inode的方式来* 访问索引节点结构之外,设备驱动程序的开发者们一般无需关心 dentry 结构。* 实际的结构里还有其他一些字段,但它们对于设备驱动程序并没有多大用处。由于驱动* 程序从不自己填写file 结构、而只是对别处创建的 file 结构进行访问、所以忽略这* 些字段是安全的。*/struct dentry *f_dentry:
(3)inode 结构
内核用 inode 结构在内部表示文件,因此它和 file 结构不同,后者表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的 file 结构,但它们都指向单个 inode 结构。
inode 结构中包含了大量有关文件的信息。作为常规,只有下面两个字段对编写驱动程
序代码有用:
struct inode {// .../* 对表示设备文件的 inode 结构,该字段包含了真正的设备编号。 */dev_t i_rdev;union {struct pipe_inode_info *i_pipe;struct block_device *i_bdev;/** struct cdev 是表示字符设备的内核的内部结构。当 inode 指向一个字符设备文* 件时,该字段包含了指向 struct cdev 结构的指针。*/struct cdev *i_cdev;}; // ...
}
i_rdev 的类型在 2.5 开发系列版本中发生了变化,这破坏了大量驱动程序代码的兼容性。为了鼓励编写可移植性更强的代码,内核开发者增加了两个新的宏,可用来从一个 inode 中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);unsigned int imajor(struct inode *inode);
为了防止因为类似的改变而出现问题,我们应该使用上述宏、而不是直接操作 i_rdev 。
3、 字符设备的注册
// include/linux/cdev.h
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};
我们前面提到,内核内部使用 struct cdev 结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或者多个上述结构(注 6)。为此,我们的代码应包含 <linux/cdev.h>,其中定义了这个结构以及与其相关的一些辅助函数。
分配和初始化上述结构的方式有两种。如果读者打算在运行时获取一个独立的 cdev 结构,则应该如下编写代码:
struct cdev *my_cdev = cdev_alloc();my_cdev->ops = &my_fops;
这时,你可以将 cdev 结构嵌入到自己的设备特定结构中,scull 就是这样做的。这种情况下,我们需要用下面的代码来初始化已分配到的结构:
// fs/char_dev.c
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{memset(cdev, 0, sizeof *cdev);INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops;
}
另外,还有一个 struct cdev 的字段需要初始化。和 file_operations 结构类似,struct cdev 也有一个所有者字段,应被设置为 THIS_MODULE。
在 cdev 结构设置好之后,最后的步骤是通过下面的调用告诉内核该结构的信息:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{p->dev = dev;p->count = count;return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
这里,p 是 cdev 结构,dev 是该设备对应的第一个设备编号,count 是应该和该设备关联的设备编号的数量。count 经常取 1,但是在某些情形下,会有多个设备编号对应于一个特定的设备。例如,考虑 SCSI 磁带驱动程序,它通过每个物理设备的多个次设备号来允许用户空间选择不同的操作模式(比如密度)。
在使用 cdev_add 时,需要牢记重要的一点。首先,这个调用可能会失败。如果它返回一个负的错误码,则设备不会被添加到系统中。但这个调用几乎总会成功返回,此时,我们又面临另一个问题:只要 cdev_add 返回了,我们的设备就 “活” 了,它的操作就会被内核调用。因此,在驱动程序还没有完全准备好处理设备上的操作时,就不能调用 cdev_add 。
要从系统中移除一个字符设备,做如下调用:
void cdev_del(struct cdev *p)
{cdev_unmap(p->dev, p->count);kobject_put(&p->kobj);
}
要请楚的是,在将 cdev 结构传递到 cdev_del 函数之后,就不应再访问 cdev 结构了。
4、快速参考
本章介绍了下列符号和头文件。file_operations 结构和 file 结构的字段清单并没有在这里给出。
#include <linux/types.h>
dev_t
dev_t 是内核中用来表示设备编号的数据类型。
/* 这两个去从设备编号中抽取出主 / 次设备号。 */
int MAJOR(dev_t dev);
int MINOR(dev_t dev);/* 这个宏由主 / 次设备号构造一个 dev_t 数据项。 */
dev_t MKDEV(unsigned int major, unsigned int minor);/** "文件系统"头文件,它是编写设备驱动程序必需的头文件,其中声明了许多重要* 的函数和数据结构。 */
#include <linux/fs.h>/** 提供给驱动程序用来分配和释放设备编号范围的函数。在期望的主设备号预先知道* 的情况下,应调用 register_chrdev_region;而对动态分配,使用 alloc_chrdev_region。
*/
int register_chrdev_region(dev_t first, unsigned int count, char *name);
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsignedint count, char *name);
void unregister_chrdev_region(dev_t first, unsigned int count);/** 老的(2.6 之前的)字符设备注册例程。2.6 内核也提供了仿效该例程的函数,但是* 新代码不应该再使用该函数。如果主设备号不是0,则不加修改地使用;否则,系* 统将为该设备动态地分配编号。
*/
int register_chrdev(unsigned int major, const char *name, structfile_operations *fops);/** 用于注销由 register_chrdev 函数注册的驱动程序。major 和 name 字符串必须包* 含与注册该驱动程序时使用的相同的值。
*/
int unregister_chrdev(unsigned int major, const char *name);
struct file_operations;
struct file;
struct inode;
大多数设备驱动程序都会用到的三个重要数据结构。file_operations 结构保存
了字符驱动程序的方法; struct file 表示一个打开的文件,而 struct inode
表示一个磁盘上的文件。
/* 用来管理 cdev 结构的函数,内核中使用该结构表示字符设备。 */
#include <linux/cdev.h>
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *dev, struct file_operations *fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);/* 一个方便使用的宏,它可用于从包含在某个结构中的指针获得结构本身的指针。 */
#include <linux/kernel.h>
container_of(pointer, type, field);/* 该头文件声明了在内核代码和用户空间之间移动数据的函数。 */
#include <asm/uaccess.h>/* 在用户空间和内核空间之间拷贝数据。 */
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
unsigned long copy_to_user (void *to, const void *from, unsigned long count);
四、调试技术
可参考 ==> 内核调试方法
十五、调试
1、 内核中的调试支持
在第二章,我们建议读者构造并安装自己的内核,而不是运行发行版自带的原有内核。运行自己内核的一个最重要的原因之一是因为内核开发者已经在内核中建立了多项用于调试的功能。但这些功能会造成额外的输出,并导致性能下降,因此发行版厂商通常会禁止发行版内核中的这些功能。但是作为一名内核开发者,调试需求具有更高优先级,从而应该乐意接受因为额外的调试支持而导致的(最小)系统负载。
这里,我们列出了用于内核开发的几个配置选项。除特别指出外,所有这些选项均出现在内核配置工具的 “kernel hacking” 菜单中。注意,并非所有体系架构都支持其中的某些选项。
- CONFIG_DEBUG_KERNEL
该选项仅仅使得其他的调试选项可用。我们应该打开该选项,但它本身不会打开所有的调试功能。 - CONFIG_DEBUG_SLAB
这是一个非常重要的选项,它打开内核内存分配函数中的多个类型的检查;打开该检查后,就可以检测许多内存溢出及忘记初始化的错误。在将已分配内存返回给调用者之前,内核将把其中的每个字节设置为 0xa5,而在释放后将其设置为 0x61。
如果读者在自己驱动程序的输出中,或者在 oops 信息中看到上述 “毒剂” 字符,则可以轻松判断问题所在。在打开该调试选项后,内核还会在每个已分配内存对象的前面和后面放置一些特殊的防护值;这样,当这些防护值发生变化时,内核就可以知道有些代码超出了内存的正常访问范围、并 “大声抱怨” 。同时,该选项还会检查更多隐蔽的错误。 - CONFIG_DEBUG_PAGEALLOC
在释放时,全部内存页从内核地址空间中移出。该选项将大大降低运行速度,但可以快速定位特定的内存损坏错误的所在位置。 - CONFIG_DEBUG_SPINLOCK
打开该选项项,内核将捕获对未初始化自旋锁的操作,也会捕获诸如两次解开同一锁的操作等其他错误。 - CONFIG_DEBUG_SPINLOCK_SLEEP
该选项将检查拥有自旋锁时的休眠企图。实际上,如果调用可能引起休眠的函数,这个选项也会生效,即使该函数可能不会导致真正的休眠。 - CONFIG_INIT_DEBUG
标记为 __init(或者 __initdata)的符号将会在系统初始化或者模块装载之后被丢弃。该选项可用来检查初始化完成之后对用于初始化的内存空间的访问企图。 - CONFIG_DEBUG_INFO
该选项将使内核的构造包含完整的调试信息。如果读者打算利用 gdb 调试内核,将需要这些信息。如果计划使用 gdb,还应该打开 CONFIG_FRAME_POINTER 选项。 - CONFIG_MAGIC_SYSRQ
打开 "SysRq 魔法(magic SysRq)"按键。我们将在本章后面的 “系统挂起” 一节中讲述该按键。 - CONFIG_DEBUG_STACKOVERFLOW
- CONFIG_DEBUG_STACK_USAGE
这些选项可帮助跟踪内核栈的溢出问题。栈溢出的确切信号是不包含任何合理的反向跟踪信息的 oops 清单。第一个选项将在内核中增加明确的溢出检查;而第二个选项将让内核监视栈的使用,并通过 SysRq 按键输出一些统计信息。 - CONFIG_KALLSYMS
该选项出现在 “General setup/Standard features(一般设置 / 标准功能)” 菜单中,将在内核中包含符号信息;该选项默认是打开的。该符号信息用于调试上下文;没有此符号,oops 清单只能给出十六进制的内核反向跟踪信息,这通常没有多少用处。 - CONFIG_IKCONFIG
- CONFIG_IKCONFIG_PROC
这些选项出现在 “General setup(一般设置)” 菜单中,会让完整的内核配置状态包含到内核中,并可通过 /proc 访问。大多数内核开发者清楚地知道自己所使用的配置,因此并不需要这两个选项(会使得内核变大)。然而,如果读者要调试的内核是由其他人建立的,则上述选项会比较有用。 - CONFIG_ACPI_DEBUG
该选项出现在 “Power management/ACPI(电源管理/ACPI)” 菜单中。该选项将打开 ACPI(Advanced Configuration and Power Interface,高级配置和电源接口)中的详细调试信息。如果怀疑自己所遇到的问题和 ACPI 相关,则可使用该选项。 - CONFIG_DEBUG_DRIVER
在 “Device drivers(设备驱动程序)” 菜单中。该选项打开驱动程序核心中的调试信息,它可以帮助跟踪底层支持代码中的问题。本书第十四章将闻述驱动程序核心相关的内容。 - CONFIG_SCSI_CONSTANTS
该选项出现在 “Device drivers/SCSI device support(设备驱动程序 /SCSI 设备支持)” 菜单中,它将打开详细的 SCSI 错误消息。如果读者要编写 SCSI 驱动程序,则可使用该选项。 - CONFIG_INPUT_EVBUG
该选项可在 “Device drivers/Input device support(设备驱动程序/输入设备支持)” 中找到,它会打开对输入事件的详细记录。如果读者要针对输入设备编写驱动程序,则可使用该选项。注意该选项会导致的安全问题:它会记录你键入的任何东西,包括密码。 - CONFIG_PROFILING
该选项可在 “Profiling support(剖析支持)” 菜单中找到。剖析通常用于系统性能的调节,但对跟踪内核挂起及相关问题也会有帮助。
在我们讲解不同的内核问题跟踪方法时,将再次遇到上述选项。在此之前,先描述一下经典的调试技术:print 语句。
2、通过打印调试
最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用 printf 显示监视信息。调试内核代码的时候,可以用 printk 来完成相同的工作。
3、 通过查询调试
(1)使用 /proc 文件系统
所有使用 /proc 的模块必须包含 <linux/proc_fs.h>,并通过这个头文件来定义正确的函数。
为创建一个只读的 /proc 文件,驱动程序必须实现一个函数,用于在读取文件时生成数据。当某个进程读取这个文件时(使用 read 系统调用),读取请求会通过这个函数发送到驱动程序模块。我们把注册接口放到本节后面,先直接讲述这个函数。
在某个进程读取我们的 /proc 文件时,内核会分配一个内存页(即 PAGE_SIZE 字节的内存块),驱动程序可以将数据通过这个内存页返回到用户空间。i 缓冲区会传入我们定义的函数,而该函数称为 read_proc 方法:
int (*read_proc)(char *page, char **start, off_t offset, int count,int *eof, void *data);
参数表中的 page 指针指向用来写入数据的缓冲区;函数应使用 start 返回实际的数据写到内存页的哪个位置(对此后面还将进一步谈到);offset 和 count 这两个参数与 read 方法相同。eof 参数指向一个整型数,当没有数据可返回时,驱动程序必须设置这个参数;data 参数是提供给驱动程序的专用数据指针,可用于内部记录。
该函数必须返回存放到内存页缓冲区的字节数,这一点与 read 函数对其他类型文件的处理相同。另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标志,而 start 的用法就有点复杂了,它可以帮助实现大(大于一个内存页)的 /proc 文件。
start 参数的用法看起来有些特别,它用来指示要返回给用户的数据保存在内存页的什么位置。在我们的 read_proc 方法被调用时,*start 的初始值为 NULL。如果保留 *start 为空,内核将假定数据保存在内存页偏移量 0 的地方;也就是说,内核将对 read_proc 作如下简单假定:该函数将虚拟文件的整个内容放到了内存页,并同时忽略 offset 参数。相反,如果我们将 *start 设置为非空值,内核将认为由 *start 指向的数据是 offset 指定的偏移量处的数据,可直接返回给用户。通常,返回少量数据的简单 read_proc 方法可忽略 start 参数,复杂的 read_proc 方法会将 *start 设置为页面,并将所请求偏移量处的数据放到内存页中。
长久以来,关于 /proc 文件还有另一个主要问题,这也是 start 意图解决的一个问题。有时,在连续的 read 调用之间,内核数据结构的 ASCII 表述会发生变化,以至于读取进程发现前后两次调用所获得的数据不一致。如果把 *start 设为一个小的整数值,那么调用程序可以利用它来增加 filp->f_pos 的值,而不依赖于返回的数据量,因此也就使 f_pos 成为 read_proc 过程的一个内部记录值。例如,如果 read_proc 函数从一个大的结构数组返回数据,并且这些结构的前五个已经在第一次调用中返回,那么可将 *start 设置为 5。下次调用中这个值将被作为偏移量;驱动程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作 “hack” ,可以在 /fs/proc/generic.c 中看到。
五、并发和竞态
1、快速参考
本章介绍了大量用来管理并发的符号,我们在这里总结了其中最重要的一些符号:
/* 定义信号量及其操作的包含文件。 */
#include <asm/semaphore.h>/* 用于声明和初始化用在互斥模式中的信号量的两个宏。 */
DECLARE_MUTEX (name);
DECLARE_MUTEX_LOCKED(name);/* 这两个函数可在运行时初始化信号量。 */
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);/** 锁定和解锁信号量。如果必要,down 会将调用进程置于不可中断的休眠状态;相* 反,down_interruptible 可被信号中断。down_trylock 不会休眠,并且会在信号量* 不可用时立即返回锁定信号量的代码最后必须使用 up 解锁该信号量。
*/
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);/* 信号量的读取者 / 写入者版本以及用来初始化这种信号量的函数。 */
struct rw_semaphore;
init_rwsem(struct rw_semaphore *sem);/* 获取并释放对读取者 / 写入者信号量的读取访问的函数。 */
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);/* 对读取者 / 写入者信号量的写入访问进行管理的函数。 */
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)/** 描述 Linux 的 completion 机制的包含文件,以及用于初始化 completion 的常用方* 法。INIT_COMPLETION 只能用于对已使用过的 completion 的重新初始化。
*/
#include <linux/completion.h>
DECLARE_COMPLETION(name);
init_completion(struct completion *c);
INIT_COMPLETION(struct completion c);/* 等待一个 completion 事件的发生。 */
void wait_for_completion(struct completion *c);/** 发出completion事件信号。complete最多只能唤醒一个等待的线程,而complete_all* 会唤醒所有的等待者。
*/
void complete(struct completion *c);
void complete_all(struct completion *c);/* 通过调用 complete 并调用当前线程的 exit 函数而发出 completion 事件信号。 */
void complete_and_exit(struct completion *c, long retval);/* 定义自旋锁接口的包含文件,以及初始化自旋锁的两种方式。 */
#include <linux/spinlock.h>
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init(spinlock_t *lock);/* 锁定自旋锁的不同方式,某些方法会禁止中断。 */
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);/* 上述函数的非自旋版本。这些函数在无法获得自旋锁时返回零,否则返回非零。 */
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);/* 释放自旋锁的相应途径。 */
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);/* 初始化读取者 / 写入者锁的两种方式。 */
rwlock_t lock = RW_LOCK_UNLOCKED
rwlock_init(rwlock_t *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_irqrestore(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);/* 释放对读取者 / 写入者自旋锁的写入访问的函数。 */
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);/* 整数变量的原子访问。对 atomic_t 变量的访问必须仅通过上述函数 */
#include <asm/atomic.h>
atomic_t v = ATOMIC_INIT(value);
void atomic_set(atomic_t *v, int i);
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);
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);
int atomic_add_negative(int i, atomic_t *v);
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);/** 对位置的原子访问,它们可用于标志或锁变量。使用这些函数可避免因为对相应位 * 的并发访问而导致的任何竞态*/
#include <asm/bitops.h>
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 的两种方式 */
#include <linux/seqlock.h>
seqlock_t lock = SEQLOCK_UNLOCKED;
seqlock_init(seqlock_t *lock);/* 用于获取受 seqlock 保护的资源的读取访问的函数。 */
unsigned int read_seqbegin(seqLock_t *lock);
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry(seqlock_t *lock, unsigned int seq);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);/* 用于获取受 seqlock 保护资源的写入访问的函数。 */
void write_seqlock(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);
int write_tryseqlock(seqlock_t *lock);/* 用于释放受 seqlock 保护的资源的写入访问的函数。 */
void write_sequnlock(seqlock_t *lock);
void write_sequnlock_irqrestore(seqlock_t *lock, unsigmed long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);/* 使用读取 - 复制 - 更新(RCU)机制时需要的包含文件。 */
#include <linux/rcupdate.h>/* 获取对受 RCU 保护的资源的读取访问的宏。 */
void rcu_read_lock;
void rcu_read_unlock;/* 准备用于安全释放受RCU 保护的资源的回调函数,该函数将在所有的处理器被调度运行。 */
void call_rcu(struct rcu_head *head, void (*func) (void *arg), void *arg);
六、高级字符驱动程序操作
1、ioctl
(1)用户空间
Linux下的ioctl()函数
// 接口#include <sys/ioctl.h>int ioctl(int fd, unsigned long cmd, ...);// ==================================================// 应用示例
uint16 data16;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {printf("socket failed\n\r");
}
if (ioctl(fd, SIOCSIFVLAN_PVID_PRI, &data16) < 0) {printf("ioctl pvid failed\n\r");
}
(2)系统实现
Linux驱动总结3- unlocked_ioctl和堵塞(waitqueue)读写函数的实现
linux unlocked_ioctl
// 2.6.34
// 从2.6.36 以后的内核已经废弃了 file_operations 中的 ioctl 函数指针,
// 取而代之的是 unlocked_ioctl, 用户空间对应的系统调用没有发生变化。
int (*ioctl) (struct inode *, struct file *, 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);
2、阻塞型 I/O
(1)休眠的简单介绍
对 Linux 设备驱动程序来讲,让一个进程进入休眠状态很容易。但是,为了将进程以一
种安全的方式进入休眠,我们需要牢记两条规则。
-
第一条规则是:永远不要在原子上下文中进入休眠。我们已经在第五章介绍过原子操作,而原子上下文就是指下面这种状态:在执行多个步骤时,不能有任何的并发访问。这意味着,对休眠来说,我们的驱动程序不能在拥有自旋锁、seqlock 或者 RCU 锁时休眠。如果我们已经禁止了中断,也不能休眠。在拥有信号量时休眠是合法的,但是必须仔细检查拥有信号量时休眠的代码。如果代码在拥有信号量时休眠,任何其他等待该信号量的线程也会休眠,因此任何拥有信号量而休眠的代码必须很短,并且还要确保拥有信号量并不会阻塞最终会唤醒我们自己的那个进程。
-
另外一个需要铭记的是:当我们被唤醒时,我们永远无法知道休眠了多长时间,或者休眠期间都发生了些什么事情。我们通常也无法知道是否还有其他进程在同一事件上休眠,这个进程可能会在我们之前被唤醒并将我们等待的资源拿走。这样,我们对唤醒之后的状态不能做任何假定,因此必须检查以确保我们等待的条件真正为真。
另外一个相关的问题是,除非我们知道有其他人会在其他地方唤醒我们,否则进程不能休眠。完成唤醒任务的代码还必须能够找到我们的进程,这样才能唤醒休眠的进程。为确保唤醒发生,需整体理解我们的代码,并清楚地知道对每个休眠而言哪些事件序列会结束休眠。能够找到休眠的进程意味着,需要维护一个称为等待队列的数据结构。顾名思义,等待队列就是一个进程链表,共中包含了等待某个特定事件的所有进程。
(2)简单休眠
当进程休眠时,它将期待某个条件会在未来成为真。我们前面提到,当一个休眠进程被唤醒时,它必须再次检查它所等待的条件的确为真。Linux 内核中最简单的休眠方式是称为 wait_event 的宏(以及它的几个变种);在实现休眠的同时,它也检查进程等待的条件。wait_event 的形式如下:
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
在上面所有的形式中,queue 是等待队列头。注意,它 “通过值” 传递,而不是通过指针。condition 是任意一个布尔表达式,上面的宏在休眠前后都要对该表达式求值;在条件为真之前,进程会保持休眠。注意,该条件可能会被多次求值,因此对该表达式的求值不能带来任何副作用。
如果使用 wait_event、进程将被置于非中断休眠,如我们先前提到的,这通常不是我们所期望的。最好的选择是使用 wait_event_interruptible,它可以被信号中断。这个版本可返回一个整数值,非零值表示休眠被某个信号中断,而驱动程序也许要返回 -ERESTARTSYS。后面的两个版本(wait_event_timeout 和 wait_event_interruptible_timeout)只会等待限定的时间;当给定的时间(以 jiffy 表示,第七章将讨论)到期时,无论 condition 为何值,这两个宏都会返回 0 值。
当然,整个过程的另外一半是唤醒。其他的某个执行线程(可能是另一个进程或者中断处理例程)必须为我们执行唤醒、因为我们的进程正在休眠中。用来唤醒休眠进程的基本函数是 wake_up,它也有多种形式,但这里先介绍其中两个:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible (wait_queue_head_t *queue);
wake_up 会唤醒等待在给定 queue 上的所有进程(实际情况要复杂一些、读者很快会看到)。另一个形式(wake_up_interruptible)只会唤醒那些执行可中断休眠的进程。通常,这两种形式很难区分(如果使用可中断休眠的话);在实践中、约定作法是在使用 wait_event 时使用 wake_up ,而在使用 wait_event_interruptible 时使用 wake_up_interruptible 。
3、快速参考
本章介绍了下面这些符号和头文件:
#include <linux/ioctl.h>
// 这个头文件声明了用于定义 ioctl命令的所有的宏。它现在包含在 <linux/fs.h> 中。
_IOC_NRBITS
_IOC_TYPEBITS
_IOC_SIZEBITS
_IOC_DIRBITS
ioctl 命令的不同位字段的可用位数。还有四个宏定义了不同的 MASK(掩码),另
外四个宏定义了不同的 SHIFT (偏移),但它们基本上仅在内部使用。由于
_IOC_SIZEBITS 在不同体系架构上的值不同,因此需要重点关注。
_IOC_NONE
_IOC_READ
_IOC_WRITE
"方向"位字段的可能值。"读"和"写"是不同的位,可以"OR"在一起来指定
读 / 写。这些值都是基于 0 的。
_IOC(dir,type,nr,size)
_IO(type,nr)
_IOR(type,nr,size)
_IOW(type,nr,size)
_IOWR(type,nr,size)
// 用于生成 ioctl 命令的宏。_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)
// 用于解码 ioctl命令的宏。特别地,_IOC_TYPE(nr)是_IOC_READ和_IOC_WRITE进行"OR"的结果。
#include <asm/uaccess.h>
int access_ok(int type, const void *addr, unsigned long size);
// 这个函数验证指向用户空间的指针是否可用。如果允许访问,access_ok 返回非零值。
VERIFY_READ
VERIFY_WRITE
在 access_ok 中 type 参数可取的值。VERIFY_WRITE 是 VERIFY_READ 的超集。
#include <asm/uaccess.h>
int put_user(datum,ptr);
int get_user(local,ptr);
int __put_user(datum,ptr);
int __get_user(local,ptr);
/** 用于向(或从)用户空间保存(或获取)单个数据项的宏。传送的字节数目由* sizeof(*ptr) 决定。前两个要先调用 access_ok,后两个(__put_user 和* __get_user)则假设 access_ok 已经被调用过了。
*/#include <linux/capability.h>
// 定义有各种 CAP_符号,用于描述用户空间进程可能拥有的权能操作。
int capable(int capability);
// 如果进程具有指定的权能,返回非零值。#include <linux/wait.h>
typedef struct { /* */ } wait_queue_head_t;
void init_waitqueue_head(wait_queue_head_t *queue);
DECLARE_WAIT_QUEUE_HEAD(queue);
/** 预先定义的 Linux 等待队列类型。wait_queue_head_t 类型必须显式地初始化,* 初始化方法可在运行时用 init_waitqueue_head,或在编译时用 DECLARE_WAIT_QUEUE_HEAD
*/void wait_event(wait_queue_head_t q, int condition);
int wait_event_interruptible(wait_queue_head_t q, int condition);
int wait_event_timeout(wait_queue_head_t q, int condition, int time);
int wait_event_interruptible_timeout(wait_queue_head_t q, int condition,int time);
// 使进程在指定的队列上休眠,直到给定的 condition值为真。void wake_up(struct wait_queue **q);
void wake_up_interruptible(struct wait_queue **q);
void wake_up_nr(struct wait_queue **q, int nr);
void wake_up_interruptible_nr(struct wait_queue **q, int nr);
void wake_up_all(struct wait_queue **q);
void wake_up_interruptible_all(struct wait_queue **q);
void wake_up_interruptible_sync(struct wait_queue **q);
/** 这些函数唤醒休眠在队列 q 上的进程。_interruptible形式的函数只能唤醒可中断的* 进程。通常,只会唤醒一个独占等待进程,但其行为可通过 _nr 或_all 形式改变。* _sync 版本的唤醒函数在返回前不会重新调度 CPU。
*/#include <linux/sched.h>
set_current_state(int state);
// 设置当前进程的执行状态。TASK_RUNNING 表示准备运行,而休眠状态是
// TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE。
void schedule(void);
// 从运行队列中选择一个可运行进程。选定的进程可以是 current 或另一个不同的进程。typedef struct { /* */ } wait_queue_t;
init_waitqueue_entry(wait_queue_t *entry, struct task_struct *task);
// wait_queue_t 类型用来将某个进程放置到一个等待队列上。void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait,int state);
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
// 可用于手工休眠代码的辅助函数。void sleep_on(wiat_queue_head_t *queue);
void interruptible_sleep_on(wiat_queue_head_t *queue);
// 已废弃的两个函数,它们将当前进程无条件地置于休眠状态。#include <linux/poll.h>
void poll_wait(struct file *filp, wait_queue_head_t *q, poll_table *p)
// 将当前进程置于某个等待队列但并不立即调度。该函数主要用于设备驱动程序的 poll 方法。int fasync_helper(struct inode *inode, struct file *filp, int mode, struct fasync_struct **fa);
// 用来实现 fasync 设备方法的辅助函数。mode 参数取传入该方法的同一值,而 fa
// 指向设备专有的 fasync_struct*。void kill_fasync(struct fasync_struct *fa, int sig, int band);
// 如果驱动程序支持异步通知,则这个函数可以用来发送一个信号给注册在 fa 中的进程。int nonseekable_open(struct inode *inode, struct file *filp);
loff_t no_llseek(struct file *file, loff_t offset, int whence);
// 任何不支持定位的设备都应该在其 open方法中调用 nonseekable_open。这类设备
// 还应该在其 llseek 方法中使用 no_llseek。
七、时间、延迟及延缓操作
1、度量时间差
(1)使用 jiffies 计数器
#include <linux/jiffies.h>
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
int time_before_eq(unsigned long a, unsigned long b);
如果 a(jiffies 的某个快照)所代表的时间比 b 靠后,则第一个宏返回真;如果 a 比 b 靠前,则第二个宏返回真;后面两个宏分别用来比较 “靠后或者相等” 及 “靠前或者相等” 。这些宏会将计数器值转换为 signed long,相减,然后比较结果。如果需要以安全的方式计算两个 jiffies 实例之间的差,也可以使用相同的技巧:
diff = (long)t2 - (long)t1;
而通过下面的方法,可将两个 jiffies 的差转换为毫秒值:
msec = diff * 1000 / HZ;
但是,我们有时需要将来自用户空间的时间表述方法(使用 struct time val 和 struct timespec)和内核表述方法进行转换。这两个结构使用两个数来表示精确的时间:在老的、流行的 struct timeval 中使用秒和毫秒值,而较新的 struct timespce中则使用秒和纳秒,前者比后者出现得早,但更常用。为了完成 jiffies 值和这些结构间的转换,内核提供了下面四个辅助函数:
#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);
对 64 位 jiffies_64 的访问不像对 jiffies 的访问那样直接。在 64 位计算机架构上,这两个变量其实是同一个;但在 32 位处理器上,对 64 位值的访问不是原子的。这意味着,在我们读取 64 位值的高 32 位及低 32 位时,可能会发生更新,从而获得错误的值。因此,对 64 位计数器的直接读取是很靠不住的,但如果必须读取 64 位计数器,则应该使用内核导出的一个特殊辅助函数,该函数为我们完成了适当的锁定:
#include <linux/jiffies.h>
u64 get_jiffies_64(void);
在上面的函数原型中使用了 u64 类型。这是由 <linux/types.h> 定义的类型之一,我们将在第十一章讨论这些类型,它其实代表了一个无符号的 64 位类型。
(2)获取当前时间
mktime 函数为内核提供的将墙钟时间转换为 jiffies 。
/* Converts Gregorian date to seconds since 1970-01-01 00:00:00.* Assumes input in normal date format, i.e. 1980-12-31 23:59:59* => year=1980, mon=12, day=31, hour=23, min=59, sec=59.** [For the Julian calendar (which was used in Russia before 1917,* Britain & colonies before 1752, anywhere else before 1582,* and is still in use by some communities) leave out the* -year/100+year/400 terms, and add 10.]** This algorithm was first published by Gauss (I think).** WARNING: this function will overflow on 2106-02-07 06:28:16 on* machines where long is 32-bit! (However, as time_t is signed, we* will already get problems at other places on 2038-01-19 03:14:08)*/
unsigned long
mktime(const unsigned int year0, const unsigned int mon0,const unsigned int day, const unsigned int hour,const unsigned int min, const unsigned int sec)
{unsigned int mon = mon0, year = year0;/* 1..12 -> 11,12,1..10 */if (0 >= (int) (mon -= 2)) {mon += 12; /* Puts Feb last since it has leap day */year -= 1;}return ((((unsigned long)(year/4 - year/100 + year/400 + 367*mon/12 + day) +year*365 - 719499)*24 + hour /* now have hours */)*60 + min /* now have minutes */)*60 + sec; /* finally seconds */
}EXPORT_SYMBOL(mktime);
do_gettimeofday 使用秒或微秒值来填充一个指向 struct timeval 的指针变量。
/*** do_gettimeofday - Returns the time of day in a timeval* @tv: pointer to the timeval to be set** NOTE: Users should be converted to using getnstimeofday()*/
void do_gettimeofday(struct timeval *tv)
{struct timespec now;getnstimeofday(&now);tv->tv_sec = now.tv_sec;tv->tv_usec = now.tv_nsec/1000;
}EXPORT_SYMBOL(do_gettimeofday);
2、延迟执行
(1)长延迟
(a)忙等待
while (time_before(jiffies, j1))cpu_relax();
(b)让出处理器
while (time_before(jiffies, j1))schedule();
(c)超时
// include/linux/wait.h
#define wait_event_timeout(wq, condition, timeout) \
({ \long __ret = timeout; \if (!(condition)) \__wait_event_timeout(wq, condition, __ret); \__ret; \
})#define __wait_event_interruptible(wq, condition, ret) \
do { \DEFINE_WAIT(__wait); \\for (;;) { \prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \if (condition) \break; \if (!signal_pending(current)) { \schedule(); \continue; \} \ret = -ERESTARTSYS; \break; \} \finish_wait(&wq, &__wait); \
} while (0)
// include/linux/sched.h
extern signed long schedule_timeout(signed long timeout);
extern signed long schedule_timeout_interruptible(signed long timeout);
extern signed long schedule_timeout_killable(signed long timeout);
extern signed long schedule_timeout_uninterruptible(signed long timeout);
(2)短延迟
当设备驱动程序需要处理硬件的延迟(latency)时,这种延迟通常最多涉及到几十个毫秒。在这种情况下,依赖于时钟滴答显然不是正确的方法。
ndelay、udelay 和 mdelay 这几个内核函数可很好完成短延迟任务,它们分别延迟指定数量的纳秒、微秒和毫秒时间。它们的原型如下:
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);// arch/avr32/lib/delay.c
inline void __const_udelay(unsigned long xloops)
{unsigned long long loops;asm("mulu.d %0, %1, %2": "=r"(loops): "r"(current_cpu_data.loops_per_jiffy * HZ), "r"(xloops));__delay(loops >> 32);
}void __udelay(unsigned long usecs)
{__const_udelay(usecs * 0x000010c7); /* 2**32 / 1000000 (rounded up) */
}void __ndelay(unsigned long nsecs)
{__const_udelay(nsecs * 0x00005); /* 2**32 / 1000000000 (rounded up) */
}// arch/xtensa/include/asm/delay.h
static inline void __delay(unsigned long loops)
{/* 2 cycles per loop. */__asm__ __volatile__ ("1: addi %0, %0, -2; bgeui %0, 2, 1b": "=r" (loops) : "0" (loops));
}
这些函数的实际实现包含在 <asm/delay.h> 中,其实现和具体的体系架构相关,有时构建于一个外部函数。所有的体系架构都会实现 udelay,但其他函数可能未被定义;如果存在没有真正定义的函数,则 <linux/delay.h> 会在 udelay 的基础上提供一个默认的版本。不管哪种情况,真正实现的延迟至少会达到所请求的时间值,但可能更长;实际上,当前所有平台都无法达到纳秒精度,但有些平台提供了子微秒精度。延迟超过请求的值通常不是问题,因为驱动程序的短延迟通常等待的是硬件,而需求往往是至少要等待给定的时间段。
udelay(以及可能的 ndelay)的实现使用了软件循环,它根据引导期间计算出的处理器速度以及 loops_pre_jiffy 整数变量确定循环的次数。如果读者要阅读实际的代码,要注意 x86 平台上的实现相当复杂,这是因为,它使用了不同的定时源,而这取决于运行代码的 CPU 类型。
为避免循环计算中的整数溢出,udelay 和 ndelay 为传递给它们的值强加了上限。如果模块无法装载、并显示未解析的符号 __bad_udelay,则说明模块在调用 udelay 时传入了太大的值。但需要注意的是,这种编译时的检查只能在常量值上进行,而且并不是所有的平台都实现了这种检查。作为一般性的规则,如果我们打算延迟上千个纳秒,则应该使用 udelay 而不是 ndelay; 类似地,毫秒级的延迟也应该利用 mdelay 而不是更细粒度的短延迟函数。
要重点记住的是,这三个延迟函数均是忙等待函数,因而在延迟过程中无法运行其他任务。这样,这些函数将重复 jitbusy 的行为,只是在不同的量级上。因此,我们应该只在没有其他实用方法时使用这些函数。
实现毫秒级(或者更长)延迟还有另一种方法,这种方法不涉及忙等待。<linux/delay.h> 文件声明了下面这些函数:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)
前两个函数将调用进程休眠以给定的 millisecs。对 msleep 的调用是不可中断的;我们可以确信进程将至少休眠给定的毫秒数。如果驱动程序正在某个等待队列上等待,而又希望有唤醒能够打断这个等待的话,则可使用 msleep_interruptible。msleep_interruptible 的返回值通常是零;但是,如果进程被提前唤醒,那么返回值就是原先请求休眠时间的剩余毫秒数。对 ssleep 的调用将使进程进入不可中断的休眠,但休眠时间以秒计。
通常,如果我们能够容忍比所请求更长的延迟,则应当使用 chedule_timeout、sleep 或者 ssleep。
3、内核定时器
如果我们需要在将来的某个时间点调度执行某个动作,同时在该时间点到达之前不会阻塞当前进程,则可以使用内核定时器。内核定时器可用来在未来的某个特定时间点(基于时钟滴答)调度执行某个函数,从而可用于完成许多任务;例如,如果硬件无法产生中断,则可以周期性地轮询设备状态。另一个内核定时器的典型应用是关闭软驱马达,或者结束其他长时间的关闭操作。在这种情况下,在 close 方法返回前进行延迟将会给应用程序带来不必要的(甚至令人惊讶的)开销。最后,内核本身也在许多情况下使用了定时器,包括在 schedule_timeout 的实现中。
一个内核定时器是一个数据结构,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的通数。其实现位于 <linux/timer.h> 和 kernel/timer.c 文件,我们将在 “内核定时器的实现” 一节中对此进行详细描述。
被调度运行的函数几乎肯定不会在注册这些函数的进程正在执行时运行。相反,这些函数会异步地运行。到此为止,我们提供的示例驱动程序代码都在进程执行系统调用的上下文中运行。但是,当定时器运行时,调度该定时器的进程可能正在休眠或在其他处理器上执行,成干脆已经退出。
这种异步执行类似于硬件中断发生时的情景(我们会在第十章详细讨论)。实际上,内核定时器常常是作为 “软件中断” 的结果而运行的。在这种原子性的上下文中运行时,代码会受到许多限制。定时器函数必须以我们在第五章 “自旋锁和原子上下文” 一节中讨论的方式原子地运行,但是这种非进程上下文还带来其他一些问题。现在我们要讨论这些限制,这些限制还会在本书后面多次出现。我们也会对此多次重复,原子上下文中的这些规则必须遵守,否则会导致大麻烦。
许多动作需要在进程上下文中才能执行。如果处于进程上下文之外(比如在中断上下文中),则必须遵守如下规则:
- 不允许访问用户空间。因为没有进程上下文,无法将任何特定进程与用户空间关联起来。
- current 指针在原子模式下是没有任何意义的,也是不可用的,因为相关代码和被中断的进程没有任何关联。
- 不能执行休眠或调度。原子代码不可以调用 schedule 或者 wait_event,也不能调用任何可能引起休眠的函数。例如,调用 kmaIloc(…,GFP_KERNEL) 就不符合本规则。信号量也不能用,因为可能引起休眠。
(1)定时器 API
// include/linux/timer.h
struct timer_list {/* ... */unsigned long expires;void (*function)(unsigned long);unsigned long data;
};void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);void add_timer(struct timer_list *timer);
int del_timer(struct timer_list *timer);
上面给出的数据结构其实包含其他一些未列出的字段,但给出的三个字段是可由定时器代码以外的代码访问。expires 字段表示期望定时器执行的 jiffies 值;到达该 jiffies 值时,将调用 function 函数,并传递 data 作为参数。如果需要通过这个参数传递多个数据项,那么可以将这些数据项据绑成一个数据结构,然后将该数据结构的指针强制转换成 unsigned long 传入。这种技巧在所有内核支持的体系架构上都是安全的,而且在内存管理(参见第十五章的讨论)中非常常见。expires 的值并不是 jiffies_64 项,这是因为定时器并不适用于长的未来时间点,而且 32 位平台上的 64 位操作会比较慢。
除了上面给出的函数及接口以外,内核定时器 API 还包括其他几个函数。下面给出这些
函数的完整描述:
int mod_timer(struct timer_list *timer, unsigned long expires);
更新某个定时器的到期时间,经常用于超时定时器(典型的例子是软驱的关马达定
时器)。我们也可以在通常使用 add_timer 的时候在不活动的定时器上调用 mod_timer。
int del_timer_sync(struct timer_list *timer);
和 del_timer 的工作类似,但该函数可确保在返回时没有任何 CPU 在运行定时器函
数。del_timer_sync 可用于在 SMP 系统上避免竞态,这和单处理器内核中的
del_timer 是一样的。在大多数情况下,应优先考虑调用这个函数而不是 del_timer
函数。如果从非原子上下文调用,该函数可能休眠,但在其他情况下会进入忙等待。
在拥有锁时,应格外小心调用 del_timer_sync,因为如果定时器函数企图获取相同
的锁,系统就会进入死锁。如果定时器函数会重新注册自己,则调用者必须首先确
保不会发生重新注册;这通常通过设置一个由定时器函数检查的 “关闭” 标志来实现。
int timer_pending(const struct timer_list * timer);
该函数通过读取 timer_list 结构的一个不可见字段来返回定时器是否正在被调度运行。
(2)内核定时器的实现
可参考 ==> (2)软定时器和延迟函数
尽管要使用内核定时器并不必知道它们的具体实现,但其实现非常有意思,而了解其内部也是值得的。
内核定时器的实现要满足如下需求及假定:
- 定时器的管理必须尽可能做到轻量级。
- 其设计必须在活动定时器大量增加时具有很好的伸缩性。
- 大部分定时器会在最多几秒或者几分钟内到期,而很少存在长期延迟的定时器。
- 定时器应该在注册它的同一 CPU 上运行。
内核开发发者使用的解决方案是利用 Per-CPU 数据结构。timer_list 结构的 base 字段包含了指向该结构的指针。如果 base 为 NULL、定时器尚未调度运行;否则,该指针会告诉我们哪个数据结构(也就是哪个 CPU)在运行定时器。Per-CPU 数据项在第八章的 “Per-CPU 变量” 一节中描述。
不管何时内核代码注册了一个定时器(通过 add_timer 或者 mod_timer),其操作最终会由 internal_add_timer(定义在 kernel/timer.c 中)执行,该函数又会将新的定时器添加到和当前 CPU 关联的 “级联表” 中的定时器双向链表中。
级联表的工作方式如下: 如果定时器在接下来的 0 ~ 255 个 jiffies 中到期,则该定时器就会被添加到 256 个链表中的一个(这取决于 expires 字段的低 8 位值),这些链表专用于短期定时器。如果定时器会在较远的未来到期(但在 16384 个 jiffies 之前),则该定时器会被添加到 64 个链表之一(这取决于 expires 字段的 9 ~ 14 位,共 6 位,值为 64)。对更远将来的定时器,相同的技巧用于 15 ~ 20 位(从 1 开始 ?)、21 ~ 26 位以及 27 - 31 位。如果定时器的 expires 字段代表了更远的未来(只可能发生在 64 位系统上),则利用延迟值 0xfffffff 做散列(hash)运算,而在过去时间内到期的定时器会在下一个定时器滴答时被调度(在高负荷的情况下,有可能注册一个已经到期的定时器,尤其在运行抢占式内核时)。
当 __run_timers 被激发时,它会执行当前定时器滴答上的所有挂起的定时器。如果 jiffies 当前是 256 的倍数,该函数还会将下一级定时器链表重新散列到 256 个短期链表中,同时还可能根据上面 jiffies 的位划分对将其他级别的定时器做级联处理。
这种方法虽然初看起来有些复杂,但能很好地处理定时器不多或有大量定时器的情况。用来管理每个活动定时器所需的必要时间和已注册的定时器数量无关,同时被限于定时器 expires 字段二进制表达上的几个逻辑操作。这种实现唯一的开销在于 512 个链表头(256 个短期链表以及 4 组 64 个的长期链表)占用了 4KB 的存储空间。
如同 /proc/jitimer 所描述的,函数 __run_timers 运行在原子上下文中。除了我们已经描述过的限制外,这带来了一个有趣的特点:定时器会在正确的时间到期,即使我们运行的不是抢占式的内核,而 CPU 会忙于内核空间。如果读者在后台读取 /proc/jitbusy 而在前台读取 /etc/jitimer 时,就能看到这个特点。尽管系统似乎被忙等待系统调用整个锁住,但内核定时器仍然可很好地工作。
但需要谨记的是,内核定时器离完美还有很大距离,因为它受到 jitter 以及由硬件中断、其他定时器和异步任务所产生的影响。和简单数字 I/O 关联的定时器对简单任务来说足够了,比如控制步进电机或者业余电子设备,但通常不适合于工业环境下的生产系统。对这类任务,我们需要借助某种实时的内核扩展。
// kernel/timer.c
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
{unsigned long expires = timer->expires;unsigned long idx = expires - base->timer_jiffies;struct list_head *vec;if (idx < TVR_SIZE) {int i = expires & TVR_MASK;vec = base->tv1.vec + i;} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {int i = (expires >> TVR_BITS) & TVN_MASK;vec = base->tv2.vec + i;} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;vec = base->tv3.vec + i;} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;vec = base->tv4.vec + i;} else if ((signed long) idx < 0) {/** Can happen if you add a timer with expires == jiffies,* or you set a timer to go off in the past*/vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK);} else {int i;/* If the timeout is larger than 0xffffffff on 64-bit* architectures then we use the maximum timeout:*/if (idx > 0xffffffffUL) {idx = 0xffffffffUL;expires = idx + base->timer_jiffies;}i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;vec = base->tv5.vec + i;}/** Timers are FIFO:*/list_add_tail(&timer->entry, vec);
}
4、tasklet
和定时问题相关的另一个内核设施是 tasklet(小任务)机制。中断管理(第十章将进一步描述)中大量使用了这种机制。
tasklet 在很多方面类似内核定时器:它们始终在中断期间
运行,始终会在调度它们的同一 CPU 上运行,而且都接收一个 unsigned long 参数。和内核定时器不同的是,我们不能要求 tasklet 在某个给定时间执行。调度一个 tasklet,表明我们只是希望内核选择某个其后的时间来执行给定的函数。这种行为对中断处理例程来说尤其有用,中断处理例程必须可能快地管理硬件中断,而大部分数据管理则可以安全地延迟到其后的时间。实际上,和内核定时器类似,tasklet 也会在 “软件中断” 上下文以原子模式
执行。软件中断是打开硬件中断的同时执行某些异步任务的一种内核机制
。
tasklet 以数据结构的形式存在,并在使用前必须初始化。调用特定的函数或者使用特定的宏来声明该结构,即可完成 tasklet 的初始化:
#include <linux/interrupt.h>
struct tasklet_struct {/* ... */void (*func) (unsigned long);unsigned long data;
);void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
tasklet 为我们提供了许多有意思的特性:
- 一个 tasklet 可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet 才会被执行。
- 和定时器类似,tasklet 可以注册自己本身。
- tasklet 可被调度以在通常的优先级或者高优先级执行。高优先级的 tasklet 总会首先执行
- 如果系统负荷不重,则 tasklet 会立即得到执行,但始终不会晚于下一个定时器滴答。
- 一个 tasklet 可以和其他 tasklet 并发,但对自身来讲是严格串行处理的,也就是说,同一 tasklet 永远不会在多个处理器上同时运行。当然我们已经指出,tasklet 始终会在调度自己的同一 CPU 上运行。
用来实现 /proc/jitasklet 和 /proc/jitasklethi 的 jit 模块代码几乎和实现 /proc/jitimer 的代码一模一样,只是前者使用了 tasklet 调用而不是定时器接口。下面的清单描述了tasklet 相关的内核接口,可在 tasklet 结构被初始化之后使用:
void tasklet_disable(struct tasklet_struct *t);
这个函数禁用指定的 tasklet。该 tasklet 仍然可以用 tasklet_schedule 调度,但其执
行被推迟,直到该 tasklet 被重新启用。如果 tasklet 当前正在运行,该函数会进入
忙等待直到 tasklet退出为止;因此,在调用 tasklet_disable 之后,我们可以确信该
tasklet 不会在系统中的任何地方运行。
void tasklet_disable_nosync(struct tasklet_struct *t);
禁用指定的 tasklet,但不会等待任何正在运行的 tasklet 退出。该函数返回后,tasklet
是禁用的,而且在重新启用之前,不会再次被调度。但是,当该函数返回时,指定
的 tasklet 可能仍在其他 CPU 上运行。
void tasklet_enable(struct tasklet_struct *t);
启用一个先前被禁用的 tasklet。如果该 tastlet 已经被调度,它很快就会运行。对
tasklet_enable 的调用必须和每个对 tasklet_disable 的调用匹配,因为内核对每个
tasklet 保存有一个 “禁用计数”。
void tasklet_schedule(struct tasklet_struct *t);
调度执行指定的 tasklet。如果在获得运行机会之前,某个 tasklet 被再次调度,则该
tasklet 只会运行一次。但是如果在该 tasklet 运行时被调度,就会在完成后再次运
行。这样,可确保正在处理事件时发生的其他事件也会被接收并注意到。这种行为
也允许 tasklet 重新调度自身。
void tasklet_hi_schedule(struct tasklet_struct *t);
调度指定的 tasklet 以高优先级执行。当软件中断处理例程运行时,它会在处理其他
软件中断任务(包括 “通常” 的 tasklet)之前处理高优先级的 tasklet。理想状态下,
只有具备低延迟需求的任务(比如填充音频缓冲区)才能使用这个函数,这样可避
免由其他软件中断处理例程引入的额外延迟。和 /proc/jitasklet 相比,/proc/jitasklethi
给出了肉眼能察觉的区别。
void tasklet_kill(struct tasklet_struct *t);
该函数确保指定的 tasklet 不会被再次调度运行;当设备要被关闭或者模块要被移除
时,我们通常调用这个函数。如果 tasklet 正被调度执行,该函数会等待其退出。如
果 tasklet 重新调度自己,则应该避免在调用 tasklet_kill 之前完成重新调度,这和
del_timer_sync 的处理类似。
tasklet 的实现在 kernel/softirq.c 中。其中有两个(通常优先级和高优先级的)tasklet 链表,它们作为 per-CPU 数据结构而声明,并且使用了类似内核定时器那样的 CPU 相关机制。tasklet 管理中使用的数据结构是个简单的链表,因为 tasklet 不必像内核定时器那样来处理时间问题。
5、工作队列
从表面看来,工作队列(workqueue)类似于 tasklet,它们都允许内核代码请求某个函数在将来的时间被调用。但是,两者之间存在一些非常重要的区别,其中包括:
- tasklet 在软件中断上下文中运行,因此,所有的 tasklet 代码都必须是原子的。相反,工作队列函数在一个特殊内核进程的上下文中运行,因此它们具有更好的灵活性。尤其是,工作队列函数可以休眠。
- tasklet 始终运行在被初始提交的同一处理器上,但这只是工作队列的默认方式。
- 内核代码可以请求工作队列函数的执行延迟给定的时间间隔。
两者的关键区别在于:tasklet 会在很短的时间段内很快执行,并且以原子模式执行、而工作队列函数可具有更长的延迟并且不必原子化。两种机制有各自适合的情形。
工作队列有 struct workqueue_struct 的类型,该结构定义在 <linux/workqueue.h> 中。在使用之前,我们必须显式地创建一个工作队列,可使用下面两个函数之一:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
每个工作队列有一个或多个专用的进程(“内核线程”),这些进程运行提交到该队列的函数。如果我们使用 create_workqueue,则内核会在系统中的每个处理器上为该工作队列创建专用的线程。在许多情况下,众多的线程可能对性能具有某种程度的杀伤力;因此,如果单个工作线程足够使用,那么应该使用 create_singlethread_workqueue 创建工作队列。
要向一个工作队列提交一个任务,需要填充一个 work_struct 结构,这可通过下面的
宏在编译时完成:
// include/linux/workqueue.h
struct work_struct {atomic_long_t data;
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)struct list_head entry;work_func_t func;
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};DECLARE_WORK(name, void (*function) (void *), void *data);
其中,name 是要声明的结构名称,function 是要从工作队列中调用的函数,而 data 是要传递给该函数的值。如果要在运行时构造 work_struct 结构,可使用下面两个宏:
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void (*function) (void *), void *data);
INIT_WORK 完成更加彻底的结构初始化工作;在首次构造该结构时,应该使用这个宏。 PREPARE_WORK 完成几乎相同的工作,但它不会初始化用来将 work_struct 结构链接到工作队列的指针。如果结构已经被提交到工作队列,而只是需要修改该结构,则应该使用 PREPARE_WORK 而不是 INIT_WORK。
如果要将工作提交到工作队列,则可使用下面的两个函数之一:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue,struct work_struct *work, unsignedlong delay);
它们都会将 work 添加到给定的 queue 。但是如果使用 queue_delayed_work,则实际的工作至少会在经过指定的 jiffies(由 delay 指定)之后才会被执行。如果工作被成功添加到队列,则上述函数的返回值为 1。返回值为非零时意味着给定的 work_struct 结构已经等待在该队列中、从而不能两次加入该队列。
在将来的某个时间,工作函数会被调用,并传入给定的 data 值。该函数会在工作线程的上下文运行,因此如果必要,它可以休眠 —— 当然,我们应该仔细考虑休眠会不会影响提交到同一工作队列的其他任务。但是该函数不能访问用户空间,这是因为它运行在内核线程,而该线程没有对应的用户空间可以访问。
如果要取消某个挂起的工作队列入口项,可调用:
int cancel_delayed_work(struct work_struct *work);
如果该入口项在开始执行前被取消,则上述函数返回非零值。在调用 cancel_delayed_work 之后,内核会确保不会初始化给定入口项的执行。但是,如 cancel_delayed_work 返回 0,则说明该入口项已经在其他处理器上运行,因此在 cancel_delayed_work 返回后可能仍在运行。为了绝对确保在 cancel_delayed_work 返回 0 之后,工作函数不会在系统中的任何地方运行,则应该随后调用下面的函数:
void flush_workqueue(struct workqueue_struct *queue);
在 flush_workqueue 返回后,任何在该调用之前被提交的工作函数都不会在系统任何地方运行。
在结束对工作队列的使用后,可调用下面的函数释放相关资源:
void destroy_workqueue(struct workqueue_struct *queue);
(1)共享队列
在许多情况下,设备驱动程序不需要有自己的工作队列。如果我们只是偶尔需要向队列中提交任务,则一种更简单、更有效的办法是使用内核提供的共享的默认工作队列。但是,如果我们使用这个工作队列,则应该记住我们正在和其他人共享该工作队列。这意味着,我们不应该长期独占该队列,即不能长时间休眠,而且我们的任务可能需要更长的时间才能获得处理器时间。
如果需要取消已提交到共享队列中的工作入口项,则可使用上面描述过的 cancel_delayed_work 函数。但是,刷新共享工作队列时需要另一个函数:
void flush_scheduled_work(void);
因为我们无法知道其他人是否在使用该队列,因此我们也无法知道在 flush_scheduled_work 返回前到底要花费多少时间。
6、快速参考
本章引入了如下符号:
(1)计时
#include <linux/param.h>
HZ
Hz 符号指出每秒钟产生的时钟滴答数。
#include <linux/jiffies.h>
volatile unsigned long jiffies
u64 jiffies_64
jiffies_64 变量会在每个时钟滴答递增,也就是说,它会在每秒递增 HZ 次。内
核代码大部分情况下使用 jiffies,在 64 位平台上,它和 jiffies_64 是一样的,
而在 32 位平台上,jiffies 是 jiffies_64 的低 32 位。
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigmed long b) ;
int time_before_eq(unsigned long a, unsigned long b);
这些布尔表达式以安全方式比较 jiffies,无需考虑计数器溢出的问题,也不必访问 jiffies_64。
u64 get_jiffies_64(void);
// 无竞态地获取 jiffies_64 的值。#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);
// 在 jiffies 表示的时间和其他表示法之间转换。
#include <asm/msr.h>
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var32);
x86 专用的宏,用来读取时间截计数器。上述宏用两个 32 位字的形式读取该计数
器,要么读取低 32 位,要么整个读取到一个 long long 型的变量中。
#include <linux/timex.h>
cycles_t get_cycles(void);
// 以平台无关的方式返回时间戳计数器。如果 CPU 不提供时间戳特性,则返回 0。#include <linux/time.h>
unsigned long mktime(year, mon, day, h, m, s);
// 根据 6 个无符号的 int 参数返回自 Epoch 以来的秒数。void do gettimeofday(struct timeval *tv);
// 以自 Epoch 以来的秒数和毫秒数的形式返回当前时间,并且以硬件能提供的最好分
// 辨率返回。在大多数平台上,分辨率是微秒或更好的级别,但某些平台只能提供 jiffies 级
// 的分辨率。struct timespec current_kernel_time(void);
// 以 jiffies 为分辨率返回当前时间。
(2)延迟
#include <linux/wait.h>
long wait_event_interruptible_timeout(wait_queue_head_t *q, condition, signed long timeout);
// 使当前进程休眠在等待队列上,并指定用 jiffies 表达的超时值。如果要进入不可中
// 断休眠,则应使用 schedule_timeout(见下)。#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);
// 调用调度器,确保当前进程可在给定的超时值之后被唤醒。调用者必须首先调用
// set_current_state 将自己置于可中断或不可中断的休眠状态。#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
// 引入整数的纳秒、微秒和毫秒级延迟。实际达到的延迟至少是请求的值,但可能更
// 长。传入每个函数的参数不能超过平台相关的限制(通常是几千)。void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
// 使进程休眠给定的毫秒数(或使用 ssleep 休眠给定的秒数)。
(3)内核定时器
#include <asm/hardirq.h>
int in_interrupt(void);
int in_atomic(void);
// 返回布尔值以告知调用代码是否在中断上下文或者在原子上下文中执行。中断上下
// 文在进程上下文之外,可能正处理硬件或软件中断。原子上下文是指不能进行调度
// 的时间点,比如中断上下文或者拥有自旋锁时的进程上下文。#include <linux/timer.h>
void init_timer(struct timer_list * timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
// 上面的函数以及静态声明定时器结构的宏是初始化 timer_list 数据结构的两种方式。void add_timer(struct timer_list * timer);
// 注册定时器结构,以在当前 CPU 上运行。
int mod_timer(struct timer_list *timer, unsigned long expires);
// 修改一个已经调度的定时器结构的到期时间。它也可以替代 add_timer 函数使用。
int timer_pending(struct timer_list * timer);
// 返回布尔值的宏、用来判断给定的定时器结构是否已经被注册运行。
void del_timer(struct timer_list * timer);
void del_timer_sync(struct timer_list * timer);
// 从活动定时器清单中删除一个定时器。后一个函数确保定时器不会在其他 CPU 上运行。
(4)tasklet
#include <linux/interrupt.h>
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
void tasklet_init(struct tasklet_struct *t, void (*func) (unsigned long),unsigned long data);
// 前面两个宏声明一个 tasklet 结构,而 tasklet_init 函数初始化一个通过分配或者其
// 他途径获得的 tasklet 结构。第二个 DESCLARE 宏禁用给定的 tasklet。void tasklet_disable(struct tasklet_struct *t);
void tasklet_disable_nosync(struct tasklet_struct *t);
void tasklet_enable(struct tasklet_struct *t);
// 禁用或重新启用某个 tasklet。每次禁止都要匹配一次使能(我们可以禁用一个已经
// 被禁用的 tasklet)。tasklet_disable 函数会在 tasklet 正在其他 CPU 上运行时等待,
// 而 nosync 版本不会完成这个额外的步骤。void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
// 调度运行某个 tasklet,可以是"通常"的 tasklet或者一个高优先级的 tasklet。当执
// 行软件中断时,高优先级的 tasklet 会被首先处理,而通常的 tasklet 最后运行。void tasklet_kill(struct tasklet_struct *t);
// 如果指定的 tasklet 被调度运行,则将其从活动链表中删除。和 tasklet_disable 类
// 似,该函数可在 SMP 系统上阻塞,以便等待正在其他 CPU 上运行的该 tasklet 终止。
(5)工作队列
#include <linux/workqueue.h>
struct workqueue_struct;
struct work_struct;
// 上述结构分别表示工作队列和工作入口项。struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
void destroy_workqueue(struct workqueue_struct *queue);
// 用于创建和销毁工作队列的函数。调用 create_workqueue 将创建一个队列,且系统
// 中的每个处理器上都会运行一个工作线程;相反,create_singlethread_workqueue
// 只会创建单个工作进程。DECLARE_WORK(name, void (*function)(void *), void *data);
INIT_WORK(struct work_struct *work, void (*furction)(void *), void *data);
PREPARE_WORK(struct work_struct *work, vo.d (*function) (void *),void *data);
// 用于声明和初始化工作队列入口项的宏。int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, structwork_struct *work, unsigned long delay);
// 用来安排工作以便从工作队列中执行的函数。int cancel_delayed_work(struct work_struct *work);
void flush_workqueue(struct workqueue_struct *queue);
// 使用 cancel_delayed_work 可从工作队列中删除一个入口项; flush_workqueue 确保
// 系统中任何地方都不会运行任何工作队列入口项。int schedule_work(struct work_struct *work);
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
void flush_scheduled_work(void);
// 使用共享工作队列的函数。
八、分配内存
1、 kmalloc函数的内幕
可参考 ==> 4、kmalloc 函数
(1)flags 参数
#include <linux/slab.h>
void *kmalloc(size_t size, int flags)
kmalloc 的第一个参数是要分配的块的大小,第二个参数是分配标志(flags),更有意思的是、它能够以多种方式控制 kmalloc 的行为。
最常用的标志是 GFP_KERNEL,它表示内存分配(最终总是调用 get_free_pages 来实现实际的分配,这就是 GFP_ 前级的由来)是代表运行在内核空间的进程执行的。换句话说,这意味着调用它的函数正代表某个进程执行系统调用。使用 GFP_KERNEL 允许 kmalloc 在空闲内存较少时把当前进程转入休眠以等待一个页面。因此,使用 GFP_KERNEL 分配内存的函数必须是可重入的。在当前进程休眠时,内核会采取适当的行动,或者是把缓冲区的内容刷写到硬盘上,或者是从一个用户进程换出内存,以获取一个内存页面。
GFP_KERNEL 分配标志并不是始终适用,有时 kmalloc 是在进程上下文之外被调用的,例如在中断处理例程、tasklet 以及内核定时器中调用。这种情况下 current 进程就不应该休眠,驱动程序则应该换用 GFP_ATOMIC 标志。内核通常会为原子性的分配预留一些空闲页面。使用 GFP_ATOMIC 标志时,kmalloc 甚至可以用掉最后一个空闲页面。不过如果连最后一页都没有了,分配就返回失败。
除了GFP_KERNEL 和 GFP_ATOMIC 外,还有一些其他的标志可用于替换或补充这两个标志,不过这两个标志已经可以满足大多数驱动程序的需要了。所有的标志都定义在 <linux/gfp.h> 中,有个别的标志使用两个下划线作为前级,比如 __GFP_DMA。另外,还有一些符号表示这些标志的常用组合,它们没有这种前级,并且有时称为 “分配优先级”。
2、 后备高速缓存
设备驱动程序常常会反复地分配很多同一大小的内存块。既然内核已经维护了一组拥有同一大小内存块的内存池,那么为什么不为这些反复使用的块增加某些特殊的内存池呢? 实际上,内核的确实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache)。设备驱动程序通常不会涉及这种使用后备高速缓存的内存行为,但也有例外, Linux 2.6 中的 USB 和 SCSI 驱动程序就使用了这种高速缓存。
Linux 内核的高速缓存管理有时称为 “slab 分配器” 。因此,相关函数和类型在 <linux/slab.h> 中声明。slab 分配器实现的高速缓存具有 kmem_cache_t 类型,可通过调用
kmem_cache_create 创建:
kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t offset,unsigned long flags,void (*constructor) (void *, kmem_cache_t *, unsigned long flags),void (*destructor)(void *, kmem_cache_t *, unsigned long flags));
该函数创建一个新的高速缓存对象,其中可以容纳任意数目的内存区域,这些区域的大小都相同,由 size 参数指定。参数 name 与这个高速缓存相关联,其功能是保管一些信息以便追踪问题,它通常被设置为将要高速缓存的结构类型的名字。高速缓存保留指向该名称的指针,而不是复制其内容,因此,驱动程序应该将指向静态存储(通常可取直接字符串)的指针传递给这个函数。名称中不能包含空白。
offset 参数是页面中第一个对象的偏移量,它可以用来确保对已分配的对象进行某种特殊的对齐,但是最常用的就是 0,表示使用默认值。flags 控制如何完成分配,是一个位掩码,可取的值如下:
- SLAB_NO_REAP
设置这个标志可以保护高速缓存在系统寻找内存的时候不会被减少。设置该标志通常不是好主意,因为我们不应该对内存分配器的自由做一些人为的、不必要的限制。 - SLAB_HWCACHE_ALIGN
这个标志要求所有数据对象跟高速缓存行(cache line)对齐;实际的操作则依赖于主机平台的硬件高速缓存布局。如果在 SMP 机器上,高速缓存中包含有频繁访问的数据项的话,则该选项将是非常好的选择。但是,为了满足高速缓存行的对齐需求,必要的填白可能浪费大量内存。 - SLAB_CACHE_DMA
这个标志要求每个数据对象都从可用于 DMA 的内存区段中分配。
还有一些标志可用于高速缓存分配的调试,详情请见 mm/slab.c 文件。但通常这些标志只在开发系统中通过内核配置选项而全局地设置。
constructor 和 destructor 参数是可选的函数(但是不能只有 destructor 而没有 constructor);前者用于初始化新分配的对象,而后者用于 “清除” 对象 —— 在内存空间被整个释放给系统之前。
constructor 和 destructor 很有用,不过使用时有一些限制。constructor 函数是在分配用于一组对象的内存时调用的。因为这些内存中可能会包含好几个对象,所以 constructor 函数可能会被多次调用。我们不能认为分配一个对象后随之就会调用一次 constructor。类似地,destructor 函数也有可能不是在一个对象释放后就立即被调用,而是在将来的某个未知的时间才被调用。constructor 和 destructor 可能允许也可能不允许休眠,这要看是否向它们传递了 SLAB_CTOR_ATOMIC 标志(CTOR 是 constructor 的简写)。
为了简便起见,程序员可以使用同一个函数同时作为 constructor 和 destructor 使用;当调用的是一个 constructor 函数的时候,slab 分配器总是传递 SLAB_CTOR_CONSTRUCTOR 标志。
一旦某个对象的高速缓存被创建,就可以调用 kmem_cache_alloc 从中分配内存对象:
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
这里、参数 cache 是先前创建的高速缓存;参数 flags 和传递给 kmalloc 的相同,并且当需要分配更多内存来满足 kmem_cache_alloc 时,高速缓存还会利用这个参数。
释放一个内存对象时使用 kmem_cache_free:
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
如果驱动程序代码中和高速缓存有关的部分已经处理完了(一个典型情况是模块被卸载的时候),这时驱动程序应该释放它的高速缓存,如下所示:
int kmem_cache_destroy(kmem_cache_t *cache);
这个释放操作只有在已将从缓存中分配的所有对象都归还后才能成功。所以,模块应该检查 kmem_cache_destroy 的返回状态;如果失败、则表明模块中发生了内存泄漏(因为有一些对象被漏掉了)。
使用后备式缓存带来的另一个好处是内核可以统计高速缓存的使用情况。高速缓存的使用统计情况可以从 /proc/slabinfo 获得。
(1)基于 slab 高速缓存的 scull:scullc
现在该举个例子了。scullc 是 scull 模块的一个缩减版本,只实现了裸设备 —— 即持久的内存区。与 scull 使用 kmalloc 不同的是,scullc 使用内存高速缓存。数据对象的大小可以在编译或加载时修改,但不能在运行时修改 —— 那样需要创建一个新的内存高速缓存,而这里不必处理那些不需要的细节问题。
scullc 是一个完整的例子,可以用于测试 slab 分配器。它和 scull 只有几行代码的不同。首先,我们必须声明自己的 slab 高速缓存:
/* 声明一个高速缓存指针,它将用于所有设备 */
kmem_cache_t *scullc_cache;
slab 高速缓存的创建代码如下所示(在模块装载阶段):
/* scullc_init: 为我们的量子创建一个高速缓存 */
scullc_cache = kmem_cache_create("scullc", scullc_quantum, 0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* 没有 ctor/dtor */
if (!scullc_cache) {scullc_cleanup();return -ENOMEM;
}
下面是分配内存量子的代码:
/* 使用内存高速缓存来分配一个量子 */
if (!dptr->data[s_pos]) {dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);if (!dptr->data[s_pos])goto nomem:memset(dptr->data[s_pos], 0, scullc_quantum);
}
下面的代码将释放内存:
for (i = 0; i < qset; i++)if (dptr->data[i])kmem_cache_free(scullc_cache, dptr->data[i]);
最后,在模块卸载期间,我们必须将高速缓存返回给系统:
/* scullc_cleanup: 释放量子使用的高速缓存 */
if (scullc_cache)kmem_cache_destroy(scullc_cache);
和 scull 相比,scullc 的最主要差别是运行速度略有提高,并且对内存的利用率更佳。由于数据对象是从内存池中分配的,而内存池中的所有内存块都具有同样大小,所以这些数据对象在内存中的位置排列达到了最大程度的密集,相反的,scull 的数据对象则会引入不可预测的内存碎片。
(2)内存池
内核中有些地方的内存分配是不允许失败的。为了确保这种情况下的成功分配,内核开发者建立了一种称为内存池(或者 “mempool” )的抽象。内存池其实就是某种形式的后备高速缓存,它试图始终保存空闲的内存,以便在紧急状态下使用。
内存池对象的类型为 mempool_t(在 <linux/mempool.h> 中定义),可使用 mempool_create 来建立内存池对象:
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn,void *pool_data);
min_nr 参数表示的是内存池应始终保持的已分配对象的最少数目。对象的实际分配和释放由 alloc_fn 和 free_fn 函数处理,其原型如下:
typedef void * (mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t) (void *element, void *pool_data);
mempool_create 的最后一个参数,即 pool_data,被传入 alloc_fn 和 free_fn。
如有必要,我们可以为 mempool 编写特定用途的函数来处理内存分配。但是,通常我们仅会让内核的 slab 分配器为我们处理这个任务。内核中有两个函数(mempool_alloc_slab 和 mempool_free_slab),它们的原型和上述内存池分配原型匹配,并利用 kmem_cache_alloc 和 kmem_cache_free 处理内存分配和释放。因此,构造内存池的代码通常如下所示:
cache = kmem_cache_create(. . .);
pool = mempool_create(MY_POOL_MINIMUM, mempool_alloc_slab, mempool_free_slab, cache);
在建立内存池之后,可如下所示分配和释放对象:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
在创建 mempool 时,就会多次调用分配函数为预先分配的对象创建内存池。之后,对 mempool_alloc 的调用将首先通过分配函数获得该对象;如果该分配失败,就会返回预先分配的对象(如果存在的话)。如果使用 mempool_free 释放一个对象,则如果预先分配的对象数目小于要求的最低数目,就会将该对象保留在内存池中;否则,该对象会返回给系统。
我们可以利用下面的函数来调整 mempool 的大小:
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
如果对该函数的调用成功,将把内存池的大小调整为至少有 new_min_nr 个预分配对象。如果不再需要内存池,可使用下面的通数将其返回给系统:
void mempool_destroy(mempool_t *pool);
在销毁 mempool 之前,必须将所有已分配的对象返回到内存池中,否则会导致内核 oops。
如果读者计划在自己的驱动程序中使用 mempool,则应记住下面这点:mempool 会分配一些内存块,空闲且不会真正得到使用。因此,使用 mempool 很容易浪费大量内存。几乎在所有情况下,最好不使用 mempool 而是处理可能的分配失败。如果驱动程序存在某种方式可以响应分配的失败,而不会导致对系统一致性的破坏,则应该使用这种方式,也就是说,应尽量避免在驱动程序代码中使用 mempool。
3、get_free_page 和相关函数
可参考 ==> 3、页操作
如果模块需要分配大块的内存,使用面向页的分配技术会更好些。整页的分配还有其他
优点,以后会在第十五章介绍。
分配页面可使用下面的函数:
get_zeroed_page(unsigned int flags);
// 返回指向新页面的指针并将页面清零。
_get_free_page(unsigned int flags);
// 类似于 get_zeroed_page,但不清零页面。
_get_free_pages(unsigned int flags, unsigned int order);
// 分配若干(物理连续的)页面,并返回指向该内存区域第一个字节的指针,但不清零页面。
参数 flags 的作用和 kmalloc 中的一样;通常使用 GFP_KERNEL 或 GFP_ATOMIC,也许还会加上 __GFP_DMA 标志(申请可用于 ISA 直接内存访问操作的内存)或者 __GFP_HIGHMEM 标志(使用高端内存)。参数 order 是要申请或释放的页面数
的以 2 为底的对数(即 log2N)。例如,order(阶数)为 0 表示一个页面,order 为 3 表示 8 个页面。如果 order 太大, 而又没有那么大的连续区域可以分配,就会返回失败。get_order 函数使用一个整数参数,可根据宿主平台上的大小(必须是 2 的幕)返回 order 值。可允许的最大 order 值是 10 或者 11(对应于 1024 或 2048 个页),这依赖于体系结构。但是,相比具有大量内存的刚刚启动的系统而言,以阶数值为 10 进行分配而成功的机会很小。
如果读者对此好奇,/proc/buddyinfo 可告诉你系统中每个内存区段上每个阶数下可获得的数据块数目。
当程序不再需要使用页面时,它可以使用下列函数之一来释放它们。第一个函数是一个宏、展开后就是对第二个函数的调用:
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
如果试图释放和先前分配数目不等的页面,内存映射关系就会被破坏,随后系统就会出错。
值得强调的是,只要符合和和 kmalloc 同样的规则,get_free_pages 和其他函数可以在任何时间调用。某些情况下函数分配内存时会失败,特别是在使用了 GFP_ATOMIC 的时候。因此,调用了这些函数的程序在分配出错时都应提供相应的处理。
尽管 kmalloc (GFP_KERNEL) 在没有空闲内存时有时会失败,但内核总会尽可能满足这个内存分配请求。因此,如果分配太多内存,系统的响应性能就很容易降下来。例如,如果往 scull 设备写入大量数据,计算机可能就会死掉;当系统为满足 kmalloc 分配请求而试图换出尽可能多的内存页时,就会变得很慢。所有资源都被贪婪的设备所吞噬,计算机很快就变的无法使用了;此时甚至已经无法为解决这个问题而生成新的进程。我们没有在 scull 模块中提到这个问题,因为它只是个例子,并不会真正在多用户系统中使用。但作为一个编程者必须小心,因为模块是特权代码,会带来新的系统安全漏洞,例如很可能会造成 Dos (denail-of-service,拒绝服务攻击)安全漏洞。
(1)alloc_pages 接口
为完整起见,本节将介绍内存分配的另一个接口,但在第十五章才会使用这个接口。现在,我们只要知道 struct page 是内核用来描述单个内存页的数据结构就足够了。我们将看到,内核中有许多地方需要使用 page 结构,尤其在需要使用高端内存(高端内存在内核空间没有对应不变的地址)的地方。
Linux 页分配器的核心代码是称为 alloc_pages_node 的函数:
struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);
这个函数具有两个变种(它们只是简单的宏),大多数情况下我们使用这两个宏:
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);
核心函数 alloc_pages_node 要求传入三个参数。nid 是 NUMA 节点的 ID 号(注 3),表示要在其中分配内存,flags 是通常的 GFP_ 分配标志,而 order 是要分配的内存大小。该函数的返回值是指向第一个 page 结构(可能返回多个页)的指针,它描述了已分配的内存;或者在失败时返回NULL。
alloc_pages 通过在当前的 NUMA 节点上分配内存而简化了 alloc_pages_node 函数,它将 numa_node_id 的返回值作为 nid 参数而调用了 alloc_pages_node 函数。另外, alloc_page 函数显然忽略了 order 参数而只分配单个页面。
为了释放通过上述途径分配的页面,我们应使用下面的函数:
void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
如果读者知道某个页面中的内容是否驻留在处理器高速缓存中,则应该使用 free_hot_page(用于驻留在高速缓存中的页)或者 free_cold_page 和内核通信。这个信息可帮助内存分配器优化内存的使用。
4、 vmalloc及其辅助函数
下面要介绍的内存分配函数是 vmalloc,它分配虚拟地址空间的连续区域。尽管这段区域在物理上可能是不连续的(要访问其中的每个页面都必须独立地调用函数 alloc_page),内核却认为它们在地址上是连续的。vmalloc 在发生错误时返回 0(NULL 地址),成功时返回一个指针,该指针指向一个线性的、大小最少为 size 的线性内存区域。
我们在这里描述 vmalloc 的原因是,它是 Linux 内存分配机制的基础。但是,我们要注意在大多数情况下不鼓励使用 vmalloc。通过 vmalloc 获得的内存使用起来效率不高,而且在某些体系架构上,用于 vmalloc 的地址空间总量相对较小。如果希望将使用 vmalloc 的代码提交给内核主线代码,则可能会受到冷遇。如果可能,应该直接和单个的页面打交道,而不是使用 vmalloc。
虽然这么说 ,但我们还是要看看如何使用 vmalloc 。该函数的原型及其相关函数(ioremap、并不是严格的分配函数,将在本节后面讨论)如下所示:
#include <linux/vmalloc.h>
void *vmalloc(unsigmed long size);
void vfree(void * addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void * addr);
要强调的是,由 kmalloc 和 __get_free_pages 返回的内存地址也是虚拟地址,其实际值仍然要由 MMU(内存管理单元,通常是 CPU 的组成部分)处理才能转为物理内存地址(注 4)。vmalloc 在如何使用硬件上没有区别,区别在于内核如何执行分配任务上。
注 4: 实际上,其些体系架构定义了保留的 “虚拟” 地址范围,用于寻址物理内存。遇到这种情况时,Linux 内核会利用这种特性,内核和 __get_free_pages 地址均位于这种内存范围。其中的区别对设备驱动程序是透明的,对不直接涉及内存管理子系统的其他内核代码来说也是透明的。
kmalloc 和 __get_free_pages 使用的(虚拟)地址范围与物理内存是一一对应的,可能会有基于常量 PAGE_OFFSET 的一个偏移。这两个函数不需要为该地址段修改页表。但另一方面,vmalloc 和 ioremap 使用的地址范围完全是虚拟的,每次分配都要通过对页表的适当设置来建立(虚拟)内存区域。
可以通过比较内存分配函数返回的指针来发现这种差别。在某些平台上(如 x86),vmalloc 返回的地址仅仅比 kmalloc 返回的地址高一些;而在其他平台上(如 MIPS 和 IA-64),它们就完全属于不同的地址范围了。vmalloc 可以获得的地址在 VMALLOC_START 到 VMALLOC_END 的范围中。这两个符号都在 <asm/pgtable.h> 中定义。
用 vmalloc 分配得到的地址是不能在微处理器之外使用的,因为它们只在处理器的内存管理单元上才有意义。当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的 DMA 地址),就不能使用 vmalloc 了。使用 vmalloc 函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。注意 vmalloc 的开销要比 __get_free_pages 大,因为它不但获取内存,还要建立页表。因此,用 vmalloc 函数分配仅仅一页的内存空间是不值得的。
使用 vmalloc 函数的一个例子函数是 create_module 系统调用,它利用 vmalloc 函数来获取装模块所需的内存空间。在调用 insmod 来重定位模块代码后,接着会调用 copy_from_user 函数把模块代码和数据复制到分配而得的空间内。这样,模块看来像是在连续的内存空间内。但通过检查 /proc/ksyms 文件就能发现模块导出的内核符号和内核本身导出的符号分布在不同的内存范围上。
用 vmalloc 分配得到的内存空间要用 vfree 函数来释放,这就像要用 kfree 函数来释放 kmalloc 函数分配得到的内存空间一样。
和 vmalloc 一样,ioremap 也建立新的页表,但和 vmalloc 不同的是,ioremap 并不实际分配内存。ioremap 的返回值是一个特殊的虚拟地址,可以用来访问指定的物理内存区域、这个虚拟地址最后要调用 iounmap 来释放掉。
ioremap 更多用于映射(物理的)PCI 缓冲区地址到(虚拟的)内核空间。例如,可以用来访问 PCI 视频设备的帧缓冲区;该缓冲区通常被映射到高物理地址,超出了系统初始化时建立的页表地址范围。PCI 的详细内容将在第十二章中讨论。
要注意,为了保持可移植性,不应把 ioremap 返回的地址当作指向内存的指针而直接访问。相反,应该使用 readb 或其他 I/O 函数(在第九章 “使用 I/O 内存” 一节中介绍)。这是因为,在如 Alpha 的一些平台上,由于 PCI 规范和 Alpha 处理器在数据传输方式上的差异,不能直接把 PCI 内存区映射到处理器的地址空间。
ioremap 和 vmalloc 函数都是面向页的(它们都会修改页表),因此重新定位或分配的内存空间实际上都会上调到最近的一个页边界。ioremap 通过把重新映射的地址向下下调到页边界,并返回在第一个重新映射页面中的偏移量的方法模拟了不对齐的映射。
vmalloc 函数的一个小缺点是它不能在原子上下文中使用,因为它的内部实现调用了 kmalloc(GFP_KERNEL) 来获取页表的存储空间,因而可能休眠。但这不是什么问题 —— 如果 __get_free_page 函数都还不能满足中断处理例程的需求的话,那应该修改软件的设计了。
5、 per-CPU变量
per-CPU(每 CPU)变量是 2.6 内核的一个有趣特性。当建立一个 per-CPU 变量时,系统中的每个处理器都会拥有该变量的特有副本。这看起来有些奇怪,但它有其优点。对 per-CPU 变量的访问(几乎)不需要锁定,因为每个处理器在其自己的副本上工作。per-CPU 变量还可以保存在对应处理器的高速缓存中,这样,就可以在频繁更新时获得更好的性能。
关于 per-CPU 变量使用的例子可见于网络子系统中。内核维护着大量计数器,这些计数器跟踪已接收到的各类数据包数量,而这些计数器每秒可能被更新上千次。网络子系统的开发者将这些统计用的计数器放在了 per-CPU 变量中,这样,他们就不需要处理缓存和锁定问题,而更新可在不用锁的情况下快速完成。在用户空间偶尔请求这些计数器的值时,只需将每个处理器的版本相加并返回合计值即可。
用于 per-CPU 变量的声明可见于 <linux/percpu.h> 中。要在编译期间创建一个 per-CPU 变量,可使用下面的宏:
DEFINE_PER_CPU(type, name);
如果该变量(称为 name)是一个数组,需在 type 中包含数组的维数。这样,具有三个整数的 per-CPU 数组变量可通过下面的语句建立:
DEFINE_PER_CPU(int[3], my_percpu_array);
对 per-CPU 变量的操作几乎不使用任何锁定即可完成。但要记得 2.6 内核是抢占式的;也就是说,当处理器在修改某个 per-CPU 变量的临界区中间,可能会被抢占,因此应该避免这种情况的发生。我们还应该避免进程正在访问一个 per-CPU 变量时被切换到另一个处理器上运行。为此,我们应该显式地调用 get_cpu_var 宏访问某给定变量的当前处理器副本,结束后调用 put_cpu_var。对 get_cpu_var 的调用将返回当前处理器变量版本的 Ivalue 值,并禁止抢占。因为返回的是 Ivalue,因此可直接赋值或者操作。例如,网络代码对一个计数器的递增使用了下面的两条语句:
get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);
我们可以使用下面的宏访问其他处理器的变量副本:
per_cpu(variable, int cpu_id);
如果我们要编写的代码涉及到多个处理器的 per-CPU 变量,这时则需要采用某种锁定机制来确保访问安全。
动态分配 per-CPU 变量也是可能的。这时,应使用下面的函数分配变量:
void *alloc_percpu(type);
void *__alloc_percpu(size_t size, size_t align);
在大多数情况下可使用 alloc_percpu 完成分配工作;但如果需要特定的对齐,则应该调用 __alloc_percpu 函数。不管使用哪个函数,可使用 free_percpu 将 per-CPU 变量返回给系统。对动态分配的 per-CPU 变量的访问通过 per_cpu_ptr 完成:
per_cpu_ptr(void *per_cpu_var, int cpu_id);
这个宏返回指向对应于给定 cpu_id 的 per_cpu_var 版本的指针。如果打算读取该变量的其他 CPU 版本,则可以引用该指针并进行相关操作。但是,如果正在操作当前处理器的版本,则应该首先确保自己不会被切换到其他处理器上运行。如果对 per-CPU 变量的整个访问发生在拥有某个自旋锁的情况下,则不会出现任何问题。但是,在使用该变量的时候通常需要使用 get_cpu 来阻塞抢占。这样,使用动态 per-CPU 变量的代码类似下面所示:
int cpu;
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);
/* 使用 ptr */
put_cpu();
如果使用编译期间的 per-CPU 变量,则 get_cpu_var 和 put_cpu_var 宏将处理这些细节。但动态的 per-CPU 变量需要更明确的保护。
Per-CPU 变量可以导出给模块,但是必须使用上述宏的特殊版本:
EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);
要在模块中访问这样一个变量,则应将其声明如下:
DECLARE_PER_CPU(type, name);
使用 DECLARE_PER_CPU(而不是 DEFINE_PER_CPU),将告诉编译器要使用一个外部引用。
如果读者打算使用 per-CPU 变量来建立简单的整数计数器,可参考 <linux/percpu_counter.h> 中已封装好的实现。最后要注意,在某些体系架构上,per-CPU 变量可使用的地址空间是受限制的。因此,如果要创建 per-CPU 变量,则应该保持这些变量较小。
6、获取大的缓冲区
我们在前面的小节中提到,大的、连续内存缓冲区的分配易流于失败。系统内存会随着时间的流逝而碎片化,这导致无法获得真正的大内存区域。因为,不需要大的缓冲区也可以有其他途径来完成自己的工作,因此内核开发者并没有将大缓冲区分配工作作为高优先级的任务来计划。在试图获得大内存区之前,我们应该仔细考虑其他的实现途径。
到目前为止,执行大的 I/O 操作的最好方式是通过离散 / 聚集操作,我们将在第十章的 “离散 / 聚集映射” 中讨论这种操作。
(1)在引导时获得专用缓冲区
如果的确需要连续的大块内存用作缓冲区,就最好在系统引导期间通过请求内存来分配。在引导时就进行分配是获得大量连续内存页面的唯一方法,它绕过了 __get_free_pages 函数在缓冲区大小上的最大尺寸和固定粒度的双重限制。在引导时分配缓冲区有点 “脏”、因为它通过保留私有内存池而跳过了内核的内存管理策略。这种技术比较粗暴也很不灵活,但也是最不容易失败的。显然,模块不能在引导时分配内存,而只有直接链接到内核的设备驱动程序才能在引导时分配内存。
还有一个值得注意的问题是,对于普通用户来说引导时的分配不是一个切实可用的选项,因为这种机制只对链接到内核映像中的代码可用。要安装或替换使用了这种分配技术的驱动程序,就只能重新编译内核并重启计算机。
内核被引导时,它可以访问系统所有的物理内存,然后调用各个子系统的初始化函数进行初始化,它允许初始化代码分配私有的缓冲区,同时减少了留给常规系统操作的 RAM 数量。
通过调用下列函数之一则可完成引导时的内存分配:
#include <linux/bootmem.h>
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);
这些函数要么分配整个页(若以 pages 结尾)、要么分配不在页面边界上对齐的内存区。除非使用具有 _low 后级的版本,否则分配的内存可能会是高端内存。如果我们正在为设备驱动程序分配缓冲区,则可能希望将其用于 DMA 操作,而高端内存并不总是支持 DMA 操作;这样,我们可能需要使用上述函数的一个 _low 变种。
很少会释放引导时分配的内存,而且也没有任何办法可将这些内存再次拿回。但是,内核也提供了一种释放这种内存的接口:
void free_bootmem(unsigned long addr, unsigned long size);
注意,通过上述函数释放的部分页面不会返回给系统 —— 但是 ,如果我们使用这种技术,则其实已经分配得到了一些完整的页面。
如果必须使用引导时的分配,则应该将驱动程序直接链接到内核。关于直接链接到内核的实现细节,可参阅内核源代码中 Documentation/kbuild 目录下的文件。
7、快速参考
与内存分配有关的函数和符号如下:
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
void kfree(void *obj);
// 最常用的内存分配接口。
#include <linux/mm.h>
GFP_USER
GFP_KERNEL
GFP_NOFS
GFP_NOIO
GFP_ATOMIC
用来控制内存分配执行方式的标志,其排列从最少限制到最多限制,GFP_USER 和
GFP_KERNEL 优先级允许当前进程休眠以满足分配请求。GFP_NOFS 和 GFP_NOIO 分
别禁用文件系统操作和所有的 I/O 操作,而 GFP_ATOMIC 根本不允许休眠。
__GFP_DMA
__GFP_HIGHMEM
__GFP_COLD
__GFP_NOWARN
__GFP_HIGH
__GFP_REPEAT
__GFP_NOFAIL
__GFP_NORETRY
上述标志在分配内存时修改内核的行为。
#include <linux/malloc.h>
kmem_cache_t *kmem_cache_create(char *name, size_t size, size_t offset,unsigned long flags, constructor(), destructor());
int kmem_cache_destroy(kmem_cache_t *cache);
// 创建和销毁一个包含固定大小内存块的 slab 高速缓存,我们可以从这个高速缓存中
// 分配具有固定大小的对象。
SLAB_NO_REAP
SLAB_HWCACHE_ALIGN
SLAB_CACHE_DMA
在创建高速缓存时指定的标志。
SLAB_CTOR_ATOMIC
SLAB_CTOR_CONSTRUCTOR
可由分配器传递给 constructor 和 destructor 函数的标志。
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
// 从高速缓存中分配和释放一个对象。
/proc/slabinfo
包含有 slab 高速缓存使用统计信息的虚拟文件。
#include <linux/mempool.h>
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,mempool_free_t *free_fn, void *data);
void mempool_destroy(mempool_t *pool);
// 用于创建内存池的函数。内存池通过保留预分配项的"急用链表"来避免内存分配的失败。void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
// 从内存池分配或者释放对象的函数。
unsigned long get_zeroed_page(int flags);
unsigned long __get_free_page(int flags);
unsigned long __get_free_pages(int flags, unsigned long order);
// 面向页的分配函数。get_zeroed_page 返回单个已清零的页面,而其他所有调用不
// 进行页面的初始化。int get_order(unsigned long size);
// 根据 PAGE_SIZE 返回当前平台上和 size 关联的分配阶数。该函数的参数必须是 2
// 的幕,而返回值至少为 0 。void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
// 这些函数释放面向页分配的内存。struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);
// Linux 内核中最底层页分配器的所有变种。void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
// 用于释放由 alloc_page 的某种形式分配的页的不同函数。
#include <linux/vmalloc.h>
void * vmalloc(unsigned long size);
void vfree(void * addr);#include <asm/io.h>
void * ioremap(unsigned long offset, unsigned long size);
void iounmap(void *addr);
// 这些面数分配或释放连续的虚拟地址空间。ioremap 通过虚拟地址访问物理内存,
// 而 vmalloc 分配空闲页面。使用 ioremap 映射的区域用 iounmap 释放,而从 vmalloc
// 获得的页面用 vfree 释放。
#include <linux/percpu.h>
DEFINE_PER_CPU(type, name);
DECLARE_PER_CPU(type, name);
// 定义和声明 per-CPU 变量的宏。per_cpu(variable, int cpu_id)
get_cpu_var(variable)
put_cpu_var(variable)
// 用于访问静态声明的 per-CPU 变量的宏。void *alloc_percpu(type);
void *__alloc_percpu(size_t size, siz_t align);
void free_percpu(void *variable);
// 执行 per-CPU 变量的运行时分配和释放的函数。int get_cpu();
void put_cpu();
per_cpu_ptr(void *variable, int cpu_id)
// get_cpu 获得对当前处理器的引用(因此避免抢占以及切换到其他处理器)并返回
// 处理器的 ID 号;而 put_cpu 返回该引用。为了访问动态分配的 per-CPU 变量,应
// 使用 per_cpu_ptr,并传递要访问的变量版本的 CPU ID 号。对某个动态的 per-CPU
// 变量的当前 CPU 版本的操作,应该包含在对 get_cpu 和 put_cpu 的调用中间。
#include <linux/bootmem.h>
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);
void free_bootmem(unsigned long addr, unsigned long size);
// 在系统引导期间执行内核分配和释放的函数。这些函数只能在直接链接到内核的驱动程序中使用。
九、与硬件通信
1、 I/O 端口和 I/O 内存
可参考 ==> (3)优化和内存屏障
字符驱动之八IO访问(硬件操作)
Android深度探索:HAL与驱动开发学习笔记–内存管理
驱动中动态内存申请(request_mem_region)
关于CPU地址空间
#include <linux/kernel.h>
void barrier(void)
// 这个函数通知编译器插入一个内存屏障,但对硬件没有影响。编译后的代码会把当
// 前 CPU 寄存器中的所有修改过的数值保存到内存中,需要这些数据的时候再重新
// 读出来。对 barrier的调用可避免在屏障前后的编译器优化,但硬件能完成自己的
// 重新排序。
#include <asm/system.h>
void rmb(void);
void read_barrier_depends (void);
void wmb(void);
void mb(void);
// 这些函数在已编译的指令流中插入硬件内存屏障;具体的实现方法是平台相关的。
// rmb (读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。
// wmb 保证写操作不会乱序, mb 指令保证了两者都不会。这些函数都是 barrier 的超集
// read_barrier_depends是一种特殊的、弱一些的读屏障形式。我们知道,rmb 避免
// 屏障前后的所有读取指令被重新排序,而 read_barrier_depends 仅仅阻止某些读取
// 操作的重新排序,这些读取依赖于其他读取操作返回的数据。它和 rmb 的区别很微
// 妙,而且并不是所有的架构上都存在这个函数。除非读者能够正确理解它们之间的
// 差别,并且有理由相信完整的读取屏障会导致额外的性能消耗,否则就应该始终使
// 用 rmb。
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
// 上述屏障宏版本也插入硬件屏障,但仅仅在内核针对 SMP 系统编译时有效;在单
// 处理器系统上,它们均会被扩展为上面那些简单的屏障调用。
2、 使用I/O端口
(1)I/O 端口分配
读者会想到,在尚未取得对这些端口的独占访问之前,我们不应对这些端口进行操作。内核为我们提供了一个注册用的接口,它允许驱动程序声明自己需要操化的端口。该接口的核心函数是 request_region:
#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
这个函数告诉内核,我们要使用起始于 first 的 n 个端口。参数 name 应该是设备的名称。如果分配成功,则返回非 NULL值。如果 request_region 返回 NULL,那么我们就不能使用这些期望的端口。
所有的端口分配可从 /proc/ioports 中得到。如果我们无法分配到需要的端口集合,则可以通过这个 /proc 文件得知哪个驱动程序已经分配了这些端口。
如果不再使用某组 I/O 端口(可能在卸载模块时),则应该使用下面的函数将这些端口返回给系统:
void release_region(unsigned long start, unsigned long n);
下面的函数允许驱动程序检查给定的 I/O 端口集是否可用:
int check_region(unsigned long first, unsigned long n);
这里,如果给定的端口不可用,则返回值是负的错误代码。我们不赞成使用这个函数,因为它的返回值并不能确保分配是否能够成功,这是因为,检查和其后的分配并不是原子的操作。我们在这里列出这个函数,是因为仍有一些驱动程序在使用它,但是我们应该始终使用 request_region,因为这个函数执行了必要的锁定,以确保分配过程以安全、原子的方式完成。
(2)操作 I/O 端口
当驱动程序请求了需要使用的 I/O 端口范围后,必须读取和/或写入这些端口。为此,大多数硬件都会把 8 位、16 位和 32 位的端口区分开来。它们不能像访问系统内存那样混淆使用(注 2)。
注 2: 有时 I/O 端口和内存一样,例如,可以将两个 8 位的操作合并成一个 16 位的操作。如 PC 的显卡就可以,但一般来说不能认为一定具有这种功能。
因此,C 语言程序必须调用不同的函数来访问大小不同的端口。如前一节所述,那些只支持内存映射的 I/O 寄存器的计算机体系架构通过把 I/O 端口地址重新映射到内存地址来伪装端口 I/O,并且为了易于移植,内核对驱动程序隐藏了这些细节。Linux 内核头文件中(在与体系架构相关的头文件 <asm/io.h> 中)定义了如下一些访问 I/O 端口的内联函数。
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
字节(8 位宽度)读写端口。port 参数在一些平台上被定义为 unsigned long,而在另一些平台上被定义为 unsigned short。不同平台上 inb 返回值的类型也不相同。
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
这些函数用于访问 16 位端口(字宽度);不能用于 S390 平台,因为这个平台只支持字节宽度的 I/O 操操作。
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
这些函数用于访问 32 位端口。longword 参数根据不同平台被定义成 unsigned long 类型或 unsigned int 类型。和字宽度 I/O 一样,“长字” I/O 在 S390 平台上也不可使用。
注意: 从现在开始,如果我们使用 unsigned 而不进一步指定类型信息的话,那么就是在谈论一个与体系架构相关的定义,此时不必关心它的准确特性。这些函数基本是可移植的,因为编译器在赋值时会自动进行强制类型转换(cast) 强制转换成 unsigned 类型可防止编译时出现的警告信息。只要程序员赋值时注意避免溢出,这种强制类型转换就不会丢失信息。在本章剩余部分将会一直保持这种 “不完整的类型定义” 的方式。
注意,这里没有定义 64 位的 I/O 操作。即使在 64 位的体系架构上,端口地址空间也只使用最大 32 位的数据通路。
(3)在用户空间访问 I/O 端口
上面这些函数主要是提供给设备驱动程序使用的,但它们也可以在用户空间使用,至少在 PC 类计算机上可以使用。GNU 的 C 库在 <sys/io.h> 中定义了这些函数。如果要在用户空间代码中使用 inb 及其相关函数,则必须满足下面这些条件:
- 编译该程序时必须带 ~O 选项来强制内联通数的展开。
- 必须用 ioperm 或 iopl 系统调用来获取对端口进行 I/O 操作的权限。ioperm 用来获取对单个端口的操作权限,而 iopl 用来获取对整个 I/O 空间的操作权限。这两个函数都是 x86 平台特有的。
- 必须以 root 身份运行该程序才能调用 ioperm 或 iopl(注 3)。或者,进程的祖先进程之一已经以 root 身份获取对端口的访问。
注 3: 从技术上说,必须有 CAP_SYS_RAWIO 的权能,但这与在当前系统上以 root 身份运行是一样的。
如果宿主平台没有 ioperm 和 iopl 系统调用,则用户空间程序仍然可以用 /dev/port 设备文件访问 I/O 端口。不过要注意,该设备文件的含义与平台密切相关,并且除 PC 平台以外,它几乎没有什么用处。
(4)串操作
以上的 I/O 操作都是一次传输一个数据,作为补充,有些处理器上实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字。这些指令称为串操作指令,它们执行这些任务时比一个 C 语言编写的循环语句快得多。下面列出的宏实现了串 I/O,它们或者使用一条机器指令实现,或者在没有串 I/O 指令的平台上使用紧凑循环实现。S390 平台上没有定义这些宏。这不会影响可移植性,因为该平台的外设总线不同,通常不会和其他平台使用同样的设备驱动程序。
串 I/O 函数的原型如下:
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
// 从内存地址 addr 开始连续读 / 写 count 数目的字节。只对单一端口 port 读取或写入数据。void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
// 对一个 16 位端口读 / 写 16 位数据。void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
// Read or write 32-bit values to a single 32-bit port.
// 对一个 32 位端口读 / 写 32 位数据。
在使用串 I/O 操作函数时,需要铭记的是: 它们直接将字节流从端口中读取或写入。因此,当端口和主机系统具有不同的字节序时,将导致不可预期的结果。使用 inw 读取端口将在必要时交换字节,以便确保读入的值匹配于主机的字节序。然而,串函数不会完成这种交换。
3、 使用I/O内存
(1)I/O 内存分配和映射
在使用之前,必须首先分配 I/O 内存区域。用于分配内存区域的接口(在 <linux/ioport.h> 中定义)如下所示:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
该函数从 start 开始分配 len 字节长的内存区域。如果成功,返回非 NULL 指针;否则返回 NULL 值。所有的 I/O 内存分配情况均可从 /proc/iomem 得到。
不再使用已分配的内存区域时,使用下面的接口释放:
void release_mem_region(unsigned long start, unsigned long len);
下面是用来检查给定的 I/O 内存区域是否可用的老函数:
int check_mem_region(unsigned long start, unsigned long len);
但是,和 check_region 一样,这个函数不安全,应避免使用。
分配 I/O 内存并不是访问这些内存之前需要完成的唯一步骤,我们还必须确保该 I/O 内存对内核而言是可访问的。获取 I/O 内存并不仅仅意味着可引用对应的指针;在许多系统上,I/O 内存根本不能通过这种方式直接访问。因此,我们必须首先建立映射。映射的建立由 ioremap 函数完成,我们在第八章的 “vmalloc 及其辅助函数” 一节中介绍过这个函数。该函数专用于为 I/O 内存区域分配虚拟地址。
一旦调用 ioremap(以及 iounmap)之后,设备驱动程序即可访问任意的 I/O 内存地址了,而无论 I/O 内存地址是否直接映射到虚拟地址空间。但要记住,由 ioremap 返回的地址不应直接引用,而应该使用内核提供的 accessor 函数。在我们介绍这些函数之前,首先复习一下 ioremap 的原型并介绍一些先前章节中跳过的细节内容。
我们根据以下的定义来调用 ioremap 函数:
#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);
首先读者会注意到新的函数:ioremap_nocache。我们并没有在第八章介绍这个函数,因为该函数的功能和硬件相关。内核头文件中有如下一段解释: “如果某些控制寄存器也在此类区域,而不希望出现写人组合或者读取高速缓存的话,则可使用该函数。” 实际上,在大多数计算机平台上,该函数的实现和 ioremap 相同: 当所有 I/O 内存已属于非缓存地址时,就没有必要实现 ioremap 的独立的、非缓存版本。
(2)访问 I/O 内存
在某些平台上,我们可以将 ioremap 的返回值直接当作指针使用。但是,这种使用不具有可移植性,而内核开发者正在致力于减少这类使用。访问 I/O 内存的正确方法是通过一组专用于此目的的函数(在 <asm/io.h> 中定义)。
要从 I/O 内存中读取,可使用下面函数之一:
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
其中,addr 应该是从 ioremap 获得的地址(可能包含一个整数偏移量);返回值则是从给定 I/O 内存读取到的值。
还有一组用于写入 I/O 内存的类似函数集如下:
void iowrite8(u8 value, void *addr);
void iowrite16(ul6 value, void *addr);
void iowrite32(u32 value, void *addr);
如果必须在给定的 I/O 内存地址处读 / 写一系列的值,则可使用上述函数的重复版本:
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, ursigned long count);
void ioread32_rep(void *addr, void *buf, ursigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);
上述函数从给定的 buf 向给定的 addr 读取或写入 count 个值。注意,count 以被写入的数据大小为单位表示,比如,ioread32_rep 从 addr 中读取 count 个 32 位的值到 buf 中。
上面给出的通数均在给定的 addr 处执行所有的 I/O 操作。如果我们要在一块 I/O 内存上执行操作,则可以使用下面的函数之一:
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);
上述函数和 C 函数库的对应函数功能一致。
如果读者阅读内核源代码,可能会遇到一组老的 I/O 内存函数。这些函数仍能工作,但不鼓励在新的代码中使用这些函数。主要原因是因为这些函数不执行类型检查,因此其安全性较差。这些函数(宏)的原型如下:
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
这些宏用来从 I/O 内存检索 8 位、16 位和 32 位的数据。
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
类似前面的函数,这些函数(宏)用来写 8 位、16 位和 32 位的数据项。
一些 64 位平台还提供了 readq 和 writeq,用于 PCI 总线上的 4 字(8 字节)内存操作。这个 4 字(quad-word)的命名是个历史遗留问题,那时候所有的处理器都只有16 位的字。实际上,现在把 32 位的数值叫做 L(长字)已经是不正确的了,不过如果对一切都重新命名、只会把事情搞得更复杂。
(3)像 I/O 内存一样使用端口
某些硬件具有一种有趣的特性: 某些版本使用 I/O 端口,而其他版本使用 I/O 内存。导出给处理器的寄存器在两种情况下都是一样的,但访问方法却不同。为了让处理这类硬件的驱动程序更加易于编写,也为了最小化 I/O 端口和内存访问之间的表面区别,2.6 内核引入了 ioport_map 函数:
void *ioport_map(unsigned long port, unsigned int count);
该函数重新映射 count 个 I/O 端口,使其看起来像 I/O 内存。此后,驱动程序可在该函数返回的地址上使用 ioread8 及其同类函数,这样就不必理会 I/O 端口和 I/O 内存之间的区别了。
当不再需要这种映射时,需要调用下面的函数来撤消:
void ioport_unmap(void *addr);
这些函数使得 I/O 端口看起来像内存。但需要注意的是、在重新映射之前、我们必须通过 request_region 来分配这些 I/O 端口。
4、 快速参考
本章引入下列与硬件管理有关的符号:
#include <linux/kernel.h>
void barrier(void)
// 这个 "软件" 内存屏障要求编译器考虑执行到该指令时所有的内存易变性。
#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
// 硬件内存屏障。要求 CPU (和编译器)执行该指令时检查所有必需的内存读、写
// (或二者兼有)已经执行完毕。
#include <asm/io.h>
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned doubleword, unsigned port);
// 这些函数用于读和写 I/O端口。如果用户空间的程序有访问端口的权限,则也可以
// 调用这些函数。unsigned inb_p(unsigned port);
// 如果 I/O 操作之后需要一小段延时,可以用上面介绍的函数的 6 个暂停式的变体。
// 这些暂停式的函数都以p结尾。void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
void insl(unsigned port, void *addr, unsigned long count);
void outsl (unsigned port, void *addr, unsigned long count);
// 这些 "串操作函数" 为输入端口与内存区之间的数据传输做了优化。这类传输是通
// 过对同一端口连续读 / 写 count 次实现的。#include <linux/ioport.h>
struct resource *request_region(unsigned long start, unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);
int check_region(unsigned long start, unsigned long len);
// 为 I/O 端口分配资源的函数。check_ 函数在成功时返回 0,出错时返回负值,但我
// 们不建议使用该函数。struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len);
int check_mem_region(unsigned long start, unsigned long len);
// 这些函数处理对内存区域的资源分配。#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void *virt_addr);
// ioremap 把一个物理地址范围重新映射到处理器的虚拟地址空间,以供内核使用。
// iounmap 用来解除这个映射。#include <asm/io.h>
unsigned int ioread8(void *addr);
unsigned int ioreadi6(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);
// 用来访问 I/O 内存的函数。void ioread8_rep(void *addr, void *buf, unsigned 1ong count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count):
void iowrite32_rep(void *addr, const void *buf, unsiged long count);
// I/O 内存访问原语的 "重复" 版本。unsigned readb(address);
unsigned readw(address);
unsigned readl (address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigmed value, address);
memset_io(address, value, count);
memcpy_fromio(dest, source, nbytes);
memcpy_toio(dest, source, nbytes);
// 也是用来访问 I/O 内存的函数,但老一些且不安全。void *ioport_map(unsigned long port, unsigned int count);
void ioport_unmap(void *addr);
// 如果驱动程序作者希望将I/O 端口作为 I/O 内存一样进行操作,则可将这些端口传
// 递给 ioport_map函数。不再使用这种映射时,应该使用 ioport_unmap 函数解除映射。