linux设备驱动读书笔记

设备驱动简介

机制:提供什么能力
策略:如何使用这些能力
在编写驱动时, 程序员应当编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求. 驱动应当做到使硬件可用, 将所有关于如何使用硬件的事情留给应用程序

编写驱动需要注意的地方:

必须注意并发/重入的问题
内核空间和用户空间不能直接操作,必须通过特别的函数(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 ). [4]有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.

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

剖析通常用在系统性能调整, 但是在追踪一些内核挂起和相关问题上也有用.

strace 命令

显示所有的用户空间程序发出的系统调用. 并以符号形式显示调用的参数和返回值.当一个系统调用失败, 错误的符号值(例如, 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 <asm/byteorder.h>
#ifdef __BIG_ENDIAN
......
#endif
#ifdef __LITTLE_ENDIAN
......
#endif
转换

#include <linux/byteorder/big_endian.h>
#include <linux/byteorder/little_endian.h>

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 <asm/unaligned.h>
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 <linux/list.h>
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 < 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_LICENSE("GPL"): 声明模块许可
MODULE_AUTHOR: 声明谁编写了模块
MODULE_DESCRIPION: 一个人可读的关于模块做什么的声明
MODULE_VERSION: 一个代码修订版本号; 看 <linux/module.h> 的注释以便知道创建版本字串使用的惯例
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: 一个字符指针值. 需要为其分配内存(charp, NOT char)
int/long/short/uint/ulong/ushort: 基本的变长整型值. 以 u 开头的是无符号值. 

num: 一个整型变量
perm: 通常的权限值, 在<linux/stat.h> 中定义. 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 的函数被简单地丢弃

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);
这个宏将test_setup这个函数放在特定的section中.
在执行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, 4Mbyteint access_ok(int type, const void *addr, unsigned long size): 验证用户空间有效性
type: VERIFY_READ/VERIFY_WRITE. 如果需要验证读写许可, 则只要 VERIFY_WRITE
addr: 一个用户空间地址, 
size: 需要验证的大小.
返回值: 1 是成功(存取没问题); 0 是失败(存取有问题). 如果它返回0, 驱动应当返回 -EFAULT
put_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); 从用户地址空间拷贝一整段数据. 任何存取用户空间的函数必须是可重入的. 此函数可能导致睡眠
这两个函数的作用不仅限于拷贝数据到和从用户空间: 它们还检查用户空间指针是否有效. 如果指针无效, 不进行拷贝; 如果在拷贝中遇到一个无效地址, 只拷贝有效部分的数据. 在第二种情况下, 返回值是未拷贝的数据数. 驱动应当查看返回值, 并且如果它不是0, 就返回 -EFAULT 给用户.
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(而不是虚拟终端)打印一条消息, 并通过附加不同的记录级别或者优先级在消息上对消息的严重程度分类.没有指定优先级的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);
从一个驱动打印消息, 你会想打印与感兴趣的硬件相关联的设备号.两个宏定义都将设备号编码进给定的缓冲区; 唯一的区别是 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 类型(在 <linux/types.h>中定义)用来标识设备编号 -- 同时包括主次部分
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 < 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;

file_operations函数:

ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
1. 通常应当更新 *offp 中的文件位置来表示在系统调用成功完成后当前的文件位置.
2. 如果"没有数据, 但是可能后来到达", 在这种情况下, read 系统调用应当阻塞.
3. 返回值
如果等于传递给 read 系统调用的 count 参数, 请求的字节数已经被传送
如果是正数, 但是小于 count, 只有部分数据被传送.
如果值为 0, 到达了文件末尾(没有读取数据).

如果值为负值表示有一个错误. 这个值指出了什么错误, 根据 <linux/errno.h>. 出错的典型返回值包括 -EINTR( 被打断的系统调用) 或者 -EFAULT( 坏地址 ).

如果一些数据成功传送接着发生错误, 返回值必须是成功传送的字节数. 在函数下一次调用前错误不会报告. 这要求驱动记住错误已经发生, 以便可以在以后返回错误状态.

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

struct iovec
{
    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);

inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. 
cmd 参数从用户那里不改变地传下来, 
arg 是可选的参数, 无论是一个整数还是指针(按照惯例应该用指针), 均以unsigned long的形式传递进来 
返回值:

-ENIVAL("Invalid argument"): 命令参数不是一个有效的POSIX 标准(通常会返回这个)
-ENOTTY: 一个不合适的 ioctl 命令. 这个错误码被 C 库解释为"设备的不适当的ioctl"(inappropriate ioctl for device)

