Linux IO: 系统调用 poll() 实现简析

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1. 前言

限于作者能力水平本文可能存在谬误因此而给读者带来的损失作者不做任何承诺。

2. 分析背景

本文基于 Linux 4.14 内核源码进行分析。

3. 系统调用 poll() 实现分析

3.1 调用的发起用户空间

用户侧应用程序在查询某 IO 事件时poll() 是可选的接口之一。以读取输入事件的代码为例

struct pollfd pfd;
int timeout;
int ready;

pfd.fd = open("/dev/input/event4", O_RDONLY);
pfd.events = POLLIN; /* 等待读取数据 */
timeout = -1; /* 超时时间单位为毫秒。负数值表示数据就绪前一直等待 */
ready = poll(pfds, nfds, timeout);
if (ready > 0) { /* 等待的数据就绪: ready 的值为就绪的 fd 数量 */
	/* 从 @fd 取数据进行处理... */
}

3.2 调用的过程内核空间

3.2.1 设备的打开过程

打开输入事件文件内核空间过程

sys_open("/dev/input/event4", O_RDONLY)
	...
	joydev_open()
		struct joydev *joydev =
				container_of(inode->i_cdev, struct joydev, cdev);
		struct joydev_client *client;

		client = kzalloc(sizeof(struct joydev_client), GFP_KERNEL);
		
		client->joydev = joydev;
		joydev_attach_client(joydev, client);

		joydev_open_device(joydev);

		file->private_data = client;
		nonseekable_open(inode, file);

3.2.2 将进程放入设备的 poll 等待队列

/*
 * @ufds: 在其上等待数据的文件句柄列表
 * @nfds: @ufds 列表长度
 * @timeout_msecs: 超时时间单位为毫秒。
 */
sys_poll(ufds, nfds, timeout_msecs)
	struct poll_wqueues table;
	struct timespec64 end_time, *to = NULL;

	/* 计算超时结束时间点 */
	if (timeout_msecs >= 0) {
		to = &end_time;
		poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
			NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
	}
	
	ret = do_sys_poll(ufds, nfds, to)
		struct poll_wqueues table;
		/*
		 * 优先使用内核栈 stack_pps[] 存放 pollfd 列表 @ufds 
		 * 如果 stack_pps[] 空间不够存放所有 @ufds 则从内核堆
		 * 分配页面存放剩余的 @ufds 。
		 */
		long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
		struct poll_list *const head = (struct poll_list *)stack_pps;
 		struct poll_list *walk = head; /* 首先用内核栈 stack_pps[] 存放 @ufds 中的 pollfd */
 		unsigned long todo = nfds; /* 总共待安置的 pollfd */

		/*
		 * 1. 建立 poll_list walk 列表。
	 	 * 将用户空间传递的长度 @nfds 的 pollfd 列表 @ufds , 
	 	 * 放入各 walk (poll_list) 中其中第1个 walk 使用内
	 	 * 核栈空间 stack_pps[] 剩余的 walk 都是从内核堆分
	 	 * 配的1个页面空间。
	 	 *			   poll_list                      poll_list
	 	 *			 -------------                ------------------
	 	 * head --> |     next    | --> ... -->  |       next       | --> NULL
	 	 *          |-------------|              |------------------|
	 	 *          |     len     |              |       len        |
	 	 *          |-------------|              |------------------|
	 	 *          |  entries[]  |              |    entries[]     |
	 	 *          | (ufds[0,i]) |              | (ufds[j,nfds-1]) |
	 	 *           -------------                ------------------
	 	 */
		len = min_t(unsigned int, nfds, N_STACK_PPS); /* 计算第1个 walk 内存放的 pollfd 数目 */
		for (;;) {
			walk->next = NULL;
			walk->len = len; /* 当前 walk 放置的 pollfd */
			if (!len)
				break;

			/* 拷贝用户空间 pollfd 到内核当前 walk 空间 */
			if (copy_from_user(walk->entries, ufds + nfds-todo,
						sizeof(struct pollfd) * walk->len))
				goto out_fds;
	
			todo -= walk->len; /* @ufds 内剩余待放置的 pollfd 个数 */
			if (!todo)
				break;

			len = min(todo, POLLFD_PER_PAGE); /* 计算下一 walk 待放置的 pollfd 个数 */
			size = sizeof(struct poll_list) + sizeof(struct pollfd) * len; /* 计算下一 walk 待放置的 pollfd 空间大小 */
			walk = walk->next = kmalloc(size, GFP_KERNEL); /* 从内核堆分配一个页面作为下一 walk 空间 */
			if (!walk) {
				err = -ENOMEM;
				goto out_fds;
			}
	}

	/*
	 * 2. 
	 * 初始化 poll 等待队列(struct poll_wqueues)
	 * 设置将进程放入 poll 等待队列的回调接口 __pollwait() 
	 * 然后驱动设备的 poll 接口通过 poll_wait() 间接的调
	 * 用 __pollwait(), 将进程放置到驱动自身的等待队列。
	 */
	poll_initwait(&table)
		init_poll_funcptr(&pwq->pt, __pollwait)
			pt->_qproc = qproc;
			pt->_key   = ~0UL; /* all events enabled */
		pwq->polling_task = current;
		pwq->triggered = 0;
		pwq->error = 0;
		pwq->table = NULL;
		pwq->inline_index = 0;
	
	/* 3. 调用设备驱动的 poll 接口: 以 poll 输入设备为例 */
	do_poll(head, &table, end_time)
		/* 计算剩余的时间 */
		if (end_time && !timed_out)
			slack = select_estimate_accuracy(end_time);
		
		for (;;) {
			struct poll_list *walk;
			
			/*
		 	 * 遍历所有的 walk 中 pollfd.
		 	 */
			for (walk = list; walk != NULL; walk = walk->next) {
				struct pollfd * pfd, * pfd_end;

				pfd = walk->entries;
				pfd_end = pfd + walk->len;
				for (; pfd != pfd_end; pfd++) {
						/* 调用驱动 poll 接口: 如 joydev_poll() */
						if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {
							count++; /* 当前 pollfd 成功, 计数加1 */
							pt->_qproc = NULL;
							...
						}
				}
				...
			}
			pt->_qproc = NULL;
			if (!count) {
				count = wait->error;
				if (signal_pending(current))
					count = -EINTR; /* 因进程由挂起的信号中断 poll() 系统调用 */
			}
			if (count || timed_out) /* 有 pollfd 的 poll 操作成功 或 超时 */
				break;
			...
			/*
			 * poll 失败 && 超时时间还未到达, 进入睡眠等待。
			 * 然后再以下两种情形被唤醒:
			 * . 驱动侧当有数据到达时, 调用 wake_up_poll() 唤醒进程;
			 * . 超时时间到达, 唤醒进程, 此时, 还会再尝试一轮 poll.
			 */
			if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
				timed_out = 1;
		}
		/* 返回成功 poll 计数 */
		return count;

	/* 4. 释放 poll 等待队列(struct poll_wqueues) */
	poll_freewait(&table);

	/* 5. 设置 poll 结果 */
	for (walk = head; walk; walk = walk->next) {
		struct pollfd *fds = walk->entries;
		int j;

		for (j = 0; j < walk->len; j++, ufds++)
			if (__put_user(fds[j].revents, &ufds->revents))
				goto out_fds;
  	}

	/* 6. 设置 poll 成功的设备 fd 数量 */
	err = fdcount;
