0 事件
-
-
tcp外带数据是优先级带数据
-
tcp的对端写半关闭(也就是对端发来FIN,写半关闭),认为是普通数据,但是 读操作返回0
-
tcp连接存在错误可认为是普通数据,也可以是
POLLERR
。但是随后读操作返回-1 -
监听套接字上的已完成队列不为空的时候,认为是普通数据或是优先级带数据。
-
非阻塞式
connect
连接完成,被认为是套接字可写。
1 select
#include <sys/select.h> #include <sys/time.h> int select(int maxfd_add_1, fd_set *readset, fd_set *write_set, fd_set * exceptset, const struct timeval *timeout); struct timeval{ long tv_sec; long tv_usec; }; /*设置fd_set,下面的四个函数都是宏,不能取地址*/ //清空 void FD_ZERO(fd_set *fdset); //设置一个位 void FD_SET(int fd, fd_set *fdset); //清除一个位 void FD_CLR(int fd, fd_set *fdset); /*检查某个位是否被设置,可用于函数返回时判断那个文件描述符就绪*/ int FD_ISSET(int fd,fd_set *fdset);
最大值为宏
-
-
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
-
select支持的文件描述符数量太小了,默认是1024
2 poll
2.1 调用函数
#include <poll.h> int poll(struct pollfd *fdarray, unsigned long n, int timeout); //成功个数,错误-1,超时0 struct pollfd{ int fd; short events; short revents; };
-
-
pollfd
的events
成员是要测试的条件,而revents
是内核要填充的,表示文件描述符当前的读写状态。
2.2 源码
poll方法不再使用位图的方式传入文件描述符符,而是采用一个结构体的方式,因而在遍历每个文件描述符的poll方法的时候,就从结构体的数组中依次遍历,因此突破了select文件描述符的限制。
但是,仍然采用对每个文件描述符poll的方法获取就绪状态。
因此仍然低效。
3 epoll
3.1 调用函数
#include <sys/epoll.h> // 创建 epollfd 的函数 int epoll_create(int size); int epoll_create1(int flags); // 对要监听的文件描述符的的增加修改删除 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 开始阻塞 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask); // epoll_wait 返回的时候,events表示发生的、 //时间 data则表示与文件描述符相关的信息,可以指向一个结构体, //也可以是直接的文件描述符 typedef union epoll_data { void *ptr; // 常用这个 int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { //感兴趣的事件如EPOLLIN,EPOLLOUT,EPOLLPRI等 uint32_t events; //一般使用data的fd成员表示感兴趣的socket epoll_data_t data; };
epoll_ctl() 这个函数是修改epoll
关注描述符的。
-
-
EPOLL_CTL_MOD
:修改 -
EPOLL_CTL_DEL
:删除
-
说明 | |
---|---|
EPOLLIN | 可读 |
EPOLLOUT | 可写 |
EPOLLPRI | 优先级带可读 |
EPOLLERR | 错误 |
EPOLLHUP | 挂断 |
EPOLLET | 将EPOLL设为边缘触发模式,这是相对于水平触发(Level Triggered)来说的 |
EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,就从关注的数组中移除。 |
3.2 源码分析
epoll_create()函数在内核中申请了一块内存区域,存储eventpoll等结构体。eventpoll中有一个满足事件的链表,就绪队列,用来存储以满足事件的文件描述符的信息。一个红黑数用来维护需要监听的文件描述符
epoll_ctl()采用增量式的修改方法,对关注的文件描述符进行更改,也就是说,每次只能修改一个。已经传入的则保存在eventpoll结构体中的红黑树中。epoll_ctl()插入的或是修改文件描述符的时候,会直接将包含文件描述符的结构体挂在到相应设备的等待队列上,并注册回调函数。加到红黑树中。这个回调函数是epoll性能优于select和poll的关键,当文件描述符就绪释,会调用该回调函数,回调函数会将本结构体挂到eventpoll的就绪队列上。如果一个线程在添加文件描述符,并且在添加到相应的等待队列的时候返回值指示该文件描述符已经就绪,而另一个线程阻塞在epoll_wait(),那么epoll_wait()的线程会被立即唤醒。(poll()方法会将相应的文件描述符添加到对应的等待队列,返回值标志当前文件描述符发生的事件,如果已经就绪,那么后面需要判断该文件描述符是否已经挂在到eventpoll的已就绪队列中,如果是,那么不操作。如果不是,那么就挂载到等待队列,并唤醒进程)。
epoll_wait()函数的主要工作是先判断已就绪队列中是否已经为空,如果为空,那么就阻塞等待,回调函数会唤醒本进程。当进程被唤醒的时候,就将eventpoll中已就绪队列中数据,填充到用户传入的内存空间中。并返回。如果有多线程的存在,那么可能被唤醒,但是别的epoll_wait()已经返回,那么在不超时的情况下,会继续阻塞。
其中填充函数会再次调用poll去获取最新的文件描述符的状态。
3.3 为什么高效
1. 每次调用不需要传入所有的文件描述符
select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
2. 每次epoll_wait()被唤醒,不需要去遍历所有文件描述符
3. epoll没有文件描述符数量上的限制。
3.4 epoll两种工作方式LT和ET
水平触发(LT)
ngnix采用ET模式。
ET只支持非阻塞模式的原因在于ET当事件触发的时候,比如写操作,那么就需要一直写,直到阻塞位置。因此只能使用非阻塞,如果使用了阻塞。那么在最后一次就会阻塞住。非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。
而LT模式可以使用非阻塞也可以使用阻塞,是因为LT模式下,没有读写一定要到底的要求。因此是,一般是不用阻塞模式的。
下面是ET模式下,正确的读写模式。
n = 0; while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { n += nread; } if (nread == -1 && errno != EAGAIN) { perror("read error"); } int nwrite, data_size = strlen(buf); n = data_size; while (n > 0) { nwrite = write(fd, buf + data_size - n, n); if (nwrite < n) { if (nwrite == -1 && errno != EAGAIN) { perror("write error"); } break; } n -= nwrite; }
ET模式下accept
多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。
使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,因此需要向socket写数据的时候才把socket加入epoll,等待可写事件。接受到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移出epoll。缺点在于很少的数据也需要加入epoll
开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。
源码级别差异:LT和ET的模式在于,在填充函数中,对每一个在已就绪队列中的文件描述符,都会再次调用poll函数,获取最新的文件描述符状态。而LT模式就在于当从已就绪队列中取出文件描述符,获取最新状态以后,会重新添加到已就绪队列中。因此在下一次epoll_wait()的时候会立即唤醒,然后获取最新状态。然后会判断数据是否可读,如果可读并且是ET模式,那么该文件描述符会被重新添加到等待队列,如果没有数据,那么就不添加了。因此可能会空转一次。
epoll 惊群
当某个等待在epoll实例上的进程被唤醒后,最终会进入到ep_scan_ready_list() 这个函数中,ep_scan_ready_list()会以回调方式调用ep_send_events_proc()来将数据复制到用户空间。而ep_scan_ready_list()函数在返回之前会再次判断epoll的就绪链表rdllist是否为空,如果不为空的话,就会再唤醒其他进程!
epoll 多线程
man中说epoll的后两个函数都是线程安全的,但是一般实现上,还是在epoll所在线程调用epoll_ctl()