目录
前言
本文主要是我学习Linux的异步通知机制所做的笔记,介绍了异步通知的一些应用层和驱动层的接口,内容会不断的完善。
所涉及到的源码的kernel版本是linux-kernel-5.3.1
1 概览异步通知
当我们访问文件的时候,比如读/写,如果当前不能读或不能写,那么我们可以选择阻塞访问,直到有数据可读或有空间可写。而异步通知是进程先打开设备文件,再通过fcntl函数,设置F_SETOWN、FASYNC标志,从而告知相应的设备:想要访问你的进程具体是哪个,且想要以异步通知的方式访问你。起到的效果是可读/可写的时候,驱动层会给进程发信号,进程收到信号之后执行相应的处理函数,在处理函数中进行读/写操作。进程不需要管什么时候文件是可访问的,就像你点了外卖以后,不需要死等,你只要接着干自己的事情就行,外卖小哥到了会给你打电话(发信号)的。
我们以一幅图来概括一下Linux的异步通知,更多的细节后面会分析:
上文所说的进程、文件之间的关系可以用下图描述:
2 应用层使用异步通知
应用程序使用异步通知的一个例子如下(仅为说明问题,因此没有对各个系统调用进行错误处理):
...
// 记录要访问的文件的描述符
static int fd;
// 信号处理函数
void signal_handler(int signum, siginfo_t *siginfo, void *act)
{
...
if (signum == SIGIO)
{
if (siginfo->si_band & POLLIN)
{
// 处理数据可读的情况
...
}
if (siginfo->si_band & POLLOUT)
{
// 处理数据可写的情况
...
}
}
}
int main()
{
int ret, flag;
struct sigaction act, oldact;
// 填充act结构
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGIO);
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = signal_handler;
// 绑定信号SIGIO及其处理函数
sigaction(SIGIO, &act, &oldact);
// 打开要访问的文件
fd = open("/dev/my_dev", O_RDWR);
// 设置文件结构的f_owner字段,进而告诉驱动信号要发给哪个进程
fcntl(fd, F_SETOWN, getpid());
// 这条代码实际上是设置struct file的f_owner成员的signum成员
// signum被设置后会带来一些影响,比如send_sigio_to_task不再默认发送SIGIO信号,而是发送signum指定的信号
// 再比如kernel_siginfo_t结构体被填充(这个结构应该是传给应用层的siginfo_t)
// 下文中驱动的部分还会讲到它(吐槽一下man手册,F_SETSIG介绍的不是很清楚呀,也可能是我太菜了-_-!!)
fcntl(fd, F_SETSIG, SIGIO);
// 设置FASYNC标志,使能异步通知
flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag | FASYNC);
// 到这里进程就可以做其他事情,不用管文件访问了,可读/可写时,驱动给进程发信号,进程自动执行signal_handler访问文件
....
return 0;
}
3 驱动层支持异步通知
3.1 响应应用层设置FASYNC——xxx_fasync
当我们在应用层调用fcntl(fd, F_SETFL, flag | FASYNC)
时,最终会调用到驱动层的struct file_operations
的fasync
成员,因此我们需要在驱动层实现xxx_fasync
,这个函数一般这么写:
static int xxx_fasync(int fd, struct file *file, int on)
{
...
return fasync_helper(fd, file, on, &device->fasync);
}
其中device
一般是我们定义的设备结构体,在这个结构体中,我们会定义一个成员struct fasync_struct *fasync
。struct fasync_struct
的定义如下:
struct fasync_struct {
rwlock_t fa_lock; // 读写锁:用于同步并发的访问
int magic;
int fa_fd; // 记录文件描述符
struct fasync_struct *fa_next; // 构成单向链表
struct file *fa_file; // 指向文件结构,文件结构的f_owner字段记录了调用fcntl(fd, F_SETOWN, getpid())的进程的pid
struct rcu_head fa_rcu; // 用于RCU机制
};
可以看到xxx_fasync
调用了fasync_helper
,概括的说,这个函数会申请一个struct fasync_struct
类型的变量,这个变量里面记录了信号要发给哪个进程。实际上这个变量会记录一个文件结构体指针,指向fd
所表示的文件结构体,而文件结构体中的f_owner
字段才记录了进程的pid。当然,我们需要先调用fcntl(fd, F_SETOWN, getpid())
,这个函数会设置f_owner
字段。
进一步的,我们可以简单的跟踪一下这个函数,它做的事情其实很简单:
3.1.1 fasync_helper
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
// 使用fcntl设置FASYNC的时候on为1,清除FASYNC的时候on为0
if (!on)
// 删除struct fasync_struct结构(丛链表移除并释放)
return fasync_remove_entry(filp, fapp);
// 添加struct fasync_struct结构(申请空间并插入链表)
return fasync_add_entry(fd, filp, fapp);
}
3.1.2 fasync_add_entry
static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
struct fasync_struct *new;
// 申请struct fasync_struct类型的空间
new = fasync_alloc();
if (!new)
return -ENOMEM;
// 如果当前struct fasync_struct链表中已经有指向filp所指向的文件结构的项
// 那么fasync_insert_entry会更新该项的fa_fd,并返回该项的地址(非零值)
// 否则将新的项(new)插入链表
if (fasync_insert_entry(fd, filp, fapp, new)) {
// 如果(new)没有被插入链表那么就释放它
fasync_free(new);
return 0;
}
return 1;
}
3.1.3 fasync_insert_entry
struct fasync_struct *fasync_insert_entry(int fd, struct file *filp, struct fasync_struct **fapp, struct fasync_struct *new)
{
struct fasync_struct *fa, **fp;
// 加锁:保证对filp指向的文件结构的访问互斥
spin_lock(&filp->f_lock);
// 加锁:保证对设备的struct fasync_struct链表(异步通知链表)的访问互斥
spin_lock(&fasync_lock);
// 遍历设备的异步通知链表
for (fp = fapp; (fa = *fp) != NULL; fp = &fa->fa_next) {
if (fa->fa_file != filp)
continue;
// 加写锁
write_lock_irq(&fa->fa_lock);
// 更新文件描述符
// 这里不太理解的是:文件结构没变,文件描述符会发生变化么?
fa->fa_fd = fd;
// 释放写锁
write_unlock_irq(&fa->fa_lock);
goto out;
}
// 初始化struct fasync_struc结构
rwlock_init(&new->fa_lock);
new->magic = FASYNC_MAGIC;
new->fa_file = filp;
new->fa_fd = fd;
// 头插式插入链表
new->fa_next = *fapp;
rcu_assign_pointer(*fapp, new);
// 将文件结构的f_flags置FASYNC
filp->f_flags |= FASYNC;
out:
// 在退出之前需要将之前加的锁解锁
spin_unlock(&fasync_lock);
spin_unlock(&filp->f_lock);
return fa;
}
3.1.4 总结
当我们在应用层调用fcntl(fd, F_SETFL, flag | FASYNC)
时,驱动层会为我们申请一个struct fasync_struct类型的变量,并将这个变量以头插的方式插入到有struct fasync_struct类型的变量构成的链表中,不妨称之为异步通知链表。该链表中有很多项,每一项的存在都说明有一个进程使能了异步通知机制,即设置了FASYNC标志(本质上是进程向异步通知链表插入了一个struct fasync_struct,这个结构中包含“如何找到进程”的信息)。
当然,要向异步通知链表插入本进程的struct fasync_struct,我们得提前申请好一个struct fasync_struct类型的指针,这个指针将指向上述链表头,一般我们把这个指针放在设备结构体中(一般会为要驱动的设备定义一个设备结构体):
3.2 驱动怎么发送信号给进程——kill_fasync
到这里,我们对异步通知应该有了更多的了解,在开始本节的分析之前,不妨再来总结一下异步通知机制:
假设进程使用异步通知的方式来访问设备A,那么通过设备A的设备文件将自身注册到设备A的异步通知链表之后,就可以干其他事情了。设备A的驱动会在适当的时机向该设备的异步通知链表中(间接)记录的每个进程发信号,而进程则在接收到信号之后运行绑定的信号处理函数,以完成对设备A的访问。
那么问题来了,什么时机称得上适当的时机呢?举例来说明,比如:有进程向设备文件写入了数据,此时设备有数据可读,因此我们可以在驱动程序xxx_write
中,写入数据之后的地方调用kill_fasync(异步通知链表, SIGIO, POLL_IN)
。
接下来我们就仔细看看kill_fasync
究竟做了什么:
3.2.1 kill_fasync
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
// 设备的异步通知链表不为空
if (*fp) {
// 获取RCU读锁
rcu_read_lock();
// 调用kill_fasync_rcu发信号(在此之前要获取RCU读锁)
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
// 释放RCU读锁
rcu_read_unlock();
}
}
3.2.2 kill_fasync_rcu
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
// 遍历设备的异步通知链表
while (fa) {
struct fown_struct *fown;
// fasync_insert_entry函数在初始化struct fasync_struct的时候会给它的magic成员赋值为FASYNC_MAGIC
// 也就是说如果magic成员不是FASYNC_MAGIC,那么说明struct fasync_struct没有被正确的初始化
if (fa->magic != FASYNC_MAGIC) {
printk(KERN_ERR "kill_fasync: bad magic number in "
"fasync_struct!\n");
return;
}
// 加读锁
read_lock(&fa->fa_lock);
if (fa->fa_file) {
// 获取设备文件的(文件结构的)f_owner字段
// 上文已经说了,该字段记录了进程的pid
// 该字段还有一个非常重要的成员:signum
// fcntl(fd, F_SETSIG, SIGIO)所做的正是将struct file的f_owner字段的signum赋值为SIGIO(详见fcntl.c的do_fcntl函数)
fown = &fa->fa_file->f_owner;
// 当我们在驱动中调用kill_fasync(异步通知链表, SIGXXX, band)
// SIGXXX会被传递给这里的sig
// 在未设置f_owner的signum时,我们不能发送SIGURG:SIGURG有它自己默认的处理机制
if (!(sig == SIGURG && fown->signum == 0))
send_sigio(fown, fa->fa_fd, band);
}
// 释放读锁
read_unlock(&fa->fa_lock);
// 遍历下一项struct fasync_struct
fa = rcu_dereference(fa->fa_next);
}
}
3.2.3 send_sigio
这个函数应该在介绍Linux的信号的时候分析更为合适,当然异步通知机制是建立在Linux的信号机制的基础上的,因此我仍然会跟踪一下这个函数,但不会特别深入的分析,等深入学习Linux的信号机制时,再对其做更深入的介绍吧!
在分析源码之前,我们先了解一下Linux的pid_type
:
enum pid_type
{
PIDTYPE_PID, // pid
PIDTYPE_TGID, // 线程组ID
PIDTYPE_PGID, // 进程组ID
PIDTYPE_SID, // 会话组ID
PIDTYPE_MAX, // 共4种类型
};
这部分内容和Linux的进程管理有关系,我在网上找了一篇博客[3],可以参考其中内容,以加深对这部分知识的理解。为不偏离主线,这里不再多说。下面就开始分析send_sigio
函数:
void send_sigio(struct fown_struct *fown, int fd, int band)
{
struct task_struct *p;
enum pid_type type;
struct pid *pid;
// 获取读锁
read_lock(&fown->lock);
// 获取pid_type
type = fown->pid_type;
// 获取指向struct pid的指针
// 这个指针指向的struct pid会告诉我们将信号发给哪个进程
pid = fown->pid;
if (!pid)
goto out_unlock_fown;
// pid_type不同,所做的处理也不同,但都是调用了send_sigio_to_task
// 我们不妨认为进入的是眼下这个分支
if (type <= PIDTYPE_TGID) {
rcu_read_lock();
// 从pid获取相应进程的task_struct
p = pid_task(pid, PIDTYPE_PID);
if (p)
// p指向信号的目的进程(一般就是打开该设备文件的那个进程)
// fown指向设备文件的文件结构的f_owner字段
// fd是我们打开的设备文件的文件描述符
// band由kill_fasync传入,可以是POLL_IN、POLL_OUT等(注意不是POLLIN、POLLOUT)
// type记录了pid_type
send_sigio_to_task(p, fown, fd, band, type);
rcu_read_unlock();
} else {
read_lock(&tasklist_lock);
do_each_pid_task(pid, type, p) {
send_sigio_to_task(p, fown, fd, band, type);
} while_each_pid_task(pid, type, p);
read_unlock(&tasklist_lock);
}
out_unlock_fown:
// 释放读锁
read_unlock(&fown->lock);
}
3.2.4 send_sigio_to_task
static void send_sigio_to_task(struct task_struct *p, struct fown_struct *fown, int fd, int reason, enum pid_type type)
{
// 获取设备文件的文件结构的f_owner字段的signum
// 回忆一下,之前说过signum是在fcntl(fd, F_SETSIG, SIGXXX)被设置为SIGXXX
int signum = READ_ONCE(fown->signum);
// 没看明白,不清楚是做什么的(猜测是校验能否向p指向的进程发送信号signum)
if (!sigio_perm(p, fown, signum))
return;
// 由signum来决定要做哪些事情
switch (signum) {
kernel_siginfo_t si;
default:
// 填充kernel_siginfo_t,这个结构里的信息应该是传给应用层的siginfo_t
clear_siginfo(&si);
si.si_signo = signum;
si.si_errno = 0;
si.si_code = reason;
if ((signum != SIGPOLL) && sig_specific_sicodes(signum))
si.si_code = SI_SIGIO;
// 确保reason是POLL_XXX,否则可能会泄漏内核栈到用户空间(为什么会泄露呢?不明白)
BUG_ON((reason < POLL_IN) || ((reason - POLL_IN) >= NSIGPOLL));
if (reason - POLL_IN >= NSIGPOLL)
si.si_band = ~0L;
else
si.si_band = mangle_poll(band_table[reason - POLL_IN]);
si.si_fd = fd;
// 调用do_send_sig_info发送信号
if (!do_send_sig_info(signum, &si, p, type))
break;
// 如果没有设置signum的话,就调用do_send_sig_info发送SIGIO信号
case 0:
do_send_sig_info(SIGIO, SEND_SIG_PRIV, p, type);
}
}
3.2.5 总结并提出自己的一点疑惑
kill_fasync 的调用栈总结如下(跟踪的比较浅):
其中,令我感到困惑的是kill_fasync
的sig
参数和signum
之间的关系。比如我们在驱动xxx_write
中调用kill_fasync(异步通知链表, SIGIO, POLL_IN)
,难道不是表明我们想发送SIGIO信号吗?如果是,那么可以看到,在kill_fasync_rcu
中,sig
参数只要不是SIGURG
就不会起到作用,而真正起作用的是signum
,即我们通过fcntl(fd, F_SETSIG, SIGXXX)
设置的字段。这样岂不是说,我们在应用层就已经决定了kill_fasync
将要发什么信号?这个地方暂时还无法理解,可能需要对Linux的信号机制有更多的认识吧。这里先将不解之处记录下来,如果有知道答案的朋友,希望不吝赐教#_#!
3.3 文件关闭时要做的清理工作
当我们结束对设备的访问时,通常会关闭设备的设备文件,关闭操作(close)在设备驱动层对应的是struct file_operations的release成员,一般会在驱动中实现xxx_release
,并将其注册到struct file_operations的release成员。
关闭设备文件时,通常要做一些清理工作,清理一些在open操作及之后申请的资源。回顾一下,我们使用异步通知的时候,进程设置FASYNC标志,进而申请了一个struct fasync_struct类型的变量,这个变量需要在关闭操作中释放,否则会造成内存泄露。
怎么去释放呢?很简单,我们只要在xxx_release
中调用xxx_fasync(-1, file, 0)
。需要注意的是:资源释放的顺序往往不是随意的。至于xxx_fasync(-1, file, 0)
究竟做了什么,这个很简单,看一下源码就知道,它其实是调用了fasync_remove_entry
来移除并释放相应的struct fasync_struct,这里不再细说。
4 参考文献
[1] 韦东山老师视频教程一期
[2] linux-kernel-5.3.1
[3] Linux 内核进程管理之进程ID