Linux的异步通知

前言

本文主要是我学习Linux的异步通知机制所做的笔记,介绍了异步通知的一些应用层和驱动层的接口,内容会不断的完善。

所涉及到的源码的kernel版本是linux-kernel-5.3.1

1 概览异步通知

当我们访问文件的时候,比如读/写,如果当前不能读或不能写,那么我们可以选择阻塞访问,直到有数据可读或有空间可写。而异步通知是进程先打开设备文件,再通过fcntl函数,设置F_SETOWNFASYNC标志,从而告知相应的设备:想要访问你的进程具体是哪个,且想要以异步通知的方式访问你。起到的效果是可读/可写的时候,驱动层会给进程发信号,进程收到信号之后执行相应的处理函数,在处理函数中进行读/写操作。进程不需要管什么时候文件是可访问的,就像你点了外卖以后,不需要死等,你只要接着干自己的事情就行,外卖小哥到了会给你打电话(发信号)的。

我们以一幅图来概括一下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_operationsfasync成员,因此我们需要在驱动层实现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 *fasyncstruct 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_fasyncsig参数和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

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值