概述
简单介绍
这三种模式是为了在一个线程下面尽可能多的管理很多个socket。这三种模式在不同的需求下,有着各自的优势,select和poll差不多,但并不一定是说epoll是最好的。比如我们就写个简单的几个client连接下的情况,这个时候用select就很香,代码更少一点,如果是在真实的生产环境下,肯定是用epoll好。
select
select管理多个socket的fd(文件描述符,通过这个可以找到socket),在这期间select会监听所有的socket。如果没有一个socket有事件发生,这个线程会让出cpu的阻塞等待,即让该线程可以去做别的事情。如果使用普通的conncet()、accept()、recv()或recvfrom()这种函数,如果没有事件发生,就必须要阻塞,直到事件发生。
如果有事件发生,select会在他的睡眠队列里设置一个entry,当 socket 接收到网卡的数据后,就会去它的睡眠队列里遍历 entry,调用 entry 设置的 callback 方法,这个 callback 方法里就能唤醒 select !
select函数原型:
#include <sys/select.h>
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval * timeout);
参数介绍:
- nfds:委托给内核处理的后面三个fd_set的最大值+1,也是遍历的时候的最大值。
- readfds:传入内核处理文件描述符的读集合,内核只需要检测这些文件描述符的读缓冲区。
- 传入传出参数
- writefds:传入内核处理文件描述符的读集合,内核只需要检测这些文件描述符的写缓冲区。
- 传入传出参数
- exceptfds:文件描述符的集合,内核检测集合中文件描述符是否有异常状态
- 传入传出参数
- timeout:设置超时的时间,可以强制操作select()函数的阻塞状态。
- NULL:没有事件就会一直阻塞。
- 秒数:等待时间。
- 0:不等待。
返回值:
- -1:函数调用失败了
- 0:监听的所有的socket_fd里面没有发生事件的socket_fd。
- >0:返回已经就绪的socket_fd。
与fd_set配合的操作函数:
// 将文件描述符fd从set中删除,即把set中fd对应的标志位置0
void FD_CLR(int fd, fd_set* set);
// 将文件描述符fd添加到set中,即把set中fd对应的标志位置1
void FD_SET(int fd, fd_set* set);
// 判断文件描述符fd是否在set中,即判断文件描述符fd在set中的标志位是否为1
void FD_ISSET(int fd, fd_set* set);
// 清空set,即全部置0,用于初始化set
void FD_ZERO(fd_set* set);
注:fd_set 是一个128个字节的数组(位图Bitmap),128*8=1024 bit,在操作fd_set的时候,实际上操作的是每一位bit,置0还是置1,与内核中的文件描述符表的每一位都对应。
- 0代表不检测这个文件描述符的状态(读缓冲区,写缓冲区等)
- 1代表检测这个文件描述符的状态(读缓冲区,写缓冲区等)
从这里可以看出select的缺点:
- 具有O(n)的无差别轮询复杂度,需要一直遍历1024位fd_set的各个位的状态
- 每次select都需要将fd_set拷贝到内核空间,开销比较大
- 需要轮询fd_set,消耗时间多
- 能够检测的最大文件描述符是1024个,这个在内核写死了
poll
相对于【select】,它不使用【Bitmap】来保存已经连接的文件描述符,使用的是链表来管理,没有了1024的限制,当然还会受到系统文件描述符限制。
poll的函数原型:
#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 -> 传出 */
};
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数介绍:
- fds: 这是一个 struct pollfd 类型的数组,里边存储了待检测的文件描述符的信息,这个数组中有三个成员:
- fd:委托内核检测的文件描述符
- events:委托内核检测的 fd 事件(输入、输出、错误),每一个事件有多个取值
- revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
- 读事件:POLLIN
- 写事件:POLLOUT
- 错误事件:POLLERR
- nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数 1 数组的元素总个数)
- timeout: 指定 poll 函数的阻塞时长
- -1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
- 0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
- 大于 0:阻塞指定的毫秒(ms)数之后,解除阻塞
函数返回值:
- 失败: 返回 - 1
- 成功:返回一个大于 0 的整数,表示检测的集合中已就绪的文件描述符的总个数
操作函数:
- 只需要poll函数:revent中包含了这个文件描述符的事件
从上面可以总结出poll的特点:
- 对比select,使用起来更加方便(将相关的文件描述符封装到了pollfd中),但是不能跨平台,只能在Linux平台
- events参数对应select中的readfds、writefds、exceptfds的传入状态
- revents参数对应select中的readfds、writefds、exceptfds的传出状态
- 与select相差不大,需要的时间复杂度一样
epoll
epoll的操作函数:
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
int epoll_create1(int flags);//flag可以是::EPOLL_CLOEXEC
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
在select/poll中低效的原因之一是它们将“添加/修改任务列表”与“等待任务列表”放在一起处理。在大部分情况下,需要监听的socket比较确定,这个时候检测去任务列表是多余的。epoll将两个步骤分开,epoll_ctl用来管理任务列表,并且使用的是红黑树结构,这种结构有利于增加、删除或修改,然后用epoll_wait检测任务列表(文件描述符)是否有事件发生,这样实现了解耦,效率大大提高。
epoll_create(int size):
- 参数:一般指定为大于0的数就可以
- 返回值:
- 失败:-1,创建错误
- 成功:一个文件描述符,通过这个就可以访问epoll实例
- 用法:int epfd = epoll_create(100);
epoll_create1(int flags):
- 参数:flags 可以设置为0 或者EPOLL_CLOEXEC,为0时函数表现与epoll_create一致,设置为EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭文件描述符。
- 返回值:与epoll_create类似
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):
- 参数:
- epfd是epoll_crate的返回值,操作epoll实例
- op是一个枚举值,控制函数执行什么类型的操作
- EPOLL_CTL_ADD:添加一个fd
- EPOLL_CTL_DEL:删除一个fd
- EPOLL_CTL_MOD:修改一个fd
- fd是文件描述符,即要添加 / 修改 / 删除的文件描述符
- event是epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
- events:委托 epoll 检测的事件
- EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
- EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
- EPOLLERR:异常事件
- data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
- events:委托 epoll 检测的事件
看一下epoll_ctl的epoll_event结构体:
// 联合体, 多个变量共用同一块内存
typedef union epoll_data {
void *ptr;
int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- 函数参数:
- epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
- events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
- maxevents:修饰第二个参数,结构体数组的容量(元素个数)
- timeout:如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
- 0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
- 大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
- -1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
- 函数返回值:
- 成功:
- 等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
- 大于 0:检测到的已就绪的文件描述符的总个数
- 失败:返回 - 1
- 成功:
epoll的两种模式 LT 和 ET
二者的差异在于 level-trigger 模式下只要某个socket处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。
所以, 在epoll的ET模式下, 正确的读写方式为:
读: 只要可读, 就一直读, 直到返回0, 或者 errno = EAGAIN
写: 只要可写, 就一直写, 直到数据发送完, 或者 errno = EAGAIN
Wikipedia:https://en.wikipedia.org/wiki/Epoll
Man手册:epoll(7) - Linux manual page
Epoll详细解释系列:
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (1) - 知乎
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (2) - 知乎
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (2) - 知乎
IO多路转接(复用)之epoll | 爱编程的大丙
最后
以上就是生动鲜花为你收集整理的I/O多路复用(select/poll/epoll)的全部内容,希望文章能够帮你解决I/O多路复用(select/poll/epoll)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复