ioctl的cmd参数应当是系统唯一的, 这是出于阻止向错误的设备发出其可识别但具体内容无法解析的命令的考虑
cmd参数由这几部分组成: type, number, direction, size

type 
魔数. 为整个驱动选择一个数(参考ioctl-number.txt). 这个成员是 8 位宽(_IOC_TYPEBITS). 
number 
序(顺序)号. 它是 8 位(_IOC_NRBITS)宽. 
direction
数据传送的方向(如果这个特殊的命令涉及数据传送). 
数据传送方向是以应用程序的观点来看待的方向

_IOC_NONE: 没有数据传输
_IOC_READ: 从系统到用户空间    
_IOC_WRITE: 从用户空间到系统
_IOC_READ|_IOC_WRITE: 数据在2个方向被传送

size 
用户数据的大小. 这个成员的宽度(_IOC_SIZEBITS)是依赖体系的. 通常是13或者14位. 

命令号相关操作:

_IO(type,nr): 创建没有参数的命令
_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): 得到参数大小

预定义命令(会被内核自动识别而不会调用驱动中定义的ioctl)分为 3 类:

1. 可对任何文件发出的(常规, 设备, FIFO, 或者 socket). 这类的magic number以'T'开头
2. 只对常规文件发出的那些.
3. 对文件系统类型特殊的那些.

驱动开发只需注意第一类命令,以及以下这些

FIOCLEX

设置 close-on-exec 标志(File IOctl Close on EXec).

FIONCLEX

清除 close-no-exec 标志(File IOctl Not CLose on EXec).

FIOASYNC

设置或者复位异步通知. 注意直到 Linux 2.2.4 版本的内核不正确地使用这个命令来修改 O_SYNC 标志. 因为两个动作都可通过 fcntl 来完成, 没有人真正使用 FIOASYNC 命令, 它在这里出现只是为了完整性.

FIOQSIZE

返回一个文件或者目录的大小; 当用作一个设备文件, 返回一个 ENOTTY 错误.

FIONBIO(File IOctl Non-Blocking I/O)

修改在 filp->f_flags 中的 O_NONBLOCK 标志. 给这个系统调用的第 3 个参数用作指示是否这个标志被置位或者清除. 注意常用的改变这个标志的方法是使用 fcntl 系统调用, 使用 F_SETFL 命令.

unsigned int (*poll) (struct file *filp, poll_table *wait);

filp: 文件描述符指针
wait: 用于poll_wait函数
返回值: 可能不必阻塞就立刻进行的操作
void poll_wait(struct file *, wait_queue_head_t *, poll_table *);

驱动通过调用函数poll_wait增加一个等待队列到poll_table结构. 这个等待队列是驱动定义并处理的, 进程唤醒方式(只唤醒其中一个还是唤醒所有等待的进程)也由驱动(read/write)来决定

返回的位掩码:

POLLIN

设备可不阻塞地读

POLLRDNORM

可以读"正常"数据. 一个可读的设备返回( POLLIN|POLLRDNORM ).

POLLRDBAND

可从设备中读取带外数据. 当前只用在 Linux 内核的一个地方(DECnet代码), 并且通常对设备驱动不可用.

POLLPRI

可不阻塞地读取高优先级数据(带外). 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.

POLLHUP

当读这个设备的进程见到文件尾, 驱动必须设置POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同selcet功能所规定的.

POLLERR

一个错误情况已在设备上发生. 当调用 poll, 设备被报告为可读可写, 因为读写都返回一个错误码而不阻塞.

POLLOUT

设备可被写入而不阻塞.

POLLWRNORM

这个位和POLLOUT有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).

POLLWRBAND

同POLLRDBAND, 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.

例子:
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, 接着设置正确的掩码位, 根据数据是否可以读或写.
如果在进程A得到poll/select/epoll通知后, 另外一个进程B将数据读走/将缓冲区填满, 这时候进程A进来进行读写操作,会如何?

int (*fasync) (int fd, struct file *filp, int mode);

异步通知. (常常假定异步能力只对 socket 和 tty 可用)
从用户的角度看异步通知的设置过程:

1. 指定一个进程作为文件的拥有者: fcntl 系统调用发出 F_SETOWN 命令,  进程ID被保存在 filp->f_owner 给以后使用
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);

从内核的角度看用户设置过程

1. 当发出 F_SETOWN, 一个值被赋值给 filp->f_owner.
2. 当发出 F_SETFL 来打开 FASYNC, 驱动的fasync方法被调用. 无论何时filp->f_flags中的FASYNC的值有改变, 都会调用驱动中的fasync方法(这个标志在文件打开时缺省地未设置).
3. 当数据到达, 向所有的注册异步通知的进程发出一个 SIGIO 信号.

