I/O-----多路复用-----select,poll,epoll讲解

必知部分

我们都知道Linux下一切皆文件,如果要对数据进行操作,那么我们就需要打开对应的文件,文件描述符为一个非负整数,每个进程在自己的PCB中存放了一张文件描述符表,而文件描述符就是这个表的索引,用于寻找对应的文件。

fd:文件描述符。0代表标准输入,1代表标准输出,2代表标准错误。

select()

select()函数主要任务就是用于监听多个文件描述符的属性的变化。select()监听的文件操作符有(可读操作符,可写操作符,异常操作符),调用select()后会阻塞在这里,直到有事件发生(即可读、可写、异常事件或者超时),当有事件发生后,我们通过遍历fdset集合来找到有事件发生的文件并对其进行处理。

nfds:要监视的文件描述符的范围,Liunx上最大值为1024个。

优点
几乎在所有的平台都能够使用,其跨平台性算是一个优点。

缺点
1.每次调用select()函数时,他都需要将fd集合从用户态拷贝一份到内核态,这个开销fd数量越多就越大,同时每次调用select(),都需要遍历fd集合来执行IO操作(时间复杂度O(n) )。

2.单个线程所能够监听的文件描述符数量很少(Linux最大只有1024个),如果我们通过修改宏定义并且重新编译内核,但是这只能解决表面问题,但是无法解决根本问题。

poll()

poll()函数和select类似,只不过poll()使用的链表进行存储(即无文件描述符的数量限制,但是数量过多也会导致性能降低),但是和select()一样,都需要将fd集合在用户态和内核态进行来回复制,随着文件描述符的增大,开销也会越来越大。(时间复杂度仍然是O(n))

poll()函数使用了结构体pollfd

struct pollfd{

int fd;         /* 文件描述符 */

short events;   /* 等待的事件 (或者说可能会发生的事件)*/

short revents;  /* 实际发生了的事件 */

}; 

当poll()调用后返回值 > 1,说明有时间发生,返回值为结构体集合中revents不为0的结构体。(我们知道只知道数量,但是无法知道究竟是那些文件描述符对应的结构体发生了事件,所以仍然需要遍历集合)

当poll() 返回值 = 0,代表无事件发生。

当poll()返回值 = - 1,代表出现异常。

select()与poll()类似,区别就在于select()使用的是fd_set结构保存文件描述符,poll()使用的是pollfd结构体存储。

epoll()

这是select()和poll()的升级版本,监听多个文件描述符用于判断是否需要进行I/O,并且使用mmap映射(类似于公共内存),使得用户态和内核态之间不需要来回拷贝。

int epoll_create(int maxfds)

创建一个epoll的句柄,maxfds用来告诉内核这个监听的数目一共有多大,返回值为epoll的描述符。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epoll用于注册,修改,删除文件描述符和其对应的事件 ,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据error错误码判断错误类型。

它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

epoll_wait()方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。

第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

 1 typedef union epoll_data {
 2     void *ptr;
 3     int fd;
 4     __uint32_t u32;
 5     __uint64_t u64;
 6 } epoll_data_t;
 7 
 8 struct epoll_event {
 9     __uint32_t events; /* Epoll events */
10     epoll_data_t data; /* User data variable */
11 };
 

events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待给定epoll实例关联的文件描述符上的事件。

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

第1个参数 epfd是 epoll的描述符。

第2个参数 events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

第3个参数 maxevents表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。

第4个参数 timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。

返回值为nfd,是发生事件对应的文件描述符,这里epoll和poll与select区别就出现了,epoll他会进行内排,将发生事件的fd排在最前面,这样我们直接遍历[0,nfd -1]的文件描述符即可完成对事件的处理,使得时间复杂度从(O(N) 降到了O(1) )。

epoll的工作模式

主要分为LT(水平触发)和ET(边缘触发)。
epoll默认使用的是LT模式,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。

LT:只要有一个事件对应的套接字缓冲区还有数据,那么就可以从epoll_wait中获取到该事件,细致点说就是电平的切换,当低电平到高电平时代表有数据需要读取,而且我们知道当数据没有被完全读取电平是不会改变的,所以下次判断是否有事件发生时,该文件描述符仍然会被读取。

ET:如果一个事件在调用epoll_wait后被获取时没有读取完,那么如果下次这个事件对应的套接字没有新事件发生,那么在此调用epoll_wait是无法获取的这个事件的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值