异步I/O+异步通知

x33g5p2x  于2022-05-16 转载在 其他  
字(6.3k)|赞(0)|评价(0)|浏览(390)

linux内核笔记(四)高级I/O操作(二)

分析第一个代码:
代码第50行 struct aiocb aiow, aior;
定义了两个分别用于读和写的异步I/O控制块

代码第56行到76行初始化了这二个控制块。

memset(&aiow, 0, sizeof(aiow));
	memset(&aior, 0, sizeof(aior));
 
	aiow.aio_fildes = fd;
	aiow.aio_buf = malloc(32);
	strcpy((char *)aiow.aio_buf, "aio test");
	aiow.aio_nbytes = strlen((char *)aiow.aio_buf) + 1;
	aiow.aio_offset = 0;
	aiow.aio_sigevent.sigev_notify = SIGEV_THREAD;
	aiow.aio_sigevent.sigev_notify_function = aiow_completion_handler;
	aiow.aio_sigevent.sigev_notify_attributes = NULL;
	aiow.aio_sigevent.sigev_value.sival_ptr = &aiow;
 
	aior.aio_fildes = fd;
	aior.aio_buf = malloc(32);
	aior.aio_nbytes = 32;
	aior.aio_offset = 0;
	aior.aio_sigevent.sigev_notify = SIGEV_THREAD;
	aior.aio_sigevent.sigev_notify_function = aior_completion_handler;
	aior.aio_sigevent.sigev_notify_attributes = NULL;
	aior.aio_sigevent.sigev_value.sival_ptr = &aior;

主要是文件描述符,用于读写的缓冲区,读写的字节数和异步I/O完成后的回调函数。

代码第79行发起一个异步写操作,该函数会立即返回, 具体的写操作在底层的驱动完成。

while (1) {
		if (aio_write(&aiow) == -1)
goto fail;

然后代码81行异步读操作,该函数会立即返回, 具体的写操作在底层的驱动完成。

if (aio_read(&aior) == -1)
			goto fail;

写完成后,注册的aiow_competion_handler写完成函数将被自动自动调用,该函数通过aio_error及aio_return获取了I/O操作的错误码及实际的写操作的返回值。给sigval.sival_ptr赋值,指向了I/O控制块aiow。

sugval.sival是第67行赋值的

aiow.aio_sigevent.sigev_value.sival_ptr = &aiow;

同样,读完成后,注册的aior_completion_handler读完成函数将会被自动调用。

除了像写完成操作中可以获取完成状态,还可以从aio_buf中获取读取的数据。

sleep()是为了模拟其他操作所消耗的时间。
在一次异步操作中,可以将多个i/o请求合并,从而完成一系列的读写操作,其对应的接口函数是lio_listio

第二次代码的编译实现:

编译加载

异步通知

关于代码1的分析:

代码41到46行对应步骤1——注册信号处理函数(注册中断处理函数)

//阻塞了SIGIO,防止信号处理函数的嵌套调用
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGIO);
	act.sa_flags = SA_SIGINFO;
	act.sa_sigaction = sigio_handler;
	if (sigaction(SIGIO, &act, &oldact) == -1)
		goto fail;

sigaction 比signal更高级,主要是信号阻塞和提供信号信息两方面。

使用sigaction 注册的信号处理函数的参数有三个,而第二个参数act就是关于信号的一些信息,我们随后会用到里面的内容。

另外,代码第41行和第42行

sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGIO);

阻塞了SIGIO自己,防止信号处理函数的嵌套调用。

//设置文件属主
	fd = open("/dev/vser0", O_RDWR | O_NONBLOCK);
	if (fd == -1) 
		goto fail;
		
	if (fcntl(fd, F_SETOWN, getpid()) == -1)
		goto fail;

代码第48行至第53行对应步骤(2)——打开设备文件,设置文件属主(目的是是驱动根据打开文件的file结构,找到相应的进程,从而向该进程发送信号)

即设置文件属主驱动在发信号时,处于一个所谓的任意进程上下文,即不知道当前运行的进程,要给一个特定的进程发信号,则需要一些额外的信息,可以通过fcntl将所属的进程信息保存在file 结构中,从而驱动可以根据file结构来找到对应的进程。

代码第54行和第55行对应步骤(3)——设置设备资源可用可用时驱动向进程发送的信号(非必须,但要用sigaction的高级特性需要)

//设置当前资源可用时,向进程发送SIGIO信号
	if (fcntl(fd, F_SETSIG, SIGIO) == -1)
		goto fail;

设置了当设备资源可用时,向进程发送SIGIO信号,虽然这是默认发送的信号,但是为了使用信号的更多信息(主要是发送信号的原因,或者说是具体资源的情况),需要显式地进行这一步操作 。

代码第56行和第59行对应步骤(4)——设置文件的FASYNC标志,使能异步通知机制(相当于中断使能位)

//打开异步通知机制
	if ((flag = fcntl(fd, F_GETFL)) == -1)
		goto fail;
	if (fcntl(fd, F_SETFL, flag | FASYNC) == -1)
		goto fail;

首先获取了文件的标志,然后再添加FASYNC标志,这就打开了异步通知的机制。