从驱动的角度看内核响应过程:

1. 内核的第一步与驱动无关
2. 内核的第二步驱动应当用下列函数响应:
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);

当 FASYNC 标志因一个打开文件而改变, 这个函数用来从相关的进程列表中添加或去除入口项. 它的所有参数除了最后一个, 都直接来自fasync方法.  
mode: 0: 去除入口项; 其他: 添加入口项
fa: 是由驱动提供的一个struct fasync_struct结构. (*fa)在第一次使用之前应该初始化成NULL, 不然可能会出错. 从函数返回的时候会被分配一块内存, 在mode=0时free掉. 所以添加与去除入口项必须配对使用

void kill_fasync(struct fasync_struct **fa, int sig, int band);

数据到达时通知相关的进程.
fa: 与fasync_help里的fa同
sig: 被传递的信号(常常是 SIGIO)
band: 异步状况. 在网络代码里可用来发送"紧急"或者带外数据

POLL_IN: 有新数据到达. 等同于 POLLIN|POLLRDNORM.
POLL_OUT: 有空间可供写入

举例: (注意: 若设备允许多次打开, 每个打开的filp需要有独立的async_queue)

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

loff_t (*llseek) (struct file *, loff_t, int);

如果未定义llseek方法, 内核缺省通过修改 filp->f_pos来实现移位
如果需要禁止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的整数倍.
设备驱动的内存区间(被映射区)的起始地址必须位于PAGE_SIZE整数倍的物理地址.
若用户空间所要求的size不是PAGE_SIZE的整数倍, 内核会自动将其扩大成整数倍

mmap的设备驱动中的原型为
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
filp: 设备驱动对应的文件描述符指针
vma: 最终用户空间得到的struct vm_area_struct. 驱动所看到的这个参数已经被内核填充了大量数据, 驱动所需要做的就是将其地址区域建立合适的页表(PMD:中间目录描述表; PTE:页表项). 若有需要可能还需要更新struct vm_area_struct::vm_ops. vm_ops是struct vm_operations_struct结构, 其定义如下

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

open: 在新增一个对mmap所映射区间的引用(比如说fork)时会调用. 注意: 在mmap时(即第一次引用)不会自动调用此回调函数,所以若有需要则应手工调用
close: 在撤销一个引用时会调用
nopage: 在映射区间发生缺页异常或做mremap(重新映射)时会调用. 注意:若vm_ops中实现了此函数, 那么mmap实现会有所不同, 详细见下

mmap的实现:
修改页表: 通常会用下列两个函数实现

int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot); 
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
用户区间的(需要被操作的)vma
virt_addr
用户虚拟地址. 这个函数建立页表为这个虚拟地址范围从 virt_addr 到 virt_addr_size.
pfn
页帧号, 也就是设备驱动中需要被映射出去的内存区间的物理地址. 
这个页帧号简单地是物理地址右移PAGE_SHIFT位. 
对大部分使用, VMA 结构的vm_paoff成员正好包含你需要的值. 
这个函数影响物理地址从 (pfn<<PAGE_SHIFT) 到 (pfn<<PAGE_SHIFT)+size.
size
需要被重新映射的区的大小, 以字节为单位.
prot
给新VMA要求的访问权限(protection). 驱动可(并且应当)使用在vma->vm_page_prot中找到的值.

remap_pfn_range用在pfn指向实际的系统RAM的情况下.

它只能访问保留页(内存管理不起作用的页)和超出物理内存的物理地址.
所以不能映射get_free_page得到的空间. 
但ioremap函数返回的虚拟地址比较特殊, 所以可以用remap_pfn_range来映射

io_remap_page_range用在phys_addr指向I/O内存时. 
实际上, 这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; 
}

nopage实现: nopage只需要返回引起异常的虚拟地址所对应的struct page的结构即可, 内核会自动将其挂入current->mm中去. 与remap_pfn_range不同,它可以映射任何空间

内核会自动调用vm_ops::nopage回调函数来实现mmap, 所以mmap本身的实现比较简单, 只需要将vm_ops挂到vma中去. 典型的看起来象下面这样:

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;
}

nopage回调函数的实现例子:

struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type) 
{
    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是唯一正确的值

如果由于某些原因, 不能返回一个正常的页(即请求的地址超出驱动的内存区), 可以返回NOPAGE_SIGBUS指示错误; 也可以返回NOPAGE_OOM来指示由于资源限制导致的失败.


注意, 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__)
.....

并发与竞争

