IO多路复用中的select,poll和epoll

IO多路复用

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

bitmap

bitmap使用bit来存一些不同的数字,如存1~9中的1,3,5,6,则bitmap只需要10位置,对应1,3,5,6位为1,即[1,0,1,0,1,1,0,0,0],这样可以节省空间。

select

文件描述符

在Linux系统中,所有进程都是文件,用文件描述符的形式表示。一个文件描述符可以对应一个连接。文件描述符的范围在32位系统中,最大值为1024个,而在64位系统中,最大值为2048个。可以调用下述命令查看

cat /proc/sys/fs/file-max

select指令

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
  • 参数
    nfds:文件描述符的最大值
    readfds:监视的文件描述符集(bitmap),以查看是否有数据可读取
    writefds:监视的文件描述符集(bitmap),以查看写操作是否将完成而不会阻塞。
    exceptfds:监视的文件描述符集(bitmap),以查看是否发生异常
    timeout:超时时间
  • 参数可以为NULL,在这种情况下,select()不会监视
  • 对select()的调用将一直阻塞,直到给定的文件描述符准备好执行I / O为止,或者直到经过可选的指定超时为止
  • 成功返回后,将修改对应的变化的集(置位)
    如果返回值为-1,表明发生了错误
    如果返回值为0,表明超时了
    如果返回值为正数,表明有n个fd准备就绪了
    `

实现IO多路复用

 for (i=0;i<5;i++)
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    //获得文件描述符的编号
    fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    //获得最大文件描述符的编号
    if(fds[i] > max)
    	max = fds[i];
  }
  
  while(1){
  	//rest置0
	FD_ZERO(&rset);	
  	for (i = 0; i< 5; i++ ) {
  		//根据文件描述符对rset置为
  		FD_SET(fds[i],&rset);
  	}
 	
 	// 内核会将有数据的rset置为
	select(max+1, &rset, NULL, NULL, NULL);
 
	for(i=0;i<5;i++) {
		//找到就绪(被置位)的文件描述符,进行IO操作
		if (FD_ISSET(fds[i], &rset)){
			memset(buffer,0,MAXBUF);
			read(fds[i], buffer, MAXBUF);
			puts(buffer);
		}
	}	
  }
  return 0;

分析:

  1. 判断IO是否有数据需要切换到内核态,因此每次select()需要对rset进行复制(如果不复制则用户态每次读取rset都要切换到用户态,更加频繁),开销很大。
  2. 每次循环都要对FSET置位,使得FSET不可重用。
  3. 每次需要O(n)的复杂度去检查rset中fds数组中哪一个对应的rset被置位了。
  4. 单个进程可监控的fd数量有限制。

poll

poll指令

struct pollfd {
      int fd; //文件描述符
      short events; 	//事件比如:读;写;读写
      short revents;
};```
```c	
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
  • 参数
    fds:文件描述符数组
    nfds:文件描述符的数量
    timeout:超时时间
  • 在poll函数中对就绪的文件描述符对应结构体的revent进行置位而不是event

实现IO多路复用

  for (i=0;i<5;i++) 
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    //获取文件描述符
    pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    //文件描述符(连接)的事件为读
    pollfds[i].events = POLLIN;
  }
  sleep(1);
  while(1){
  	puts("round again");
	poll(pollfds, 5, 50000);
 
	for(i=0;i<5;i++) {
		if (pollfds[i].revents & POLLIN){
			//置位,可复用
			pollfds[i].revents = 0;	
			memset(buffer,0,MAXBUF);
			read(pollfds[i].fd, buffer, MAXBUF);
			puts(buffer);
		}
	}
  }

分析:

  1. fds数组没有大小限制(和select的bitmap相比)。
  2. 置位操作只对revents,因此每次循环只需要恢复revents即可,可以复用。
  3. 依然无法解决select的用户态到内核态复制fds数组,以及O(n)的复杂度检查每一个文件描述符。

epoll

是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll接口

Epoll 系统调用在内核中创建和管理上下文,将任务分为3个步骤:

  1. 使用epoll_create在内核中创建上下文
  2. 使用epoll_ctl向/从上下文中添加和删除文件描述符
  3. 使用epoll_wait等待上下文中的事件

epoll_create

int epoll_create(int size);

  • 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。epoll_create会在内核建立一颗专门用来存放fd结点的红黑树,后续如果有新增的fd结点,都会注册到这个epoll红黑树上(提高了描述符集合注册和删除操作的效率)。

epoll_ctl

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

  • 参数
    epfd:epoll_create()的返回值
    op:操作,用三个宏表示:1)注册新的fd到epfd中;2)修改已经注册的fd的监听事件;3)从epfd中删除一个fd;
    fd:需要监听的文件描述符
    event:需要监听什么事件,用一个结构体表示。(和poll比没有revent)
  • select和poll会一次性将监听的所有fd都复制到内核中,而epoll不一样,当需要添加一个新的fd时,会调用epoll_ctr,给这个fd注册一个回调函数,然后将该fd结点注册到内核中的红黑树中。当该fd对应的设备活跃时,会调用该fd上的回调函数,将该结点存放在一个就绪链表中。

epoll_wait

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

  • 参数:
    epfd:epoll_create()的返回值
    event:需要监听什么事件,用一个结构体表示。(和poll比没有revent)
    maxevents:告之内核这个(所有event的个数)events的大小,不能大于创建epoll_create()时的size。
    timeout:超时时间
  • 返回结果是需要处理事件的个数,0表示超时。
  • epoll_wait的做法也很简单,其实直接就是从就绪链表中取结点,这也解决了轮询的问题,时间复杂度变成O(1)。

实现IO多路复用

  struct epoll_event events[5];
  int epfd = epoll_create(10);
  ...
  ...
  for (i=0;i<5;i++) 
  {
    static struct epoll_event ev;
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    // 将ev添加到epfd中,注册回调事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); 
  }
  
  while(1){
  	puts("round again");
  	// 返回需要处理事件的个数
  	nfds = epoll_wait(epfd, events, 5, 10000);
	
	for(i=0;i<nfds;i++) {
			memset(buffer,0,MAXBUF);
			read(events[i].data.fd, buffer, MAXBUF);
			puts(buffer);
	}
  }

工作模式(水平触发和边缘触发)

LT模式(默认):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

参考

select、poll、epoll之间的区别(搜狗面试)
浅谈select,poll和epoll的区别
【并发】IO多路复用select/poll/epoll介绍
https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XYD0TygzaUl

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值