select poll epoll都是IO多路复用机制。这里的复用其实能够理解为复用的线程,即一个(或者较少的)线程完成多个IO的读写。这里总结下这三个函数的区别。html
1 select的函数原型是node
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
使用的时候须要将fd_set从用户空间copy到内核空间。select的使用方式相似以下socket
while true { select(streams[]) for i in streams[] { //须要遍历全部的fd_set if i has data // 判断是否有数据 read until unavailable } }
2 select的核心是do_select()。do_select首先会注册回调函数__pollwait,__pollwait会在被调用的时候将当前进程添加到设备的等待队列里。ide
do_select会在一个for循环里调用设备的f_op->poll。而该函数有两个做用,一个是调用poll_wait()函数,一个是检测设备当前状态。而poll_wait会调用回调函数__pollwait,将当前进程加入到设备等待队列里。函数
设备本身实现了当有读写的时候会唤醒等待队列里的进程。若是当前没有设备可读写,那么do_select()就将当前进程睡眠。设备会在有读写的时候唤醒进程。唤醒后设备必须从新轮询一遍全部的设备,调用poll来检测设备当前的状态以肯定哪些可写可读。.net
int do_select(int n, fd_set_bits *fds, struct timespec *end_time) { struct poll_wqueues table; poll_table *wait; poll_initwait(&table); // 注册回调函数__pollwait wait = &table.pt; // … for (;;) { // … for (i = 0; i < n; ++rinp, ++routp, ++rexp) { // … struct fd f; f = fdget(i); if (f.file) { const struct file_operations *f_op; // 重要 f_op = f.file->f_op; // 重要 mask = DEFAULT_POLLMASK; if (f_op->poll) { wait_key_set(wait, in, out, bit, busy_flag); // 对每一个fd进行I/O事件检测 mask = (*f_op->poll)(f.file, wait); // 函数指针,每一个设备自定义本身的poll。每一个设备拥有一个struct file_operations结构体,这个结构体里定义了各类用于操做设备的函数指针,具体怎么操做是设备本身定义的 } fdput(f); // … } } // 退出循环体 if (retval || timed_out || signal_pending(current)) break; // 没有可读写,让进程进入休眠 if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack)) timed_out = 1; } }
3 file 结构线程
struct file { struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; // … } __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
4 file_operations结构指针
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); // select()轮询设备fd的操做函数, poll调用poll_wait, 而poll_wait会调用回调函数__pollwait, __pollwait将当前进程加到等待队列里 unsigned int (*poll) (struct file *, struct poll_table_struct *); // … };
5 简单总结来说,select会遍历fd_set,调用f_op->poll(此poll非select/poll的poll),若是有可读/写的fd则返回可读/写的fd,若是没有则在每一个fd的等待队列中加入当前进程,当前进程进入睡眠。当有fd可读/写的时候会唤醒当前进程,当前进行从新遍历fd_set,返回可读/写的全部fd。rest
6 从中也能够看出select的几大缺点:code
poll的实现原理和select相似,只是接口的方式不一样。
1 epoll的函数原型是
int epoll_create(int size); // 建立一个epoll对象,通常epollfd = epoll_create() int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // (epoll_add/epoll_del的合体),往epoll对象中增长/删除某一个流的某一个事件好比epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回 int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); // 等待直到注册的事件发生
epoll的使用方式相似以下:
while true { active_stream[] = epoll_wait(epollfd) // 只返回可读/写的fd,而不是像select同样,返回全部的fd for i in active_stream[] { read or write till unavailable } }
2 epoll_create。epoll会向内核注册一个文件系统,调用epoll_create时:
3 epoll_ctl。 当咱们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上以外,还会给内核中断处理程序注册一个回调函数,告诉内核,若是这个句柄的中断到了,就把它放到准备就绪list链表里。因此,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到list里了
4 epoll_wait。 epoll_wait调用时,仅仅观察这个list链表里有没有数据便可。有数据就返回,没有数据就sleep
5 可见,