锁使用的规则
不允许一个持锁者第 2 次请求锁
非static函数必须明确在函数内部加锁(而不应该留给外部调用前处理); 静态函数可自行处理
当多个锁必须同时获得时,应当以同一顺序申请
当本地与内核的锁必须同时获得时,先申请本地的锁
当mutex与spin lock必须同时获得时, 先申请mutex(若先申请spin lock, 那么另一个想要申请spin lock的进程会一直自旋耗用大量资源)
semaphore

void sema_init(struct semaphore *sem, int val);

初始化一个semaphore, 并将其赋值为val

mutex:

semaphore的特殊形式, val仅允许在1和0之间变动
DECLARE_MUTEX(name);

定义并初始化一个未上锁的mutex

DECLARE_MUTEX_LOCKED(name);

定义并初始化一个已上锁的mutex

void init_MUTEX(struct semaphore *sem);

初始化一个未上锁的mutex

void init_MUTEX_LOCKED(struct semaphore *sem);

初始化一个已上锁的mutex

void down(struct semaphore *sem);

递减semaphore值, 如果必要就让当前进程睡眠等待直到semaphore可用

int down_interruptible(struct semaphore *sem);

同down, 但是操作是可中断的.

它允许一个在等待一个semaphore的用户空间进程被用户中断. 作为一个通用的规则, 不应该使用不可中断的操作, 除非实在是没有选择. 不可中断操作将创建不可杀死的进程.

如果操作是可中断的, 函数返回一个非零值, 并且调用者不持有semaphore. 正确的使用 down_interruptible 需要一直检查返回值并且针对性地响应.

举例:

DECLARE_MUTEX(mutex);

...

if (down_interruptible(&mutex))

return -ERESTARTSYS;

int down_trylock(struct semaphore *sem);

如果在调用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

用其他方式避免使用锁

环形缓冲

原子变量: 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_timer

int 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 <linux/highmem.h> 
void *kmap(struct page *page); 
void kunmap(struct page *page);

kmap为系统中的任何页返回一个内核虚拟地址. 
对于低内存页, 它只返回页的逻辑地址; 
对于高内存, kmap 在内核地址空间的一个专用部分中创建一个特殊的映射. 
使用 kmap 创建的映射应当一直使用 kunmap 来释放. 映射的总数是有限的, 所以不再使用时需要及时unmap
kmap 调用维护一个计数器, 因此如果 2 个或 多个函数都在同一个页上调用 kmap, 操作也是正常的
注意: 当没有映射可用时, kmap可能睡眠. 

#include <linux/highmem.h> 
#include <asm/kmap_types.h> 
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_NOWARN:当一个分配无法满足时阻止内核发出警告
__GFP_HIGH:标识了一个高优先级请求, 它允许消耗被内核保留给紧急状况的最后的内存页.
__GFP_REPEAT 
__GFP_NOFAIL 
__GFP_NORETRY

这些标志修改若不能满足分配时如何动作
__GFP_REPEAT允许重复尝试. 
__GFP_NOFAIL 标志告诉分配器不要失败, 但强烈不推荐. 
__GFP_NORETRY 告知分配器如果得不到请求的内存就立即放弃

get_zeroed_page(unsigned int flags);

分配一个页面并将其清零

get_free_page(unsigned int flags);

分配一个页面

__get_free_pages(unsigned int flags, unsigned int order);

分配多个物理上连续的页面

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);

释放页面
flags: 同kmalloc的用法相同
order: 请求/释放的页数. 可以用int get_order(unsigned long)函数将size转换成order

注意: 申请的page数限制: 2^MAX_ORDER, 2的MAX_ORDER次方个page. 通常MAX_ORDER=10, 也就是最多2^10=1024个page, 4Mbyte

struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);

在NUMA节点上分配数个页 

struct page *alloc_pages(unsigned int flags, unsigned int order);

在当前节点上分配数个页

struct page *alloc_page(unsigned int flags);

在当前节点上分配一个页
nid: 是要分配内存的NUMA节点ID, 
flags: 是通常的GFP_分配标志
order: 是分配的大小.

void __free_page(struct page *page);

释放单个页

void __free_pages(struct page *page, unsigned int order);

释放数个页

void free_hot_page(struct page *page);

将页释放到缓存中. 释放到缓冲中有助于下次(在不远的将来)使用时提高速度

void free_cold_page(struct page *page);

将页释放并不在缓冲中保存

void *vmalloc(unsigned long size);

在虚拟内存空间分配一块内存. 注意: 低效, 可能导致睡眠, 且不受鼓励. 使用的正确时机是为一个大的且只存在于软件中的顺序缓冲分配内存时