之后主函数初始化后一直休眠,等待驱动发来的信号。当进程收到驱动发来的信号后,注册的信号处理函数 sigio_handler自动被调用,函数的第一个参数是信号值,第二个参数是信号的附带信息(ID号、发送时间等)si_band成员将记录资源是可读还是可写,从而进行操作。

补充异步通知方面的内核代码
fs/fcnl.c

static int setfl(int fd, struct file * filp, unsigned long arg)
{
 	........
	if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
		error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
	........
}

static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
		struct file *filp)
{
long err = -EINVAL;

	switch (cmd) {
	.......
	case F_SETFL:
		err = setfl(fd, filp, arg);
		break;
		......
		}
	.......
SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{		
.......
	err = do_fcntl(fd, cmd, arg, f.file);
.......
}
.......

static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
	struct fasync_struct *new;

	new = fasync_alloc();
	if (!new)
		return -ENOMEM;
	/*
	 * fasync_insert_entry() returns the old (update) entry if
	 * it existed.
	 *
	 * So free the (unused) new entry and return 0 to let the
	 * caller know that we didn't add any new fasync entries.
	 */
	if (fasync_insert_entry(fd, filp, fapp, new)) {
		fasync_free(new);
		return 0;
	}

	return 1;
}
.......
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
	if (!on)
		return fasync_remove_entry(filp, fapp);
	return fasync_add_entry(fd, filp, fapp);
}
.......
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
	while (fa) {
		struct fown_struct *fown;
		unsigned long flags;

		if (fa->magic != FASYNC_MAGIC) {
			printk(KERN_ERR "kill_fasync: bad magic number in "
			       "fasync_struct!\n");
			return;
		}
		spin_lock_irqsave(&fa->fa_lock, flags);
		if (fa->fa_file) {
			fown = &fa->fa_file->f_owner;
			/* Don't send SIGURG to processes which have not set a
			   queued signum: SIGURG has its own default signalling
			   mechanism. */
			if (!(sig == SIGURG && fown->signum == 0))
				send_sigio(fown, fa->fa_fd, band);
		}
		spin_unlock_irqrestore(&fa->fa_lock, flags);
		fa = rcu_dereference(fa->fa_next);
	}
}

void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
	/* First a quick test without locking: usually
	 * the list is empty.
	 */
	if (*fp) {
		rcu_read_lock();
		kill_fasync_rcu(rcu_dereference(*fp), sig, band);
		rcu_read_unlock();
	}
}

分析:

代码

SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)

是fcntl系统调用对应的代码,它调用了do_fcntl来完成具体的操作。

代码

case F_SETFL:
		err = setfl(fd, filp, arg);
		break;

代码判断如果是F_ SETFL 则调用setfl 函数,setfl 会调用驱动代码中的fasync接口函数(代码第68行)error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);

,并传递FASYNC标志是否被设置。驱动中的fasync接口函数会调用fasync_ helper 函数(代码第680行)
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

, fasync_helper 函数根据FASYNC标志是否设置来决定在链表中添加一个struct fasync_ struct 节点还是删除一个节点,而这个结构中最主要的成员就是fa_ file, 它是一个打开文件的结构,还包含了进程信息(前面设置的文件属主)。当资源可用时,驱动调用kill fasync函数发送信号,该函数会遍历struct fasync_struct 链表,从而找到所有要接收信号的进程,并调用send_sigio 依次发送信号(代码第692行至第714行)。如下:

static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
	while (fa) {
		struct fown_struct *fown;
		unsigned long flags;

		if (fa->magic != FASYNC_MAGIC) {
			printk(KERN_ERR "kill_fasync: bad magic number in "
			       "fasync_struct!\n");
			return;
		}
		spin_lock_irqsave(&fa->fa_lock, flags);
		if (fa->fa_file) {
			fown = &fa->fa_file->f_owner;
			/* Don't send SIGURG to processes which have not set a
			   queued signum: SIGURG has its own default signalling
			   mechanism. */
			if (!(sig == SIGURG && fown->signum == 0))
				send_sigio(fown, fa->fa_fd, band);
		}
		spin_unlock_irqrestore(&fa->fa_lock, flags);
		fa = rcu_dereference(fa->fa_next);
	}
}

从上面了解后,总结出驱动代码的完成以下几个操作:

  1. 构造struct fasync_ struct 链表的头。
  2. 实现fasync接口函数,调用fasync_ helper 函数来构造struct fasyne_struct 节点,并加入到链表。
  3. 在资源可用时,调用kill fasync发送信号,并设置资源的可用类型是可读还是可写。
  4. 在文件最后一次关闭时,即在release接口中,需要显式调用驱动实现的fasyne接口函数,将节点从链表中删除,这样进程就不会再收到信号。

实现了众多接口的虛拟串口驱动的完整代码如下:
看顶头博客

分析:
代码第30行定义了链表的指针,实现了步骤(1);代码第170行至第173行和代码第185行,实现了步骤(2); 代码第66行和第90行发送信号,实现了步骤(3),注意此时资源状态为POLL_ IN和POLL_OUT, 在信号发送函数中会转换成POLLIN和POLLOUT; 代码第45行完成了步骤(4)。

编译和测试的命令如下:

注意:在编译应用程序时,需要在命令行中加入-D_GNU_SOURCE来定义 GNU_SOURCE, 因为F SETSIG不是POSIX标准。

这里就是gcc -o test test.c -D_GNU_SOURCE

相关文章