out_fds:
	/* 7. 释放 1. 中建立的 poll_list */
	walk = head->next;
	while (walk) {
		struct poll_list *pos = walk;
		walk = walk->next;
		kfree(pos);
	}

	return err; /* 返回 poll 成功的设备 fd 数量 */

看一下具体设备驱动的 poll 过程分析

/* 上接 do_pollfd() */
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait,
				     	bool *can_busy_poll,
				     	unsigned int busy_flag)
{
	int fd = pollfd->fd;
	struct fd f = fdget(fd);

	mask = DEFAULT_POLLMASK;
	pwait->_key = pollfd->events|POLLERR|POLLHUP;
	pwait->_key |= busy_flag;
	/* 调用驱动设备的 poll 接口如 joydev_poll() */
	mask = f.file->f_op->poll(f.file, pwait)
		joydev_poll()
			poll_wait(file, &joydev->wait, wait)
				/* 将进程放入 poll 等待队列 @wait_address */
				p->_qproc(filp, wait_address, p) = __pollwait()
					struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
					/* 分配1个 poll_table_entry: 用于将进程放置到等待队列的表项 */
					struct poll_table_entry *entry = poll_get_entry(pwq);
					entry->filp = get_file(filp); /* 关联的设备文件句柄 */
					entry->wait_address = wait_address; /* 关联的等待队列 */
					entry->key = p->_key; /* pollfd->events | POLLERR | POLLHUP */
					/* 
	 				 * 设置被唤醒时调用的接口 pollwake(): 
	 				 * 由 poll timeout 超时时触发, 或者驱动数据就绪时调用 wake_up_XXX() 接口触发.
	 				 */
					init_waitqueue_func_entry(&entry->wait, pollwake);
					entry->wait.private = pwq; /* 私有数据poll 等待队列项所在的等待队列 poll_wqueues */
					add_wait_queue(wait_address, &entry->wait); /* 将进程放置到设备的 poll 等待队列 */
	if (mask & busy_flag)
		*can_busy_poll = true;
	/* Mask out unneeded events. */
	mask &= pollfd->events | POLLERR | POLLHUP;
	fdput(f);
}