void vfree(void * addr);

释放内存

void *ioremap(unsigned long offset, unsigned long size);

将一个物理地址范围映射到虚拟地址中(通过建立页表的方式)
ioremap对于映射一个PCI缓冲的(物理)地址到(虚拟)内核空间是非常有用的 
出于移植上的考虑, 应当使用IO内存操作函数代替直接内存操作

void *ioremap_nocache(unsigned long phys_addr, unsigned long size);

ioremap的无缓存版本, 通常用于硬件IO地址/寄存器的映射

void *ioport_map(unsigned long port, unsigned int count);

将IO端口映射为IO内存

void ioport_unmap(void *addr);

释放上述映射
ioport_map/ioport_unmap使得IO端口在操作上与IO内存相同, 但在使用前的资源申请上, 必须使用IO端口的资源申请函数request_region

void iounmap(void * addr);

释放ioremap建立的映射关系

后备缓存: 也称为"slab 分配器"

kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t offset, unsigned long flags, 
                                                   void (*constructor)(void *, kmem_cache_t *, unsigned long flags), 
                                                   void (*destructor)(void *, kmem_cache_t *, unsigned long flags));

创建一个新的可以驻留任意数目全部同样大小的内存区的缓存.
name: 缓存区名字. 通常, 它被设置为被缓存的结构类型的名子. 驱动应当传递一个指向在静态存储区的指针, 且不能包含空格.
size: 每个缓存对象的大小
offset: 是页内的第一个对象的偏移. 它可被用来确保被分配的对象的特殊对齐. 
flags: 控制如何进行分配. 
取值可以如下:

SLAB_NO_REAP: 在系统查找内存时保护缓存不被削减. 设置这个标志通常是个坏主意
SLAB_HWCACHE_ALIGN: 每个数据对象被对齐到一个缓存行. 实际对齐依赖主机平台的缓存分布. 
SLAB_CACHE_DMA: 每个数据对象在DMA内存区分配.

constructor:  构造函数. 可选参数, 用来初始化新分配的对象
destructor: 析构函数. 可选参数, 在内存被释放回给系统之前用来"清理"内部数据

注意: 这两者都可能在分配/释放后的某个时间内调用(而不是在分配/释放返回前调用)
根据是否被传递 SLAB_CTOR_ATOMIC 标志, 析构函数和构造函数可能或不可能允许睡眠.
调用构造函数时, slab分配器常常传递SLAB_CTOR_CONSTRUCTOR标志

void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

分配缓存对象
cache: 已经创建的缓存.
flags: 与传递给kmalloc的flags同

void kmem_cache_free(kmem_cache_t *cache, const void *obj);

为释放一个缓存对象

int kmem_cache_destroy(kmem_cache_t *cache);

释放缓存区. 典型地当模块被卸载时调用. 只在从这个缓存中分配的所有的对象都已返回给它时才成功.

内存池: mempool, 一种特殊的后备缓存. 用于内核中内存分配不允许失败处. 它尽力一直保持一个空闲内存列表给紧急时使用.
注意: 内存池会消耗大量的内存. 所以若分配失败不会影响系统性能, 则不应当使用内存池

mempool_t *mempool_create(int min_nr,  mempool_alloc_t *alloc_fn,  mempool_free_t *free_fn,  void *pool_data);

创建内存池
min_nr: 内存池应当一直保留的最小数量的空闲对象.
alloc_fn: 分配对象函数, 通常可以默认使用slab分配器提供的mempool_alloc_slab. alloc_fn原型为

typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);

free_fn: 释放对象函数, 通常可以默认使用slab分配器提供的mempool_free_slab. free_fn原型为

typedef void (mempool_free_t)(void *element, void *pool_data);

pool_data: 作为alloc_fn/ free_fn的参数

void *mempool_alloc(mempool_t *pool, int gfp_mask);

从内存池中分配一个对象. 当内存池创建时, 会自动多次调用mempool_alloc; 之后每次mempool_alloc调用将先尝试在系统内存中分配内存, 若失败则从内存池中分配
void mempool_free(void *element, mempool_t *pool);

将从内存池中分配的对象释放.

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);

调整内存池的大小至少有new_min_nr个对象.

void mempool_destroy(mempool_t *pool);

释放内存池. 释放前必须释放所有被分配的对象, 
否则会产生一个内核 oops.

Pre-CPU变量: 2.6内核支持

