poll源码剖析

Poll源码剖析

本篇博客部分参考 董昊的poll和epoll内核源码剖析,作者博客链接http://donghao.org/uii/

剖析的是内核版本为2.6.9

重要类型定义

// 每一个poll系统调用只有一个poll_wqueues,是总的结构,但是对外接口是poll_table pt的地址
// 进入此模块后,使用container_of求出poll_wqueues的地址。绝对,面向对象的用法,减少了耦合性
struct poll_wqueues {
	poll_table pt;
	struct poll_table_page * table;
	int error;
};

// ==============================================================================================

// poll_table的变量是一个函数指针
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

typedef struct poll_table_struct 
{
	poll_queue_proc qproc;
} poll_table;

struct poll_table_entry {
	struct file * filp;
	wait_queue_t wait;					// 等待队列项
	wait_queue_head_t * wait_address;	// 等待队列头
};

struct poll_table_page {
	struct poll_table_page * next;
	struct poll_table_entry * entry;
	struct poll_table_entry entries[0];  // 指向结构体的下一个地址,不占空间
};

// ==============================================================================================

// 一页最多可放的struct pollfd的个数
#define POLLFD_PER_PAGE  ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd))

// 链表结构体,链表的每个结点大小是一页,struct pollfd就是通过struct pollfd entries[0]来访问
struct poll_list {
	struct poll_list *next;
	int len;
	struct pollfd entries[0];
};

重要的函数

// poll的内核实现
asmlinkage long sys_poll(struct pollfd __user * ufds, unsigned int nfds, long timeout);

// 初始化poll的poll_wqueues结构变量,将变量的成员poll_table对应的回调函数设置为__pollwait
void poll_initwait(struct poll_wqueues *pwq);

// 回调函数
void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

// 针对传入的fd,调用其各自的poll函数(由设备驱动程序实现)
static void do_pollfd(unsigned int num, struct pollfd * fdpage, poll_table ** pwait, int *count)// 调用do_pollfd,循环内等待事件发生,直到count大于0跳出循环
static int do_poll(unsigned int nfds,  struct poll_list *list, struct poll_wqueues *wait, long timeout)

poll的调用过程

