select/poll epoll 函数 [本质上都是同步I/O]
//epoll是linux特有的,而select和poll是在POSIX中规定的,跨平台支持更好
对于socket连接很多,并且连接基本都是活跃的,select / poll 的性能与 epoll 是差不多的
当有大量的 idle-connection,epoll效率高。
//区别就在于活跃量,select/poll 遍历所有文件描述符,epoll管理活跃的文件描述符
同步I/O:都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的
异步I/O:则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间(需要使用mmap函数进行虚拟内存映射)
与多线程(TPC(Thread Per Connection)模型)和多进程(典型的Apache模型(Process Per Connection,简称PPC))相比,
I/O 多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。
1. select()函数 synchronous I/O multiplexing
//内核通过轮询方式对long类型的数组进行维护文件描述符
#include <sys/select.h>
int select(int nfds,
fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
第一个参数:要监视的文件描述符的范围,取监视的描述符数的最大值+1,在Linux上最大值一般为1024。
第二个参数:监视的可读描述符集合,只要有文件描述符读操作准备就绪,会自动修改地址内容。
第三个参数:监视的可写描述符集合。
第四个参数:监视的错误异常描述符集合。
以上三个参数若不需要使用,直接设为 NULL
第五个参数: 超时时间,告知内核等待所指定描述字中的任何一个就绪可花多少时间。
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:成功:返回就绪文件描述符数量(3个描述符集合)
超时返回 0
失败返回-1
函数功能:
监视并等待多个文件描述符的属性变化 (读 写 错误异常)
与select()搭配使用的宏定义
//清空集合
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);
三种可能场景:
永远等待下去:timeout 设置为空指针 NULL,且没有一个描述符准备好。
等待固定时间:timeout 设置为某个固定时间,描述符属性有变化返回。超时描述符属性未有变化,返回 0
不等待(不阻塞):timeout 设置为 0 分 0 微妙,描述符属性有变化返回。描述符属性未有变化,返回 0
2. poll()函数 wait for some event on a file descriptor
//与select()类似,内核通过链表进行维护,理论上对文件描述符的最大数量没有限制,但是数量过大后性能也是会下降
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
第一个参数:被监控的文件描述符的属性,通过传递多个结构体,来实现对多个文件描述符进行监控
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
}
fd:被监视的文件描述符,
events:设置文件描述符的属性,通过以下参数进行位或运算进行设置
POLLIN - 普通或优先带数据可读
POLLRDNORM - 普通数据可读
POLLRDBAND - 优先级带数据可读
POLLPRI - 高优先级数据可读
POLLOUT - 普通或优先带数据可写
POLLWRNORM - 普通数据可写
POLLWRBAND - 优先级带数据可写
revents:返回文件描述符的属性变化,通过读取该变量来查看文件描述符的变化
events的设置参数也适用于revents
revents特有的错误事件数值
POLLERR - 发生错误
POLLHUP - 发生挂起
POLLNVAL - 描述符不是打开的文件
第二个参数:用来指定第一个参数的结构体数量
第三个参数:指定等待的毫秒数
-1 永远等待,直到事件返回
0 立即返回
>0 指定的毫秒数
函数功能:
监视并等待多个文件描述符的属性变化
3. epoll()函数 I/O event notification facility
//内核将epoll中的事件与设备网卡驱动程序建立回调关系,当相应的事件发生时会调用这个回调方法。
//这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
#include <sys/epoll.h>
/* open an epoll file descriptor */
1. int epoll_create(int size);
参数:监听的数量,size > 0
函数功能:
生成 epoll 专用的文件描述符
/* control interface for an epoll descriptor */
2. int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
第一个参数:epoll描述符,epoll_create()的返回值
第二个参数:对第三个参数fd进行操作,通过以下宏定义的方式
EPOLL_CTL_ADD - 注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD - 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL - 从 epfd 中删除一个 fd;
第三个参数:需要监听的fd
第四个参数:描述链接文件描述符fd对象的事件
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events 通过以下参数进行位或运算进行设置
EPOLLIN :对应的文件描述符可读
EPOLLOUT:对应的文件描述符可写
EPOLLPRI:对应的文件描述符有紧急的数据可读
EPOLLERR:对应的文件描述符发生错误
EPOLLHUP:对应的文件描述符被挂起
EPOLLET :将EPOLL设为边缘触发(Edge Trigger)模式,
默认为水平触发(Level Trigger)
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,
需要再次把这个socket加入到EPOLL队列里
函数功能:
操作 EPOLL 文件描述符
/* wait for an I/O event on an epoll filedescriptor */
3. int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
第一个参数:epoll描述符,epoll_create()的返回值
第二个参数:分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中
(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)
第三个参数:event的个数
第四个参数:超时时间,毫秒
设为-1,将阻塞
函数功能:
等待事件的产生,收集在 epoll 监控的事件中已经发生的事件
水平触发(Level Trigger)
LT 模式:支持block和no-block socket。
当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。
下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
效率会低于ET触发,尤其在大并发,大流量的情况下。
但是LT对代码编写要求比较低,不容易出现问题。
LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
边缘触发(Edge Trigger)
ET 模式:只支持no-block socket。
当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。
如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
该模式效率非常高,尤其在高并发,大流量的情况下,会比LT少很多epoll的系统调用。
但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。