当你创建一个Pre-CPU变量, 系统中每个处理器获得它自己的这个变量拷贝
存取Pre-CPU变量(几乎)不需要加锁, 因为每个处理器使用它自己的拷贝. Pre-CPU变量也可存在于它们各自的处理器缓存中, 这样对于频繁更新的量子带来了显著的更好性能.

在网络子系统中, 内核维护计数器来跟踪接收到的每种报文类型的数量, 这些计数器可能每秒更新几千次. 网络开发者将统计计数器放进Pre-CPU变量, 这样更新是无锁并且快速的. 
用户空间请求看到计数器的值, 只需将每个处理器的变量相加即可. 例如, 一个网络代码中的计数器时使用这 2 个语句来递增的:

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);

DEFINE_PER_CPU(type, name);

定义一个Pre-CPU变量 
例如, 定义一个有3个整数的数组

DEFINE_PER_CPU(int[3], my_percpu_array);

EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);

输出一个Per-CPU变量

DECLARE_PER_CPU(type, name);

告知编译器进行变量name是外部Per-CPU变量.

get_cpu_var();

存取当前CPU的Pre-CPU变量的值, 并禁止抢占

put_cpu_var();

对当前CPU的Pre-CPU变量赋值 , 并重新开放抢占

per_cpu(variable, int cpu_id);

存取另一个CPU的Pre-CPU变量

void *alloc_percpu(type);

动态分配一个Pre-CPU变量

void *__alloc_percpu(size_t size, size_t align);

动态分配一个需要对齐的Pre-CPU变量

per_cpu_ptr(void *per_cpu_var, int cpu_id);

存取一个动态分配的Pre-CPU变量. 存取时没有对抢占进行保护, 所以使用动态Pre-CPU变量的代码会看来如此:

int cpu; 
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);
/* work with ptr */
put_cpu();

获得大量缓冲:

如果你真的需要一个大的物理上连续的缓冲, 最好的方法是在启动时请求内存来分配它.
#include <linux/bootmem.h>
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);
void free_bootmem(unsigned long addr, unsigned long size);

与硬件通信:

读写屏障(读写同步)
由于编译器的优化, C语言访问寄存器时, 可能会把寄存器的读写操作优化成对缓存的读写操作, 所以在需要对实际地址空间操作的指令后加入一些提示编译器对前面的指令不要优化的提示
void barrier(void)

编译器插入一个内存屏障但是对硬件没有影响. 编译的代码将所有的当前改变的并且驻留在 CPU 寄存器的值存储到内存, 并且在需要时重新读取. 对屏障的调用阻止编译器跨越屏障的优化.

void rmb(void);

读屏障

void read_barrier_depends(void);

读屏障的弱一些的形式, read_barrier_depends blocks only the reordering of reads that dependon data from other reads

void wmb(void);

写屏障

void mb(void);

读写屏障

void smp_rmb(void);

多处理器的rmb版本

void smp_read_barrier_depends(void);

多处理器的read_barrier_depends版本

void smp_wmb(void);

多处理器的wmb版本

void smp_mb(void);

多处理器的mb版本

IO端口分配

struct resource *request_region(unsigned long first, unsigned long n, const char *name);

向内核申请一段资源(IO地址空间)
first: 资源的起始地址
n: 端口数(资源长度)
name: 名字

void release_region(unsigned long start, unsigned long n);

释放资源
start: 需要释放的资源的起始地址
n: 需要释放的资源的长度

int check_region(unsigned long first, unsigned long n);

检查一段资源是否可用, 若不可用则返回负错误码. 这个函数不推荐使用, 因为在check与request之间可能会有其他进程将资源申请走

IO内存分配

struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len); 
int check_mem_region(unsigned long start, unsigned long len); 
函数作用及参数同IO端口资源分配

IO端口操作: 出于移植性上的考虑, 对IO端口的操作不应当直接读写而应该通过下列函数

unsigned inb(unsigned port);

读字节端口

void outb(unsigned char byte, unsigned port);

写字节端口

unsigned inw(unsigned port);

读字

void outw(unsigned short word, unsigned port);

写字

unsigned inl(unsigned port);

读32位端口

void outl(unsigned longword, unsigned port);

写32位端口.

void insb(unsigned port, void *addr, unsigned long count);

读一组8位数据
port: 读地址
addr: 读到的数据存储的地址
count: 读的数目

void outsb(unsigned port, void *addr, unsigned long count);

写一组8位数据, 参数同上

void insw(unsigned port, void *addr, unsigned long count);

读一组16位数据, 参数同上

void outsw(unsigned port, void *addr, unsigned long count);

写一组16位数据, 参数同上

void insl(unsigned port, void *addr, unsigned long count);

