IO多路复用

1 什么是IO多路复用

IO多路复用(IO Multiplexing)一种同步IO模型,单个进程/线程可以同时处理多个IO请求。一个进程/线程可以监视多个文件描述符;一旦某个文件描述符就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个进程/线程。
  一个进程/线程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程/线程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。

1.1 为什么会出现IO多路复用

在网络编程中,服务端监听端口时的花费很大,有两种模式:同步阻塞(BIO)和同步非阻塞(NIO)。

1.1.1 同步阻塞(BIO)

服务端采用单线程,当accept一个请求后,若在recv和send调用时阻塞,将无法accept其他客户端请求,必须等待当前请求处理完成,无法处理并发。

单线程阻塞

//单线程阻塞
while(true)
{
	int client_fd = accept(listenfd, &client_addr, 0);
	if(client_fd) {
		fds.append(client_fd);
	}

	for (fd in fds) {
    // recv阻塞(会影响上面的accept)
    if (recv(fd)) {
      	// logic
    }
  }  
}

多线程阻塞
服务器端采用多线程,当accept一个请求后,使用一个新的线程进行recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写事件的线程数不会超过20%,每次accept后都开一个线程也是一种资源浪费

//多线程阻塞
while(true)
{
	int client_fd = accept(listenfd, &client_addr, 0);
	//开启线程recv数据
	// recv阻塞,服务端可以再次进入accept流程
	if(recv(fd)) {
		//process
	}
}

1.1.2 同步非阻塞

服务器端当accept一个请求后,加入fds集合,每次轮询一遍fds集合recv(非阻塞)数据,没有数据则立即返回错误,每次轮询所有fd(包括没有发生读写事件的fd)会很浪费cpu。

while(1) {
  // accept非阻塞(cpu一直忙轮询)
  client_fd = accept(listen_fd)
  if (client_fd != null) {
    fds.append(client_fd)
  }  
  for (fd in fds) {
    // recv非阻塞
    setNonblocking(client_fd)
    if (len = recv(fd) && len > 0) {
    }
  }  
}

1.2 现在的做法IO多路复用

服务端采用单线程进行select/poll/epoll等系统调用获取活动fd列表,遍历有事件的fd进行accept/recv/send, 使得单线程处理并发。
只在调用select、poll、epoll时阻塞,accept/recv不阻塞,这样既能避免线程一直处于cpu忙的状态,还能保证及时获取到活动的文件描述符

while(1) {
  // 通过内核获取有读写事件发生的fd,只要有一个则返回,无则阻塞
  // 整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,accept/recv是不会阻塞
  for (fd in select(fds)) {
	    if (fd == listen_fd) {
	        client_fd = accept(listen_fd)
	        fds.append(client_fd)
	    } elseif (len = recv(fd) && len != -1) { 
	      	// logic
	    }
  }  
}

1.2.1 select

//select函数接口
#include <sys/select.h>
#include <sys/time.h>

#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)

// 数据结构 (bitmap)
typedef struct {
    unsigned long fds_bits[__FDSET_LONGS];
} fd_set;

// API
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout
);// 返回值就绪描述符的数目

