阅读《Linux高性能服务器编程》的笔记
I/O多路转接
一 基础概念
- I/O复用能同时监听多个文件描述符,但它本身是阻塞的
- 当多个文件描述符同时阻塞时,如果不采取额外的措施,只能依次处理,这样服务器看起来就像串行工作
二 select系统调用
-
用途
- 在一段指定时间内,监听文件描述符上的可读、可写和异常事件
-
API
#include <sys/select.h> int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout); /* nfds:被监听的文件描述符总数。通常为所有文件描述符中最大值+1,因为文件描述符从0开始计数 readfds、writefds、exceptfds:可读、可写、异常事件对应的文件描述符集合。 timeout:超时时间,结构体中时间=0立即返回,NULL(一直阻塞) 返回值: >0:就绪文件描述符总数 =0:在超时时间内没有文件描述符就绪 =-1:失败,设置errno 如果在select等待期间,程序收到信号,select立即返回-1,errno=EINTR */ FD_ZERO(fd_set *fdset);//清除fdset所有位 FD_SET(int fd, fd_set* fdset);//设置fdset的位fd FD_CLR(int fd, fd_set* fdset);//清楚fdset的位fd int FD_ISSET(int fd, fd_set* fdset);//测试fdset的位fd是否被设置 struct timeval { long tv_sec; //秒数 long tv_usec; //微秒数 };
-
文件描述符就绪条件
//select读取带外数据 fd_set read_fds; fd_set exception_fds; FD_ZERO(&read_fds); FD_ZERO(&exception_fds); FD_SET(cfd, &read_fds); FD_SET(cfd, &exception_fds); int ret = select(cfd + 1, &read_fds, NULL, &exception_fds, NULL); if(ret < 0) { //error } if(FD_ISSET(cfd, &read_fds)) { ret = recv(cfd, buf, sizeof(buf) - 1, 0) if (ret <= 0) { break; } printf(); } //对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据 else if (FD_ISSET(cfd, &exception_fds)) { ret = recv(cfd, buf, sizeof(buf) - 1, MSG_OOB); if(ret <= 0) { break; } printf(); }
三 poll系统调用
-
用途
- 在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者
-
API
#include <poll.h> int poll(struct pollfd* fds, nfds_t nfds, int timeout); /* fds:指定所有我们感兴趣的文件描述符上发生的可读、可写、异常事件 nfds:指定被监听事件集合fds的大小 timeout:超时时间,毫秒。-1(poll阻塞)。0(poll立即返回) 返回值和select相同 */ struct pollfd { int fd; //文件描述符 short events; //注册的事件,一系列事件的按位或 short revents; //实际发生的事件,由内核填充 } //通常应用程序需要根据recv调用的返回值区分socket上接收到的是有效数据还是对方关闭连接的请求 //POLLRDHUP事件:socket接收到关闭连接请求时触发
四 epoll系列系统调用
-
API
#include <sys/epoll.h> int epoll_create(int size) /* size:内核事件表大小,只是一个提示 返回值:一个根节点,表示内核事件表 */ int epoll_ctl(int efd, int op, int fd, struct epoll_event* event); /* epfd:根节点,epoll_create的返回值 op:操作类型。EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL fd:需要操作的文件描述符 event:指定事件 返回值:0(成功),-1(失败,设置errno) */ struct epoll_event { _unit32_t events; //epoll事件 epoll_data_t data; //用户数据 } /* events:EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLET、EPOLLONESHOT */ typedef union epoll_data { void *ptr;//指定与fd相关的用户信息 int fd;//事件从属的目标文件描述符 uint32_t u32; uint64_t u64; }epoll_data_t; int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); /* maxevents:最多监听多少个事件 返回值:成功(返回就绪的文件描述符个数),-1(失败,设置errno) */ //epoll_wait如果检测到事件,就将所有就绪的事件从内核事件表(内核态)中复制到events中(用户态)
五 LT和ET模式
- LT模式
- 默认工作模式,epoll相当于一个效率较高的poll
- 当epoll_wait检测到有事件发生并将此事件通知应用程序后,**应用程序可以不立即处理该事件。**当应用程序下次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。
- ET模式
- EPOLLET
- 当epoll_wait检测到其上有事件发生并将该事件通知给应用程序后,**应用程序必须立即处理该事件,**因为后续的epoll_wait调用将不再向应用程序通知这一事件
- 因此ET模式比LT模式效率要高。因为ET模式很大程度上降低了同一个epoll事件被重复触发的次数。
- ET模式下每个文件描述符都应该是非阻塞轮询的,因为后面epoll不会再去通知该事件了
六 EPOLLONESHOT事件
- 一个线程读取完某个socket上的数据后开始处理这些数据,处理过程中,该socket上又有新数据可读(EPOLLIN再次被触发),此时另一个线程也来读取数据,就导致了两个线程操作同一个socket
- EPOLLONESHOT:一个socket连接在任一时刻都只被一个线程处理。
- 注册了EPOLLONESHOT事件的socket一旦被一个线程处理完,就应该立即重置该socket上的EPOLLONESHOT事件,却保下次能让其他工作线程有机会处理这个socket。