读一组32位数据
void outsl(unsigned port, void *addr, unsigned long count);

写一组32位数据

注意:

读写一组数据的端口操作函数可能会转换字节序以适应主系统的字节对齐规则(Little-endian, big-endian)
若读写太快可能导致端口未及时响应, 故可在上述函数加_p后缀以减缓读写速度

IO内存操作:

unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

读特定长度的数据, addr应当是ioremap获得的地址, 下同

void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

写特定长度的数据

void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);

循环读写一块区域

void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

操作整块IO内存

老版本的IO内存操作函数: 不建议使用

unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 

作为IO内存的IO端口:

ioport_map/ioport_unmap(见内存分配部分) 将IO端口映射成IO内存(即操作方法同), 但在使用前的资源申请上, 必须使用IO端口的资源申请函数request_region

中断处理

在/proc/interrupts中可以看到各个irq的占用情况及发生的次数
int request_irq(unsigned int irq,
                irqreturn_t (*handler)(int, void *, struct pt_regs *),
                unsigned long flags,
                const char *dev_name,
                void *dev_id);

申请一个中断, 返回0表示成功
irq: 请求的中断号
handler: 中断回调函数
flags: 中断标志位

SA_INTERRUPT: FIQ, 快速中断; 若未设置此位则为普通IRQ. FIQ与IRQ之间的区别是在FIQ的全过程中断都是被关闭的, IRQ则不关闭
SA_SHIRQ: 共享中断
SA_SAMPLE_RANDOM: 表示此中断随机产生, 故对用于产生随机数的"嫡池"有贡献

dev_name: 占用中断的设备的名字
dev_id: 中断回调函数的void *参数, 用于在共享中断上区别各个服务

void free_irq(unsigned int irq, void *dev_id);

释放一个中断

int can_request_irq(unsigned int irq, unsigned long flags);

查询一个irq是否被占用(不建议使用)

探测当前设备使用的irq:(不一定每次都正确, 因为在探测期间可能有其他设备的中断产生)

unsigned long probe_irq_on(void); 
这个函数返回一个未安排的中断的位掩码. 驱动必须保留返回的位掩码, 并且在后面传递给probe_irq_off. 
在这个调用之后, 驱动应当安排它的设备产生至少一次中断.
int probe_irq_off(unsigned long); 
在设备已请求一个中断后, 驱动调用这个函数, 作为参数传递之前由probe_irq_on返回的位掩码. 
probe_irq_off 返回在"probe_on"之后发出的中断号. 
如果没有中断发生, 返回0(因此, IRQ0不能探测, 但是没有用户设备能够在任何支持的体系上使用它). 
如果多于一个中断发生(模糊的探测), probe_irq_off 返回一个负值.

中断处理函数应当返回一个值指示是否真正有一个中断处理. 内核用返回值来检测和抑制假中断. 
如果处理者发现它的设备确实有发生中断, 它应当返回IRQ_HANDLED; 否则返回值应当是IRQ_NONE. 
如果你的设备没有给你方法来告知是否它确实中断, 你应当返回IRQ_HANDLED
你也可用这个宏产生返回值:
IRQ_RETVAL(handled)
若无真实的中断产生, hangled应取0

void disable_irq(int irq);

禁止中断并等待当前正在执行的中断处理(如果有)结束

void disable_irq_nosync(int irq);

禁止中断不等待版本

void enable_irq(int irq);

使能指定中断
注意:使能与禁止必须配对
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
void local_irq_restore(unsigned long flags); 
void local_irq_enable(void);

禁止/使能全部中断
注意: local_irq_enable无条件打开中断, 故local_irq_enable与local_irq_disable可能不配对

中断的前半部和后半部:

为了能够快速退出中断服务, 但又能够处理足够的工作, 通常都将中断处理函数分成两部分, 前半部为注册到中断中去的中断服务函数, 后半部由中断服务函数调度, 通常由Tasklet或工作队列来实现. 参考"调度"章节

共享中断

共享中断不能自动探测中断线
共享中断不能关闭/开启中断

中断驱动IO

输入输出缓存可以提高系统效率
中断长时间无响应的情况应当在设计时考虑进去

参考书目:
<Linux设备驱动第三版(中文版)>. Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman著. quickwhale<quickwhale@hotmail.com> 译
<Linux设备驱动程序(第三版)> Jonathan Corbet,Alessandro Rubini,Greg Kroah-Hartman 著 魏永明,耿岳,钟书毅 译
<Linux内核源代码情景分析> 毛德操 胡希明著
以及其他杂七杂八的网络文章

