NIO底层的实现
poll函数的定义如下:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其中的参数类型pollfd的定义如下:
struct pollfd { int fd; short events; short revents; };
poll系统调用对应的内核中的代码为sys_poll,整体的流程是先把用户想知道的事件从用户内存弄到内核空间中来。然后调用文件的poll函数来监听对应的事件。如果在指定的时间内发生了,那么就要通知用户态的进程了。为了更清楚地阅读代码,先来看涉及到的数据结构,poll_wqueues用来保存发生的事件:
poll_list用来保存从用户空间得到的用户感兴趣的事件:
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; if (nfds > current->files->max_fdset && nfds > OPEN_MAX) return -EINVAL; if (timeout) { if ((unsigned long) timeout < MAX_SCHEDULE_TIMEOUT / HZ) timeout = (unsigned long)(timeout*HZ+999)/1000+1; else timeout = MAX_SCHEDULE_TIMEOUT; } // 初始化poll_wqueues,设置了回调函数为__pollwait poll_initwait(&table); head = NULL; walk = NULL; i = nfds; err = -ENOMEM; // 每次都分配一个页面来保存传入的pollfd数组,下面的循环用来分割 while(i!=0) { struct poll_list *pp; pp = kmalloc(sizeof(struct poll_list)+ sizeof(struct pollfd)*(i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i), 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); 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_wqueues中我们看到了一个回调函数poll_queue_proc,在初始化poll_wqueues的时候(也就是poll_initwait函数中)会被初始化为__pollwait。
void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *_p) { // 根据_p的位置就能找到所在的poll_wqueues所在的位置 struct poll_wqueues *p = container_of(_p, struct poll_wqueues, pt); struct poll_table_page *table = p->table; // 如果为空或者没有足够的空间 if (!table || POLL_TABLE_FULL(table)) { struct poll_table_page *new_table; new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL); 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; } { struct poll_table_entry * entry = table->entry; table->entry = entry+1; get_file(filp); entry->filp = filp; entry->wait_address = wait_address; // 初始化等待队列项,挂载当前线程 init_waitqueue_entry(&entry->wait, current); add_wait_queue(wait_address,&entry->wait); } }
接下来会调动do_poll函数,在这个函数里并没有做什么实际性的工作,循环处理poll_list下面的每个页面(每个页面上有用户进程感兴趣的事件),然后再do_pollfd函数中循环处理一个页面上所有的pollfd。这个函数中实际上还是循环调用file_operation结构中的poll函数。在返回的结果中判断我们感兴趣的事件有没有发生。具体的代码如下:
static void do_pollfd(unsigned int num, struct pollfd * fdpage, poll_table ** pwait, int *count) { int i; // 循环遍历一个页面上保存的所有的pollfd 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); } // 如果发生了,就将pwait清空,后面就不用再把当前线程挂入等待队列 if (mask) { *pwait = NULL; (*count)++; } } // 把发生的事件记录下来,用来返回 fdp->revents = mask; } }
到此为止,如果发现我们感兴趣的事件发生了,那就把回调函数设置为null,这样就算以后再发现有事件发生也不会做任何实质性的操作,而是记录一下数目而已。相应的在do_poll函数中发现对应的事件发生了就会中断循环返回。再底层的比如file_operation中的poll是怎么实现的话以后有事件再看。