3.2.3 设备数据就绪唤醒 poll 等待队列中的进程

以输入设备事件为例

joydev_event()
	...
	/* 输入设备有输入事件来临唤醒睡眠在设备 poll 等待队列的进程 */
	wake_up_interruptible(&joydev->wait)
		...
		pollwake()
			struct poll_table_entry *entry;
			
			entry = container_of(wait, struct poll_table_entry, wait);
			/* 指定事件类型(POLLIN 等)没有发生, 则不做唤醒动作 */
			if (key && !((unsigned long)key & entry->key)) 
				return 0;
			__pollwake(wait, mode, sync, key)
				struct poll_wqueues *pwq = wait->private;
				
				pwq->triggered = 1; /* 标记 poll 等待队列 poll_wqueues 中有数据就绪 */
				default_wake_function(&dummy_wait, mode, sync, key) /* 唤醒进程 */

3.3 调用的返回

经由 sys_poll() 系统调用因请求的设备数据未就绪、而陷入设备 poll 等待队列的进程在设备数据就绪后从系统调用 sys_poll() 返回。本来对于 sys_poll() 的返回流程没有什么好说的但下面的代码返回片段经常给人带来困惑

sys_poll()
	...
	ret = do_sys_poll(ufds, nfds, to);
	
	if (ret == -EINTR) { /* sys_poll() 因信号而中断 */
		struct restart_block *restart_block;

		restart_block = &current->restart_block;
		restart_block->fn = do_restart_poll;
		restart_block->poll.ufds = ufds;
		restart_block->poll.nfds = nfds;

		if (timeout_msecs >= 0) {
			restart_block->poll.tv_sec = end_time.tv_sec;
			restart_block->poll.tv_nsec = end_time.tv_nsec;
			restart_block->poll.has_timeout = 1;
		} else
			restart_block->poll.has_timeout = 0;

		/*
		 * 从这个返回值可能会经常以为系统调用会自动发起
		 * 但实际情况往往并非如此至少在 ARM 平台不会自动
		 * 重新发起 poll() 调用。
		 */
		ret = -ERESTART_RESTARTBLOCK;
	}
	return ret;	

我们看 ARM 平台对于因信号中断的系统调用是怎么处理的

do_work_pending()
	if (thread_flags & _TIF_SIGPENDING) { /* 挂起信号导致系统调用的中断 */
		int restart = do_signal(regs, syscall)
			unsigned int retval = 0, continue_addr = 0, restart_addr = 0;
			int restart = 0;
			
			if (syscall) {
				continue_addr = regs->ARM_pc; /* 紧跟发起系统调用的 swi 指令的下一条指令的地址 */
				restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4); /* 如果是返回用户间后再重新发起系统调用要将 PC 重新指向 swi 指令 */
				retval = regs->ARM_r0; /* 系统调用返回值 */
				switch (retval) {
				case -ERESTART_RESTARTBLOCK: /* 系统调用返回 ERESTART_RESTARTBLOCK */
					restart -= 2;
					...
					restart++;
					/* 
					 * 由于 R0 已经覆写为系统调动的返回值我们用在进入系统调用进入内核空间时
					 * 重复保存的 R0 (系统调用的第1个参数) 来恢复系统调用的第1个参数。
					 */
					regs->ARM_r0 = regs->ARM_ORIG_r0;
					/* 返回用户空间后重新发起系统调用: 将 User 模式的 PC 重新指向 swi 指令 */
					regs->ARM_pc = restart_addr;
					break;
			}
			
			if (get_signal(&ksig)) { /* 取出一个挂起的信号 */
				if (unlikely(restart) && regs->ARM_pc == restart_addr) {
					if (retval == -ERESTARTNOHAND ||
			    		retval == -ERESTART_RESTARTBLOCK
			   			|| (retval == -ERESTARTSYS
						&& !(ksig.ka.sa.sa_flags & SA_RESTART))) {
							/* 所以即使 poll() 因信号中断时设
							 * 置了 restart_block 且返回了 
							 * -ERESTART_RESTARTBLOCK 了错误码
							 * ARM 依然将错误码重置为了 -EINTR 同时也还是从系统调用发起的位置之后继续执行
							 */
							regs->ARM_r0 = -EINTR;
							regs->ARM_pc = continue_addr;
						}
				}
			}
			
		if (unlikely(restart)) {
			/*
			 * Restart without handlers.
			 * Deal with it without leaving
			 * the kernel space.
			 */
			return restart; /* 如果走到这里会重新发起重新调用 */
		}
	}

这里返回流程涉及了系统调用和信号处理的细节可以分别参考博文
Linux系统调用实现简析
Linux信号处理简析
进行了解。

4. 番外

如果想了解 select() 的实现可以参考本篇对 poll() 实现的解析因为它们的实现有大部分逻辑是相似的。

5. 参考资料

man poll()

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux