策略:如何使用这些能力
在编写驱动时, 程序员应当编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求. 驱动应当做到使硬件可用, 将所有关于如何使用硬件的事情留给应用程序
内核空间和用户空间不能直接操作,必须通过特别的函数(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)
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{
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 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{
struct char_device *i_cdev; 内核的内部结构, 代表字符设备. 当节点是一个字符设备文件时, 这个成员包含一个指针, 指向这个结构
UTS_RELEASE
这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, "2.6.10".
LINUX_VERSION_CODE
这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). []有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.
KERNEL_VERSION(major,minor,release)
这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如, KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.
module-objs := file1.o file2.o #最终模块用到的obj列表
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) modulesendif
modprobe:加载指定的模块及其相关模块. 它查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果未定义的symbols, modprobe 在模块搜索路径(/etc/modprobe.conf)中寻找并加载其他定义了symbols的模块
rmmod: 从内核中去除指定模块
lsmod: 打印内核中当前加载的模块的列表. 通过读取/proc/modules或/sys/module 的 sysfs 虚拟文件系统工作
-1 Invalid module format:编译模块用的内核源代码版本与当前运行的内核的版本不匹配
调试技术
CONFIG_DEBUG_STACK_USAGE
CONFIG_IKCONFIG_PROC
-t: 来显示每个系统调用执行的时间
-T: 来显示调用中花费的时间
-e: 来限制被跟踪调用的类型
-o: 来重定向输出到一个文件. 缺省地, strace 打印调用信息到 stderr.
gdb /usr/src/linux/vmlinux /proc/kcore
第一个参数是非压缩的 ELF 内核可执行文件的名子, 不是 zImage 或者 bzImage
第二个参数是核心文件的名子.
读到的是内核即时映象,内核仍在运行,所以有些数据可能会与即时值不匹配--刷新映象:core-file /proc/kcore
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:
一个内核 oops(异常?) 发生时
命中一个断点时
mds address
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
接口特定的类型, 请参考原文
其他移植性问题:
页大小: PAGE_SIZE
页偏移: PAGE_SHIFT
字节序:
#ifdef __BIG_ENDIAN
......
#endif
#ifdef __LITTLE_ENDIAN
......
#endif
#include
u32 le32_to_cpu (u32);
cpu_to_le16/le16_to_cpu/cpu_to_le64/....
cpus_to_le16/le16_to_cpus/cpus_to_le64/....
带's'后缀的是有符号版
get_unaligned(ptr);
put_unaligned(val, ptr);
long IS_ERR(const void *ptr); 判断一个返回值是否有效
long PTR_ERR(const void *ptr); 提取返回的错误码, 在提取前需要判断返回值是否有效
#include
struct list_head { struct list_head *next, *prev; };
LIST_HEAD(struct list_head);
list_entry(struct list_head *ptr, type_of_struct, field_name);
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 < new->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_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): 声明模块加载时允许设置的数组参数
type: 是数组元素的类型
charp: 一个字符指针值. 需要为其分配内存(charp, NOT char)
int/long/short/uint/ulong/ushort: 基本的变长整型值. 以 u 开头的是无符号值.
perm: 通常的权限值, 在
示例:
在模块中声明如下: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"
大部分注册函数以 register_ 做前缀
__init/__initdata: 给定的函数/数据只是在初始化使用. 模块加载后丢掉这个初始化函数, 使它的内存可做其他用途.
__devinit/__devinitdata: 内核没有配置支持 hotplug 设备时等同于__init/_initdata.
container_of(pointer, container_type, container_field);通过一个结构体成员的地址得到结构体的地址
比如:
struct test{int a; int b; int c; inte}test_t;
&test_t == container_of(&(test_t.c), struct test, c)
__setup("test=", test_setup);
在执行init/main.c::checksetup()时会去kernel boot commandline中寻找字符串"test=xxx": 如果有找到,就用"xxx"作为参数调用test_setup; 否则不运行
在insmod中如果参数里带有"test=xxx"也会运行
void *kmalloc(size_t size, int flags); 试图分配 size 字节的内存; 返回值是指向那个内存的指针或者如果分配失败为NULL. flags 参数用来描述内存应当如何分配
申请的空间大小限制: 大概为128K
void kfree(void *ptr);分配的内存应当用 kfree 来释放. 传递一个 NULL 指针给 kfree 是合法的.
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): 验证用户空间有效性
addr: 一个用户空间地址,
size: 需要验证的大小.
返回值: 1 是成功(存取没问题); 0 是失败(存取有问题). 如果它返回0, 驱动应当返回 -EFAULT
put_user(datum, ptr)
__put_user(datum, ptr)
put_user 检查用户空间确保能写. 在成功时返回 0, 并且在错误时返回 -EFAULT.
__put_user 进行更少的检查(它不调用 access_ok),
驱动应当调用put_user来节省几个周期; 或者拷贝几个项时, 在第一次数据传送之前调用access_ok一次, 之后使用__put_user
get_user(local, ptr)
__get_user(local, ptr)
如果使用上述四个函数时, 发现一个来自编译器的奇怪消息, 例如"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 取值有以下这些:
int printk(const char * fmt, ...);向console(而不是虚拟终端)打印一条消息, 并通过附加不同的记录级别或者优先级在消息上对消息的严重程度分类.没有指定优先级的printk语句缺省是DEFAULT_MESSAGE_LOGLEVEL, 在 kernel/printk.c里指定作为一个整数. 在2.6.10内核中, DEFAULT_MESSAGE_LOGLEVEL是KERN_WARNING, 但这个值在不同的内核中可能不一样.
- 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);
void set_current_state(int new_state); 设置当前进程的运行状态
在新代码中不鼓励使用下面这种方式
current->state = TASK_INTERRUPTIBLE
如果处理器当前在中断上下文(包括软中断和硬中断)运行就返回非零
int in_atomic(void)
若调度被禁止(即当前状态是原子态, 包括硬中断,软件中断以及持有自旋锁时), 返回值是非零. 在持有自旋锁这种情况, current 可能是有效的, 但是禁止存取用户空间, 因为它能导致调度发生.
无论何时使用 in_interrupt(), 应当真正考虑是否 in_atomic 是你实际想要的
主次设备号:
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): 获取一个或多个设备编号来使用
count 是你请求的连续设备编号的总数
name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);动态分配一个主编号
fisetminor 应当是请求的第一个要用的次编号; 它常常是 0.
count 是你请求的连续设备编号的总数
name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中
void unregister_chrdev_region(dev_t first, unsigned int count); 释放设备编号
count 是你请求的连续设备编号的总数
设备注册:
struct cdev *cdev_alloc(void);为struct cdev申请内存空间
void cdev_init(struct cdev *cdev, struct file_operations *fops);初始化struct cdev结构. 其成员owner应当设置为 THIS_MODULE
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);将设备注销
name 是驱动的名子(出现在 /proc/devices)
fops 是缺省的 file_operations 结构.
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 < KERNEL_VERSION(2,6,0)
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 < 0)
goto __mod_init_err1;
//make devfs
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
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 < KERNEL_VERSION(2,6,0)
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;
2. 如果"没有数据, 但是可能后来到达", 在这种情况下, read 系统调用应当阻塞.
3. 返回值
如果是正数, 但是小于 count, 只有部分数据被传送.
如果值为 0, 到达了文件末尾(没有读取数据).
如果值为负值表示有一个错误. 这个值指出了什么错误, 根据
ssize_t write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
1. 通常应当更新 *offp 中的文件位置来表示在系统调用成功完成后当前的文件位置.
2. 返回值:
如果值等于 count, 要求的字节数已被传送
如果正值, 但是小于 count, 只有部分数据被传送
如果值为 0, 什么没有写. 这个结果不是一个错误
一个负值表示发生一个错误
ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
{
void __user *iov_base;
__kernel_size_t iov_len;
};
每个 iovec 描述了一块要传送的数据; 它开始于 iov_base (在用户空间)并且有 iov_len 字节长. count 参数告诉有多少 iove结构.
若未定义此二函数. 内核使用 read 和 write 来模拟它们,
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
cmd 参数从用户那里不改变地传下来,
arg 是可选的参数, 无论是一个整数还是指针(按照惯例应该用指针), 均以unsigned long的形式传递进来
返回值:
-ENOTTY: 一个不合适的 ioctl 命令. 这个错误码被 C 库解释为"设备的不适当的ioctl"(inappropriate ioctl for device)
ioctl的cmd参数应当是系统唯一的, 这是出于阻止向错误的设备发出其可识别但具体内容无法解析的命令的考虑
cmd参数由这几部分组成: type, number, direction, size
_IOC_READ: 从系统到用户空间
_IOC_WRITE: 从用户空间到系统
_IOC_READ|_IOC_WRITE: 数据在2个方向被传送
_IOR(type, nre, datatype): 创建从驱动中读数据的命令
_IOW(type,nr,datatype): 创建写数据的命令
_IOWR(type,nr,datatype): 创建双向传送的命令
_IOC_TYPE(cmd):得到magic number
_IOC_NR(cmd):得到顺序号
_IOC_DIR(cmd): 得到传送方向
_IOC_SIZE(cmd): 得到参数大小
2. 只对常规文件发出的那些.
3. 对文件系统类型特殊的那些.
wait: 用于poll_wait函数
返回值: 可能不必阻塞就立刻进行的操作
void poll_wait(struct file *, wait_queue_head_t *, poll_table *);
返回的位掩码:
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
struct scull_pipe *dev = filp->private_data;
unsigned int mask = 0;
/*
* The buffer is circular; it is considered full
* if "wp" is right behind "rp" and empty if the
* two are equal.
*/
down(&dev->sem);
poll_wait(filp, &dev->inq, wait);
poll_wait(filp, &dev->outq, wait);
if (dev->rp != dev->wp)
mask |= POLLIN | POLLRDNORM; /* readable */
if (spacefree(dev))
mask |= POLLOUT | POLLWRNORM; /* writable */
up(&dev->sem);
return mask;
}
这个代码简单地增加了 2 个 scullpipe 等待队列到 poll_table, 接着设置正确的掩码位, 根据数据是否可以读或写.
从用户的角度看异步通知的设置过程:
2. 在设备中设置 FASYNC 标志: fcntl 系统调用发出 F_SETFL 命令
这样设置后设备有 新数据到达/缓冲有空间 的时候就会发送一个SIGIO信号到filp->f_owner 中的进程(如果值为负值则发给整个进程组).
举例:
signal(SIGIO, &input_handler); /* dummy sample; sigaction() is better */
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags = fcntl(STDIN_FILENO, F_GETFL); //get original setting
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
2. 当发出 F_SETFL 来打开 FASYNC, 驱动的fasync方法被调用. 无论何时filp->f_flags中的FASYNC的值有改变, 都会调用驱动中的fasync方法(这个标志在文件打开时缺省地未设置).
3. 当数据到达, 向所有的注册异步通知的进程发出一个 SIGIO 信号.
2. 内核的第二步驱动应当用下列函数响应:
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
mode: 0: 去除入口项; 其他: 添加入口项
fa: 是由驱动提供的一个struct fasync_struct结构. (*fa)在第一次使用之前应该初始化成NULL, 不然可能会出错. 从函数返回的时候会被分配一块内存, 在mode=0时free掉. 所以添加与去除入口项必须配对使用
fa: 与fasync_help里的fa同
sig: 被传递的信号(常常是 SIGIO)
band: 异步状况. 在网络代码里可用来发送"紧急"或者带外数据
POLL_OUT: 有空间可供写入
struct fasync_struct *async_queue = NULL;
static int fasync(int fd, struct file *filp, int mode)
{
return fasync_helper(fd, filp, mode, &async_queue);
}
当数据到达, 用下面的语句来通知异步读者.
if (async_queue)
kill_fasync(&async_queue, SIGIO, POLL_IN);
在release方法中应该调用
/* remove this filp from the asynchronously notified filp's */
fasync(-1, filp, 0);
如果需要禁止lseek操作, 需要在open中调用
int nonseekable_open(struct inode *inode; struct file *filp);
并把file_operations::llseek设为no_llseek(loff_t no_llseek(struct file *file, loff_t offset, int whence))
举例:
loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
struct scull_dev *dev = filp->private_data;
loff_t newpos;
switch(whence)
{
case 0: /* SEEK_SET */
newpos = off;
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + off;
break;
case 2: /* SEEK_END */
newpos = dev->size + off;
break;
default: /* can't happen */
return -EINVAL;
}
if (newpos < 0)
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
mmap操作: 将设备驱动里的一段内存映射到用户空间. 通过在current->mm中增加具有物理地址->虚拟地址映射关系的pmd, pte项来实现
设备驱动的内存区间(被映射区)的起始地址必须位于PAGE_SIZE整数倍的物理地址.
若用户空间所要求的size不是PAGE_SIZE的整数倍, 内核会自动将其扩大成整数倍
mmap的设备驱动中的原型为
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
vma: 最终用户空间得到的struct vm_area_struct. 驱动所看到的这个参数已经被内核填充了大量数据, 驱动所需要做的就是将其地址区域建立合适的页表(PMD:中间目录描述表; PTE:页表项). 若有需要可能还需要更新struct vm_area_struct::vm_ops. vm_ops是struct vm_operations_struct结构, 其定义如下
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int unused);
};
close: 在撤销一个引用时会调用
nopage: 在映射区间发生缺页异常或做mremap(重新映射)时会调用. 注意:若vm_ops中实现了此函数, 那么mmap实现会有所不同, 详细见下
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long phys_addr, unsigned long size, pgprot_t prot);
vma
这个页帧号简单地是物理地址右移PAGE_SHIFT位.
对大部分使用, VMA 结构的vm_paoff成员正好包含你需要的值.
这个函数影响物理地址从 (pfn<
remap_pfn_range用在pfn指向实际的系统RAM的情况下.
所以不能映射get_free_page得到的空间.
但ioremap函数返回的虚拟地址比较特殊, 所以可以用remap_pfn_range来映射
实际上, 这2个函数除了在SPARCcpu上, 每个体系上都是一致的. 并且在大部分情况下被使用看到remap_pfn_range.
例子:
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
return 0;
}
static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
if (offset >= __pa(high_memory) || (filp->f_flags & O_SYNC))
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
vma->vm_ops = &simple_nopage_vm_ops;
simple_vma_open(vma);
return 0;
}
struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)如果由于某些原因, 不能返回一个正常的页(即请求的地址超出驱动的内存区), 可以返回NOPAGE_SIGBUS指示错误; 也可以返回NOPAGE_OOM来指示由于资源限制导致的失败.
{
struct page *pageptr;
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physaddr = address - vma->vm_start + offset;
unsigned long pageframe = physaddr >> PAGE_SHIFT;
if (!pfn_valid(pageframe))
return NOPAGE_SIGBUS;
pageptr = pfn_to_page(pageframe);
get_page(pageptr);
if (type)
*type = VM_FAULT_MINOR;
return pageptr;
}
这里,
get_page是增加此页面的引用计数,必须实现
type是返回错误类型, 对设备驱动来说, VM_FAULT_MINOR是唯一正确的值
注意, PCI 内存被映射在最高的系统内存之上, 并且在系统内存中没有这些地址的入口, 所以没有对应struct page来返回指针, nopage不能在这些情况下使用--必须使用remap_pfn_range代替.
如果nopage方法被留置为 NULL, 处理页出错的内核代码映射零页到出错的虚拟地址.
零页是一个写时拷贝的页, 任何引用零页的进程都看到一个填满 0 的页. 如果进程写到这个页, 内核将一个实际的页挂到进程中去.
因此, 如果一个进程通过调用mremap扩展一个映射的页, 并且驱动还没有实现nopage, 那么进程将不会因为一个段错误而是因为一个零填充的内存结束
防止被缓存的方法可以参考driver/video/fbmem.c->fb_mmap的做法, 比如其中提及的arm体系防止空间被缓存的做法如下
.....
#elif defined(__arm__)
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* This is an IO map - tell maydump to skip this VMA */
vma->vm_flags |= VM_IO;
#elif defined(__sh__)
.....
非static函数必须明确在函数内部加锁(而不应该留给外部调用前处理); 静态函数可自行处理
当多个锁必须同时获得时,应当以同一顺序申请
当本地与内核的锁必须同时获得时,先申请本地的锁
当mutex与spin lock必须同时获得时, 先申请mutex(若先申请spin lock, 那么另一个想要申请spin lock的进程会一直自旋耗用大量资源)
它允许一个在等待一个semaphore的用户空间进程被用户中断. 作为一个通用的规则, 不应该使用不可中断的操作, 除非实在是没有选择. 不可中断操作将创建不可杀死的进程.
如果操作是可中断的, 函数返回一个非零值, 并且调用者不持有semaphore. 正确的使用 down_interruptible 需要一直检查返回值并且针对性地响应.
举例:
int down_trylock(struct semaphore *sem);DECLARE_MUTEX(mutex);
...
if (down_interruptible(&mutex))
return -ERESTARTSYS;
如果在调用down_trylock时semaphore不可用, 它将立刻返回一个非零值.
void up(struct semaphore *sem);
释放semaphore
void 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);
这一系列函数基本与mutex对应版本同. 区别是, 可以有多个进程同时拥有读锁, 但仅允许一个进程拥有写锁
另外一个特性: 如果有进程尝试写锁定后(即使没有拥有写锁),所有其他尝试读锁定的进程都将等待,直到写锁定解除.
completion
DECLARE_COMPLETION(name)
定义并初始化一个completion
INIT_COMPLETION(struct completion c);
重新初始化一个completion, 主要是用在被唤醒的进程重新进入等待前的初始化
void init_completion(struct completion *c)
初始化一个completion
void wait_for_completion(struct completion *c)
进行一个不可打断的等待
void complete(struct completion *c)
唤醒一个等待的进程
void complete_call(struct completion *c)
唤醒所有等待的进程
void complete_and_exit(struct completion *c, long retval)
在内核线程A收到退出命令后,通知另一个内核线程B退出, 并等待B退出完成; B退出完成后调用complete通知A, 如果这种情况下用的是completion机制而A最后等待complete的时候调用的是这个函数,那么,A一收到B退出的通知就会结束整个线程
spin lock
spin lock是一个互斥设备, 只能有 2 个值:"上锁"和"解锁".
内核抢占(高优先级的进程抢占低优先级的进程)在持有spin lock期间被禁止
spinlock_t my_lock = SPIN_LOCK_UNLOCKED
编译时初始化
void 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)
加锁
spin_lock_irqsave在获得锁之前在当前处理器禁止中断, 之前的中断状态会保存在flags里. 按说flags按值传递的, 如何保存irq状态呢? 因为spin_lock_irqsave不是函数而是宏. ^_^
spin_lock_irq如果可以确定没有其他地方禁止中断(因为对应的unlock函数会打开中断), 可以使用这个函数
spin_lock_bh在获取锁之前禁止软中断.
之所以需要引入irq/soft irq开关支持,是因为如果在线程内拥有锁, 这时候有中断进来, 而中断也要拥有锁才能工作, 就导致了死锁
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)
与加锁对应的个个版本的解锁
int spin_trylock(spinlock_t *lock)
int spin_trylock_bh(spinlock_t *lock)
非阻塞操作. 没有禁止中断的"try"版本
rwlock_t my_rwlock = RW_LOCK_UNLOCKED
rwlock_init(&my_rwlock)
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)
int write_trylock(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)
读写锁版本的spin lock
用其他方式避免使用锁
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, 并返回值
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_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 在它能够获得锁时返回非零.
读: 直接操作
写: 将数据读出来, 更新数据, 将原指针指向新数据(这一步需要另外做原子操作的保护)
作为在真实世界中使用 RCU 的例子, 考虑一下网络路由表. 每个外出的报文需要请求检查路由表来决定应当使用哪个接口. 这个检查是快速的, 并且, 一旦内核发现了目标接口, 它不再需要路由表入口项. RCU 允许路由查找在没有锁的情况下进行, 具有相当多的性能好处. 内核中的 Startmode 无线 IP 驱动也使用 RCU 来跟踪它的设备列表.
在读这一边, 使用一个 RCU-保护的数据结构的代码应当用 rcu_read_lock 和 rcu_read_unlock 调用将它的引用包含起来.
RCU 代码往往是象这样:
rcu_read_lock();
stuff = find_the_stuff(args...);
do_something_with(stuff);
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 文件操作受到非阻塞标志影响
下列情况下应该实现阻塞
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)
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_interruptible_nr(wait_queue_head_t *queue, int nr);
wake_up_interruptible_all(wait_queue_head_t *queue);
6.2.5.1. 一个进程如何睡眠
放弃处理器是最后一步, 但是要首先做一件事: 你必须先检查你在睡眠的条件. 做这个检查失败会引入一个竞争条件; 如果在你忙于上面的这个过程并且有其他的线程刚刚试图唤醒你, 如果这个条件变为真会发生什么? 你可能错过唤醒并且睡眠超过你预想的时间. 因此, 在睡眠的代码下面, 典型地你会见到下面的代码:
if (!condition)
schedule();
通过在设置了进程状态后检查我们的条件, 我们涵盖了所有的可能的事件进展. 如果我们在等待的条件已经在设置进程状态之前到来, 我们在这个检查中注意到并且不真正地睡眠. 如果之后发生了唤醒, 进程被置为可运行的不管是否我们已真正进入睡眠.
如果在if判断之后,schedule之前,有其他进程试图唤醒当前进程, 那么当前进程就会被置为可运行的(但可能会到下次调度才会再次运行), 所以这个过程是安全的
wait: 等待队列入口项
state: 进程的新状态; TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE
调用prepare_to_wait之后, 需再次检查确认需要等待, 便可调用schedule释放CPU
在schedule返回之后需要调用下面的函数做一些清理动作
wake_up调用在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止(即进行互斥等待的进程一次只以顺序的方式唤醒一个). 但内核仍然每次唤醒所有的非互斥等待者.
用函数
void interruptible_sleep_on(wait_queue_head_t *queue);
这些函数无法避免竞争(在条件判断和sleep_on之间会存在竞争). 见上面"6.2.5.1. 一个进程如何睡眠"的分析
jiffies_64: 从系统开机到现在经过的tick, 64bit宽
jiffies: jiffies_64的低有效位, 通常为unsigned long. 编程时需要考虑jiffies隔一段时间会回绕(重新变成0)的问题
u64 get_jiffies_64(void);
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);
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
短延时(很容易回绕), 高精度
不是所有平台都支持
while (time_before(jiffies, j1))
cpu_relax();
while (time_before(jiffies, j1)) {
schedule();
}
long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);
signed long schedule_timeout(signed long timeout);
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
获得的延时至少是要求的值, 但可能更多
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)
如果进程被提早唤醒(msleep_interruptible), 返回值是剩余的毫秒数
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
function: 时间到时需要回调的函数
data: function的参数
struct tasklet_struct {
void (*func)(unsigned long);
unsigned long data;
data: func的参数
注意:
如果这个tasklet正在运行, tasklet_disable忙等待直到这个tasklet退出
注意:
如果一个tasklet在运行前被再次调度(tasklet_schedule), 它只运行一次.
如果它在运行中被调度, 它在完成此次运行后会再次运行; 这保证了在其他事件被处理当中发生的事件收到应有的注意. (这个做法也允许一个 tasklet 重新调度它自己)
如果这个tasklet正在运行, 这个函数等待直到它执行完毕.
如果这个tasklet重新调度它自己, 可能会导致 tasklet_kill 一直等待(所以需要在一个tasklet重新调度自己前需要加入一些条件限制).
工作队列就在这个特殊的内核上下文中运行, 所以可以是非原子的, 但不能存取用户空间
注意:缺省队列对所有驱动程序来说都是可用的;但是只有经过GP许可的驱动程序可以用自定义的工作队列
DECLARE_WORK(name, void (*function)(void *), void *data);
这两个函数如果返回非0意味着队列中已经有工作work存在了
若此函数返回0表示工作正在运行(可能在其他cpu上). 这种情况下, 工作会继续,但不会被再次添加到工作队列中去
若需要确保指定的工作没有运行,需要在这个函数后跟随下列函数:
void flush_scheduled_work(void);
DMA-capable memory: x86平台中, DMA区用在RAM的前16MB, 因为传统的ISA设备只能在这个区域内做DMA操作; PCI 设备没有这个限制.
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_low时,kswapd被buddy分配器唤醒,开始释放页;
当free_pages达到pages_high时,kswapd将被唤醒,此时kswapd不会考虑如何平衡该zone,直到有pages_high空闲页为止。
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_RESERVED 告知内存管理系统不要试图交换出这个VMA; 它应当在大部分设备映射中设置.
物理地址与页的关系:
物理地址(PA)-->页帧号(PFN, 此页在页索引数组中的index): PA右移PAGE_SHIFT位得到PFN
struct page *virt_to_page(void *kaddr);
对于高内存, 那个地址仅当这个页已被映射才存在. 大部分情况下, 使用 kmap 的一个版本而不是 page_address.
void *kmap(struct page *page);
void kunmap(struct page *page);
对于低内存页, 它只返回页的逻辑地址;
对于高内存, kmap 在内核地址空间的一个专用部分中创建一个特殊的映射.
使用 kmap 创建的映射应当一直使用 kunmap 来释放. 映射的总数是有限的, 所以不再使用时需要及时unmap
kmap 调用维护一个计数器, 因此如果 2 个或 多个函数都在同一个页上调用 kmap, 操作也是正常的
注意: 当没有映射可用时, kmap可能睡眠.
#include
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
注意带atomic的kmaps必须是原子的
flags: 分配的标志. "GFP"是_get_free_page的缩写
GFP_ATOMIC:用来从中断处理和进程上下文之外的其他代码中分配内存.
GFP_USER:用来为用户空间页来分配内存; 它可能睡眠.
GFP_HIGHUSER:如同 GFP_USER, 但是从高端内存分配
GFP_NOIO
GFP_NOFS
GFP_NOFS:不允许进行任何文件系统调用;
GFP_NOIO:不允许任何I/O初始化.
它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是不允许递归的文件系统调用
__GFP_DMA:这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的.
__GFP_HIGHMEM:这个标志指示分配的内存可以位于高端内存. 确切的含义是平台依赖的.
__GFP_COLD:
它对分配页作 DMA 读是有用的, 因为此时在处理器缓冲中出现的数据是无用的.