Linux IO: poll() 实现简析

1. 前言

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

2. 分析背景

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

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

3.1 调用的发起:用户空间

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

struct pollfd pfd;
int timeout, 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;
		}
		/* 返回成功的 pollfd 计数 */
		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 成功的设备 pollfd 数量 */
	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 成功的设备 pollfd 数量 */

看一下具体设备驱动的 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) /* 唤醒进程 */

不管是什么样的代码,核心都是围绕数据进行,无非是数据间关系的建立,以及数据的修改。阅读理解代码,就是理清数据之间的关系,了解数据修改的逻辑。我们重点看一下上述代码分析中,建立 poll 等待队列 相关的数据结构,然后用一张图来描述这些数据结构之间的关系。先看具体设备无关的数据结构:

/* poll()/select() 辅助数据结构 */
struct poll_wqueues {
	poll_table pt; /* 参看后面的说明 */
	/* 
	 * 在调用 poll_get_entry() 分配用于将 poll()/select() 发起进
	 * 程放入等待队列的 poll_table_entry 对象时,如果 inline_entries[] 
	 * 不再有空闲对象(即 inline_index >= N_INLINE_POLL_ENTRIES),
	 * 则从内核分配一个页面,用于分配 poll_table_entry 对象。
	 * 多个 poll_table_page 组建成如下图列表:
	 *                           struct poll_table_page              struct poll_table_page
	 *                          ----------------------              ----------------------
	 * poll_wqueues::table --> |         next         | --> ... -> |         next         | --> NULL
	 *                         |----------------------|            |----------------------|
	 *                         |   poll_table_entry   |            |   poll_table_entry   |
	 *                         |      entries[]       |            |      entries[]       |
	 *                          ----------------------              ----------------------
	 */
	struct poll_table_page *table;
	struct task_struct *polling_task; /* 发起 poll()/select() 调用的进程 */
	int triggered; /* 标记进程请求设备 fd 中至少有一个数据就绪了 */
	int error; /* 错误码 */
	/*
	 * @inline_entries[] 数组中空闲 poll_table_entry 对象索引。
	 * 在调用 poll_get_entry() 分配用于将 poll()/select() 发起
	 * 进程(即 @polling_task)放入 poll 等待队列的 poll_table_entry 
	 * 对象时,首先从从 @inline_entries[] 数组中分配空闲 poll_table_entry 
	 * 对象。如果 @inline_entries[] 中不再有空闲对象,从内核分配一个页面,
	 * 构建 poll_table_page 对象,用于分配 poll_table_entry 对象 。
	 */
	int inline_index;
	struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};

typedef struct poll_table_struct {
	poll_queue_proc _qproc; /* __pollwait(): 用于将进程放入 pollfd 指向设备的等待队列 */
	unsigned long _key; /* poll 事件,如 POLLIN|POLLERR|POLLHUP */
} poll_table;

/* 设备 poll 等待队列表项 */
struct poll_table_entry {
	/* pollfd 文件对象 */
	struct file *filp;
	/* poll 事件,如 POLLIN|POLLERR|POLLHUP ,来自 poll_table::_key */
	unsigned long key;
	/* @wait_address 指向等待队列 (wait_queue_head_t) 的表项 */
	wait_queue_entry_t wait;
	/* @wait 所在的等待队列 */
	wait_queue_head_t *wait_address;
};

再看具体设备相关的数据结构:

/* 以上面分析代码中的输入设备为例 */
struct joydev {
	...
	wait_queue_head_t wait; /* 设备的 poll()/select() 等待队列头 */
	...
};

最后看它们的关联,通过前面的代码,上述数据结构建立如下面的关系:
在这里插入图片描述

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()

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
workQueue.poll是一个方法调用,用于从工作队列中获取并移除一个任务。具体来说,它会返回工作队列中的下一个任务,如果队列为空,则返回null。这个方法通常在线程池的工作线程中被调用,用于获取下一个要执行的任务。\[1\]在线程池的源码中,可以看到在执行任务之前,会先调用workQueue.poll方法来获取任务。\[2\]在线程池的实现中,每个工作线程都是通过Worker类来表示的,Worker类实现了Runnable接口,并且在构造函数中将任务赋值给firstTask成员变量。\[3\]因此,当工作线程运行时,会通过workQueue.poll方法获取下一个要执行的任务,并将其赋值给firstTask,然后调用任务的run方法来执行任务的逻辑。 #### 引用[.reference_title] - *1* *2* [异步编程学习之路(五)-线程池原理及使用,java架构师面试问题](https://blog.csdn.net/m0_65485166/article/details/122198958)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [java并发与多线程(三)--线程池原理解析](https://blog.csdn.net/qq47653423/article/details/122272131)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值