详解IO多路转接select、poll、epoll的工作原理和实现

- 多路IO转接

  • 1 - select

    • 原理:select委托内核监听多个文件描述符的变化,当内核监听到文件描述符变化时,select函数会返回有多少个文件描述符发生了变化,但不会告诉用户是哪些个文件描述符发生了变化,用户需要自己遍历文件描述符集合来判断是哪些文件描述符有数据到达。
    • 数据结构:由于select是通过数组实现的,数组大小为1024个bit,所以和1024个文件描述符相对应,因此不能突破1024的限制,最大只能监听1024个文件描述符。每一个bit对应一个文件描述符,将要内核监听的fd_set集合中的对应文件描述符的bit位置为1,内核就知道了需要监听这个比特位所对应的文件描述符。
    • 优点:跨平台,Linux和Windows都可以用。而且select比多进程和多线程效率高的原因在于,它可以一次监听多个文件描述符的变化,而多进程和多线程同一时刻只能监听一个文件描述符的变化,而且创建多进程和多线程需要耗费大量的系统资源。
    • 函数
      • int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
        • nfds为你要内核监听的最大的文件描述符 + 1,因为内核需要遍历fd_set数组,只需要遍历到你所拥有的最大文件描述符即可。
        • readfds文件读集合,为传入传出参数,这个集合是你需要内核监听的文件描述符的集合,当内核监听到有文件描述符所对应的读缓存区有数据到达时,会将该位置1,没有数据到达的文件描述符的标志位置0,所以内核返回的是修改过的读集合。
        • writefds文件写集合,监听文件描述符对应的写缓冲区的,同上。
        • exceptfds异常文件描述符集合,委托内核监听哪些文件描述符发生了异常,同上。
        • timeout 规定select在多长时间内返回,
          • NULL为直到有文件描述符发生变化才返回
          • timeval设置为{0, 0}表示不阻塞
          • timeval设置为{x, 0}表示阻塞x秒后返回
        • 返回值int,表示发生状态改变的文件描述符的数量。
    • 要创建fd_set集合,需要配合以下的小函数使用
      • void FD_CLR(int fd, fd_set *set); 从集合中删除文件描述符
      • int FD_ISSET(int fd, fd_set *set); 判断此文件描述符是否在集合中,在返回1,不在返回0
      • void FD_SET(int fd, fd_set *set); 将文件描述符添加到集合中
      • void FD_ZERO(fd_set *set);fd_set集合的每一个标志位清0
    • 例子
    //
    // Created by liubin on 2020/11/18.
    //
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PORT 9999
    
    int main()
    {
        int sfd = -1;
        int max_fd = -1;
        int cli_fd = -1;
        int ret = -1;
        int idx = -1;
        int num = -1;
        char buf[1024] = {0};
        int fg = 1;
        struct sockaddr_in addr;
        struct sockaddr_in cli_addr;
        socklen_t cli_len = sizeof(cli_addr);
        fd_set rd_set ;
        fd_set temp_set ;
    
        // 1- create socket
        sfd = socket(AF_INET, SOCK_STREAM, 0);
        // 2- setsockopt
        setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &fg, sizeof(fg));
        // 2- bind
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = INADDR_ANY;
        addr.sin_port = htons(PORT);
        bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
        // 3- listen
        listen(sfd, 128);
    
        // 4- select
        FD_ZERO(&rd_set);
        FD_SET(sfd, &rd_set);
        max_fd = sfd;
    
        while (1)
        {
            temp_set = rd_set;
            ret = select(max_fd + 1, &temp_set, NULL, NULL, NULL);
    
            if (FD_ISSET(sfd, &temp_set))
            {
                // have new connection
                cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
                FD_SET(cli_fd, &rd_set);
                max_fd = max_fd > cli_fd ? max_fd : cli_fd;
            }
            // have data
            for (idx = sfd + 1; idx <= max_fd; idx++)
            {
                if (FD_ISSET(idx, &temp_set))
                {
                    memset(buf, 0, 1024);
                    num = read(idx, buf, sizeof(buf));
                    if (-1 == num)
                    {
                        perror("Error : read");
                        return -1;
                    }
                    if (0 == num)
                    {
                        // client close
                        FD_CLR(idx, &rd_set);
                        close(idx);
                        printf("client close the connect!\n");
                    }
                    if (num > 0)
                    {
                        printf("recv data from client [%s]\n", buf);
                    }
                }
            }
        }
    }
    
  • 2- poll

    • 原理:pollselect原理相似,只是函数参数略有不同而已,poll也是需要将委托内核监听的文件描述符传递给函数,当内核监听到委托的文件描述符集合中的状态发生变化时,就返回发生变化的文件描述符的个数。
    • 数据结构:poll内部是通过链表实现的。因此可以突破1024的限制,也就是说可以监听的文件描述符个数多余1024个。
    • 优点:可以突破1024的限制,非跨平台,只能在Linux系统使用。
    • 函数:
      • int poll(struct pollfd *fds, nfds_t nfds, int timeout);
        struct pollfd {
               int   fd;         /* 需要内核监听的文件描述符 */
               short events;     /* 需要监听文件描述符的事件 POLLIN(读缓冲区)、POLLOUT(写缓冲区)、POLLERR(异常) */
               short revents;    /* 返回监听到的事件 */
        };
        
        • fds 一个结构体数组,需要内核监听的文件描述符和需要监听的文件描述的事件。传入参数
        • nfds 当前需要内核监听的最大文件描述符 + 1
        • timeout 毫秒值
          • -1表示直到有文件描述符发生变化才解阻塞
          • 0表示立即返回
          • >0表示阻塞x秒后返回
        • 返回值int表示要监听的文件描述符状态发生变化的数量。
    • 例子
    //
    // Created by liubin on 2020/11/18.
    //
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PORT 9999
    
    int main()
    {
        int ret = -1;
        int num = -1;
        int idx = -1;
        int sfd = -1;
        int cli_fd = -1;
        int fg = 1;
        int max_fd = -1;
        char buf[1024] = {0};
        struct sockaddr_in addr;
        struct sockaddr_in cli_addr;
        struct pollfd p_fd[200];
        socklen_t cli_len = sizeof(cli_addr);
    
        // 1- create socket
        sfd = socket(AF_INET, SOCK_STREAM, 0);
        // 2- setsockopt
        setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg));
        // 3- bind
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = INADDR_ANY;
        addr.sin_port = htons(PORT);
        bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
        // 4- listen
        listen(sfd, 128);
    
        // 5- poll
        // init pollfd
        for (idx = 0; idx < 200; idx++)
        {
            p_fd[idx].fd = -1;
            p_fd[idx].events = POLLIN;
        }
        // insert sfd to array of pollfd
        p_fd[0].fd = sfd;
        max_fd = 0;
    
        while (1)
        {
            // -1 wait for change of fd
            ret = poll(p_fd, max_fd + 1, -1);
            if (-1 == ret)
            {
                perror("Error: poll");
                return -1;
            }
            if (p_fd[0].revents & POLLIN)
            {
                // have new connection
                cli_fd = accept(p_fd[0].fd, (struct sockaddr *)&cli_addr, &cli_len);
                printf("new client connect.....IP=%s, port=%d\n",
                        inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
                for (idx = 1; idx < 200; idx++)
                {
                    if (-1 == p_fd[idx].fd)
                    {
                        p_fd[idx].fd = cli_fd;
                        max_fd = max_fd > idx ? max_fd : idx;
                        break;
                    }
                }
            }
    
            // hava data
            for (idx = 1; idx <= max_fd; idx++)
            {
                if (p_fd[idx].revents & POLLIN)
                {
                    memset(buf, 0, 1024);
                    num = recv(p_fd[idx].fd, buf, sizeof(buf), 0);
                    if (-1 == num)
                    {
                        perror("Error: recv");
                        return -1;
                    }
                    if (0 == num)
                    {
                        // client close
                        close(p_fd[idx].fd);
                        p_fd[idx].fd = -1;
                        printf("client close connect!\n");
                    }
                    if (num > 0)
                    {
                        printf("recv data [%s]\n", buf);
                        send(p_fd[idx].fd, buf, strlen(buf), 0);
                    }
                }
            }
    
        }
    }
    
  • 3 - epoll

    • 原理:同样是委托内核监听文件描述符状态的变化,然后返回给我们有几个文件描述符状态发生的变化,而且会告诉我们是哪几个,我们不再需要再去遍历监听的文件描述符集合去找到底是哪几个发生了变化。
    • 数据结构:epoll内部是通过红黑树实现的,红黑树类似于平衡二叉树,它通过epoll_create创建树的根节点,然后将一个个要监听的文件描述符挂在树上,不是挂文件描述符,而是挂struct epoll_event结构体,这个结构体里面有要监听的文件描述符的事件和其他信息。当要监听的文件描述符状态发生变化时,epoll会将发生变化的文件描述符所对应的struct epoll_event结构体返回,因为是通过树实现的,因此遍历就比较快,比select的数组实现和poll的链表实现等遍历都要快很多。
    • 优点:效率最高,比select的数组实现和poll的链表实现等遍历都要快很多。
    • 函数:
      • int epoll_create(int size);
        • size参数为要监听的文件描述符数量,是个软限制,当文件描述符超过这个数量时会自动增加。
        • 返回值:返回树的根节点epfd,这个根节点用来在后续在树上挂要监听的文件描述符
      • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
        struct epoll_event {
           uint32_t     events;    /* 委托内核监听的事件 */
           epoll_data_t data;      /* 与要监听的文件描述符相关的信息 */
        };
        /* 可以只用fd或者用指针传递与该文件描述符绑定的更多信息 */
        typedef union epoll_data {
           void    *ptr;
           int      fd;
           uint32_t u32;
           uint64_t u64;
        } epoll_data_t;
        
        • epfd即树的根节点
        • op对树进行的操作,在树中增加、修改、删除节点等操作
        • fd要操作的文件描述符
        • event要操作的文件描述符所对应的事件EPOLLIN(监听读缓冲区),EPOLLOUT(监听写缓冲区),EPOLLERR(文件描述符发生异常)
        • 返回值:成功返回0,失败返回-1
      • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
        • epfd 树的根节点
        • events返回的一个数组的首地址,数组中是发生变化的文件描述符所对应的结构体,传出参数。
        • maxevents返回的发生状态变化的最大数量
        • timeout 毫秒值
          • -1表示直到有文件描述符发生变化才解阻塞
          • 0表示立即返回
          • >0表示阻塞x秒后返回
        • 返回值int表示要监听的文件描述符状态发生变化的数量。
    • 实例
    //
    // Created by liubin on 2020/11/19.
    //
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PORT 9999
    #define MAX_CONN 256
    
    int main()
    {
        int ret = -1;
        int count = -1;
        int idx = -1;
        int num = -1;
        int sfd = -1;
        int epfd = -1;
        int cli_fd = -1;
        int fg = 1;
        char buf[1024] = {0};
        struct sockaddr_in addr;
        struct sockaddr_in cli_addr;
        socklen_t cli_len = sizeof(cli_addr);
        struct epoll_event ev = {0};
        struct epoll_event cli_ev = {0};
        struct epoll_event ev_array[MAX_CONN] = {0};
    
        // 1- create socket
        sfd = socket(AF_INET, SOCK_STREAM, 0);
        // 2- setsockopt
        setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg));
        // 3- bind
        addr.sin_family = AF_INET;
        addr.sin_port = htons(PORT);
        addr.sin_addr.s_addr = INADDR_ANY;
        bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
        // 4- listen
        listen(sfd, 128);
    
        // 5- epoll
        epfd = epoll_create(MAX_CONN);
        ev.events = EPOLLIN;
        ev.data.fd = sfd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
    
        while (1)
        {
            num = epoll_wait(epfd, ev_array, MAX_CONN, -1);
            for (idx = 0; idx < num; idx++)
            {
                if (sfd == ev_array[idx].data.fd)
                {
                    // have new connection
                    cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
                    cli_ev.events = EPOLLIN;
                    cli_ev.data.fd = cli_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev);
                    printf("have new client connect.........IP=%s, port=%d\n",
                            inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
                }
                if (sfd != ev_array[idx].data.fd)
                {
                    // hava data
                    memset(buf, 0, sizeof(buf));
                    count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0);
                    if (0 == count)
                    {
                        // client close
                        ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL);
                        printf("ret = %d\n", ret);
                        close(ev_array[idx].data.fd);
                        printf("client close the connect!\n");
                    }
                    if (count > 0)
                    {
                        printf("recv data from client [%s]\n", buf);
                        send(ev_array[idx].data.fd, buf, strlen(buf), 0);
                    }
                }
            }
        }
    
        return 0;
    }
    
    • epoll的三种工作模式(以读缓冲区举例)
      • 1 - 水平触发模式-LT
        • 只要对应的文件描述符所对应的读缓冲区有数据,epoll_wait就返回,与客户端发送信息的次数无关。
        //
        // Created by liubin on 2020/11/19.
        //
        #include 
        #include 
        #include 
        #include 
        #include 
        #include 
        
        #define PORT 9999
        #define MAX_CONN 256
        
        int main()
        {
            int times = 0;
            int ret = -1;
            int count = -1;
            int idx = -1;
            int num = -1;
            int sfd = -1;
            int epfd = -1;
            int cli_fd = -1;
            int fg = 1;
            char buf[1] = {0};
            struct sockaddr_in addr;
            struct sockaddr_in cli_addr;
            socklen_t cli_len = sizeof(cli_addr);
            struct epoll_event ev = {0};
            struct epoll_event cli_ev = {0};
            struct epoll_event ev_array[MAX_CONN] = {0};
        
            // 1- create socket
            sfd = socket(AF_INET, SOCK_STREAM, 0);
            // 2- setsockopt
            setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg));
            // 3- bind
            addr.sin_family = AF_INET;
            addr.sin_port = htons(PORT);
            addr.sin_addr.s_addr = INADDR_ANY;
            bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
            // 4- listen
            listen(sfd, 128);
        
            // 5- epoll
            epfd = epoll_create(MAX_CONN);
            ev.events = EPOLLIN;
            ev.data.fd = sfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
        
            while (1)
            {
                num = epoll_wait(epfd, ev_array, MAX_CONN, -1);
                for (idx = 0; idx < num; idx++)
                {
                    if (sfd == ev_array[idx].data.fd)
                    {
                        // have new connection
                        cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
                        cli_ev.events = EPOLLIN;
                        cli_ev.data.fd = cli_fd;
                        epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev);
                        printf("have new client connect.........IP=%s, port=%d\n",
                                inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
                    }
                    if (sfd != ev_array[idx].data.fd)
                    {
                        // hava data
                        memset(buf, 0, sizeof(buf));
                        // read 1 bytes one time
                        count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0);
                        if (0 == count)
                        {
                            // client close
                            ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL);
                            printf("ret = %d\n", ret);
                            close(ev_array[idx].data.fd);
                            printf("client close the connect!\n");
                        }
                        if (count > 0)
                        {
                            // printf("recv data from client [%s]\n", buf);
                            times++;
                            write(STDOUT_FILENO, buf, 1);
                            printf("--------%d--------\n", times);
                            send(ev_array[idx].data.fd, buf, 1, 0);
                        }
                    }
                }
            }
        
            return 0;
        }
        
        
        
      • 2 - 边沿触发模式-ET
        • 客户端发送一次数据,服务器的epoll_wait就返回一次,不管你上一次的读缓冲区数据有灭有读完,没有读完的上次数据仍在读缓冲区内。将文件描述符设置为EPOLLET即可。
        //
        // Created by liubin on 2020/11/19.
        //
        #include 
        #include 
        #include 
        #include 
        #include 
        #include 
        
        #define PORT 9999
        #define MAX_CONN 256
        
        int main()
        {
            int times = 0;
            int ret = -1;
            int count = -1;
            int idx = -1;
            int num = -1;
            int sfd = -1;
            int epfd = -1;
            int cli_fd = -1;
            int fg = 1;
            char buf[1] = {0};
            struct sockaddr_in addr;
            struct sockaddr_in cli_addr;
            socklen_t cli_len = sizeof(cli_addr);
            struct epoll_event ev = {0};
            struct epoll_event cli_ev = {0};
            struct epoll_event ev_array[MAX_CONN] = {0};
        
            // 1- create socket
            sfd = socket(AF_INET, SOCK_STREAM, 0);
            // 2- setsockopt
            setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg));
            // 3- bind
            addr.sin_family = AF_INET;
            addr.sin_port = htons(PORT);
            addr.sin_addr.s_addr = INADDR_ANY;
            bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
            // 4- listen
            listen(sfd, 128);
        
            // 5- epoll
            epfd = epoll_create(MAX_CONN);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = sfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
        
            while (1)
            {
                num = epoll_wait(epfd, ev_array, MAX_CONN, -1);
                for (idx = 0; idx < num; idx++)
                {
                    if (sfd == ev_array[idx].data.fd)
                    {
                        // have new connection
                        cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
                        // Set ET Mode
                        cli_ev.events = EPOLLIN | EPOLLET;
                        cli_ev.data.fd = cli_fd;
                        epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev);
                        printf("have new client connect.........IP=%s, port=%d\n",
                                inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
                    }
                    if (sfd != ev_array[idx].data.fd)
                    {
                        // hava data
                        memset(buf, 0, sizeof(buf));
                        // read 1 bytes one time
                        count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0);
                        if (0 == count)
                        {
                            // client close
                            ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL);
                            printf("ret = %d\n", ret);
                            ret = close(ev_array[idx].data.fd);
                            printf("close ret = %d\n", ret);
                            printf("client close the connect!\n");
                        }
                        if (count > 0)
                        {
                            // printf("recv data from client [%s]\n", buf);
                            times++;
                            write(STDOUT_FILENO, buf, 1);
                            printf("--------%d--------\n", times);
                            send(ev_array[idx].data.fd, buf, 1, 0);
                        }
                    }
                }
            }
        
            return 0;
        }
        
        
        
      • 3 - 边沿非阻塞模式
        • 第二种方式在加上将文件描述符设置为O_NONBLOCK,为了一次读完缓冲区内的数据,可以循环读数据,返回-1和errno=EAGAIN表示该次数据读完,返回0表示客户端断开连接,这种模式工作效率最高。
        //
        // Created by liubin on 2020/11/19.
        //
        #include 
        #include 
        #include 
        #include 
        #include 
        #include 
        #include 
        #include 
        
        #define PORT 9999
        #define MAX_CONN 256
        
        int main()
        {
            int flag = -1;
            int times = 0;
            int ret = -1;
            int count = -1;
            int idx = -1;
            int num = -1;
            int sfd = -1;
            int epfd = -1;
            int cli_fd = -1;
            int fg = 1;
            char buf[1] = {0};
            struct sockaddr_in addr;
            struct sockaddr_in cli_addr;
            socklen_t cli_len = sizeof(cli_addr);
            struct epoll_event ev = {0};
            struct epoll_event cli_ev = {0};
            struct epoll_event ev_array[MAX_CONN] = {0};
        
            // 1- create socket
            sfd = socket(AF_INET, SOCK_STREAM, 0);
            // 2- setsockopt
            setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg));
            // 3- bind
            addr.sin_family = AF_INET;
            addr.sin_port = htons(PORT);
            addr.sin_addr.s_addr = INADDR_ANY;
            bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
            // 4- listen
            listen(sfd, 128);
        
            // 5- epoll
            epfd = epoll_create(MAX_CONN);
            ev.events = EPOLLIN;
            ev.data.fd = sfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
        
            while (1)
            {
                num = epoll_wait(epfd, ev_array, MAX_CONN, -1);
                for (idx = 0; idx < num; idx++)
                {
                    if (sfd == ev_array[idx].data.fd)
                    {
                        // have new connection
                        cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
        
                        flag = fcntl(cli_fd, F_GETFL);
                        flag |= O_NONBLOCK;
                        fcntl(cli_fd, F_SETFL, flag);
        
                        // Set NonBlock Mode
                        cli_ev.events = EPOLLIN | EPOLLET;
                        cli_ev.data.fd = cli_fd;
                        epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev);
                        printf("have new client connect.........IP=%s, port=%d\n",
                                inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
                    }
                    if (sfd != ev_array[idx].data.fd)
                    {
                        // hava data
                        memset(buf, 0, sizeof(buf));
                        // read 1 bytes one time
                        while ( (count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0)) > 0)
                        {
                            write(STDOUT_FILENO, buf, 1);
                            send(ev_array[idx].data.fd, buf, 1, 0);
                        }
                        if (0 == count)
                        {
                            // client close
                            ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL);
                            printf("ret = %d\n", ret);
                            ret = close(ev_array[idx].data.fd);
                            printf("close ret = %d\n", ret);
                            printf("client close the connect!\n");
                        }
                        if (-1 == count && errno == EAGAIN)
                        {
                            continue;
                        }
                    }
                }
            }
        
            return 0;
        }
        

你可能感兴趣的:(c++逆袭之路,内核,epoll,linux)