策略:如何使用这些能力
在编写驱动时, 程序员应当编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求. 驱动应当做到使硬件可用, 将所有关于如何使用硬件的事情留给应用程序
编写驱动需要注意的地方:
内核空间和用户空间不能直接操作,必须通过特别的函数(copy_from_user/copy_to_user)来操作
内核线程只有一个非常小的堆栈; 它可能小到一个4096 字节的页. 驱动模块的函数必须与内核函数共享这个堆栈. 因此, 声明一个巨大的自动变量不是一个好主意; 如果需要大的结构, 应当在调用时动态分配.
以双下划线(__)开始的函数通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调用这个函数, 确信你知道你在做什么."
内核代码不能做浮点算术
每个进程的系统栈空间分配的大小为2个连续的物理页面(通常来讲是8K),而task_struct占了大约1K(在栈的底部), 所以系统空间非常有限,在中断/软中断/驱动程序中不允许嵌套太深或使用大量局部变量
编写缺少进程上下文的函数需要注意:
current指针在原子态没有意义, 并且不能使用因为相关的代码没有和已被中断的进程的联系.
不能进行睡眠或者调度. 原子代码不能调用 schedule 或者某种 wait_event, 也不能调用任何其他可能睡眠的函数. 例如, 调用 kmalloc(..., GFP_KERNEL) 是违犯规则的. 旗标也必须不能使用因为它们可能睡眠.
重要的数据结构的重要成员
struct task_struct {(得到当前进程task_struct结构指针的宏为: current)
volatile long state: 进程状态. -1 unrunnable; 0 runnable; >0 stopped
mm_segment_t addr_limit: 线程地址空间: 0-0xBFFFFFFF for user-thead; 0-0xFFFFFFFF for kernel-thread
struct mm_struct *mm: 虚存管理与映射相关信息,是整个用户空间的抽象
unsigned long sleep_time:
pid_t pid: 进程pid
uid_t uid,euid,suid,fsuid:
gid_t gid,egid,sgid,fsgid:
gid_t groups[NGROUPS]:
kernel_cap_t cap_effective, cap_inheritable, cap_permitted:
int keep_capabilities:1:
struct user_struct *user:
char comm[16]: 命令名称. 由当前进程执行的程序文件的基本名称( 截短到 15 个字符, 如果需要 )
struct tty_struct *tty:
unsigned int locks:
struct rlimit rlim[RLIM_NLIMITS]: 当前进程各种资源分配的限制, 如current->rlim[RLIMIT_STACK]是对用户空间堆栈大小的限制
struct files_struct *files: 打开的文件
};
struct file_operations{
struct module *owner;是一个指向拥有这个结构的模块的指针. 这个成员用来当模块在被使用时阻止其被卸载. 一般初始化为: THIS_MODULE
loff_t (*llseek) (struct file *, loff_t, int);用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char *, size_t, loff_t *);从设备中获取数据. 空指针导致read系统调用返回-EINVAL("Invalid argument") . 非负返回值代表了成功读取的字节数
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);发送数据给设备. 空指针导致write 系统调用返回-EINVAL. 非负返回值代表成功写的字节数.
unsigned int (*poll) (struct file *, struct poll_table_struct *);3 个系统调用的后端: poll, epoll, 和 select. 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);提供了发出设备特定命令的方法. 注意:有几个 ioctl 命令被内核识别而不会调用此方法
int (*mmap) (struct file *, struct vm_area_struct *);请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL,系统调用返回 -ENODEV.
int (*open) (struct inode *, struct file *);open一个设备文件. 如果这个项是 NULL, 设备打开一直成功
int (*release) (struct inode *, struct file *);在文件结构被释放时引用这个操作. 即在最后一个打开设备文件的文件描述符关闭时调用(而不是每次close时都调用)
int (*fsync) (struct file *, struct dentry *, int datasync);fsync系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
int (*fasync) (int, struct file *, int);通知设备它的 FASYNC 标志(异步通知)的改变. 这个成员可以是NULL 如果驱动不支持异步通知.
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);包含多个内存区的单个读操作; 如果为 NULL, read方法被调用( 可能多于一次 ).
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);包含多个内存区的单个写操作; 如果为 NULL, write方法被调用( 可能多于一次 ).
}
struct file{
struct dentry *f_dentry;关联到文件的目录入口( dentry )结构. 设备驱动不需要关心, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.
struct file_operations *f_op;和文件关联的操作. 可改变之, 并在返回后新方法会起作用. 例如, 关联到主编号1 (/dev/null, /dev/zero...)的open根据打开的次编号来更新filp->f_op
unsigned int f_flags;文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作
mode_t f_mode;文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 检查是有内核做的,所以驱动里不需要再次检查
loff_t f_pos;当前读写位置. 驱动可以读这个值, 但是正常地不应该改变它; 读和写应当使用它们的最后一个参数来更新一个位置. 一个例外是在 llseek 方法中, 它的目的就是改变文件位置.
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
void *private_data; 可自由使用或者忽略它.
}
struct inode{
kdev_t i_rdev;对于代表设备文件的节点, 这个成员包含实际的设备编号. 需要这两个函数来操作: unsigned int iminor(struct inode *inode)/unsigned int imajor(struct inode *inode)
struct char_device *i_cdev; 内核的内部结构, 代表字符设备. 当节点是一个字符设备文件时, 这个成员包含一个指针, 指向这个结构
}
下图展示了函数调用和函数指针在模块中如何使用来增加新功能到一个运行中的内核.
编译和加载
内核版本的问题
linux/version.h中有下面的宏定义:
UTS_RELEASE
这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, "2.6.10".
LINUX_VERSION_CODE
这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). [
]有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.
KERNEL_VERSION(major,minor,release)
这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如, KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.
特殊GNU make变量名
obj-m := module.o #最终模块名
module-objs := file1.o file2.o #最终模块用到的obj列表
make命令中的"M="选项使 makefile 在试图建立模块目标(obj-m 变量中指定的)前, 回到你的模块源码目录
Makefile示例:
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
加载卸载:
insmod:仅加载指定的模块)
modprobe:加载指定的模块及其相关模块. 它查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果未定义的symbols, modprobe 在模块搜索路径(/etc/modprobe.conf)中寻找并加载其他定义了symbols的模块
rmmod: 从内核中去除指定模块
lsmod: 打印内核中当前加载的模块的列表. 通过读取/proc/modules或/sys/module 的 sysfs 虚拟文件系统工作
错误信息:
unresolved symbols: 加载是找不到symbols. 可能是使用了未定义的symbols; 也可能是需要使用modprobe试一下
-1 Invalid module format:编译模块用的内核源代码版本与当前运行的内核的版本不匹配
内核的config配置
kernel hacking菜单
CONFIG_DEBUG_KERNEL:
这个选项只是使其他调试选项可用; 它应当打开, 但是它自己不激活任何的特性.
CONFIG_DEBUG_SLAB:
这个重要的选项打开了内核内存分配函数的几类检查. 激活这些检查, 就可能探测到一些内存覆盖和遗漏初始化的错误.被分配的每一个字节在递交给调用者之前都设成 0xa5, 随后在释放时被设成 0x6b.内核还会在每个分配的内存对象的前后放置特别的守护值; 如果这些值曾被改动, 内核知道有人已覆盖了一个内存分配区.
CONFIG_DEBUG_PAGEALLOC:
满的页在释放时被从内核地址空间去除(full pages are removed from the kernel address space when freed)(?). 这个选项会显著拖慢系统, 但是它也能快速指出某些类型的内存损坏错误.
CONFIG_DEBUG_SPINLOCK
激活这个选项, 内核捕捉对未初始化的自旋锁的操作, 以及各种其他的错误( 例如 2 次解锁同一个锁 ).
CONFIG_DEBUG_SPINLOCK_SLEEP
这个选项激活对持有自旋锁时进入睡眠的检查. 实际上, 如果你调用一个可能会睡眠的函数, 它就发出警告, 即便这个有疑问的调用没有睡眠.
CONFIG_INIT_DEBUG
用__init (或者 __initdata) 标志的项在系统初始化或者模块加载后都被丢弃. 这个选项激活了对代码的检查, 这些代码试图在初始化完成后存取初始化时内存.
CONFIG_DEBUG_INFO
这个选项使得内核在建立时包含完整的调试信息. 如果你想使用 gdb 调试内核, 你将需要这些信息. 如果你打算使用 gdb, 你还要激活 CONFIG_FRAME_POINTER.
CONFIG_MAGIC_SYSRQ
激活"魔术 SysRq"键.
CONFIG_DEBUG_STACKOVERFLOW
CONFIG_DEBUG_STACK_USAGE
这些选项能帮助跟踪内核堆栈溢出. 堆栈溢出的确证是一个 oops 输出, 但是没有任何形式的合理的回溯. 第1个选项给内核增加了明确的溢出检查; 第 2 个使得内核监测堆栈使用并作一些统计, 这些统计可以用魔术 SysRq 键得到.
CONFIG_KALLSYMS
这个选项(在"Generl setup/Standard features"下)使得内核符号信息建在内核中; 缺省是激活的. 符号选项用在调试上下文中; 没有它, 一个 oops 列表只能以 16 进制格式给你一个内核回溯, 这不是很有用.
CONFIG_IKCONFIG
CONFIG_IKCONFIG_PROC
这些选项(在"Generl setup"菜单)使得完整的内核配置状态被建立到内核中, 可以通过 /proc 来使其可用. 大部分内核开发者知道他们使用的哪个配置, 并不需要这些选项(会使得内核更大). 但是如果你试着调试由其他人建立的内核中的问题, 它们可能有用.
Power management/ACPI菜单
CONFIG_ACPI_DEBUG
这个选项打开详细的 ACPI (Advanced Configuration and Power Interface) 调试信息, 如果你怀疑一个问题和 ACPI 相关可能会用到
Device drivers菜单
CONFIG_DEBUG_DRIVER
打开了驱动核心的调试信息, 可用以追踪低层支持代码的问题.
Device drivers/SCSI device support菜单
CONFIG_SCSI_CONSTANTS
建立详细的 SCSI 错误消息的信息. 如果你在使用 SCSI 驱动, 你可能需要这个选项.
Device drivers/Input device support菜单
CONFIG_INPUT_EVBUG
如果你使用一个输入设备的驱动, 这个选项可能会有用. 然而要小心这个选项的安全性的隐含意义: 它记录了你键入的任何东西, 包括你的密码.
Profiling support菜单
CONFIG_PROFILING
剖析通常用在系统性能调整, 但是在追踪一些内核挂起和相关问题上也有用.
显示所有的用户空间程序发出的系统调用. 并以符号形式显示调用的参数和返回值.当一个系统调用失败, 错误的符号值(例如, ENOMEM)和对应的字串(Out of memory) 都会显示.
-t: 来显示每个系统调用执行的时间
-T: 来显示调用中花费的时间
-e: 来限制被跟踪调用的类型
-o: 来重定向输出到一个文件. 缺省地, strace 打印调用信息到 stderr.
GDB调试
调试内核:
gdb /usr/src/linux/vmlinux /proc/kcore
第一个参数是非压缩的 ELF 内核可执行文件的名子, 不是 zImage 或者 bzImage
第二个参数是核心文件的名子.
注意事项:
无法检查module的相关内容
不能修改内核数据, 不能单步,不能设置断点
读到的是内核即时映象,内核仍在运行,所以有些数据可能会与即时值不匹配--刷新映象:core-file /proc/kcore
调试模块(内核版本2.6.7 以上)
Linux 可加载模块是 ELF 格式的可执行映象;ELF被分成几个sections. 其中有 3 个典型的sections与调试会话相关:
.text
这个节包含有模块的可执行代码. 调试器必须知道在哪里以便能够给出回溯或者设置断点.
.bss
在编译时不初始化的任何变量在 .bss 中
.data
在编译时需要初始化的任何变量在 .data 里.
为了gdb能够调试可加载模块需要通知调试器一个给定模块的各个sections加载在哪里. 这个信息在 /sys/module/module_name/sections下. 包含名子为 .text , .bss, .data等文件; 每个文件的内容是那个section的基地址.
gdb的add-symbol-file命令用来加载模块相关信息
add-symbol-file 模块名 text所在的基地址 -s .bss bss所在基地址 -s .data data所在基地址
add-symbol-file ../scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836be0
KDB调试
KDB是来自oss.sgi.com的一个非官方补丁. 应用KDB时不应该运行任何程序, 特别的, 不能打开网络. 一般地以单用户模式启动系统
进入KDB:
Pause(或者 Break) 键启动调试器
一个内核 oops(异常?) 发生时
命中一个断点时
命令:
bp function_name
在下一次内核进入function_name时停止
bt
打印出调用回溯中每个函数的参数
mds variable_name
mds address
查看变量/内存数据
mm address value
将value赋给address所指向的内存
......
不同体系结构下各个类型的大小
arch Size: 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 4 4 8 1 2 4 8
x86_64 1 2 4 8 8 8 1 2 4 8
应当安排有明确类型大小的数据类型, 如u8, u16, ... uint8_t, uint16_t, ...
接口特定的类型, 请参考原文
其他移植性问题:
Tick: HZ
页大小: PAGE_SIZE
页偏移: PAGE_SHIFT
字节序:
条件编译
#include
#ifdef __BIG_ENDIAN
......
#endif
#ifdef __LITTLE_ENDIAN
......
#endif
转换
#include
#include
u32 cpu_to_le32 (u32);
u32 le32_to_cpu (u32);
cpu_to_le16/le16_to_cpu/cpu_to_le64/....
cpus_to_le16/le16_to_cpus/cpus_to_le64/....
带's'后缀的是有符号版
数据对齐:
存取不对齐的数据应当使用下列宏
#include
get_unaligned(ptr);
put_unaligned(val, ptr);
指针返回值:
有时候内核函数会返回已编码的指针值来指示错误, 这类返回值是否有效的测试应当使用下列宏
void *ERR_PTR(long error); 将错误码转换成指针形式
long IS_ERR(const void *ptr); 判断一个返回值是否有效
long PTR_ERR(const void *ptr); 提取返回的错误码, 在提取前需要判断返回值是否有效
链表:
鼓励使用内核自带的struct list_head结构来构造双向链表
#include
struct list_head { struct list_head *next, *prev; };
LIST_HEAD(struct list_head);
编译时初始化
INIT_LIST_HEAD(struct list_head*)
运行时初始化
list_add(struct list_head *new, struct list_head *head);
在head后链入new. 常用来构造FILO
list_add_tail(struct list_head *new, struct list_head *head);
在head前链入new, 常用来构造FIFO
list_del(struct list_head *entry);
把entry从链表中脱链
list_del_init(struct list_head *entry);
把entry从链表中脱链并重新初始化entry
list_move(struct list_head *entry, struct list_head *head);
将entry移动到head后
list_move_tail(struct list_head *entry, struct list_head *head);
将entry移动到head前
list_empty(struct list_head *head);
判断链表是否为空
list_splice(struct list_head *list, struct list_head *head);
从链表head后断开, 将list链表链接进去
list_entry(struct list_head *ptr, type_of_struct, field_name);
从list_head地址得到包含list_head的结构体的开始地址.
类似的宏为container_of(pointer, container_type, container_field); 从结构体成员地址得到结构体指针
list_for_each(struct list_head *cursor, struct list_head *list)
这个宏创建一个for循环, 执行一次, cursor 指向链表中的下个入口项
list_for_each_prev(struct list_head *cursor, struct list_head *list)
这个版本反向遍历链表.
list_for_each_safe(struct list_head *cursor, struct list_head *next, struct list_head *list)
如果循环可能删除链表中的项, 使用这个版本.
list_for_each_entry(type *cursor, struct list_head *list, member)
直接得到包含list_head的结构体的地址
list_for_each_entry_safe(type *cursor, type *next, struct list_head *list, member)
如果循环可能删除链表中的项, 使用这个版本
举例:
struct list_head todo_list;
...
void todo_add_entry(struct todo_struct *new)
{
struct list_head *ptr;
struct todo_struct *entry;
list_for_each(ptr, &todo_list)
{
entry = list_entry(ptr, struct todo_struct, list);
if (entry->priority priority) {
list_add_tail(&new->list, ptr);
return;
}
}
list_add_tail(&new->list, &todo_struct)
}
模块特殊宏/函数(注意大小写)
module_init(initialization_function): 声明模块初始化函数
module_exit(cleanup_function): 声明模块注销函数
EXPORT_SYMBOL(name): 声明符号在模块外可用
EXPORT_SYMBOL_GPL(name): 声明符号仅对使用 GPL 许可的模块可用.
MODULE_LICENSE("GPL"): 声明模块许可
MODULE_AUTHOR: 声明谁编写了模块
MODULE_DESCRIPION: 一个人可读的关于模块做什么的声明
MODULE_VERSION: 一个代码修订版本号; 看 的注释以便知道创建版本字串使用的惯例
MODULE_ALIAS: 模块为人所知的另一个名子
MODULE_DEVICE_TABLE: 来告知用户空间, 模块支持那些设备
module_param(name, type, perm): 声明模块加载时允许设置的参数( 2.6.11之前版本中为MODULE_PARM
module_param_array(name,type,num,perm): 声明模块加载时允许设置的数组参数
name: 是你的参数(数组)的名子
type: 是数组元素的类型
bool/invbool: 一个布尔型( true 或者 false)值(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.
charp
int/long/short/uint/ulong/ushort: 基本的变长整型值. 以 u 开头的是无符号值.
num: 一个整型变量
perm: 通常的权限值, 在 中定义. S_IRUGO: 可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR: 允许 root 改变参数. 注意, 如果一个参数被 sysfs 修改, 模块看到的参数值也改变了, 但模块不会有任何通知
示例:
在模块中声明如下:
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
调用时如下:
insmod moduel_name howmany=10 whom="Mom"
模块初始化函数原型为
static int __init function(void);
大部分注册函数以 register_ 做前缀
__init/__initdata: 给定的函数/数据只是在初始化使用. 模块加载后丢掉这个初始化函数, 使它的内存可做其他用途.
__devinit/__devinitdata: 内核没有配置支持 hotplug 设备时等同于__init/_initdata.
模块注销函数原型为
static void __exit function(void);
__exit/__exitdata: 如果模块直接建立在内核里, 或者如果内核配置成不允许模块卸载, 标识为__exit 的函数被简单地丢弃
比如:
struct test{int a; int b; int c; inte}test_t;
&test_t ==
这个宏将test_setup这个函数放在特定的section中.
在执行init/main.c::checksetup()时会去kernel boot commandline中寻找字符串"test=xxx": 如果有找到,就用"xxx"作为参数调用test_setup; 否则不运行
在insmod中如果参数里带有"test=xxx"也会运行
get_free_page申请的page数限制: 2^MAX_ORDER, 2的MAX_ORDER次方个page. 通常MAX_ORDER=10, 也就是最多2^10=1024个page, 4Mbyte
int access_ok(int type, const void *addr, unsigned long size): 验证用户空间有效性
type: VERIFY_READ/VERIFY_WRITE. 如果需要验证读写许可, 则只要 VERIFY_WRITE
addr: 一个用户空间地址,
size: 需要验证的大小.
返回值: 1 是成功(存取没问题); 0 是失败(存取有问题). 如果它返回0, 驱动应当返回 -EFAULTput_user(datum, ptr)
__put_user(datum, ptr)
写 datum 到用户空间. 它们相对(copy_to_user)快. 传送的数据大小依赖 prt 参数的类型. 比如: prt 是一个char指针就传送一个字节
put_user 检查用户空间确保能写. 在成功时返回 0, 并且在错误时返回 -EFAULT.
__put_user 进行更少的检查(它不调用 access_ok),
驱动应当调用put_user来节省几个周期; 或者拷贝几个项时, 在第一次数据传送之前调用access_ok一次, 之后使用__put_user
get_user(local, ptr)
__get_user(local, ptr)
从用户空间读单个数据, 获取的值存储于本地变量 local;
如果使用上述四个函数时, 发现一个来自编译器的奇怪消息, 例如"coversion to non-scalar type requested". 必须使用 copy_to_user 或者 copy_from_user.
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); 从用户地址空间拷贝一整段数据. 任何存取用户空间的函数必须是可重入的. 此函数可能导致睡眠
这两个函数的作用不仅限于拷贝数据到和从用户空间: 它们还检查用户空间指针是否有效. 如果指针无效, 不进行拷贝; 如果在拷贝中遇到一个无效地址, 只拷贝有效部分的数据. 在第二种情况下, 返回值是 未拷贝的
int capable(int capability);
在进行一个特权操作之前, 一个设备驱动应当检查调用进程有合适的能力
capability 取值有以下这些:
CAP_DAC_OVERRIDE
这个能力来推翻在文件和目录上的存取的限制(数据存取控制, 或者 DAC).
CAP_NET_ADMIN
进行网络管理任务的能力, 包括那些能够影响网络接口的.
CAP_SYS_MODULE
加载或去除内核模块的能力.
CAP_SYS_RAWIO
进行 "raw" I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯.
CAP_SYS_ADMIN
一个捕获-全部的能力, 提供对许多系统管理操作的存取.
CAP_SYS_TTY_CONFIG
进行 tty 配置任务的能力.
CAP_SYS_ADMIN
在任务缺乏一个更加特定的能力时, 可以选这个来测试
int printk(const char * fmt, ...);向 console(而不是虚拟终端)
按消息的严重程度从高到低为:
KERN_EMERG
用于紧急消息, 常常是那些崩溃前的消息.
KERN_ALERT
需要立刻动作的情形.
KERN_CRIT
严重情况, 常常与严重的硬件或者软件失效有关.
KERN_ERR
用来报告错误情况; 设备驱动常常使用 KERN_ERR 来报告硬件故障.
KERN_WARNING
有问题的情况的警告, 这些情况自己不会引起系统的严重问题.
KERN_NOTICE
正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告.
KERN_INFO
信息型消息. 在这个级别, 很多驱动在启动时打印它们发现的硬件的信息.
KERN_DEBUG
用作调试消息.
使用举例:
printk(KERN_INFO "hello, world\n");
int printk_ratelimit(void); 在你认为打印一个可能会常常重复的消息之前调用来避免重复输出很多相同的调试信息. 如果这个函数返回非零值, 继续打印你的消息, 否则跳过打印.
使用举例
if (printk_ratelimit())
printk(KERN_NOTICE "The printer is still on fire\n");
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);
从一个驱动打印消息, 你会想打印与感兴趣的硬件相关联的设备号.两个宏定义都将设备号编码进给定的缓冲区; 唯一的区别是 print_dev_t 返回打印的字符数, 而 format_dev_t 返回缓存区
void set_current_state(int new_state); 设置当前进程的运行状态
new_state: TASK_INTERRUPTIBLE/TASK_RUNNING/TASK_INTERRUPTIBLE/TASK_UNTINTERRUPTIBLE/...
在新代码中不鼓励使用下面这种方式
current->state = TASK_INTERRUPTIBLE
int in_interrupt(void)
如果处理器当前在中断上下文(包括软中断和硬中断)运行就返回非零
int in_atomic(void)
若调度被禁止(即当前状态是原子态, 包括硬中断,软件中断以及持有自旋锁时), 返回值是非零. 在持有自旋锁这种情况, current 可能是有效的, 但是禁止存取用户空间, 因为它能导致调度发生.
无论何时使用 in_interrupt(), 应当真正考虑是否 in_atomic 是你实际想要的
主编号标识设备驱动; 次编号被内核用来决定引用哪个设备
dev_t 类型(在 中定义)用来标识设备编号 -- 同时包括主次部分
MAJOR(dev_t dev): 从dev_t中取得主设备号
MINOR(dev_t dev): 从dev_t中取得次设备号
MKDEV(int major, int minor): 讲主次设备号转换成dev_t
int register_chrdev_region(dev_t first, unsigned int count, char *name): 获取一个或多个设备编号来使用
first 是你要分配的起始设备编号. first 的次编号部分常常是 0
count 是你请求的连续设备编号的总数
name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);动态分配一个主编号
dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数.
fisetminor 应当是请求的第一个要用的次编号; 它常常是 0.
count 是你请求的连续设备编号的总数
name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中
void unregister_chrdev_region(dev_t first, unsigned int count); 释放设备编号
first 是你要分配的起始设备编号. first 的次编号部分常常是 0
count 是你请求的连续设备编号的总数
struct cdev *cdev_alloc(void);为struct cdev申请内存空间void cdev_init(struct cdev *cdev, struct file_operations *fops);初始化struct cdev结构. 其成员owner应当设置为 THIS_MODULE
cdev 是需要初始化的struct cdev结构
fops: 是关联到这个驱动的方法集合(read/write等)
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);将设备注册到内核
dev 是struct cdev结构
num 是这个设备响应的第一个设备号
count 是应当关联到设备的设备号的数目. 常常 count 是 1, 但是有多个设备号对应于一个特定的设备的情形.
void cdev_del(struct cdev *dev);将设备注销
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major 是感兴趣的主编号
name 是驱动的名子(出现在 /proc/devices)
fops 是缺省的 file_operations 结构.
int unregister_chrdev(unsigned int major, const char *name);
major和name 必须和传递给register_chrdev的相同, 否则调用会失败.
devfs_handle_t devfs_register (devfs_handle_t dir,
const char *name,
unsigned int flags,
unsigned int major, unsigned int minor,
umode_t mode,
void *ops, void *info);创建设备节点
dir:需要创建的设备文件所在目录,默认为/dev
name: 需要创建的设备文件名
flags: 通常取DEVFS_FL_DEFAULT
major: 主设备号
minor: 次设备号
mode: 此设备文件的读写权限
ops: 此设备的file_operations结构
info:
#define DEV_ID ((void*)123456)
#define DEV_NAME "XXXXXXXXXXXXXX"
#define DEV_MAJOR 200
#define DEV_IRQ IRQ_XXXX
#define DEV_IRQ_MODE SA_SHIRQ
...
//regist char device
#if LINUX_VERSION_CODE
ret = register_chrdev(DEV_MAJOR, DEV_NAME, &fops);
#else
cdev_init(&dev_char, &fops);
dev_char.owner = THIS_MODULE;
dev_char.ops = &fops;
ret = cdev_add(&dev_char, MKDEV(DEV_MAJOR, 0), 1);
#endif
if (ret
goto __mod_init_err1;
//make devfs
#if LINUX_VERSION_CODE
devfs_handle = devfs_register(NULL, DEV_NAME, DEVFS_FL_DEFAULT,
DEV_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR, &fops, NULL);
if (NULL == devfs_handle)
{
ret = -1;
goto __mod_init_err2;
}
#else
dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(dev_class))
{
ret = PTR_ERR(dev_class);
goto __mod_init_err2;
}
class_device_create(dev_class, MKDEV(DEV_MAJOR, 0), NULL, DEV_NAME);
ret = devfs_mk_cdev(MKDEV(DEV_MAJOR, 0), S_IFCHR | S_IRUGO | S_IWUSR, DEV_NAME);
if(ret)
goto __mod_init_err3;
#endif
......
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
__mod_init_err3:
class_device_destroy(dev_class, MKDEV(DEV_MAJOR, 0));
class_destroy(dev_class);
#endif
__mod_init_err2:
#if LINUX_VERSION_CODE
unregister_chrdev(DEV_MAJOR, DEV_NAME);
#else
cdev_del(&dev_char);
#endif
__mod_init_err1:
free_irq(DEV_IRQ, DEV_ID);
#endif//end of "ifndef INPUT_DEVICE"
__mod_init_err0:
return ret;
file_operations函数:
第 2 章 建立和运行模块
时间差不多该开始编程了. 本章介绍所有的关于模块和内核编程的关键概念. 在这几页里, 我们建立并运行一个完整(但是相对地没有什么用处)的模块, 并且查看一些被所有模块共用的基本代码. 开发这样的专门技术对任何类型的模块化的驱动都是重要的基础. 为避免一次抛出太多的概念, 本章只论及模块, 不涉及任何特别的设备类型.
在这里介绍的所有的内核项 ( 函数, 变量, 头文件, 和宏 )在本章的结尾的参考一节里有说明.
2.1. 设置你的测试系统
在本章开始, 我们提供例子模块来演示编程概念. ( 所有的例子都可从 O' Reilly' s 的 FTP 网站上得到, 如第 1 章解释的那样 )建立, 加载, 和修改这些例子, 是提高你对驱动如何工作以及如何与内核交互的理解的好方法.
例子模块应该可以在大部分的 2.6.x 内核上运行, 包括那些由发布供应商提供的. 但是, 我们建议你获得一个主流内核, 直接从 kernel.org 的镜像网络, 并把它安装到你的系统中. 供应商的内核可能是主流内核被重重地打了补丁并且和主流内核有分歧; 偶尔, 供应商的补丁可能改变了设备驱动可见的内核 API. 如果你在编写一个必须在特别的发布上运行的驱动, 你当然要在相应的内核上建立和测试. 但是, 处于学习驱动编写的目的, 一个标准内核是最好的.
不管你的内核来源, 建立 2.6.x 的模块需要你有一个配置好并建立好的内核树在你的系统中. 这个要求是从之前内核版本的改变, 之前只要有一套当前版本的头文件就足够了. 2.6 模块针对内核源码树里找到的目标文件连接; 结果是一个更加健壮的模块加载器, 还要求那些目标文件也是可用的. 因此你的第一个商业订单是具备一个内核源码树( 或者从 krenel.org 网络或者你的发布者的内核源码包), 建立一个新内核, 并且安装到你的系统. 因为我们稍后会见到的原因, 生活通常是最容易的如果当你建立模块时真正运行目标内核, 尽管这不是需要的.
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" οnclick="if(!this.resized) {return true;} else {window.open('http://www.deansys.com/doc/ldd3/images/note.png');}" alt="" src="http://www.deansys.com/doc/ldd3/images/note.png" border=0>
注意
你应当也考虑一下在哪里进行你的模块试验, 开发和测试. 我们已经尽力使我们的例子模块安全和正确, 但是 bug 的可能性是经常会有的. 内核代码中的错误可能会引起一个用户进程的死亡, 或者偶尔, 瘫痪整个系统. 它们正常地不会导致更严重地后果, 例如磁盘损伤. 然而, 还是建议你进行你的内核试验在一个没有包含你负担不起丢失的数据的系统, 并且没有进行重要的服务. 内核开发者典型地会保留一台"牺牲"系统来测试新的代码.
因此, 如果你还没有一个合适的系统, 带有一个配置好并建立好的源码树在磁盘上, 现在是时候建立了. 我们将等待. 一旦这个任务完成, 你就准备好开始摆布内核模块了.
2.2. Hello World 模块
许多编程书籍从一个 "hello world" 例子开始, 作为一个展示可能的最简单的程序的方法. 本书涉及的是内核模块而不是程序; 因此, 对无耐心的读者, 下面的代码是一个完整的 "hello world"模块:
#include
#include
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证; 没有这样的说明, 在模块加载时内核会抱怨.
printk 函数在 Linux 内核中定义并且对模块可用; 它与标准 C 库函数 printf 的行为相似. 内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助. 模块能够调用 printk 是因为, 在 insmod 加载了它之后, 模块被连接到内核并且可存取内核的公用符号 (函数和变量, 下一节详述). 字串 KERN_ALERT 是消息的优先级. [
3
]
我们在此模块中指定了一个高优先级, 因为使用缺省优先级的消息可能不会在任何有用的地方显示, 这依赖于你运行的内核版本, klogd 守护进程的版本, 以及你的配置. 现在你可以忽略这个因素; 我们在第 4 章讲解它.
你可以用 insmod 和 rmmod 工具来测试这个模块. 注意只有超级用户可以加载和卸载模块.
% make
make[1]: Entering directory `/usr/src/linux-2.6.10'
CC
/home/ldd3/src/misc-modules/hello.o
Building modules, stage 2.
MODPOST
CC /home/ldd3/src/misc-modules/hello.mod.o
LD
/home/ldd3/src/misc-modules/hello.ko
make[1]: Leaving directory `/usr/src/linux-2.6.10'
% su
root# insmod ./hello.ko
Hello, world
root# rmmod hello
Goodbye cruel world
root#
请再一次注意, 为使上面的操作命令顺序工作, 你必须在某个地方有正确配置和建立的内核树, 在那里可以找到 makefile (/usr/src/linux-2.6.10, 在展示的例子里面 ). 我们在 "编译和加载" 这一节深入模块建立的细节.
依据你的系统用来递交消息行的机制, 你的输出可能不同. 特别地, 前面的屏幕输出是来自一个字符控制台; 如果你从一个终端模拟器或者在窗口系统中运行 insmod 和 rmmod, 你不会在你的屏幕上看到任何东西. 消息进入了其中一个系统日志文件中, 例如 /var/log/messages (实际文件名子随 Linux 发布而变化). 内核递交消息的机制在第 4 章描述.
如你能见到的, 编写一个模块不是如你想象的困难 -- 至少, 在模块没有要求做任何有用的事情时. 困难的部分是理解你的设备, 以及如何获得最高性能. 通过本章我们深入模块化内部并且将设备相关的问题留到后续章节.
[
3
] 优先级只是一个字串, 例如 , 前缀于 printk 格式串之前. 注意在 KERN_ALERT 之后缺少一个逗号; 添加一个逗号在那里是一个普通的讨厌的错误 ( 幸运的是, 编译器会捕捉到 ).
2.3. 内核模块相比于应用程序
在我们深入之前, 有必要强调一下内核模块和应用程序之间的各种不同.
不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说, 模块初始化函数的任务是为以后调用模块的函数做准备; 好像是模块说, " 我在这里, 这是我能做的."模块的退出函数( 例子里是 hello_exit )就在模块被卸载时调用. 它好像告诉内核, "我不再在那里了, 不要要求我做任何事了."这种编程的方法类似于事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是. 另外一个主要的不同, 在事件驱动的应用程序和内核代码之间, 是退出函数: 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.
偶然地, 卸载模块的能力是你将最欣赏的模块化的其中一个特色, 因为它有助于减少开发时间; 你可测试你的新驱动的连续的版本, 而不用每次经历漫长的关机/重启周期.
作为一个程序员, 你知道一个应用程序可以调用它没有定义的函数: 连接阶段使用合适的函数库解决了外部引用. printf 是一个这种可调用的函数并且在 libc 里面定义. 一个模块, 在另一方面, 只连接到内核, 它能够调用的唯一的函数是内核输出的那些; 没有库来连接.在 hello.c 中使用的 printk 函数, 例如, 是在内核中定义的 printf 版本并且输出给模块. 它表现类似于原始的函数, 只有几个小的不同, 首要的一个是缺乏浮点的支持.
图
连接一个模块到内核
展示了函数调用和函数指针在模块中如何使用来增加新功能到一个运行中的内核.
图 2.1. 连接一个模块到内核
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" οnclick="if(!this.resized) {return true;} else {window.open('http://www.deansys.com/doc/ldd3/images/snagitldd3/ldd3-2-1.png');}" alt="" src="http://www.deansys.com/doc/ldd3/images/snagitldd3/ldd3-2-1.png" border=0>
因为没有库连接到模块中, 源文件不应当包含通常的头文件, 和非常特殊的情况是仅有的例外. 只有实际上是内核的一部分的函数才可以在内核模块里使用. 内核相关的任何东西都在头文件里声明, 这些头文件在你已建立和配置的内核源码树里; 大部分相关的头文件位于 include/linux 和 include/asm, 但是别的 include 的子目录已经添加到关联特定内核子系统的材料里了.
单个内核头文件的作用在书中需要它们的时候进行介绍.
另外一个在内核编程和应用程序编程之间的重要不同是每一个环境是如何处理错误: 在应用程序开发中段错误是无害的, 一个调试器常常用来追踪错误到源码中的问题, 而一个内核错误至少会杀掉当前进程, 如果不终止整个系统. 我们会在第 4 章看到如何跟踪内核错误.
2.3.1. 用户空间和内核空间A module runs in kernel space, whereas applications run in user space. This concept is at the base of operating systems theory. 一个模块在内核空间运行, 而应用程序在用户空间运行. 这个概念是操作系统理论的基础.
操作系统的角色, 实际上, 是给程序提供一个一致的计算机硬件的视角. 另外, 操作系统必须承担程序的独立操作和保护对于非授权的资源存取. 这一不平凡的任务只有 CPU 增强系统软件对应用程序的保护才有可能.
每种现代处理器都能够加强这种行为. 选中的方法是 CPU 自己实现不同的操作形态(或者级别). 这些级别有不同的角色, 一些操作在低些级别中不允许; 程序代码只能通过有限的几个门从一种级别切换到另一个. Unix 系统设计成利用了这种硬件特性, 使用了两个这样的级别. 所有当今的处理器至少有两个保护级别, 并且某些, 例如 x86 家族, 有更多级别; 当几个级别存在时, 使用最高和最低级别. 在 Unix 下, 内核在最高级运行( 也称之为超级模式 ), 这里任何事情都允许, 而应用程序在最低级运行(所谓的用户模式), 这里处理器控制了对硬件的直接存取以及对内存的非法存取.
我们常常提到运行模式作为内核空间和用户空间. 这些术语不仅包含存在于这两个模式中不同特权级别, 还包含有这样的事实, 即每个模式有它自己的内存映射 -- 它自己的地址空间.
Unix 从用户空间转换执行到内核空间, 无论何时一个应用程序发出一个系统调用或者被硬件中断挂起时. 执行系统调用的内核代码在进程的上下文中工作 -- 它代表调用进程并且可以存取该进程的地址空间. 换句话说, 处理中断的代码对进程来说是异步的, 不和任何特别的进程有关.
模块的角色是扩展内核的功能; 模块化的代码在内核空间运行. 经常地一个驱动进行之前提到的两种任务: 模块中一些的函数作为系统调用的一部分执行, 一些负责中断处理.
2.3.2. 内核的并发
内核编程与传统应用程序编程方式很大不同的是并发问题. 大部分应用程序, 多线程的应用程序是一个明显的例外, 典型地是顺序运行的, 从头至尾, 不必要担心其他事情会发生而改变它们的环境. 内核代码没有运行在这样的简单世界中, 即便最简单的内核模块必须在这样的概念下编写, 很多事情可能马上发生.
内核编程中有几个并发的来源. 自然的, Linux 系统运行多个进程, 在同一时间, 不止一个进程能够试图使用你的驱动. 大部分设备能够中断处理器; 中断处理异步运行, 并且可能在你的驱动试图做其他事情的同一时间被调用. 几个软件抽象( 例如内核定时器, 第 7 章介绍 )也异步运行. 而且, 当然, Linux 可以在对称多处理器系统( SMP )上运行, 结果是你的驱动可能在多个 CPU 上并发执行. 最后, 在 2.6, 内核代码已经是可抢占的了; 这个变化使得即便是单处理器会有许多与多处理器系统同样的并发问题.
结果, Linux 内核代码, 包括驱动代码, 必须是可重入的 -- 它必须能够同时在多个上下文中运行. 数据结构必须小心设计以保持多个执行线程分开, 并且代码必须小心存取共享数据, 避免数据的破坏. 编写处理并发和避免竞争情况( 一个不幸的执行顺序导致不希望的行为的情形 )的代码需要仔细考虑并可能是微妙的. 正确的并发管理在编写正确的内核代码时是必须的; 由于这个理由, 本书的每一个例子驱动都是考虑了并发下编写的. 用到的技术在我们遇到它们时再讲解; 第 5 章也专门讲述这个问题, 以及并发管理的可用的内核原语.
驱动程序员的一个通常的错误是假定并发不是一个问题, 只要一段特别的代码没有进入睡眠( 或者 "阻塞" ). 即便在之前的内核( 不可抢占), 这种假设在多处理器系统中也不成立. 在 2.6, 内核代码不能(极少)假定它能在一段给定代码上持有处理器. 如果你不考虑并发来编写你的代码, 就极有可能导致严重失效, 以至于非常难于调试.
2.3.3. 当前进程
尽管内核模块不象应用程序一样顺序执行, 内核做的大部分动作是代表一个特定进程的. 内核代码可以引用当前进程, 通过存取全局项 current, 它在 中定义, 它产生一个指针指向结构 task_struct, 在 定义. current 指针指向当前在运行的进程. 在一个系统调用执行期间, 例如 open 或者 read, 当前进程是发出调用的进程. 内核代码可以通过使用 current 来使用进程特定的信息, 如果它需要这样. 这种技术的一个例子在第 6 章展示.
实际上, current 不真正地是一个全局变量. 支持 SMP 系统的需要强迫内核开发者去开发一种机制, 在相关的 CPU 上来找到当前进程. 这种机制也必须快速, 因为对 current 的引用非常频繁地发生. 结果就是一个依赖体系的机制, 常常, 隐藏了一个指向 task_struct 的指针在内核堆栈内. 实现的细节对别的内核子系统保持隐藏, 一个设备驱动可以只包含 并且引用当前进程. 例如, 下面的语句打印了当前进程的进程 ID 和命令名称, 通过存取结构 task_struct 中的某些字段.
printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);
存于 current->comm 的命令名称是由当前进程执行的程序文件的基本名称( 截短到 15 个字符, 如果需要 ).
2.3.4. 几个别的细节
内核编程与用户空间编程在许多方面不同. 我们将在本书的过程中指出它们, 但是有几个基础性的问题, 尽管没有保证它们自己有一节内容, 也值得一提. 因此, 当你深入内核时, 下面的事项应当牢记.
应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈; 它可能小到一个, 4096 字节的页. 你的函数必须与这个内核空间调用链共享这个堆栈. 因此, 声明一个巨大的自动变量从来就不是一个好主意; 如果你需要大的结构, 你应当在调用时间内动态分配.
常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调用这个函数, 确信你知道你在做什么."
内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存和恢复浮点处理器的状态 -- 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮点, 额外的负担不值得.
2.4. 编译和加载
本章开头的 "hello world" 例子包含了一个简短的建立并加载模块到系统中去的演示. 当然, 整个过程比我们目前看到的多. 本节提供了更多细节关于一个模块作者如何将源码转换成内核中的运行的子系统.
2.4.1. 编译模块
第一步, 我们需要看一下模块如何必须被建立. 模块的建立过程与用户空间的应用程序的建立过程有显著不同; 内核是一个大的, 独立的程序, 对于它的各个部分如何组合在一起有详细的明确的要求. 建立过程也与以前版本的内核的过程不同; 新的建立系统用起来更简单并且产生更正确的结果, 但是它看起来与以前非常不同. 内核建立系统是一头负责的野兽, 我们就看它一小部分. 在内核源码的 Document/kbuild 目录下发现的文件, 任何想理解表面之下的真实情况的人都要阅读一下.
有几个前提, 你必须在能建立内核模块前解决. 第一个是保证你有版本足够新的编译器, 模块工具, 以及其他必要工具. 在内核文档目录下的文件 Documentation/Changes 一直列出了需要的工具版本; 你应当在向前走之前参考一下它. 试图建立一个内核(包括它的模块), 用错误的工具版本, 可能导致不尽的奇怪的难题. 注意, 偶尔地, 编译器的版本太新可能会引起和太老的版本引起的一样的问题. 内核源码对于编译器做了很大的假设, 新的发行版本有时会一时地破坏东西.
如果你仍然没有一个内核树在手边, 或者还没有配置和建立内核, 现在是时间去做了. 没有源码树在你的文件系统上, 你无法为 2.6 内核建立可加载的模块. 实际运行为其而建立的内核也是有帮助的( 尽管不是必要的 ).
一旦你已建立起所有东西, 给你的模块创建一个 makefile 就是直截了当的. 实际上, 对于本章前面展示的" hello world" 例子, 单行就够了:
obj-m := hello.o
熟悉 make , 但是对 2.6 内核建立系统不熟悉的读者, 可能奇怪这个 makefile 如何工作. 毕竟上面的这一行不是一个传统的 makefile 的样子. 答案, 当然, 是内核建立系统处理了余下的工作. 上面的安排( 它利用了由 GNU make 提供的扩展语法 )表明有一个模块要从目标文件 hello.o 建立. 在从目标文件建立后结果模块命名为 hello.ko.
反之, 如果你有一个模块名为 module.ko, 是来自 2 个源文件( 姑且称之为, file1.c 和 file2.c ), 正确的书写应当是:
obj-m := module.o
module-objs := file1.o file2.o
对于一个象上面展示的要工作的 makefile, 它必须在更大的内核建立系统的上下文被调用. 如果你的内核源码数位于, 假设, 你的 ~/kernel-2.6 目录, 用来建立你的模块的 make 命令( 在包含模块源码和 makefile 的目录下键入 )会是:
make -C ~/kernel-2.6 M=`pwd` modules
这个命令开始是改变它的目录到用 -C 选项提供的目录下( 就是说, 你的内核源码目录 ). 它在那里会发现内核的顶层 makefile. 这个 M= 选项使 makefile 在试图建立模块目标前, 回到你的模块源码目录. 这个目标, 依次地, 是指在 obj-m 变量中发现的模块列表, 在我们的例子里设成了 module.o.
键入前面的 make 命令一会儿之后就会感觉烦, 所以内核开发者就开发了一种 makefile 方式, 使得生活容易些对于那些在内核树之外建立模块的人. 这个窍门是如下书写你的 makefile:
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
再一次, 我们看到了扩展的 GNU make 语法在起作用. 这个 makefile 在一次典型的建立中要被读 2 次. 当从命令行中调用这个 makefile , 它注意到 KERNELRELEASE 变量没有设置. 它利用这样一个事实来定位内核源码目录, 即已安装模块目录中的符号连接指回内核建立树. 如果你实际上没有运行你在为其而建立的内核, 你可以在命令行提供一个 KERNELDIR= 选项, 设置 KERNELDIR 环境变量, 或者重写 makefile 中设置 KERNELDIR 的那一行. 一旦发现内核源码树, makefile 调用 default: 目标, 来运行第 2 个 make 命令( 在 makefile 里参数化成 $(MAKE))象前面描述过的一样来调用内核建立系统. 在第 2 次读, makefile 设置 obj-m, 并且内核的 makefile 文件完成实际的建立模块工作.
这种建立模块的机制你可能感觉笨拙模糊. 一旦你习惯了它, 但是, 你很可能会欣赏这种已经编排进内核建立系统的能力. 注意, 上面的不是一个完整的 makefile; 一个真正的 makefile 包含通常的目标类型来清除不要的文件, 安装模块等等. 一个完整的例子可以参考例子代码目录的 makefile.
2.4.2. 加载和卸载模块
模块建立之后, 下一步是加载到内核. 如我们已指出的, insmod 为你完成这个工作. 这个程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中任何未解决的符号连接到内核的符号表上. 但是不象连接器, 内核不修改模块的磁盘文件, 而是内存内的拷贝. insmod 接收许多命令行选项(详情见 manpage), 它能够安排值给你模块中的参数, 在连接到当前内核之前. 因此, 如果一个模块正确设计了, 它能够在加载时配置; 加载时配置比编译时配置给了用户更多的灵活性, 有时仍然在用. 加载时配置在本章后面的 "模块参数" 一节讲解.
感兴趣的读者可能想看看内核如何支持 insmod: 它依赖一个在 kernel/module.c 中定义的系统调用. 函数 sys_init_module 分配内核内存来存放模块 ( 这个内存用 vmalloc 分配; 看第 8 章的 "vmalloc 和其友" ); 它接着拷贝模块的代码段到这块内存区, 借助内核符号表解决模块中的内核引用, 并且调用模块的初始化函数来启动所有东西.
如果你真正看了内核代码, 你会发现系统调用的名子以 sys_ 为前缀. 这对所有系统调用都是成立的, 并且没有别的函数. 记住这个有助于在源码中查找系统调用.
modprobe 工具值得快速提及一下. modprobe, 如同 insmod, 加载一个模块到内核. 它的不同在于它会查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果发现有, modprobe 在定义相关符号的当前模块搜索路径中寻找其他模块. 当 modprobe 找到这些模块( 要加载模块需要的 ), 它也把它们加载到内核. 如果你在这种情况下代替以使用 insmod , 命令会失败, 在系统日志文件中留下一条 " unresolved symbols "消息.
如前面提到, 模块可以用 rmmod 工具从内核去除. 注意, 如果内核认为模块还在用( 就是说, 一个程序仍然有一个打开文件对应模块输出的设备 ), 或者内核被配置成不允许模块去除, 模块去除会失败. 可以配置内核允许"强行"去除模块, 甚至在它们看来是忙的. 如果你到了需要这选项的地步, 但是, 事情可能已经错的太严重以至于最好的动作就是重启了.
lsmod 程序生成一个内核中当前加载的模块的列表. 一些其他信息, 例如使用了一个特定模块的其他模块, 也提供了. lsmod 通过读取 /proc/modules 虚拟文件工作. 当前加载的模块的信息也可在位于 /sys/module 的 sysfs 虚拟文件系统找到.
2.4.3. 版本依赖
记住, 你的模块代码一定要为每个它要连接的内核版本重新编译 -- 至少, 在缺乏 modversions 时, 这里不涉及因为它们更多的是给内核发布制作者, 而不是开发者. 模块是紧密结合到一个特殊内核版本的数据结构和函数原型上的; 模块见到的接口可能一个内核版本与另一个有很大差别. 当然, 在开发中的内核更加是这样.
内核不只是认为一个给定模块是针对一个正确的内核版本建立的. 建立过程的其中一步是对一个当前内核树中的文件(称为 vermagic.o)连接你的模块; 这个东东含有相当多的有关要为其建立模块的内核的信息, 包括目标内核版本, 编译器版本, 以及许多重要配置变量的设置. 当尝试加载一个模块, 这些信息被检查与运行内核的兼容性. 如果不匹配, 模块不会加载; 代之的是你见到如下内容:
# insmod hello.ko
Error inserting './hello.ko': -1 Invalid module format
看一下系统日志文件(/var/log/message 或者任何你的系统被配置来用的)将发现导致模块无法加载特定的问题.
如果你需要编译一个模块给一个特定的内核版本, 你将需要使用这个特定版本的建立系统和源码树. 前面展示过的在例子 makefile 中简单修改 KERNELDIR 变量, 就完成这个动作.
内核接口在各个发行之间常常变化. 如果你编写一个模块想用来在多个内核版本上工作(特别地是如果它必须跨大的发行版本), 你可能只能使用宏定义和 #ifdef 来使你的代码正确建立. 本书的这个版本只关心内核的一个主要版本, 因此不会在我们的例子代码中经常见到版本检查. 但是这种需要确实有时会有. 在这样情况下, 你要利用在 linux/version.h 中发现的定义. 这个头文件, 自动包含在 linux/module.h, 定义了下面的宏定义:
UTS_RELEASE
这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, "2.6.10".
LINUX_VERSION_CODE
这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). [
4
]有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.
KERNEL_VERSION(major,minor,release)
这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如, KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.
大部分的基于内核版本的依赖性可以使用预处理器条件解决, 通过利用 KERNEL_VERSION 和 LINUX_VERSION_VODE. 版本依赖不应当, 但是, 用繁多的 #ifdef 条件来搞乱驱动的代码; 处理不兼容的最好的方式是把它们限制到特定的头文件. 作为一个通用的原则, 明显版本(或者平台)依赖的代码应当隐藏在一个低级的宏定义或者函数后面. 高层的代码就可以只调用这些函数, 而不必关心低层的细节. 这样书写的代码易读并且更健壮.
2.4.4. 平台依赖性
每个电脑平台有其自己的特点, 内核设计者可以自由使用所有的特性来获得更好的性能. in the target object file ???
不象应用程序开发者, 他们必须和预编译的库一起连接他们的代码, 依附在参数传递的规定上, 内核开发者可以专用某些处理器寄存器给特别的用途, 他们确实这样做了. 更多的, 内核代码可以为一个 CPU 族里的特定处理器优化, 以最好地利用目标平台; 不象应用程序那样常常以二进制格式发布, 一个定制的内核编译可以为一个特定的计算机系列优化.
例如, IA32 (x86) 结构分为几个不同的处理器类型. 老式的 80386 处理器仍然被支持( 到现在 ), 尽管它的指令集, 以现代的标准看, 非常有限. 这个体系中更加现代的处理器已经引入了许多新特性, 包括进入内核的快速指令, 处理器间的加锁, 拷贝数据, 等等. 更新的处理器也可采用 36 位( 或者更大 )的物理地址, 当在适当的模式下, 以允许他们寻址超过 4 GB 的物理内存. 其他的处理器家族也有类似的改进. 内核, 依赖不同的配置选项, 可以被建立来使用这些附加的特性.
清楚地, 如果一个模块与一个给定内核工作, 它必须以与内核相同的对目标处理器的理解来建立. 再一次, vermagic.o 目标文件登场. 当加载一个模块, 内核为模块检查特定处理器的配置选项, 确认它们匹配运行的内核. 如果模块用不同选项编译, 它不会加载.
如果你计划为通用的发布编写驱动, 你可能很奇怪你怎么可能支持所有这些不同的变体. 最好的答案, 当然, 是发行你的驱动在 GPL 兼容的许可之下, 并且贡献它给主流内核. 如果没有那样, 以源码形式和一套脚本发布你的驱动, 以便在用户系统上编译可能是最好的答案. 一些供应商已发行了工具来简化这个工作. 如果你必须发布你的驱动以二进制形式, 你需要查看由你的目标发布所提供的不同的内核, 并且为每个提供一个模块版本. 要确认考虑到了任何在产生发布后可能发行的勘误内核. 接着, 要考虑许可权的问题, 如同我们在第 1 章的" 许可条款" 一节中讨论的. 作为一个通用的规则, 以源码形式发布东西是你行于世的易途.
[
4
] 这允许在稳定版本之间多达 256 个开发版本.
2.5. 内核符号表
我们已经看到 insmod 如何对应共用的内核符号来解决未定义的符号. 表中包含了全局内核项的地址 -- 函数和变量 -- 需要来完成模块化的驱动. 当加载一个模块, 如何由模块输出的符号成为内核符号表的一部分. 通常情况下, 一个模块完成它自己的功能不需要输出如何符号. 你需要输出符号, 但是, 在任何别的模块能得益于使用它们的时候.
新的模块可以用你的模块输出的符号, 你可以堆叠新的模块在其他模块之上. 模块堆叠在主流内核源码中也实现了: msdos 文件系统依赖 fat 模块输出的符号, 某一个输入 USB 设备模块堆叠在 usbcore 和输入模块之上.
模块堆叠在复杂的工程中有用处. 如果一个新的抽象以驱动程序的形式实现, 它可能提供一个特定硬件实现的插入点. 例如, video-for-linux 系列驱动分成一个通用模块, 输出了由特定硬件的低层设备驱动使用的符号. 根据你的设置, 你加载通用的视频模块和你的已安装硬件对应的特定模块. 对并口的支持和众多可连接设备以同样的方式处理, 如同 USB 内核子系统. 在并口子系统的堆叠在图
并口驱动模块的堆叠
中显示; 箭头显示了模块和内核编程接口间的通讯.
图 2.2. 并口驱动模块的堆叠
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" οnclick="if(!this.resized) {return true;} else {window.open('http://www.deansys.com/doc/ldd3/images/snagitldd3/ldd3-2-2.png');}" alt="" src="http://www.deansys.com/doc/ldd3/images/snagitldd3/ldd3-2-2.png" border=0>
当使用堆叠的模块时, 熟悉 modprobe 工具是有帮助的. 如我们前面讲的, modprobe 函数很多地方与 insmod 相同, 但是它也加载任何你要加载的模块需要的其他模块. 所以, 一个 modprobe 命令有时可能代替几次使用 insmod( 尽管你从当前目录下加载你自己模块仍将需要 insmod, 因为 modprobe 只查找标准的已安装模块目录 ).
使用堆叠来划分模块成不同层, 这有助于通过简化每一层来缩短开发时间. 这同我们在第 1 章讨论的区分机制和策略是类似的.
linux 内核头文件提供了方便来管理你的符号的可见性, 因此减少了命名空间的污染( 将与在内核别处已定义的符号冲突的名子填入命名空间), 并促使了正确的信息隐藏. 如果你的模块需要输出符号给其他模块使用, 应当使用下面的宏定义:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
上面宏定义的任一个使得给定的符号在模块外可用. _GPL 版本的宏定义只能使符号对 GPL 许可的模块可用. 符号必须在模块文件的全局部分输出, 在任何函数之外, 因为宏定义扩展成一个特殊用途的并被期望是全局存取的变量的声明. 这个变量存储于模块的一个特殊的可执行部分( 一个 "ELF 段" ), 内核用这个部分在加载时找到模块输出的变量. ( 感兴趣的读者可以看 获知详情, 尽管并不需要这些细节使东西动起来. )
2.6. 预备知识
我们正在接近去看一些实际的模块代码. 但是首先, 我们需要看一些需要出现在你的模块源码文件中的东西. 内核是一个独特的环境, 它将它的要求强加于要和它接口的代码上.
大部分内核代码包含了许多数量的头文件来获得函数, 数据结构和变量的定义. 我们将在碰到它们时检查这些文件, 但是有几个文件对模块是特殊的, 必须出现在每一个可加载模块中. 因此, 几乎所有模块代码都有下面内容:
#include
#include
moudle.h 包含了大量加载模块需要的函数和符号的定义. 你需要 init.h 来指定你的初始化和清理函数, 如我们在上面的 "hello world" 例子里见到的, 这个我们在下一节中再讲. 大部分模块还包含 moudleparam.h, 使得可以在模块加载时传递参数给模块. 我们将很快遇到.
不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包含一行 MODULE_LICENSE:
MODULE_LICENSE("GPL");
内核认识的特定许可有, "GPL"( 适用 GNU 通用公共许可的任何版本 ), "GPL v2"( 只适用 GPL 版本 2 ), "GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", 和 "Proprietary". 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊"了. 象我们在第 1 章"许可条款"中提到的, 内核开发者不会热心帮助在加载了私有模块后遇到问题的用户.
可以在模块中包含的其他描述性定义有 MODULE_AUTHOR ( 声明谁编写了模块 ), MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ), MODULE_VERSION ( 一个代码修订版本号; 看 的注释以便知道创建版本字串使用的惯例), MODULE_ALIAS ( 模块为人所知的另一个名子 ), 以及 MODULE_DEVICE_TABLE ( 来告知用户空间, 模块支持那些设备 ). 我们会讨论 MODULE_ALIAS 在第 11 章以及 MUDULE_DEVICE_TABLE 在第 12 章.
各种 MODULE_ 声明可以出现在你的源码文件的任何函数之外的地方. 但是, 一个内核代码中相对近期的惯例是把这些声明放在文件末尾.
2.7. 初始化和关停
如已提到的, 模块初始化函数注册模块提供的任何功能. 这些功能, 我们指的是新功能, 可以由应用程序存取的或者一整个驱动或者一个新软件抽象. 实际的初始化函数定义常常如:
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);
初始化函数应当声明成静态的, 因为它们不会在特定文件之外可见; 没有硬性规定这个, 然而, 因为没有函数能输出给内核其他部分, 除非明确请求. 声明中的 __init 标志可能看起来有点怪; 它是一个给内核的暗示, 给定的函数只是在初始化使用. 模块加载者在模块加载后会丢掉这个初始化函数, 使它的内存可做其他用途. 一个类似的标签 (__initdata) 给只在初始化时用的数据. 使用 __init 和 __initdata 是可选的, 但是它带来的麻烦是值得的. 只是要确认不要用在那些在初始化完成后还使用的函数(或者数据结构)上. 你可能还会遇到 __devinit 和 __devinitdata 在内核源码里; 这些只在内核没有配置支持 hotplug 设备时转换成 __init 和 _initdata. 我们会在 14 章谈论 hotplug 支持.
使用 moudle_init 是强制的. 这个宏定义增加了特别的段到模块目标代码中, 表明在哪里找到模块的初始化函数. 没有这个定义, 你的初始化函数不会被调用.
模块可以注册许多的不同设施, 包括不同类型的设备, 文件系统, 加密转换, 以及更多. 对每一个设施, 有一个特定的内核函数来完成这个注册. 传给内核注册函数的参数常常是一些数据结构的指针, 描述新设施以及要注册的新设施的名子. 数据结构常常包含模块函数指针, 模块中的函数就是这样被调用的.
能够注册的项目远远超出第 1 章中提到的设备类型列表. 它们包括, 其他的, 串口, 多样设备, sysfs 入口, /proc 文件, 执行域, 链路规程. 这些可注册项的大部分都支持不直接和硬件相关的函数, 但是处于"软件抽象"区域里. 这些项可以注册, 是因为它们以各种方式(例如象 /proc 文件和链路规程)集成在驱动的功能中.
对某些驱动有其他的设施可以注册作为补充, 但它们的使用太特别, 所以不值得讨论它们. 它们使用堆叠技术, 在"内核符号表"一节中讲过. 如果你想深入探求, 你可以在内核源码里查找 EXPORT_SYMBOL , 找到由不同驱动提供的入口点. 大部分注册函数以 register_ 做前缀, 因此找到它们的另外一个方法是在内核源码里查找 register_ .
2.7.1. 清理函数
每个非试验性的模块也要求有一个清理函数, 它注销接口, 在模块被去除之前返回所有资源给系统. 这个函数定义为:
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
清理函数没有返回值, 因此它被声明为 void. __exit 修饰符标识这个代码是只用于模块卸载( 通过使编译器把它放在特殊的 ELF 段). 如果你的模块直接建立在内核里, 或者如果你的内核配置成不允许模块卸载, 标识为 __exit 的函数被简单地丢弃. 因为这个原因, 一个标识 __exit 的函数只在模块卸载或者系统停止时调用; 任何别的使用是错的. 再一次, moudle_exit 声明对于使得内核能够找到你的清理函数是必要的.
如果你的模块没有定义一个清理函数, 内核不会允许它被卸载.
2.7.2. 初始化中的错误处理
你必须记住一件事, 在注册内核设施时, 注册可能失败. 即便最简单的动作常常需要内存分配, 分配的内存可能不可用. 因此模块代码必须一直检查返回值, 并且确认要求的操作实际上已经成功.
如果在你注册工具时发生任何错误, 首先第一的事情是决定模块是否能够无论如何继续初始化它自己. 常常, 在一个注册失败后模块可以继续操作, 如果需要可以功能降级. 在任何可能的时候, 你的模块应当尽力向前, 并提供事情失败后具备的能力.
如果证实你的模块在一个特别类型的失败后完全不能加载, 你必须取消任何在失败前注册的动作. 内核不保留已经注册的设施的每模块注册, 因此如果初始化在某个点失败, 模块必须能自己退回所有东西. 如果你无法注销你获取的东西, 内核就被置于一个不稳定状态; 它包含了不存在的代码的内部指针. 这种情况下, 经常地, 唯一的方法就是重启系统. 在初始化错误发生时, 你确实要小心地将事情做正确.
错误恢复有时用 goto 语句处理是最好的. 我们通常不愿使用 goto, 但是在我们的观念里, 这是一个它有用的地方. 在错误情形下小心使用 goto 可以去掉大量的复杂, 过度对齐的, "结构形" 的逻辑. 因此, 在内核里, goto 是处理错误经常用到, 如这里显示的.
下面例子代码( 使用设施注册和注销函数)在初始化在任何点失败时做得正确:
int __init my_init_function(void)
{
int err;
/* registration takes a pointer and a name */
err = register_this(ptr1, "skull");
if (err)
goto fail_this;
err = register_that(ptr2, "skull");
if (err)
goto fail_that;
err = register_those(ptr3, "skull");
if (err)
goto fail_those;
return 0; /* success */
fail_those:
unregister_that(ptr2, "skull");
fail_that:
unregister_this(ptr1, "skull");
fail_this:
return err; /* propagate the error */
}
这段代码试图注册 3 个(虚构的)设施. goto 语句在失败情况下使用, 在事情变坏之前只对之前已经成功注册的设施进行注销.
另一个选项, 不需要繁多的 goto 语句, 是跟踪已经成功注册的, 并且在任何出错情况下调用你的模块的清理函数. 清理函数只回卷那些已经成功完成的步骤. 然而这种选择, 需要更多代码和更多 CPU 时间, 因此在快速途径下, 你仍然依赖于 goto 作为最好的错误恢复工具.
my_init_function 的返回值, err, 是一个错误码. 在 Linux 内核里, 错误码是负数, 属于定义于 的集合. 如果你需要产生你自己的错误码代替你从其他函数得到的返回值, 你应当包含 以便使用符号式的返回值, 例如 -ENODEV, -ENOMEM, 等等. 返回适当的错误码总是一个好做法, 因为用户程序能够把它们转变为有意义的字串, 使用 perror 或者类似的方法.
显然, 模块清理函数必须撤销任何由初始化函数进行的注册, 并且惯例(但常常不是要求的)是按照注册时相反的顺序注销设施.
void __exit my_cleanup_function(void)
{
unregister_those(ptr3, "skull");
unregister_that(ptr2, "skull");
unregister_this(ptr1, "skull");
return;
}
如果你的初始化和清理比处理几项复杂, goto 方法可能变得难于管理, 因为所有的清理代码必须在初始化函数里重复, 包括几个混合的标号. 有时, 因此, 一种不同的代码排布证明更成功.
使代码重复最小和所有东西流线化, 你应当做的是无论何时发生错误都从初始化里调用清理函数. 清理函数接着必须在撤销它的注册前检查每一项的状态. 以最简单的形式, 代码看起来象这样:
struct something *item1;
struct somethingelse *item2;
int stuff_ok;
void my_cleanup(void)
{
if (item1)
release_thing(item1);
if (item2)
release_thing2(item2);
if (stuff_ok)
unregister_stuff();
return;
}
int __init my_init(void)
{
int err = -ENOMEM;
item1 = allocate_thing(arguments);
item2 = allocate_thing2(arguments2);
if (!item2 || !item2)
goto fail;
err = register_stuff(item1, item2);
if (!err)
stuff_ok = 1;
else
goto fail;
return 0; /* success */
fail:
my_cleanup();
return err;
}
如这段代码所示, 你也许需要, 也许不要外部的标志来标识初始化步骤的成功, 要依赖你调用的注册/分配函数的语义. 不管要不要标志, 这种初始化会变得包含大量的项, 常常比之前展示的技术要好. 注意, 但是, 清理函数当由非退出代码调用时不能标志为 __exit, 如同前面的例子.
2.7.3. 模块加载竞争
到目前, 我们的讨论已来到一个模块加载的重要方面: 竞争情况. 如果你在如何编写你的初始化函数上不小心, 你可能造成威胁到整个系统的稳定的情形. 我们将在本书稍后讨论竞争情况; 现在, 快速提几点就足够了:
首先时你应该一直记住, 内核的某些别的部分会在注册完成之后马上使用任何你注册的设施. 这是完全可能的, 换句话说, 内核将调用进你的模块, 在你的初始化函数仍然在运行时. 所以你的代码必须准备好被调用, 一旦它完成了它的第一个注册. 不要注册任何设施, 直到所有的需要支持那个设施的你的内部初始化已经完成.
你也必须考虑到如果你的初始化函数决定失败会发生什么, 但是内核的一部分已经在使用你的模块已注册的设施. 如果这种情况对你的模块是可能的, 你应当认真考虑根本不要使初始化失败. 毕竟, 模块已清楚地成功输出一些有用的东西. 如果初始化必须失败, 必须小心地处理任何可能的在内核别处发生的操作, 直到这些操作已完成.
2.8. 模块参数
驱动需要知道的几个参数因不同的系统而不同. 从使用的设备号( 如我们在下一章见到的 )到驱动应当任何操作的几个方面. 例如, SCSI 适配器的驱动常常有选项控制标记命令队列的使用, IDE 驱动允许用户控制 DMA 操作. 如果你的驱动控制老的硬件, 还需要被明确告知哪里去找硬件的 I/O 端口或者 I/O 内存地址. 内核通过在加载驱动的模块时指定可变参数的值, 支持这些要求.
这些参数的值可由 insmod 或者 modprobe 在加载时指定; 后者也可以从它的配置文件(/etc/modprobe.conf)读取参数的值. 这些命令在命令行里接受几类规格的值. 作为演示这种能力的一种方法, 想象一个特别需要的对本章开始的"hello world"模块(称为 hellop)的改进. 我们增加 2 个参数: 一个整型值, 称为 howmany, 一个字符串称为 whom. 我们的特别多功能的模块就在加载时, 欢迎 whom 不止一次, 而是 howmany 次. 这样一个模块可以用这样的命令行加载:
insmod hellop howmany=10 whom="Mom"
一旦以那样的方式加载, hellop 会说 "hello, Mom" 10 次.
但是, 在 insmod 可以修改模块参数前, 模块必须使它们可用. 参数用 moudle_param 宏定义来声明, 它定义在 moduleparam.h. module_param 使用了 3 个参数: 变量名, 它的类型, 以及一个权限掩码用来做一个辅助的 sysfs 入口. 这个宏定义应当放在任何函数之外, 典型地是出现在源文件的前面. 因此 hellop 将声明它的参数, 并如下使得对 insmod 可用:
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 类型颠倒了值, 所以真值变成 false, 反之亦然.
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 字段是一个权限值; 你应当使用 中定义的值. 这个值控制谁可以存取这些模块参数在 sysfs 中的表示. 如果 perm 被设为 0, 就根本没有 sysfs 项. 否则, 它出现在 /sys/module[
5
] 下面, 带有给定的权限. 使用 S_IRUGO 作为参数可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR 允许 root 来改变参数. 注意, 如果一个参数被 sysfs 修改, 你的模块看到的参数值也改变了, 但是你的模块没有任何其他的通知. 你应当不要使模块参数可写, 除非你准备好检测这个改变并且因而作出反应.
[
5
] 然而, 在本书写作时, 有讨论将参数移出 sysfs.
2.9. 在用户空间做
一个第一次涉及内核问题的 Unix 程序员, 可能会紧张写一个模块. 编写一个用户程序来直接读写设备端口可能容易些.
确实, 有几个论据倾向于用户空间编程, 有时编写一个所谓的用户空间设备驱动对比钻研内核是一个明智的选择. 在本节, 我们讨论几个理由, 为什么你可能在用户空间编写驱动. 本书是关于内核空间驱动的, 但是, 所以我们不超越这个介绍性的讨论.
用户空间驱动的好处在于:
·
完整的 C 库可以连接. 驱动可以进行许多奇怪的任务, 不用依靠外面的程序(实现使用策略的工具程序, 常常随着驱动自身发布).
·
程序员可以在驱动代码上运行常用的调试器, 而不必走调试一个运行中的内核的弯路.
·
如果一个用户空间驱动挂起了, 你可简单地杀掉它. 驱动的问题不可能挂起整个系统, 除非被控制的硬件真的疯掉了.
·
用户内存是可交换的, 不象内核内存. 一个不常使用的却有很大一个驱动的设备不会占据别的程序可以用到的 RAM, 除了在它实际在用时.
·
一个精心设计的驱动程序仍然可以, 如同内核空间驱动, 允许对设备的并行存取.
·
如果你必须编写一个封闭源码的驱动, 用户空间的选项使你容易避免不明朗的许可的情况和改变的内核接口带来的问题.
例如, USB 驱动能够在用户空间编写; 看(仍然年幼) libusb 项目, 在 libusb.sourceforge.net 和 "gadgetfs" 在内核源码里. 另一个例子是 X 服务器: 它确切地知道它能处理哪些硬件, 哪些不能, 并且它提供图形资源给所有的 X 客户. 注意, 然而, 有一个缓慢但是固定的漂移向着基于 frame-buffer 的图形环境, X 服务器只是作为一个服务器, 基于一个内核空间的真实的设备驱动, 这个驱动负责真正的图形操作.
常常, 用户空间驱动的编写者完成一个服务器进程, 从内核接管作为单个代理的负责硬件控制的任务. 客户应用程序就可以连接到服务器来进行实际的操作; 因此, 一个聪明的驱动经常可以允许对设备的并行存取. 这就是 X 服务器如何工作的.
但是用户空间的设备驱动的方法有几个缺点. 最重要的是:
·
中断在用户空间无法用. 在某些平台上有对这个限制的解决方法, 例如在 IA32 体系上的 vm86 系统调用.
·
只可能通过内存映射 /dev/mem 来使用 DMA, 而且只有特权用户可以这样做.
·
存取 I/O 端口只能在调用 ioperm 或者 iopl 之后. 此外, 不是所有的平台支持这些系统调用, 而存取/dev/port可能太慢而无效率. 这些系统调用和设备文件都要求特权用户.
·
响应时间慢, 因为需要上下文切换在客户和硬件之间传递信息或动作.
·
更不好的是, 如果驱动已被交换到硬盘, 响应时间会长到不可接受. 使用 mlock 系统调用可能会有帮助, 但是常常的你将需要锁住许多内存页, 因为一个用户空间程序依赖大量的库代码. mlock, 也, 限制在授权用户上.
·
最重要的设备不能在用户空间处理, 包括但不限于, 网络接口和块设备.
如你所见, 用户空间驱动不能做的事情毕竟太多. 感兴趣的应用程序还是存在: 例如, 对 SCSI 扫描器设备的支持( 由 SANE 包实现 )和 CD 刻录器 ( 由 cdrecord 和别的工具实现 ). 在两种情况下, 用户级别的设备情况依赖 "SCSI gneric" 内核驱动, 它输出了低层的 SCSI 功能给用户程序, 因此它们可以驱动它们自己的硬件.
一种在用户空间工作的情况可能是有意义的, 当你开始处理新的没有用过的硬件时. 这样你可以学习去管理你的硬件, 不必担心挂起整个系统. 一旦你完成了, 在一个内核模块中封装软件就会是一个简单操作了.
2.10. 快速参考
本节总结了我们在本章接触到的内核函数, 变量, 宏定义, 和 /proc 文件. 它的用意是作为一个参考. 每一项列都在相关头文件的后面, 如果有. 从这里开始, 在几乎每章的结尾会有类似一节, 总结一章中介绍的新符号. 本节中的项通常以在本章中出现的顺序排列:
insmod
modprobe
rmmod
用户空间工具, 加载模块到运行中的内核以及去除它们.
#include
module_init(init_function);
module_exit(cleanup_function);
指定模块的初始化和清理函数的宏定义.
__init
__initdata
__exit
__exitdata
函数( __init 和 __exit )和数据 (__initdata 和 __exitdata)的标记, 只用在模块初始化或者清理时间. 为初始化所标识的项可能会在初始化完成后丢弃; 退出的项可能被丢弃如果内核没有配置模块卸载. 这些标记通过使相关的目标在可执行文件的特定的 ELF 节里被替换来工作.
#include
最重要的头文件中的一个. 这个文件包含很多驱动使用的内核 API 的定义, 包括睡眠函数和许多变量声明.
struct task_struct *current;
当前进程.
current->pid
current->comm
进程 ID 和 当前进程的命令名.
obj-m
一个 makefile 符号, 内核建立系统用来决定当前目录下的哪个模块应当被建立.
/sys/module
/proc/modules
/sys/module 是一个 sysfs 目录层次, 包含当前加载模块的信息. /proc/moudles 是旧式的, 那种信息的单个文件版本. 其中的条目包含了模块名, 每个模块占用的内存数量, 以及使用计数. 另外的字串追加到每行的末尾来指定标志, 对这个模块当前是活动的.
vermagic.o
来自内核源码目录的目标文件, 描述一个模块为之建立的环境.
#include
必需的头文件. 它必须在一个模块源码中包含.
#include
头文件, 包含在建立的内核版本信息.
LINUX_VERSION_CODE
整型宏定义, 对 #ifdef 版本依赖有用.
EXPORT_SYMBOL (symbol);
EXPORT_SYMBOL_GPL (symbol);
宏定义, 用来输出一个符号给内核. 第 2 种形式输出没有版本信息, 第 3 种限制输出给 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
module_param(variable, type, perm);
宏定义, 创建模块参数, 可以被用户在模块加载时调整( 或者在启动时间, 对于内嵌代码). 类型可以是 bool, charp, int, invbool, short, ushort, uint, ulong, 或者 intarray.
#include
int printk(const char * fmt, ...);
内核代码的 printf 类似物.
原子变量: atomic_t. 所有原子量操作必须使用如下函数
atomic_t v = ATOMIC_INIT(0): 声明并初始化atomic变量
void atomic_set(atomic_t *v, int i): 设置值
int atomic_read(atomic_t *v): 读值
void atomic_add(int i, atomic_t *v): 值加i
void atomic_sub(int i, atomic_t *v): 值减i
void atomic_inc(atomic_t *v): 值+1
void atomic_dec(atomic_t *v): 值-1
int atomic_inc_and_test(atomic_t *v): 值+1, 并测试值是否为0, 若为0返回真, 否则返回假
int atomic_dec_and_test(atomic_t *v): 值-1, 并测试值是否为0, 若为0返回真, 否则返回假
int atomic_sub_and_test(int i, atomic_t *v): 值-i, 并测试值是否为0, 若为0返回真, 否则返回假
int atomic_add_negative(int i, atomic_t *v): 值+i, 并测试值是否为负, 若为负返回真, 否则返回假
int atomic_add_return(int i, atomic_t *v): 值+i, 并返回值
int atomic_sub_return(int i, atomic_t *v): 值-i, 并返回值
int atomic_inc_return(atomic_t *v): 值+1, 并返回值
int atomic_dec_return(atomic_t *v): 值-1, 并返回值
位操作:
位操作依赖于体系结构. nr 参数(描述要操作哪个位)常常定义为 int, 也可能是 unsigned long; 要修改的地址常常是一个 unsigned long 指针, 但是几个体系使用 void * 代替.
void set_bit(nr, void *addr): 在 addr 指向的数据项中将第 nr 位设置为1
void clear_bit(nr, void *addr): 在 addr 指向的数据项中将第 nr 位设置为0
void change_bit(nr, void *addr): 对 addr 指向的数据项中的第 nr 位取反
test_bit(nr, void *addr): 返回 addr 指向的数据项中的第 nr 位
int test_and_set_bit(nr, void *addr): 在 addr 指向的数据项中将第 nr 位设置为1, 并返回设置前的值
int test_and_clear_bit(nr, void *addr): 在 addr 指向的数据项中将第 nr 位设置为0, 并返回设置前的值
int test_and_change_bit(nr, void *addr): 对 addr 指向的数据项中的第 nr 位取反, 并返回取反前的值
Mark: 大部分现代代码不使用位操作,而是使用自选锁
seqlock: 2.6内核提供的,
seqlock_t lock1 = SEQLOCK_UNLOCKED;
seqlock_t lock2;
seqlock_init(&lock2);
读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作. 在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试. 结果是, 读者代码象下面的形式:
unsigned int seq;
do {
seq = read_seqbegin(&the_lock);
/* Do what you need to do */
} while read_seqretry(&the_lock, seq);
这个类型的锁常常用在保护某种简单计算, 需要多个一致的值. 如果这个计算最后的测试表明发生了一个并发的写, 结果被简单地丢弃并且重新计算.
如果你的 seqlock 可能从一个中断处理里存取, 你应当使用 IRQ 安全的版本来代替:
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);
写者必须获取一个排他锁来进入由一个 seqlock 保护的临界区. 为此, 调用:
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);
还有一个 write_tryseqlock 在它能够获得锁时返回非零.
读-拷贝-更新(Read-Copy-Update)
针对很多读但是较少写的情况. 所有操作通过指针.
读: 直接操作
写: 将数据读出来, 更新数据, 将原指针指向新数据(这一步需要另外做原子操作的保护)
作为在真实世界中使用 RCU 的例子, 考虑一下网络路由表. 每个外出的报文需要请求检查路由表来决定应当使用哪个接口. 这个检查是快速的, 并且, 一旦内核发现了目标接口, 它不再需要路由表入口项. RCU 允许路由查找在没有锁的情况下进行, 具有相当多的性能好处. 内核中的 Startmode 无线 IP 驱动也使用 RCU 来跟踪它的设备列表.
在读这一边, 使用一个 RCU-保护的数据结构的代码应当用 rcu_read_lock 和 rcu_read_unlock 调用将它的引用包含起来.
RCU 代码往往是象这样:
struct my_stuff *stuff;
rcu_read_lock();
stuff = find_the_stuff(args...);
do_something_with(stuff);
rcu_read_unlock();
rcu_read_lock 调用是很快的: 它禁止内核抢占但是没有等待任何东西. 在读"锁"被持有时执行的代码必须是原子的. 在对 rcu_read_unlock 调用后, 没有使用对被保护的资源的引用.
需要改变被保护的结构的代码必须进行几个步骤: 分配一个新结构, 如果需要就从旧的拷贝数据; 接着替换读代码所看到的指针; 释放旧版本数据(内存).
在其他处理器上运行的代码可能仍然有对旧数据的一个引用, 因此不能立刻释放. 相反, 写代码必须等待直到它知道没有这样的引用存在了. 因为所有持有对这个数据结构引用的代码必须(规则规定)是原子的, 我们知道一旦系统中的每个处理器已经被调度了至少一次, 所有的引用必须消失. 这就是 RCU 所做的; 它留下了一个等待直到所有处理器已经调度的回调; 那个回调接下来被运行来进行清理工作.
改变一个 RCU-保护的数据结构的代码必须通过分配一个 struct rcu_head 来获得它的清理回调, 尽管不需要以任何方式初始化这个结构. 通常, 那个结构被简单地嵌入在 RCU 所保护的大的资源里面. 在改变资源完成后, 应当调用:
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
给定的func在安全的时候调用来释放资源
全部 RCU 接口比我们已见的要更加复杂; 它包括, 例如, 辅助函数来使用被保护的链表. 详细内容见相关的头文件
阻塞 I/O
运行在原子上下文时不能睡眠. 持有一个自旋锁, seqlock, RCU 锁或中断已关闭时不能睡眠. 但在持有一个旗标时睡眠是合法的
不能对醒后的系统状态做任何的假设, 并且必须检查来确保你在等待的条件已经满足
确保有其他进程会做唤醒动作
明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK/O_NDELAY 标志来指示. 只有 read, write, 和 open 文件操作受到非阻塞标志影响
下列情况下应该实现阻塞
如果一个进程调用 read 但是没有数据可用(尚未), 这个进程必须阻塞. 这个进程在有数据达到时被立刻唤醒, 并且那个数据被返回给调用者, 即便小于在给方法的 count 参数中请求的数量.
如果一个进程调用 write 并且在缓冲中没有空间, 这个进程必须阻塞, 并且它必须在一个与用作 read 的不同的等待队列中. 当一些数据被写入硬件设备, 并且在输出缓冲中的空间变空闲, 这个进程被唤醒并且写调用成功, 尽管数据可能只被部分写入如果在缓冲只没有空间给被请求的 count 字节.
若需要在open中实现对O_NONBLOCK的支持,可以返回-EAGAIN
DECLARE_WAIT_QUEUE_HEAD(name);
定义并初始化一个等待队列
init_waitqueue_head(wait_queue_head_t *name);
初始化一个等待队列
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
queue: 是要用的等待队列头. 注意它是"通过值"传递的, 而不是指针
condition: 需要检查的条件, 只有这个条件为真时才会返回. 注意条件可能被任意次地求值, 因此它不应当有任何边界效应(side effects, 按我的理解就是当外界条件未改变时,每次计算得到的结果应该相同)
timeout: 超时值. 表示要等待的 jiffies 数, 是一个相对时间值. 如果这个进程被其他事件唤醒, 它返回以 jiffies 表示的剩余超时值
上述4个函数带interruptible的是可被中断的
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
wake_up 唤醒所有的在给定队列上等待的进程. wake_up_interruptible限制只唤醒因wait_event_interruptible/wait_event_interruptible_timeout睡眠的进程
wake_up_nr(wait_queue_head_t *queue, int nr);
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);
类似 wake_up, 但它们能够唤醒nr个互斥等待者, 而不只是一个. 注意: 0被解释为请求所有的互斥等待者都被唤醒
wake_up_all(wait_queue_head_t *queue);
wake_up_interruptible_all(wait_queue_head_t *queue);
唤醒所有的进程, 不管它们是否进行互斥等待(尽管可中断的类型仍然跳过在做不可中断等待的进程)
wake_up_interruptible_sync(wait_queue_head_t *queue);
正常地, 一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器. 换句话说, 调用 wake_up 可能不是原子的. 如果调用 wake_up 的进程运行在原子上下文(它可能持有一个自旋锁, 例如, 或者是一个中断处理), 这个重调度不会发生. 但是, 如果你需要明确要求不要被调度出处理器在那时, 你可以使用wake_up_interruptible的"同步"变体. 这个函数唤醒其他进程, 但不会让新唤醒的进程抢占CPU
6.2.5.1. 一个进程如何睡眠
放弃处理器是最后一步, 但是要首先做一件事: 你必须先检查你在睡眠的条件. 做这个检查失败会引入一个竞争条件; 如果在你忙于上面的这个过程并且有其他的线程刚刚试图唤醒你, 如果这个条件变为真会发生什么? 你可能错过唤醒并且睡眠超过你预想的时间. 因此, 在睡眠的代码下面, 典型地你会见到下面的代码:
if (!condition)
schedule();
通过在设置了进程状态后检查我们的条件, 我们涵盖了所有的可能的事件进展. 如果我们在等待的条件已经在设置进程状态之前到来, 我们在这个检查中注意到并且不真正地睡眠. 如果之后发生了唤醒, 进程被置为可运行的不管是否我们已真正进入睡眠.
如果在if判断之后,schedule之前,有其他进程试图唤醒当前进程, 那么当前进程就会被置为可运行的(但可能会到下次调度才会再次运行), 所以这个过程是安全的
手动睡眠
DEFINE_WAIT(my_wait);
定义并初始化一个等待队列入口项
init_wait(wait_queue_t*);
初始化等待队列入口项
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
添加你的等待队列入口项到队列, 并且设置进程状态.
queue: 等待队列头
wait: 等待队列入口项
state: 进程的新状态; TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE
调用prepare_to_wait之后, 需再次检查确认需要等待, 便可调用schedule释放CPU
在schedule返回之后需要调用下面的函数做一些清理动作
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
在此之后需要再次检查是否需要再次等待(条件已经满足还是被其他等待的进程占用?)
互斥等待
当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项添加到开始.
wake_up调用在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止(即进行互斥等待的进程一次只以顺序的方式唤醒一个). 但内核仍然每次唤醒所有的非互斥等待者.
用函数
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
代替手动睡眠中的prepare_to_wait即可实现互斥等待
老版本的函数
void sleep_on(wait_queue_head_t *queue);
void interruptible_sleep_on(wait_queue_head_t *queue);
这些函数无法避免竞争(在条件判断和sleep_on之间会存在竞争). 见上面"6.2.5.1. 一个进程如何睡眠"的分析
时间
HZ: 1秒产生的tick. 是一个体系依赖的值. 内核范围此值通常为100或1000, 而对应用程序,此值始终是100
jiffies_64: 从系统开机到现在经过的tick, 64bit宽
jiffies: jiffies_64的低有效位, 通常为unsigned long. 编程时需要考虑jiffies隔一段时间会回绕(重新变成0)的问题
u64 get_jiffies_64(void);
得到64位的jiffies值
int time_after(unsigned long a, unsigned long b);
若a在b之后,返回真
int time_before(unsigned long a, unsigned long b);
若a在b之前,返回真
int time_after_eq(unsigned long a, unsigned long b);
若a在b之后或a与b相等,返回真
int time_before_eq(unsigned long a, unsigned long b);
若a在b之前或a与b相等,返回真
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);
struct timeval 和 struct timespec 与 jiffies 之间的转换
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
read tsc/read tsc low/read tsc long long: 读取短延时(很容易回绕), 高精度的时间值. 不是所有平台都支持
cycles_t get_cycles(void);
类似rdtsc, 但每个平台都提供. 若CPU为提供相应功能则此函数一直返回0
注意:
它们在一个 SMP 系统中不能跨处理器同步. 为保证得到一个一致的值, 你应当为查询这个计数器的代码禁止抢占
短延时(很容易回绕), 高精度
不是所有平台都支持
unsigned long mktime (unsigned int year, unsigned int mon, unsigned int day, unsigned int hour, unsigned int min, unsigned int sec);
转变一个墙上时钟时间到一个 jiffies 值
void do_gettimeofday(struct timeval *tv);
获取当前时间
struct timespec current_kernel_time(void);
获取当前时间. 注意: 返回值是个结构体不是结构体指针
延时
长延时之忙等待(cpu密集型进程会有动态降低的优先级)
while (time_before(jiffies, j1))
cpu_relax();
让出CPU(无法确定什么时候重新得到CPU, 即无法确定什么时候循环结束, 也即无法保证延时精度)
while (time_before(jiffies, j1)) {
schedule();
}
long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);
timeout表示要等待的 jiffies 数, 是相对时间值, 不是一个绝对时间值. 如果超时到, 这些函数返回 0; 如果这个进程被其他事件唤醒, 它返回以 jiffies 表示的剩余超时值.
set_current_state(TASK_INTERRUPTIBLE);
signed long schedule_timeout(signed long timeout);
不需要等待特定事件的延时
注意: 上述几个函数在超时事件到与被调度之间有一定的延时
短延时之忙等待
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
这几个函数是体系特定的. 每个体系都实现 udelay, 但是其他的函数可能或者不可能定义
获得的延时至少是要求的值, 但可能更多
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)
使调用进程进入睡眠给定的毫秒数.
如果进程被提早唤醒(msleep_interruptible), 返回值是剩余的毫秒数
内核定时器
定时器是基于中断的(通常来讲是通过软中断), 所以会缺少进程上下文.
定时器
Timer运行在非进程上下文中. 通常只能提供tick为单位的精度
struct timer_list {
/* ... */
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
};
expires: 超时时间
function: 时间到时需要回调的函数
data: function的参数
void init_timer(struct timer_list *timer);
初始化一个timer
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
定义并初始化一个timer
void add_timer(struct timer_list * timer);
将timer加入调度. 每次这个timer被调度后, 就会从调度链表中去掉, 所以如果需要反复运行,需要重新add_timer或者用mod_timerint del_timer(struct timer_list * timer);
将timer从调度链表中去除
int mod_timer(struct timer_list *timer, unsigned long expires);
更新一个定时器的超时时间(并将其重新加入到调度链表中去). mod_timer也可代替add_timer用于激活timer
int del_timer_sync(struct timer_list *timer);
同del_timer 一样工作, 并且还保证当它返回时, 定时器函数不在任何 CPU 上运行. 这个函数应当在大部分情况下比 del_timer 更优先使用. 如果它在非原子上下文被调用可能导致睡眠, 在其他情况下会忙等待. 在持有锁时要十分小心调用. 另外: 如果一个timer重新激活自己, 可能会导致此函数一直等待下去(解决方法是在重新激活自己前加入必要的条件限制)
int timer_pending(const struct timer_list * timer);
返回真或假来指示是否定时器当前是否正被调度运行
调度
Tasklet: 在软中断上下文中运行, 所以必须是原子的
如果系统不在重载下, 可能立刻运行; 否则,也不会晚于下一个tick. 通常,系统在退出irq之前会检查有没有softirq需要运行, 一般来说Tasklet会在这个时候被调度
一个tasklet可能和其他tasklet并发执行, 但是对它自己是严格地串行的 同样的tasklet在一个时刻只能运行在一个CPU(通常为调度它的CPU)上
struct tasklet_struct {
/* ... */
void (*func)(unsigned long);
unsigned long data;
};
func: 需要运行的函数
data: func的参数
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
初始化一个tasklet结构
DECLARE_TASKLET(name, func, data);
定义并初始化一个tasklet结构
DECLARE_TASKLET_DISABLED(name, func, data);
定义并初始化一个被禁止调度了的tasklet结构
void tasklet_disable(struct tasklet_struct *t);
禁止一个tasklet.
注意:
这个tasklet仍然可能被调度, 但是直到它被使能之后才会执行.
如果这个tasklet正在运行, tasklet_disable忙等待直到这个tasklet退出
void tasklet_disable_nosync(struct tasklet_struct *t);
同tasklet_disable, 但如果这个tasklet正在运行, 则不等待其退出
void tasklet_enable(struct tasklet_struct *t);
使能一个之前被禁止的tasklet.
注意:
tasklet_enable必须匹配tasklet_disable, 因为内核跟踪每个tasklet的"禁止次数".
void tasklet_schedule(struct tasklet_struct *t);
调度tasklet执行.
如果一个tasklet在运行前被再次调度(tasklet_schedule), 它只运行一次.
如果它在运行中被调度, 它在完成此次运行后会再次运行; 这保证了在其他事件被处理当中发生的事件收到应有的注意. (这个做法也允许一个 tasklet 重新调度它自己)
void tasklet_hi_schedule(struct tasklet_struct *t);
调度tasklet在更高优先级执行.
void tasklet_kill(struct tasklet_struct *t);
确保了这个tasklet不会被再次调度运行.
如果这个tasklet正在运行, 这个函数等待直到它执行完毕.
如果这个tasklet重新调度它自己, 可能会导致 tasklet_kill 一直等待(所以需要在一个tasklet重新调度自己前需要加入一些条件限制).
工作队列:
每个工作队列有一个或多个专用的进程("内核线程")
工作队列就在这个特殊的内核上下文中运行, 所以可以是非原子的, 但不能存取用户空间
注意:缺省队列对所有驱动程序来说都是可用的;但是只有经过GP许可的驱动程序可以用自定义的工作队列
DECLARE_WORK(name, void (*function)(void *), void *data);
定义并初始化一个工作队列
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
初始化一个工作队列
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);
同INIT_WORK, 但不把它链接到工作队列中
struct workqueue_struct *create_workqueue(const char *name);
这个函数在每个处理器上都创建一个专用的内核线程
struct workqueue_struct *create_singlethread_workqueue(const char *name);
同create_workqueue, 但只在一个cpu上创建线程
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
添加工作到给定的队列
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay);
添加工作work到给定的队列queue, 但这个工作会延迟delay个jiffies才会执行
这两个函数如果返回非0意味着队列中已经有工作work存在了
int cancel_delayed_work(struct work_struct *work);
取消一个已经加入队列但未运行的工作
若此函数返回0表示工作正在运行(可能在其他cpu上). 这种情况下, 工作会继续,但不会被再次添加到工作队列中去
若需要确保指定的工作没有运行,需要在这个函数后跟随下列函数:
void flush_workqueue(struct workqueue_struct *queue);
清空队列中的所有工作
void destroy_workqueue(struct workqueue_struct *queue);
销毁一个工作队列
内核共享队列:
int schedule_work(struct work_struct *work);
向内核缺省工作队列(共享队列)中添加一个任务
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
向内核缺省工作队列(共享队列)中添加一个任务并延迟执行
int cancel_delayed_work(struct work_struct *work);
void flush_scheduled_work(void);
共享队列中的相应版本
内存分配:
Linux 内核最少 3 个内存区: DMA-capable memory, low memory, high memory.
DMA-capable memory: x86平台中, DMA区用在RAM的前16MB, 因为传统的ISA设备只能在这个区域内做DMA操作; PCI 设备没有这个限制.
Low memory: 因为所有可用的内存最初都是被映射到内核空间再进行分配的, 而内核空间的大小有限(通常被配置为1G), 所以实际能够利用的内存大小也是有限的, 这部分内存就是low memory. 内核的很多数据结构都必须放在low memory中
High memory: 由于low memory大小有限, 在配置了大量内存的主机里, 大于low memory部分的空间只能通过明确的虚拟映射映射进来. 这部分大于low memory部分的内存就叫High memory. High memory没有逻辑地址. 打开内核的High memory支持会导致性能下降
User virtual addresses: 用户(进程)虚拟地址. 这是被用户程序见到的常规地址. 用户地址在长度上是 32 位或者 64 位, 依赖底层的硬件结构. 每个进程有它自己的虚拟地址空间.
Physical addresses: 物理地址. 在处理器和系统内存之间使用的地址. 物理地址是3或者64位的量. 32位系统在某些情况下可使用更大的物理地址.
Bus addresses: 总线地址. 在外设和内存之间使用的地址. 通常, 它们和处理器使用的物理地址相同; 但一些体系可提供一个I/O内存管理单元(IOMMU), 它在总线和主内存之间重映射地址. 一个IOMMU可用多种方法使事情简单(例如, 使散布在内存中的缓冲对设备看来是连续的)
Kernel logical addresses: 内核逻辑地址. 这些组成了正常的内核地址空间. 这些地址映射了部分(也许全部)主存并且常常被当作物理内存来对待.
在大部分的体系上, 逻辑地址和它们的相关物理地址只差一个常量偏移. 逻辑地址使用硬件的本地指针大小, 因此, 可能不能在配置大量内存(大于4G)的32位系统上寻址所有的物理内存.
逻辑地址常常存储于unsigned long或者void *类型的变量中.
从 kmalloc 返回的内存有内核逻辑地址.
Kernel virtual addresses: 内核虚拟地址. 类似于逻辑地址, 它们都是从内核空间地址到物理地址的映射. 内核虚拟地址不必有逻辑地址空间具备的线性的, 一对一到物理地址的映射
所有的逻辑地址都是内核虚拟地址, 但是许多内核虚拟地址不是逻辑地址.
例如, vmalloc分配的内存有虚拟地址(但没有直接物理映射). kmap 函数也返回虚拟地址.
虚拟地址常常存储于指针变量.
内存相关的数据结构:
从物理系统供给内存的角度看, 各个数据结构总体关系如下:
一个线性的存储区被称为节点(node)
typedef struct pglist_data {
zone_t node_zones[MAX_NR_ZONES]; 该节点的zone类型,一般包括ZONE_HIGHMEM、ZONE_NORMAL和ZONE_DMA三类
zonelist_t node_zonelists[GFP_ZONEMASK+1]; 分配时内存时zone的排序。由free_area_init_core()通过page_alloc.c::build_zonelists()设置zone的顺序
int nr_zones; 该节点的zone个数, 可以从1 到3(即上面的ZONE_HIGHMEM、ZONE_NORMAL和ZONE_DMA),但不是所有的节点都需要有3个zone
struct page *node_mem_map; 当前节点的struct page数组, 可能为全局mem_map中的某个位置
unsigned long *valid_addr_bitmap; 节点内存空洞的位图
struct bootmem_data *bdata;
unsigned long node_start_paddr; 该节点的起始物理地址
unsigned long node_start_mapnr; 全局mem_map中的页偏移
unsigned long node_size; 该zone内的页框总数
int node_id; 该节点的ID, 全系统节点ID从0开始. 系统中所有节点都维护在pgdat_list列表中
struct pglist_data *node_next;
}pg_data_t;
节点中的内存被分为多块(通常为DMA memory, low memory, high memory), 这样的块被称为zone.
通常有这样的划分: ZONE_DMA: 0 -- 16MB;ZONE_NORMAL: 16MB - 896MB;ZONE_HIGHMEM: 896MB --
typedef struct zone_struct {
/*Commonly accessed fields*/
spinlock_t lock; 操作此结构时需要得到的自旋锁
unsigned long free_pages; 剩余的空闲页总数
unsigned long pages_min, pages_low, pages_high; zone中空闲页的阀值, 详细见下
int need_balance; 告诉kswapd需要对该zone的页进行交换
/** free areas of different sizes*/
free_area_t free_area[MAX_ORDER]; 根据连续页大小分组的空闲页链表组, 连续页大小分为 2^0, 2^1, 2^2, ... 2^MAX_ORDER个连续页
/** Discontig memory support fields.*/
struct pglist_data *zone_pgdat; 父管理结构, 见上
struct page *zone_mem_map; 当前zone的mem_map, 即全局mem_map中该zone所引用的第一页位置
unsigned long zone_start_paddr; zone开始的物理地址(包括此地址)
unsigned long zone_start_mapnr; 在全局mem_map中的索引(或下标)
/** rarely used fields:*/
char *name; zone名字, “DMA”,“Normal”或“HighMem”
unsigned long size; zone的大小, 以页为单位
} zone_t;
当free_pages达到pages_min时,buddy分配器将采用同步方式进行kswapd的工作;
当free_pages达到pages_low时,kswapd被buddy分配器唤醒,开始释放页;
当free_pages达到pages_high时,kswapd将被唤醒,此时kswapd不会考虑如何平衡该zone,直到有pages_high空闲页为止。
typedef struct page {
struct list_head list; 通过此结构挂到空闲队列/干净缓冲队列/脏缓冲队列等
struct address_space *mapping; /* The inode (or ...) we belong to. */
unsigned long index; PFN, page frame number, 在页索引数组中的index
struct page *next_hash; 指向页高速缓存哈希表中下一个共享的页
atomic_t count; 页引用计数
unsigned long flags; 一套描述页状态的一套位标志. 这些包括 PG_locked, 它指示该页在内存中已被加锁, 以及 PG_reserved, 它防止内存管理系统使用该页.
struct list_head lru; /* Pageout list, eg. active_list; protected by pagemap_lru_lock !! */
wait_queue_head_t wait; 等待这一页的页队列
struct page **pprev_hash; 与next_hash相对应
struct buffer_head * buffers; 把缓冲区映射到一个磁盘块
void *virtual; 被映射成的虚拟地址(Kernel virtual address, NULL if not kmapped, ie. highmem)
struct zone_struct *zone; 属于哪个管理区, 其结构见上
} mem_map_t;
从系统对内存的需求的角度来看, 又有如下结构
struct vm_area_struct {
struct mm_struct * vm_mm; 指向父mm(用户进程mm)
unsigned long vm_start; 当前area开始的地址
unsigned long vm_end; 当前area结束的地址
struct vm_area_struct *vm_next; 指向同一个mm中的下一个area
pgprot_t vm_page_prot; 当前area的存取权限
unsigned long vm_flags; 描述这个区的一套标志. 详细见下
rb_node_t vm_rb;
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops; 一套内核可用来操作此区间的函数, 详细见mmap
unsigned long vm_pgoff; 若与文件关联, 则为在文件中的偏移, 以PAGE_SIZE为单位
struct file * vm_file; 如果当前area与一个文件关联, 则指向文件的struct file结构
unsigned long vm_raend;
void * vm_private_data;
};
vm_flags: 对驱动编写者最感兴趣的标志是 VM_IO 和 VM_RESERVUED.
VM_IO 标志一个VMA作为内存映射的I/O区. VM_IO 标志阻止这个区被包含在进程核转储中.
VM_RESERVED 告知内存管理系统不要试图交换出这个VMA; 它应当在大部分设备映射中设置.
物理地址与页的关系:
页大小: PAGE_SIZE
物理地址(PA)-->页帧号(PFN, 此页在页索引数组中的index): PA右移PAGE_SHIFT位得到PFN
struct page *virt_to_page(void *kaddr);
内核逻辑地址转换成struct page指针. 注意: 需要一个逻辑地址, 不能使用来自vmalloc的内存或者high memory
struct page *pfn_to_page(int pfn);
页帧号(PFN, page frame number)转换成struct page指针
void *page_address(struct page *page);
返回这个页的内核虚拟地址, 如果这样一个地址存在.
对于高内存, 那个地址仅当这个页已被映射才存在. 大部分情况下, 使用 kmap 的一个版本而不是 page_address.
#include
void *kmap(struct page *page);
void kunmap(struct page *page);
kmap为系统中的任何页返回一个内核虚拟地址.
对于低内存页, 它只返回页的逻辑地址;
对于高内存, kmap 在内核地址空间的一个专用部分中创建一个特殊的映射.
使用 kmap 创建的映射应当一直使用 kunmap 来释放. 映射的总数是有限的, 所以不再使用时需要及时unmap
kmap 调用维护一个计数器, 因此如果 2 个或 多个函数都在同一个页上调用 kmap, 操作也是正常的
注意: 当没有映射可用时, kmap可能睡眠.
#include
#include
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
kmap_atomic 是kmap 的一种高性能形式. 每个体系都给原子的kmaps维护一个槽(专用的页表项); kmap_atomic的调用者必须在type参数中指定哪个槽. 对驱动有意义的唯一插口是 KM_USER0和KM_USER1(对于直接从来自用户空间的调用运行的代码), 以及KM_IRQ0和KM_IRQ1(对于中断处理).
注意带atomic的kmaps必须是原子的
void *kmalloc(size_t size, int flags);
在内核里分配一块内存
size: 分配的块的大小. 内核只能分配某些预定义的, 固定大小的字节数组. kmalloc 能够处理的最小分配是 32 或者 64 字节. kmalloc 能够分配上限128 KB
flags: 分配的标志. "GFP"是_get_free_page的缩写
GFP_KERNEL:表示在进程上下文中调用. 可能会导致睡眠, 所以所在函数必须是可重入的且不能在原子上下文中使用
GFP_ATOMIC:用来从中断处理和进程上下文之外的其他代码中分配内存.
GFP_USER:用来为用户空间页来分配内存; 它可能睡眠.
GFP_HIGHUSER:如同 GFP_USER, 但是从高端内存分配
GFP_NOIO
GFP_NOFS
同GFP_KERNEL, 但是它们有更多限制:
GFP_NOFS:不允许进行任何文件系统调用;
GFP_NOIO:不允许任何I/O初始化.
它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是不允许递归的文件系统调用
上面的分配标志可以与下列标志相或来作为参数, 下面的标志改变分配区域:
__GFP_DMA:这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的.
__GFP_HIGHMEM:这个标志指示分配的内存可以位于高端内存. 确切的含义是平台依赖的.
__GFP_COLD:
这个标志请求一个"冷缓冲"页. (因为正常地, 内存分配器尽力返回"热缓冲"的页.)
它对分配页作 DMA 读是有用的, 因为此时在处理器缓冲中出现的数据是无用的.
__GFP_