多路转接IO之select,poll,epoll

文章目录

  • select
  • poll
  • epoll

什么是多路转接IO
用来实现大量描述符的就绪事件监控,可以在程序中能够只针对就绪的描述符进行IO操作,提高效率,避免对没有就绪的描述符进行操作导致的阻塞,避免对大量没有就绪的描述符进行操作带来的效率降低
IO就绪事件:IO事件的就绪(可读/可写/异常)

  • 可读事件:一个描述符当前是否有数据可读,
  • 可写事件:一个描述符当前是否可以写入数据(一般对于socket来说)
  • 异常事件:一个描述符是否发生了某些异常(比如连接断开,挂起之类)

select

select系统调用是用来让我们的程序监视多个文件句柄的状态变化的,程序会在这里等待,一直到被监视的文件句柄发生了改变(可以是一个也可以是多个)

select函数原型

#include
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)
//nfds: 需要监视的最大文件描述符值+1,为了减少内核中的遍历次数
//readfds: 可读文件描述符的集合
//writefds:可写文件描述符的集合
//exceptfds:异常文件描述符的集合
//timeout: 这个参数有三种
//1.NULL:表示select()没有timeout,select将一直阻塞,直到某个文件描述符发生事件再返回
//2.0:检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
//3.特定的时间值:如果在指定的时间段里没有描述符就绪发生,select将超时返回
//返回值:返回值<0表示监控出错,返回值=0表示没有描述符就绪,返回值>0,表示就绪的描述符个数

fd_set结构
这个结构实际上是一个位图,用位图中的为表示需要监控的文件描述符,并且提供了一组操作fd_set的接口

void FD_CLR(int fd,fd_set *set);//清除描述词组set中相关fd的位
int FD_ISSET(int fd,fd_Set *set);//测试描述词组set中相关fd的位是否为真
void FD_SET(int fd,fd_set *set);//设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set);//清除描述词组set的全部位

select使用流程

  • 1.定义想要监控的事件的描述符集合,初始化集合,将需要监控这个事件的描述符添加到集合中
  • 2.开始监控,将集合拷贝到内核中进行监控,进行轮询遍历判断,若有描述符就绪或者超时,则 调用返回,返回之前将集合中没有就绪的描述符移除,select会在返回之前将所有集合中没有就绪的描述符都从集合中移除出去,这样再调用返回之后,集合中剩下的就是已经就绪的描述符
  • 3.轮询判断那个描述符还在集合中,确定那个描述符就绪了那个事件,进而进行响应的操作

select中的集合:fd_set结构体,结构体中只有一个成员,就是一个数组,这个数组用来做位图
描述符是一个数字,添加描述符就是这个数字对应的比特位置为1,表示此描述符被添加到集合中
select最多可以监控多少描述符:取决于宏_FD_SETSIZE,默认1024

select优缺点分析
优点

  • 1.跨平台移植性比较强
  • 2.监控的超时事件控制比较精细

缺点

  • 1.所能监控的描述符数量有上限限制,取决于宏 _FD_SETSIZE,默认为1024
  • 2.每次监控都需要将集合向内核中拷贝,因为每次返回都会将集合内未就绪描述符移除,所以每次监控都需要重新向集合中添加描述符
  • 3.监控原理是在内核中轮询遍历,性能会随着描述符的增多而下降
  • 4.会修改描述符集合,每次监控需要重新添加描述符
  • 5.无法直接交付就绪描述符,只返回就绪集合,需要遍历就绪集合才能直到那些描述符就绪

poll

poll解决了select的文件描述符上限的问题,然后相较于select做了一些优化,模型差不多

poll函数模型

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//fds:要监控的描述符事件结构体,nfds:实际上第一个参数描述符事件的结构体数量,timeout:超时等待时间(ms)

//fds结构体
struct pollfd{
      int fd;//要监控的描述符
      short events;//描述符想要监控的事件,按位操作,是一个监控指定事件的标志位
      short revents;//实际就绪的事件
}
//events:POLLIN:可读,POLLOUT:可写
//revents:当poll接口调用返回时,这个描述符实际就绪的事件就会写入revents中,可以通过此成员进行判断就绪的描述符
//返回值:返回值>0表示就绪的描述符个数,返回值=0表示监控超时,返回值<0表示监控出错

poll使用流程

  • 1.定义描述符事件结构体数组,将需要监控的描述符以及对应的事件信息填充到数组中
struct pollfd fds[10]; 
fds[0].fd = 0;//标准监控输入
fds[0].events = POLLIN | POLLOUT;//对标准输入监控可读事件以及可写事件
  • 2.发起监控调用poll,将数组中数据拷贝到内核中进行轮询遍历监控,有描述符就绪或等待超时后返回,返回时将这个描述符就绪的事件填充到对应的结构体的revents中(若描述符没有就绪,则revents中数据为0)
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
  • 3.监控调用返回后,遍历数组中每个节点的revents,根据当前节点的描述符就绪了什么事件而进行什么操作

