IO多路复用

一 IO基本概念

  • IO 是什么?
    IO的全称是input/ouput, 在计算机的语境中是用于描述计算与外界进行输入输出,例如敲击键盘输入,显示器输出打印信息, 客户端向服务端发送数据等都是IO。
  • IO多路复用是什么
    IO多路复用主要是通过单个进程或线程去监视多个文件描述符,复用是单个进程或线程,主要应用于服务端高并发的程序中。而所谓的多路一般是指多个tcp连接。如果连接的数量不多,也通过多线程的方式去维护每个tcp/udp连接
  • 思考: 为什么会用出现多路复用的情况,这是为了提升系统的并发性响应性,由于传统式的IO具有阻塞性。通过一个线程或进程来监视多个文件描述符,当其中某个文件描述符有可读或者可写事件时,系统会通知应用程序进行相应的处理,这种处理机制大大提升了系统的并发性。传统式的阻塞IO方式可能导致线程长时间被阻塞,而IO多路复用则可以通过非阻塞的方式进行IO操作

二 IO多路复用的几种方式

1. select
  • 函数原型
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 参数
    nfds: 需要检测的文件描述符数量
    readfds: 检测可读事件文件描述符的集合, 通过调用FD_SET添加可读事件,也可通过过FD_CLR清除可读事件
    writefds: 检测可写事件的文件描述符的集合
    exceptfds: 检测异常事件的文件描述集合
    timeout :0:无论是否有事件发生,函数调用完后立即返回,-1:阻塞等待,直到有事件发生或有错误发生
  • 涉及的相关接口
// 这里的fd 实际使用都是以 句柄 传入
FD_ZERO(fd_set *fdset);              // 将set清零使集合中不含任何fd
FD_SET(int fd, fd_set *fdset);       // 将fd加入set集合
FD_CLR(int fd, fd_set *fdset);       // 将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset);     // 检测fd是否在set集合中,不在则返回0
  • 功能: 用于监听可读、可写、异常套接字集合的事件,如果存在多个事件,则返回事件数量。
  • 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <poll.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>

#define SER_PORT 6000
#define BACKLOG 7
#define CLI_NUM 20
#define MAXLINE 2048