FD_ZERO(int fd, fd_set* fds)   // 清空集合
FD_SET(int fd, fd_set* fds)    // 将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds)  // 判断指定描述符是否在集合中 
FD_CLR(int fd, fd_set* fds)    // 将给定的描述符从文件中删除  
//selec使用示例
int main() {
  /*
   * 这里进行一些初始化的设置,
   * 包括socket建立,地址的设置等,
   * int listenfd = sockeet(AF_INET, ...);
   */
 
  fd_set read_fs, write_fs;
  struct timeval timeout;
  int max = listenfd ;
  int nfds = 0; // 记录就绪的事件数,可以减少遍历的次数
  FD_ZERO(&read_fs);
  FD_ZERO(&write_fs);
  FD_SET(listenfd, &read_fs);

  while (1) {
	    // 阻塞获取,每次需要把fd从用户态拷贝到内核态
	    fd_set tmp1 = read_fs;
  		fd_set tmp2 = write_fs;
	    nfds = select(listenfd + 1, &tmp1, &tmp2, NULL, &timeout);
	    for (int i = 0; i <= max && nfds; ++i) {
	          if (listenfd == i) {
		          int client = accept(listenfd, NULL, NULL, NULL);
		          FD_SET(client, &read_fd);
		          max = clientfd>max? clientfd: max;
		      }
		      if (FD_ISSET(i, &tmp1) && i != listenfd) {
		          // 这里处理read事件
		          char buf[1024];
		          int len = recv(i,buf,sizeof(buf),0);
		          if(0 == len)
		          {
		          	 FD_CLR(i, &read_fs);
		          	 close(i);
		          }
		          else
		          {
		          	printf("%s\n",buf);
		          }
		      }
		      if (FD_ISSET(i, &tmp2) && i != listenfd) {
		         // 这里处理write事件
		      }
		      --nfds;
	    }
  }

缺点:

  1. 单个进程打开的fd有限,通过FD_SETIZE设置,默认1024,且文件描述符的值最大就只能达到这么大
  2. 每次调用select都需要memset
  3. select后,需要通过遍历的方式才能确定活动的文件描述符,开销太大。
  4. 活动的文件描述符数组需要从用户态传进内核态,也有不小的消耗。

1.2.1 select的并发

主线程select,子线程执行recv.

#include <sys/select.h>
#include <pthread.h>

typedef struct fdinfo
{
	int fd;
	int *maxfd;
	fd_set* rdset;
};
pthread_mutex_t mutex;

void* acceptconn(void* args)
{
	fdinfo* fdinfo = (fdinfo*)(args);
	int cfd= accept(fdinfo->fd, NULL, NULL);
	pthread_mutex_lock(&mutex);
    FD_SET(cfd, fdinfo->rdset);
    *fdinfo->max = cfd>*fdinfo->max? cfd: *fdinfo->max;
    pthread_mutex_unlock(&mutex);
    free(fdinfo);
    return NULL;
}

void* recv(void* args)
{
	fdinfo* fdinfo = (fdinfo*)(args);
    char buf[1024];
     int len = recv(fdinfo->fd, buf,sizeof(buf),0);
     if(-1 == len)
     {
     	free(info);
     	printf("error\n");
     	exit(1);
     }
     if(0 == len)
     {
     	 pthread_mutex_lock(&mutex);
     	 FD_CLR(fdinfo->fd, fdinfo->rdset);
     	 pthread_mutex_unlock(&mutex);
     	 close(fdinfo->fd);
     	 free(info);
     	 return NULL;
     }
     else
     {
     	printf("%s\n",buf);
     	int ret = send(info->fd, buf, strlen(buf)+1, 0);
     }
     free(info);
     return NULL;
}

int main() {
  /*
   * 这里进行一些初始化的设置,
   * 包括socket建立,地址的设置等,
   * int listenfd = sockeet(AF_INET, ...);
   */
 
  fd_set read_fs, write_fs;
  struct timeval timeout;
  int max = listenfd ;
  int nfds = 0; // 记录就绪的事件数,可以减少遍历的次数
  FD_ZERO(&read_fs);
  FD_ZERO(&write_fs);
  FD_SET(listenfd, &read_fs);
  pthread_mutex_init(&mutex,NULL);

  while (1) {
	    // 阻塞获取,每次需要把fd从用户态拷贝到内核态
	    pthread_mutex_lock(&mutex);
	    fd_set tmp1 = read_fs;
  		fd_set tmp2 = write_fs;
  		pthread_mutex_unlock(&mutex);
	    nfds = select(listenfd + 1, &tmp1, &tmp2, NULL, &timeout);
	    for (int i = 0; i <= max && nfds; ++i) {
	          if (listenfd == i) {
		          pthread_t tid;
		          fdinfo *info = (fdinfo*)mallocc(sizeof(fdinfo));
		          pthread_create(&tid, NULL, acceptConn, info);
		          pthread_attach(tid);
		      }
		      if (FD_ISSET(i, &tmp1) && i != listenfd) {
		      	  pthread_t tid;
		          fdinfo *info = (fdinfo*)mallocc(sizeof(fdinfo));
		          pthread_create(&tid, NULL, recv, info);
		          pthread_attach(tid);
		      }
		      if (FD_ISSET(i, &tmp2) && i != listenfd) {
		         // 这里处理write事件
		      }
		      --nfds;
	    }
	    //
	    pthread_destory_mutex(&mutex);
  }

1.2.2 poll

poll和函数类似,只是没有数量的限制,相对于sselect有优势,但是无法跨平台,所以一般不会使用。

//poll函数接口
#include <poll.h>
// 数据结构
struct pollfd {
    int fd;                         // 需要监视的文件描述符
    short events;                   // 需要内核监视的事件
    short revents;                  // 实际发生的事件
};
 
// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
1.2.2.1 使用实例
//poll使用示例
#define MAX_POLLFD_LEN 4096  
 
int main() {
  /*
   * 在这里进行一些初始化的操作,
   * 比如初始化数据和socket等。
   */
 
  pollfd fds[MAX_POLLFD_LEN];
  memset(fds, 0, sizeof(fds));
  for(int i =0 ;i<MAX_POLLFD_LEN; ++i)
  {
  	  fds[i].fd = -1;
  }
  fds[0].fd = listenfd;
  fds[0].events = POLLRDNORM;
  int max  = 1;  // 队列的实际长度
  int timeout = 0;
 
  int current_size = max;
  while (1) {
    // 阻塞获取,每次需要把fd从用户态拷贝到内核态
    int nfds = poll(fds, max, timeout);
    if (fds[0].revents & POLLRDNORM) {
        int connfd = accept(listenfd, NULL);
        //将新的描述符添加到读描述符集合中
        for(int i=0;i<MAX_POLLFD_LEN;++i)
        {
        	if(fds[i].fd == -1) {
        		fds[i]=connfd;break;
        	}
        }
    }
    // 每次需要遍历所有fd,判断有无读写事件发生
    for (int i = 1; i <= max && nfds; ++i) {     
      if (fds[i].revents & POLLRDNORM) { 
         int ret = read(fds[i].fd, buf, MAXLINE);
         if (n == 0) {
             close(fds[i].fd);
             fds[i].fd = -1;
         }
         } else {
             // 这里处理write事件     
         }
         --nfds;
      }
    }
  }

缺点:

  1. 单个进程打开的fd有限1024个
  2. 每次调用select都需要memset
  3. select后,需要通过遍历的方式才能确定活动的文件描述符,开销太大。
  4. 活动的文件描述符数组需要从用户态传进内核态,也有不小的消耗。

1.2.3 epoll

通过epoll创建的数据区是内核和用户态共享的,不再需要拷贝。
且epoll是用链表组织的,有活动的文件描述符时仍然是将对应的结构体的置位,但是节点会直接指向下个活动节点,就不再需要遍历判断活动的文件描述符。

//epoll函数接口
#include <sys/epoll.h>
 
// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};
 
// API
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// 负责检测可读队列,没有可读 socket 则阻塞进程
//epoll使用示例
int main(int argc, char* argv[])
{
   /*
   * 在这里进行一些初始化的操作,
   * 比如初始化数据和socket等。
   */
 	epoll_event c;
    // 内核中创建ep对象
    int epfd=epoll_create(256);
    // 需要监听的socket放到ep中
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd,&event);
 
    while(1) {
      // 阻塞获取
      int nfds = epoll_wait(epfd, events, 20, 0);
      for(i=0;i<nfds;++i) {
          if(events[i].data.fd==listenfd) {
              // 这里处理accept事件
              int connfd = accept(listenfd);
              epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
          } else if (events[i].events&EPOLLIN) {
              // 这里处理read事件
              read(sockfd, BUF, MAXLINE);
              //读完后准备写
              epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&event);
          } else if(events[i].events&EPOLLOUT) {
              // 这里处理write事件
              write(sockfd, BUF, n);
              //写完后准备读
              epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&event);
          }
      }
    }
    return 0;
}
1.2.3.3 LT模式

采用read操作对一个IO没有读取完数据,则下次epoll时还会得到活动的文件描述符

1.2.3.4 ET模式

read后,即使没有读完,下次epoll时若没有新的数据流进入则不会提示是活动的文件描述符

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值