IO多路复用(select、poll、epoll)

一直都对IO多路复用搞不清楚,写篇文章将所看到的内容记录一下,防止遗忘。

IO多路复用

IO多路复用就是使用内核机制来轮询一组文件描述符,监视这写fd是否有IO事件发生,如果有IO发生程序会被告之。
IO 多路复用的方式主要有 select、poll、epoll,这三个函数都会进行阻塞,所以可以放在 while(true)循环里使用,不会造成 CPU 的空转

select

select()系统调用提供了一种实现同步多路复用的机制

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 只有一个函数,调用 select 时,需要将监听句柄和最大等待时间作为参数传递进去,select 会发生阻塞,直到一个事件发生了,或者等到最大 1 秒钟(tv 定义了这个时间长度)就返回。
select方法的参数

  • 读文件描述符集合、写文件描述符集合、异常描述符集合、超时时间

  • 一般更关心文件读操作,所以其他位置可以置为null,第二个参数一般是引用一个bitmap用来代表哪些文件描述符是被监听的。

select源码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
 
#define MAXBUF 256
 
void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd,num=1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 
  connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
 
  printf("child {%d} connected \n", getpid());
  while(1){
        int sl = (random() % 10 ) +  1;
        num++;
     	sleep(sl);
  	sprintf (msg, "Test message %d from client %d", num, getpid());
  	n = write(sockfd, msg, strlen(msg));	/* Send message */
  }
 
}
 
int main()
{
  char buffer[MAXBUF];
  int fds[5];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n,i,max=0;;
  int sockfd, commfd;
  fd_set rset;
  for(i=0;i<5;i++)
  {
  	if(fork() == 0)
  	{
  		child_process();
  		exit(0);
  	}
  }
 
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof (addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));
  listen (sockfd, 5); 
 
  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){
	FD_ZERO(&rset);
  	for (i = 0; i< 5; i++ ) {
  		FD_SET(fds[i],&rset);
  	}
 
   	puts("round again");
	select(max+1, &rset, NULL, NULL, NULL);
 
	for(i=0;i<5;i++) {
		if (FD_ISSET(fds[i], &rset)){
			memset(buffer,0,MAXBUF);
			read(fds[i], buffer, MAXBUF);
			puts(buffer);
		}
	}	
  }
  return 0;
}

源码中比较重要的就是main部分,上半部分主要做了两件事:

  • 创建socket客户端

  • 创建了5个文件描述符,并把这五个文件描述符放入了数组中。

select函数的执行流程:

  • select是阻塞函数,当没有数据时,会一直阻塞在select那一行。

  • 当有数据时将rset中对应的那一位置为1

  • select函数返回,不再阻塞

  • 遍历文件描述符数组,判断哪个fd被置位了。

  • 读取数据,然后处理

select函数的缺点:

  • bitmap默认大小为1024,虽然可以调整,但还是有限度的。

  • rset每次循环都必须重新置位为0,不可重复使用。

  • 尽管将rset从用户态拷贝到内核态,由内核态判断是否有数据,但是还是有拷贝的开销。

  • 当有数据时select就会返回,但是select函数并不知道哪个文件描述符有数据了,后面还需要再次对文件描述符数组进行遍历。效率比较低。

poll

与select()不同,它具有低效的三个基于位掩码的文件描述符集,poll()使用单个nfds pollfd结构数据。原型更简单:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

poll的参数:

  • 自定义的结构体数组

  • 数组的长度

  • 超时时间

pollfd 结构对事件和返回事件有不同的字段,因此不需要每次都创建它

struct pollfd {
      int fd;
      short events; 
      short revents;
};
  • fd: 文件描述符

  • events:在意的事件是什么,如果在于读就是pollin,如果在意写就是pollout

  • revents:对events的回馈,开始时为0,当有数据可读时就置为pollin,类似于select的rset

将上面示例更改为使用poll:

  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);
		}
	}
  }

就像对 select 所做的那样,我们需要检查每个 pollfd 对象以查看其文件描述符是否已准备好,但不需要每次迭代都构建集合
poll的执行流程:

  • 将5个fd从用户态拷贝到内核态

  • poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置位pollin

  • poll方法返回

  • 循环遍历,查找哪个fd被置位为pollin了

  • 将revents重新位置0,便于复用

  • 对置位的fd进行读取和处理

相比于select解决了bitmap大小限制问题和rset不可重用的情况。

epoll

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

  • 使用 epoll_create 在内核中创建上下文

  • 使用 epoll_ctl 在上下文中添加和删除文件描述符

  • 使用 epoll_wait 等待上下文中的事件

将上面的代码示例改为epoll

  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;
    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);
	}
  }

epoll的执行流程:

  • 当有数据的时候,会把相应的文件描述符"置位",但是epoll没有revent标志位,所以并不是真正的置位。这时候会把有数据的文件描述符放到队首。

  • epoll会返回有数据的文件描述符的个数

  • 根据返回的个数 读取前N个文件描述即可

  • 读取、 处理

Epoll 、select 和 poll

  • 可以在等待时添加和删除文件描述符

  • epoll_wait 仅返回具有就绪文件描述符的对象

  • epoll 具有更好的性能 O(1) 而不是 O(n)

  • epoll 可以表现为水平触发或边缘触发

  • epoll 是特定于 Linux 的,因此不可移植

posted @ 2022-04-19 19:20  YoungerWb  阅读(184)  评论(0编辑  收藏  举报