char buff[1024] = {0};
int main(int argc, char const *argv[])
{

   int ser_fd;
   struct sockaddr_in ser_addr, cli_addr;
   int cli_addr_len;
   int cli_fds[CLI_NUM] = {0};

   ser_fd = socket(AF_INET, SOCK_STREAM, 0);
   if (ser_fd == -1)
   {
       perror("socket failed\n");
       return -1;
   }

   memset(&ser_addr, 0, sizeof(ser_addr));
   ser_addr.sin_family = AF_INET;
   ser_addr.sin_port = htons(SER_PORT);
   ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);

   if (bind(ser_fd, (const struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0)
   {
       perror("bind error");
       return -1;
   }

   if (listen(ser_fd, BACKLOG) < 0)
   {
       perror("listen error");
       return -1;
   }
   #define FDS_MAX_NUM 1024
   fd_set rset, tmp;
   FD_ZERO(&rset);
   FD_SET(ser_fd, &rset);
   int maxfd = ser_fd;

   int cli[FDS_MAX_NUM];
   int i;
   for(i = 0; i < FDS_MAX_NUM; i++){
       cli[i] = -1;
   }
   struct timeval  timeout;
   timeout.tv_sec = 1;
   timeout.tv_usec = 0;
   while(1){
       tmp = rset;
       int nready = select(maxfd + 1, &tmp, NULL, NULL, &timeout);
       
       if(FD_ISSET(ser_fd, &tmp)){
           
           int cli_fd = accept(ser_fd, NULL, NULL);
           if(cli_fd < 0){
               continue;
           }        
           printf("new connection \n");
           for(i = 0; i < FDS_MAX_NUM; i++){
               if(cli[i] == -1){
                   cli[i] = cli_fd;
                   FD_SET(cli_fd, &rset);
                   if(maxfd < cli_fd){
                       maxfd = cli_fd;
                   }
                   break;
               }
           }
           if(--nready == 0){  
               continue;
           }
       }

       for(i = 0; i < FDS_MAX_NUM; i++){
           char buf[1024] = {0};

           if(FD_ISSET(cli[i], &tmp)){
            
               int nbytes = recv(cli[i], buf, 1024, 0);
               if(nbytes == 0){
                   FD_CLR(cli[i], &rset);
                   close(cli[i]);
                   
               }else if(nbytes > 0){
                   printf("recv: %s\n", buf);
               }
           }
       }
   }
   return 0;
}
  • 优点:
  1. 相比较采用多线程的网络IO,可以减少性能消耗
  2. 可移植性比较好,大部分平台可以支持select 函数调用
  3. 可以进行多个IO的处理,比如同时有多个客户端请求,服务端采用select函数进行事件监听,一旦有事件,select 函数返回事件数量,可以用 数组的方式存储客户端套接字,在通过轮询数组的方式,去找到触犯事件的套接字
  • 缺点:
  1. 与select 相关的函数比较多,select参数比较多,比较不好理解
  2. 每次都要从用户态拷贝事件集合到内核态, 比如代码示例的while循环中频繁地调用select函数,将事件集从用户态拷贝到内核态,比较消耗性能
  3. select监听的文件描述符的数量是有限的,一般来说是1024个,可以通过观察fd_set类型可知
#define __FD_SETSIZE		1024
typedef long int __fd_mask;
#define __NFDBITS	(8 * (int) sizeof (__fd_mask))
typedef struct{
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;
2. poll
  • 函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 参数
    struct pollfd fds
struct pollfd{
   int fd;      //文件描述符
   short events;  //等待的事件
   short revents; //实际发生了的事件
}

nfds_t nfds: 需要检测的文件描述符数量
int timeout: 监听事件的事件,-1: 立即返回 , 0:

  • 返回值
    大于0:poll()返回的结构体revents 域不为0的文件描述符数量
    0:如果在超时前没有任何事件发生, 返回0
    -1:失败
  • 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <poll.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define SER_PORT 6000
#define BACKLOG 7
#define CLI_NUM 20
#define MAXLINE 2048

int main(int argc, char const *argv[])
{

   int ser_fd;
   struct sockaddr_in ser_addr, cli_addr;
   int cli_addr_len;
   int cli_fds[CLI_NUM] = {0};

   ser_fd = socket(AF_INET, SOCK_STREAM, 0);
   if (ser_fd == -1)
   {
       perror("socket failed\n");
       return -1;
   }

   memset(&ser_addr, 0, sizeof(ser_addr));
   ser_addr.sin_family = AF_INET;
   ser_addr.sin_port = htons(SER_PORT);
   ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);

   if (bind(ser_fd, (const struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0)
   {
       perror("bind error");
       return -1;
   }

   if (listen(ser_fd, BACKLOG) < 0)
   {
       perror("listen error");
       return -1;
   }
   
   #define FD_NUM_MAX 20
   struct pollfd fds[FD_NUM_MAX] = {0};
   int i = 0, maxfd = ser_fd;
   for(i = 0; i < FD_NUM_MAX; i++){
       fds[i].fd = -1;
   }
   fds[ser_fd].fd = ser_fd;
   fds[ser_fd].events = POLLIN;
   char buf[1024] = {0};
  
   while(1){
       int nready = poll(fds, FD_NUM_MAX, -1);  //-1 阻塞等待, 0 ,非阻塞, 大于0, 等待时间
       
       if(fds[ser_fd].revents & POLLIN){  //监听到新的连接
           int clifd = accept(fds[ser_fd].fd, NULL, NULL);
           if(clifd < 0){
               continue;
           }

           fds[clifd].fd = clifd;
           fds[clifd].events = POLLIN;
           maxfd = clifd;
           if(--nready == 0){
               continue;
           }
       }

       for(i = ser_fd + 1; i < maxfd; i++){
           if(fds[i].fd >0 && fds[i].revents & POLLIN){
               int nbytes = read(fds[i].fd, buf, 1024);
               
               if(nbytes <= 0){  //说明连接关闭了
                   printf("cli %d: cli closed\n", fds[i].fd);
                   fds[i].fd = -1;
                   close(fds[i].fd);
                   continue;    
               }

               printf("buf: %s\n", buf);  //如果从客户端ctl + c 这一行将会一直输出buf 
               memset(buf, 0, 1024);
           }

       }

   }

   return 0;

}
  • 优点:
  1. 接口使用比较简单,只有poll接口。可以使用struct pollfd 类型的变量将监听的套接字事件类型进行封装一起即可。不需要关注可读可写的事件集合,只需在events上进行标注,通过revent 进行观察事件即可
  2. 监听的套接字数量没有被设定上限。
  • 缺点:
  1. 与select 一样需要频繁的将监听的事件集合从用户态拷贝到内核态, 内核态在进行轮询遍历是否有对应的事件发生
3. epoll
  • 函数原型
int epoll_create(int size)

功能: 创建一个epoll句柄,size 用来告诉内核监听的数量,实际开发中,默认填写1

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

功能:epoll事件注册函数, 根据op 的不同,执行相应的操作,不如向epoll对象中添加修改或者删除对应的事件
参数:
epfd: epoll 句柄
op: EPOLL_CTL_ADD:注册新的fd 到epfd,
EPOLL_CTL_MOD: 修改已经注册的fd监听事件,
EPOLL_CTL_DEL:从epfd 中删除一个fd

  fd: 需要监听的文件描述符
  event: 告诉内核需要监听的事件
struct epoll_event{
  __uint32_t events;
  epoll_data_t data;
}

其中events可以用以下几个宏的集合
EPOLLIN: 表示对应的文件描述符可读(所谓的可读,是内核态的已经准备好数据)。
EPOLLOUT:表示对应的文件描述可写。
EPOLLET: 边沿触发,相较于EPOLLLT只会触发一次。触发的条件为内核缓冲区的数据从无到有,不论内核缓冲区的数据是否被应用层读完, 只触发一次。如果想要从内核缓冲区读取完数据,可以使用while(1){ int nsize = recv(fd, buffer, buffer_size, 0); if(!nsize) break;}不断地从内核缓冲区读取数据。
EPOLLLT: 水平触发,如果内核态的数据的没有被读取,就会一直触发可读事件,为epoll 的默认触发方式。
返回值:0: 成功,-1:失败

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

功能:等待事件发生
参数:
epfd: epoll 的描述符
events: 分配好的epoll_event 结构体数组,epoll 将会把发生的事件复制到events数组中
maxevents: 表示events数组的大小
timeout: 表示在没有检测到事件发生时最多等待的时间。如果timeout为0,表示立即放回不用返回;如果为-1,表示阻塞等待,内核缓冲区有数据

  • 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <poll.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define SER_PORT 6000
#define BACKLOG 7
#define CLI_NUM 20
#define MAXLINE 2048

int main(int argc, char const *argv[])
{

    int ser_fd;
    struct sockaddr_in ser_addr, cli_addr;
    int cli_addr_len;
    int cli_fds[CLI_NUM] = {0};

    ser_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (ser_fd == -1)
    {
        perror("socket failed\n");
        return -1;
    }

    memset(&ser_addr, 0, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(SER_PORT);
    ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(ser_fd, (const struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0)
    {
        perror("bind error");
        return -1;
    }

    if (listen(ser_fd, BACKLOG) < 0)
    {
        perror("listen error");
        return -1;
    }

    int epfd = epoll_create(1);
    if(epfd < 0){
        return -1;
    }
    struct epoll_event ev, events[1024] = {0};
    ev.data.fd = ser_fd;
    ev.events = EPOLLIN;
    
    epoll_ctl(epfd, EPOLL_CTL_ADD, ser_fd, &ev);  //添加服务端的套接字在监听事件集合中
    while(1){
        int nready = epoll_wait(epfd, events, 1024, -1);
        if(nready < 0){
            continue;
        }

        int i = 0;
        for(i = 0; i < nready; i++){
            int connfd = events[i].data.fd;
            if(connfd == ser_fd){
                int clifd = accept(ser_fd, NULL, NULL);
                if(clifd <= 0){
                    continue;
                }

                ev.events = EPOLLIN|EPOLLET;  //添加边沿出触发(内核缓冲区的数据从无到有,不论内核缓冲区的数据是否被应用层读完, 只触发一次)
                ev.data.fd = clifd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &ev);
            
            }else if(events[i].events & EPOLLIN){
                char buff[1024] = {0};
                int nbyte = recv(connfd, buff, 1024, 0);

                if(nbyte == 0){
                    printf("close\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    close(events[i].data.fd);
                    
                }else if(nbyte > 0){
                    printf("recv: %s\n", buff);
                }                
            }
        }
    }



    #endif
    return 0;

}
  • 优点
  1. 相较于select/poll ,不用频繁地将所有的监听事件集合从用户态拷贝到内核态,通过使用epoll_ctl()可以增加或减少要监听的fd
  2. 监听的套接字数量没有被设定上限
  3. IO的效率不随fd的数量的增加而线性下降,只会活跃的fd 进行操作
  • 缺点
  1. 跨平台性不太好,只能在linux 下使用
  2. 调用逻辑相较于select还是有点复杂点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值