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_state
和 signal_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这个方法如果返回,有两种情况
- 有文件就绪,将当前进程唤醒
- 超时时间到达
因为不知道是那种情况返回的,所以我们需要在把所有文件的状态监测一遍(就绪计数器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系统调用的原理了
-
先注册回调函数__poll_wait,再初始化table变量(类型 为struct poll_wqueues)
-
接着拷贝用户传入的struct pollfd(其实主要是fd),然后轮流调用所有fd对应的poll,如果前面遍历的文件都没有就绪,文件插入wait queue节点(把current挂到各个fd对应的设备等待队列上)
-
遍历完成后检查状态
- 如果已经有就绪的文件转到5
- 如果有信号产生,重启poll (转到2)
- 挂起进程等待超时或唤醒,超时或被唤醒后再次遍历所有文件取得每个文件的就绪状态
-
将所有文件的就绪状态复制到用户空间
-
清理申请的资源
通过上述的总结,我们还可以归纳出poll的调用链
sys_poll() -> poll_initwait()
-> do_poll() -> do_pollfd() -> f_op->poll() [ -> poll_wait() -> __poll_wait()]
-> poll_freewait()
以及poll的粗略调用过程
优缺点
-
这里弥补了select的最大文件描述符的不足,只要内存够用,你就可以尽情的想监听多少文件描述符就监听多少文件描述符
-
重复的拷贝,重复的加入监听队列照样和select一样,主要的性能瓶颈在于当监听很多的文件描述时,大量页分配和参数传递
-
当fd很多时,就会大量的调用do_pollfd函数调用,造成效率低下