linux设备驱动读书笔记相关推荐

  1. linux设备驱动读书笔记(转)

    linux设备驱动读书笔记 设备驱动简介 机制:提供什么能力 策略:如何使用这些能力 在编写驱动时, 程序员应当编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求. ...

  2. Linux设备驱动开发笔记

    0 Linux 操作系统知识 Linux是一个操作系统. 优点: 免费: 丰富的文档和社区支持: 跨平台移植: 源代码开放: 有许多免费开源软件. 家用电脑用Windows,服务器端用Linux. 操 ...

  3. linux设备驱动学习笔记(1)

    学习了将近半个月的设备驱动程序的编写,也有一些体会,这里写下来也给学习做一个总结,为后面的学习做更好的准备. 首先,个人感觉驱动程序的设计是很有套路的,最基本的要求就是要掌握这些套路.所谓的套路就是一 ...

  4. linux音频设备接口,linux音频设备驱动读书笔记

    在linux系统中,先后出现了音频设备的两种框架:OSS和ALSA. 针对不同的数字音频子系统,出现了几种微处理器或DSP与音频器件间用于数字转换的接口. 音频设备的硬件接口 (1)PCM接口. (2 ...

  5. linux 两个驱动 竞争,Linux设备驱动第五章(并发和竞争)读书笔记(国外英文资料).doc...

    Linux设备驱动第五章(并发和竞争)读书笔记(国外英文资料) Linux设备驱动第五章(并发和竞争)读书笔记(国外英文资料) The fifth chapter is concurrency and ...

  6. 嵌入式Linux设备驱动程序开发指南14(Linux设备驱动使用DMA)——读书笔记

    Linux设备驱动使用DMA 十四.Linux设备驱动使用DMA 14.1 简介 14.2 缓存一致性 14.3 DMA控制器接口 14.4 流式DMA模块 14.4.1 sdma_sam_m2m.c ...

  7. 《Linux设备驱动开发详解》学习笔记一

    Linux设备驱动开发详解学习笔记<一> 书名:<Linux设备驱动开发详解>第二版 主机环境:Linux version 2.6.25-14.fc9.i686@Fedora ...

  8. 《linux设备驱动开发详解》笔记——15 linux i2c驱动

    <linux设备驱动开发详解>笔记--15 linux i2c驱动 15.1 总体结构 如下图,i2c驱动分为如下几个重要模块 核心层core,完成i2c总线.设备.驱动模型,对用户提供s ...

  9. linux设备驱动归纳总结

    前言: (总结已经基本写完,这段时间我会从新排版和修正.错误总会有的,望能指正!) 前段时间学习了嵌入式驱动,趁着没开始找工作,这段时间我会每天抽出时间来复习. 我的总结是根据学习时的笔记(李杨老师授 ...

最新文章

  1. CVPR 2021 | 微软提出“解构式关键点回归“, 刷新COCO自底向上多人姿态检测记录!
  2. 判断一颗二叉树是否是平衡二叉树
  3. 成为单片机高手必知的三个重要步骤(干货分享)
  4. conflicting types for ‘方法名’ 的错误
  5. oem监控mysql_OEM12c 安装配置MySQL Plug-in用来监控MySQL
  6. Apache Flink CDC 批流融合技术原理分析
  7. python xlwt使用_Python使用xlwt模块 操作Excel文件
  8. 配对t检验的应用条件是什么_配对t检验在实际工作中的应用
  9. 教你如何在电脑上建一个绝密文件夹
  10. 专访|搜狐媒体UED设计总监李伟巍:如何做好UED设计
  11. 【源码安装python3.8】
  12. Scrapy爬取1908电影网电影数据
  13. 北京车辆从4S店提车以后自己上牌怎么操作
  14. 全球各国城镇人口数量(1960-2019年)
  15. 7Z命令在linux下的使用
  16. 更改大商创立即购买【仿淘宝】
  17. 俄罗斯、乌克兰程序员薪资大曝光!
  18. 浙江大学计算机学院 00级,浙江大学教师划分为13个等级
  19. Android 文件的加密及解密
  20. 在BIOS中如何更改启动顺序

热门文章

  1. Python画玫瑰花
  2. pcie ecam --- Linux kernel 实现欣赏
  3. 如何快速搭建公司网站?
  4. 小程序源码:求职招聘微信小程序-多玩法安装简单
  5. 【研究日记】虚拟歌姬自动调教之歌曲音频切割的问题
  6. 解决matplotlib不显示图片
  7. 英文中常见连读规律总结
  8. Mac上键入数学符号怎样输入
  9. 1036:镂空三角形
  10. ESP32开发--使用NVS存储数据