poll优缺点
优点

  • 1.poll可以通过描述符事件结构体的方式简化了select的三种描述符集合的操作流程
  • 2.poll所能监控的描述符,没有最大数量限制,需要监控多少描述符就定义多大数组即可
  • 3.poll每次监控不需要重新定义事件结构体
    缺点
  • 1.监控原理是在内核中进行轮询判断,会随着描述符的增多而导致性能下降
  • 2.无法跨平台移植
  • 3.每次监控调用返回后需要进行遍历判断才知道那个描述符就绪了什么事件
  • 4.每次监控需要将数组拷贝到内核进行监控
  • 5.监控的超时等待时间单位为ms

epoll

epoll是Linux下性能最高的多路转接模型
根据man手册,epoll是在poll的基础上,为了实现处理大批量操作句柄而在poll的基础上改进的

epoll流程

  • 1.在内核中创建eventpoll结构体(使用完毕之后需要调用close()关闭),返回一个描述符作为代码中的操作句柄
int epoll_create(int size);
//size: 要监控的描述符最大数量,在linux2.6.8后被忽略,不再有最大数量限制,大于0即可
  • 2.对需要监控的描述符组织事件结构体,将描述符以及对应事件结构添加到内核的eventpoll结构体中
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
//epfd:epoll操作句柄()epoll_create()的返回值)
//op: 取值有三个
//EPOLL_CTL_ADD:添加新的描述符到epfd
//EPOLL_CTL_MOD:修改注册的描述符的监听事件
//EPOLL_CTL_DEL:从epfd中删除一个描述符
//fd:epoll操作句柄,需要监听的fd

//event:监控描述符对应的事件结构体信息
struct epoll_event{
    uint32_t events;//表示要监控的事件,以及监控调用返回后实际就绪的事件(EPOLLIN/EPOLLOUT)
    union epoll_data{
        int fd;//描述符
        void* ptr;//当ptr中包含描述符时,可以直接操作
    }data;
}
  • 3.开始监控,当有描述符就绪或者等待超时之后监控调用返回
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
//epfd:epoll的操作句柄,通过这个句柄找到内核中指定的eventpoll结构
//events:epoll_event描述符的事件结构体数组的首地址,用于获取就绪的描述符对应的事件结构体,不能为空指针
//maxevents:events数组的节点数量,主要为了防止就绪太多,向events中放置的时候越界访问
//timeout:超时等待时间,单位为ms,-1表示永久阻塞,0则立即返回
//返回值:大于0表示就绪的描述符个数,等于0表示等待超时,小于0表示监控出错

epoll监控原理

  • epoll底层通过红黑树实现的,红黑树中存放着文件描述符和其关心的事件,以文件描述符作为key值,然后底层还有一个就绪链表,这个链表的作用是,当内核检测到红黑树中有事件就绪时,产生中断,然后调用回调函数,将该节点中的就绪事件添加到就绪链表中
  • 同步阻塞操作,发起调用之后,监控由系统完成,系统为每一个描述符做了事件回调函数,当描述符就绪时,回调函数就会把描述符的对应事件信息添加到一个链表中,进程发起调用之后,每间隔一段时间就看一下epoll_event中的双向链表是否为NULL确定是否有描述符就绪(不进行遍历判断,而是根据这个回调函数将就绪对应事件放到链表中,所以只判断有没有就绪的就可以,所以不会因为描述符的增多而导致性能下降

epoll优缺点
优点

  • 1.监控的描述符没有数量上限
  • 2.所有的描述符事件信息只需向内核拷贝一次
  • 3.监控采用异步阻塞,性能不会随着描述符的增多而降低
  • 4.返回就绪的描述符事件信息,可以直接对就绪的描述符进行操作,不需要遍历
    缺点
  • 1.无法跨平台
  • 2.监控时间单位最小为ms

epoll的事件触发模式
水平触发(默认触发方式):EPOLLLT(select和poll只有这种触发方式)
触发方式

  • 可读事件:接收缓冲区中的数据大小大于低水位标记
  • 可写事件:发送缓冲区的剩余空间大小大于低水位标记

边缘触发:需要在epoll_event结构体中的events成员设置EPOLLET,有新数据到来会触发事件

  • 可读事件:不关注接收缓冲区中是否有数据,每次有新数据来时才触发一次,所以每次有新数据到来时最好将所有数据一次性读完,否则epoll的边缘触发不会触发第二次事件,第二次新数据来时会触发
  • 可写事件:剩余空间只有从无到有时会触发

水平触发和边缘触发那个好?
根据不同场景使用不同的触发方式

  • 假设现在需要接收一条数据,但是发现缓冲区中的数据不完整,此时 如果要读取数据,就需要额外的维护,等到下一条数据过来(因为需要通过下一条数据来触发事件),进行读取补全数据,而如果因为数据不完整,不将这个数据读出来,则水平触发会一直触发事件,但是此时因为数据不完整,又不能读取,这种情况下使用边缘触发会好一点,因为有新数据到来就会满足边缘触发进而触发事件

你可能感兴趣的:(网络基础,Linux,epoll)