// sys_poll的实现-----------------------------------------------------------------------
asmlinkage long sys_poll(struct pollfd __user * ufds, unsigned int nfds, long timeout)
{
	struct poll_wqueues table;
 	int fdcount, err;
 	unsigned int i;
	struct poll_list *head;
 	struct poll_list *walk;

	/* Do a sanity check on nfds ... */
    /* 用户给的nfds数不可以超过一个struct file结构支持的最大fd数(默认是256) */
	if (nfds > current->files->max_fdset && nfds > OPEN_MAX)
		return -EINVAL;

	if (timeout) {
		/* Careful about overflow in the intermediate values */
		if ((unsigned long) timeout < MAX_SCHEDULE_TIMEOUT / HZ)
			timeout = (unsigned long)(timeout*HZ+999)/1000+1;
		else /* Negative or overflow */
			timeout = MAX_SCHEDULE_TIMEOUT;
	}

	poll_initwait(&table);

首先sys_poll调用了poll_initwait函数,将table变量初始化,我们来看看poll_initwait的实现:

// poll_initwait--------------------------------------------------------------------
void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

void poll_initwait(struct poll_wqueues *pwq)
{
	(&pwq->pt)->qproc = __pollwait;		// 为了方便观看,此处已被“翻译”
	pwq->error = 0;
	pwq->table = NULL;
}

很明显,poll_initwait的主要动作就是把table变量的成员poll_table对应的回调函数置为__pollwait。这个__pollwait不仅是poll系统调用需要,select系统调用也一样是用这个__pollwait,说白了,这是个操作系统的异步操作的“御用”回调函数。对于__pollwait的实现放到后面来说。

再继续来看sys_poll函数:

// sys_poll的实现,接上-----------------------------------------------------------------------
	head = NULL;
	walk = NULL;
	i = nfds;	// 描述符的个数
	err = -ENOMEM;
	while(i!=0) {	// 建立链表,链表的结构为struct poll_list + struct pollfd
		struct poll_list *pp;
		pp = kmalloc(sizeof(struct poll_list)+
				sizeof(struct pollfd)*
				(i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i),	// 判断一页是否可以放下所有的struct pollfd
					GFP_KERNEL);
		if(pp==NULL)	// 申请空间失败
			goto out_fds;
		pp->next=NULL;
		pp->len = (i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i);
		if (head == NULL)
			head = pp;
		else
			walk->next = pp;

		walk = pp;
		if (copy_from_user(pp->entries, ufds + nfds-i, 	// 将文件描述符从用户态拷贝到内核态
				sizeof(struct pollfd)*pp->len)) {
			err = -EFAULT;
			goto out_fds;
		}
		i -= pp->len;
	}
	fdcount = do_poll(nfds, head, &table, timeout);	// 调用do_poll

这里主要就是建立了一个链表,每个链表节点的大小是一个page大小(4k),链表的结点由struct poll_list的指针掌控,而众多的struct pollfd就通过struct_list的entries成员访问,上面的循环就 是把用户态的struct pollfd拷进这些entries里

通常用户程序的poll调用就监控几个fd,所以上面这个链表通常也就只需要一个节点,即操作系统的一页。但是,当用户传入的fd很多时,由于poll系统调用每次都要把所有struct pollfd拷进内核,所以参数传递和页分配此时就成了poll系统调用的性能瓶颈

在上面代码的最后调用了do_poll函数,函数实现如下:

// do_poll----------------------------------------------------------------------------------------------
static int do_poll(unsigned int nfds,  struct poll_list *list, struct poll_wqueues *wait, long timeout)
{
	int count = 0;
	poll_table* pt = &wait->pt;

	if (!timeout)
		pt = NULL;
 
	for (;;) {
		struct poll_list *walk;
		set_current_state(TASK_INTERRUPTIBLE);
		walk = list;
		while(walk != NULL) {	// 遍历整个链表,调用do_pollfd处理文件描述符
			do_pollfd( walk->len, walk->entries, &pt, &count);
			walk = walk->next;
		}
		pt = NULL;
		if (count || !timeout || signal_pending(current))
			break;
		count = wait->error;
		if (count)	// count大于0,跳出循环
			break;
		timeout = schedule_timeout(timeout);  // 让current挂起,别的进程跑,timeout到了以后再回来运行current
	}
	__set_current_state(TASK_RUNNING);
	return count;
}

do_poll函数的 set_current_statesignal_pending保障了当用户程序在调用poll后挂起时,发信号可以让程序迅速推出poll调用,而通常的系统调用是不会被信号打断的。

通过观察do_poll我们可以发现,这个函数主要是一页一页的调用do_pollfd()函数,通过do_pollfd函数完成查询该页上的pollfd有没有事件到达,如果有事件到达就将mask设置给pollfd中的revents,如果是本次poll调用的第一次对这个文件描述符做poll方法就会将该进程加入该文件的等待队列中。主要是在循环内等待,直到count大于0才跳出循环。

在这里需要注意的是,当用户传入的fd很多时(比如1000个),对do_pollfd就会调用很多次,poll效率瓶颈的另一原因就在这里

接下来我们可以看do_pollfd的实现:

// do_pollfd--------------------------------------------------------------------------------------
static void do_pollfd(unsigned int num, struct pollfd * fdpage, poll_table ** pwait, int *count)
{
	int i;

	for (i = 0; i < num; i++) {
		int fd;
		unsigned int mask;
		struct pollfd *fdp;

		mask = 0;
		fdp = fdpage+i;
		fd = fdp->fd;
		if (fd >= 0) {
			struct file * file = fget(fd);
			mask = POLLNVAL;	// 描述符不是一个打开的文件
			if (file != NULL) {
				mask = DEFAULT_POLLMASK;
				if (file->f_op && file->f_op->poll)
					mask = file->f_op->poll(file, *pwait);
				mask &= fdp->events | POLLERR | POLLHUP;
				fput(file);
			}
			if (mask) {
				*pwait = NULL;
				(*count)++;
			}
		}
		fdp->revents = mask;	// 填充事件的相应
	}
}

我们可以看到,do_pollfd就是针对每个传进来的fd,调用它们各自对应的poll函数,简化一下调用过程,如下:

int i;
for (i = 0; i < num; i++) {
	struct file* file = fget(fd);
	file->f_op->poll(file, &(table->pt));
}

如果fd对应的是某个socket,do_pollfd调用的就是网络设备驱动实现的poll;如果fd对应的是某个ext 3文件系统上的一个打开文件,那do_pollfd调用的就是ext 3文件系统驱动实现的poll。一句话,这个file->f_op->poll是设备驱动程序实现的,那设备驱动程序的poll实现通常又是什么样子呢?其实,设备驱动程序的标准实现是:调用poll_wait,即以设备自己的等待队列为参数(通常设备都有自己的等待队列,不然一个不支持异步操作的设备会让人很郁闷)调用struct poll_table的回调函数

在看poll_wait的实现之前,我们先回顾一下do_poll与do_pollfd所做的事情:

do_poll与do_pollfd只是将当前进程挂到fd对应的等待队列中,然后调用chedule_timeout方法让当前进程阻塞。如果有文件就绪,就会唤醒当前进程,然后在循环跑一边将所有用户关注的fd的状态监测一遍,这样就绪的count肯定不为0,我们调用的poll也就返回了!或者是这段时间没有就绪的文件描述符,那么schedule_timeout也会返回,这个时候在监测一遍所有文件的状态,那么count就是0,poll也返回了,只是这就是超时返回的。

也就是schedule_timeout这个方法如果返回,有两种情况

  1. 有文件就绪,将当前进程唤醒
  2. 超时时间到达

因为不知道是那种情况返回的,所以我们需要在把所有文件的状态监测一遍(就绪计数器count是否为0,也就是返回给用户调用的poll的返回值),才能知道是那种情况返回的。

当我们理清了do_poll与do_pollfd之后,接下来接着看poll_wait的实现:

// poll_wait---------------------------------------------------------------------------------------
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && wait_address)
		p->qproc(filp, wait_address, p);
}

