1. 通常应当更新 *offp 中的文件位置来表示在系统调用成功完成后当前的文件位置.
2. 如果"没有数据, 但是可能后来到达", 在这种情况下, read 系统调用应当阻塞.
3. 返回值
如果等于传递给 read 系统调用的 count 参数, 请求的字节数已经被传送
如果是正数, 但是小于 count, 只有部分数据被传送.
如果值为 0, 到达了文件末尾(没有读取数据).
如果值为负值表示有一个错误. 这个值指出了什么错误, 根据 . 出错的典型返回值包括 -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进来进行读写操作,会如何?
并发与竞争
锁使用的规则
不允许一个持锁者第 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
void *kmap(struct page *page);
void kunmap(struct page *page);
kmap为系统中的任何页返回一个内核虚拟地址.
对于低内存页, 它只返回页的逻辑地址;
对于高内存, kmap 在内核地址空间的一个专用部分中创建一个特殊的映射.
使用 kmap 创建的映射应当一直使用 kunmap 来释放. 映射的总数是有限的, 所以不再使用时需要及时unmap
kmap 调用维护一个计数器, 因此如果 2 个或 多个函数都在同一个页上调用 kmap, 操作也是正常的
注意: 当没有映射可用时, kmap可能睡眠.
#include
#include
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
kmap_atomic 是kmap 的一种高性能形式. 每个体系都给原子的kmaps维护一个槽(专用的页表项); kmap_atomic的调用者必须在type参数中指定哪个槽. 对驱动有意义的唯一插口是 KM_USER0和KM_USER1(对于直接从来自用户空间的调用运行的代码), 以及KM_IRQ0和KM_IRQ1(对于中断处理).
注意带atomic的kmaps必须是原子的
void *kmalloc(size_t size, int flags);
在内核里分配一块内存
size: 分配的块的大小. 内核只能分配某些预定义的, 固定大小的字节数组. kmalloc 能够处理的最小分配是 32 或者 64 字节. kmalloc 能够分配上限128 KB
flags: 分配的标志. "GFP"是_get_free_page的缩写
GFP_KERNEL:表示在进程上下文中调用. 可能会导致睡眠, 所以所在函数必须是可重入的且不能在原子上下文中使用
GFP_ATOMIC:用来从中断处理和进程上下文之外的其他代码中分配内存.
GFP_USER:用来为用户空间页来分配内存; 它可能睡眠.
GFP_HIGHUSER:如同 GFP_USER, 但是从高端内存分配
GFP_NOIO
GFP_NOFS
同GFP_KERNEL, 但是它们有更多限制:
GFP_NOFS:不允许进行任何文件系统调用;
GFP_NOIO:不允许任何I/O初始化.
它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是不允许递归的文件系统调用
上面的分配标志可以与下列标志相或来作为参数, 下面的标志改变分配区域:
__GFP_DMA:这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的.
__GFP_HIGHMEM:这个标志指示分配的内存可以位于高端内存. 确切的含义是平台依赖的.
__GFP_COLD:
这个标志请求一个"冷缓冲"页. (因为正常地, 内存分配器尽力返回"热缓冲"的页.)
它对分配页作 DMA 读是有用的, 因为此时在处理器缓冲中出现的数据是无用的.
__GFP_