可以发现,poll_wait就是调用poll对应的回调函数,poll系统调用对应的回调函数就是__poll_wait,我们可以看看__poll_wait的实现

// __pollwait---------------------------------------------------------------------------
void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *_p)
{
	struct poll_wqueues *p = container_of(_p, struct poll_wqueues, pt);	// 获取poll_wqueues的地址
	struct poll_table_page *table = p->table;

	if (!table || POLL_TABLE_FULL(table)) {	// 判断是否存在struct poll_table_page,存在则跳过,否则创建
		struct poll_table_page *new_table;

		new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL);	// 如果空间不够,一下子申请PAGE_SIZE的大小
		if (!new_table) {
			p->error = -ENOMEM;
			__set_current_state(TASK_RUNNING);
			return;
		}
		new_table->entry = new_table->entries;
		new_table->next = table;
		p->table = new_table;
		table = new_table;
	}

	/* Add a new entry */
	{
		struct poll_table_entry * entry = table->entry;
		table->entry = entry+1; 	// 直接在当前页中往下给entry分配内存
	 	get_file(filp);
	 	entry->filp = filp;
		entry->wait_address = wait_address;
		init_waitqueue_entry(&entry->wait, current);// 初始化等待队列项的成员为current
		add_wait_queue(wait_address,&entry->wait);	// 将current加入到等待队列中
	}
}

__poll_wait的作用就是创建了上图所示的数据结构(一次__poll_wait即一次设备poll调用只创建一个 poll_table_entry),并通过struct poll_table_entry的wait成员,把current挂在了设备的等待队列上,此处的等待队列是wait_address。

当已经有就绪文件或者挂起的进程被唤醒再次遍历所有文件之后,就会进入到sys_poll的最后一部分,也就是将填充好的事件类型拷贝到用户态,代码的实现如下:

// sys_poll-------------------------------------------------------------------
	/* OK, now copy the revents fields back to user space. */
	walk = head;
	err = -EFAULT;
	while(walk != NULL) {
		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;
		}
		walk = walk->next;
  	}
	err = fdcount;
	if (!fdcount && signal_pending(current))
		err = -EINTR;
out_fds:
	walk = head;
	while(walk!=NULL) {
		struct poll_list *pp = walk->next;
		kfree(walk);
		walk = pp;
	}
	poll_freewait(&table);
	return err;
}

代码的主要逻辑清晰明了,简单易懂,这里不再过多的赘述,读者可以自行查看。

总结

现在我们可以回顾一下poll系统调用的原理了

  1. 先注册回调函数__poll_wait,再初始化table变量(类型 为struct poll_wqueues)

  2. 接着拷贝用户传入的struct pollfd(其实主要是fd),然后轮流调用所有fd对应的poll,如果前面遍历的文件都没有就绪,文件插入wait queue节点(把current挂到各个fd对应的设备等待队列上)

  3. 遍历完成后检查状态

    1. 如果已经有就绪的文件转到5
    2. 如果有信号产生,重启poll (转到2)
    3. 挂起进程等待超时或唤醒,超时或被唤醒后再次遍历所有文件取得每个文件的就绪状态
  4. 将所有文件的就绪状态复制到用户空间

  5. 清理申请的资源

通过上述的总结,我们还可以归纳出poll的调用链

sys_poll() -> poll_initwait()
    	   -> do_poll() -> do_pollfd() -> f_op->poll() [ -> poll_wait() -> __poll_wait()]
    	   -> poll_freewait()

以及poll的粗略调用过程
在这里插入图片描述

优缺点

  1. 这里弥补了select的最大文件描述符的不足,只要内存够用,你就可以尽情的想监听多少文件描述符就监听多少文件描述符

  2. 重复的拷贝,重复的加入监听队列照样和select一样,主要的性能瓶颈在于当监听很多的文件描述时,大量页分配和参数传递

  3. 当fd很多时,就会大量的调用do_pollfd函数调用,造成效率低下

©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页