Linux 网络编程——IO Multiplexing之select, poll, epoll详解

上一章节Linux 网络编程——多进程,多线程模型已经介绍了两种服务器并发模型,但其并发量受进程/线程数量限制。这一章节,我们将介绍三种IO多路复用模型,可以实现单进程监听多个网络IO事件。

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

SELECT模型

Common API
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set);  clear the set
void FD_CLR(int fd, fd_set *set); remove a given file descriptor from the set
int  FD_ISSET(int fd, fd_set *set); tests to see if a file descriptor is part of the set
void FD_SET(int fd, fd_set *set); add a given file descriptor from the set
SELECT模型服务器demo
#include <arpa/inet.h>  // 大小端转换
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>  // socket套接字
#include <unistd.h>

#define _SERVER_PORT 8080
#define BACK_LOG 128
#define IP_SIZE 16
#define BUFFER_SIZE 1600

int server_net_init(void) {
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(_SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //设置本机任意IP
    // inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr);
    // //转换并设置自定义IP
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ==
        -1) {
        perror("bind Error");
        exit(0);
    }
    listen(server_fd, BACK_LOG);
    printf("TCP Server Waiting for Connect\n");
    return server_fd;
}

int server_recv_response(int sockfd) {
    char recv_buffer[BUFFER_SIZE];
    bzero(recv_buffer, BUFFER_SIZE);
    ssize_t recvlen;
    if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) {
        printf("%s", recv_buffer);
        send(sockfd, recv_buffer, recvlen, 0);
        bzero(recv_buffer, BUFFER_SIZE);
    }
    if (recvlen == -1) {
        perror("recv error");
        exit(-1);
    } else if (recvlen == 0) {
        printf("client exit\n");
        return -1;
    }
    return 0;
}

void select_starting(int sockfd) {
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen;
    int readycode;             // 就绪量
    fd_set nset, oset;         // nset传入,oset传出
    int clientfd_array[1021];  // 客户端sock数组
    int maxfd;                 // 最大的描述符
    char client_ip[IP_SIZE];
    bzero(client_ip, IP_SIZE);
    // 初始化
    maxfd = sockfd;
    FD_ZERO(&nset);
    FD_SET(sockfd, &nset);
    for (int i = 0; i < 1021; i++) clientfd_array[i] = -1;

    printf("Select Server Running ...\n");

    while (1) {
        oset = nset;
        readycode =
            select(maxfd + 1, &oset, NULL, NULL, NULL);  // 阻塞监听sock读取事件
        while (readycode) {  // 循环处理就绪事件
            // 辨别就绪
            if (FD_ISSET(sockfd, &oset)) {
                addrlen = sizeof(client_addr);
                if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr,
                                        &addrlen)) > 0) {
                    printf(
                        "TCP Server Accept Success:client ip[%s] client "
                        "prot[%d] \n",
                        inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
                                  client_ip, IP_SIZE),
                        ntohs(client_addr.sin_port));
                    if (client_fd > maxfd) maxfd = client_fd;
                    FD_SET(client_fd, &nset);
                    for (int i = 0; i < 1021; i++)
                        if (clientfd_array[i] == -1) {
                            clientfd_array[i] = client_fd;
                            break;
                        }
                    FD_CLR(sockfd, &oset);
                } else {
                    perror("accept error");
                    exit(0);
                }
            } else {  // clientfd就绪
                for (int i = 0; i < 1021; i++)
                    if (clientfd_array[i] != -1)
                        if (FD_ISSET(clientfd_array[i], &oset)) {
                            if (server_recv_response(clientfd_array[i]) == -1) {
                                FD_CLR(clientfd_array[i], &nset);
                                clientfd_array[i] = -1;
                            }
                            FD_CLR(sockfd, &oset);
                            break;
                        }
            }
            --readycode;
        }
    }
}

int main(void) {
    int sfd = server_net_init();  // 服务端网络初始化
    select_starting(sfd);         // 阻塞等到连接

    close(sfd);

    return 0;
}

SELECT模型的优点:

  • SELECT模型采用IO 复用技术,业务处理交替执行,可以仅用单进程实现一对多处理效果
  • 每个平台语言都对select有兼容和实现,便于移植
  • SELECT模型定时阻塞支持微秒级别,有较高的时间精度

SELECT模型的缺点:

  • 监听最大数为1024,不能满足高并发需求。
  • SELECT监听采用轮询模式,随着轮询的数量增多,IO处理能力呈线性下降。
  • SELECT对于sockfd的事件监听是批处理的,如果你希望不同的sockfd监听不同的网络事件,SELECT无法实现。
  • 在退出时,SELECT对每个文件描述符集进行适当的修改,以指示哪些文件描述符实际上已更改状态。因此,如果在循环中使用select(),则必须在此之前重新初始化这些集。
  • 单进程限制,阻塞问题导致任务无法继续,单进程模型不允许业务处理时间过长。
  • SELECT模型在使用的时候会出现大量的无意义的重复开销,浪费系统资源(每次往set监听集合增加监听sockfd描述符时,select()都会将整个监听集合拷贝到内核层,并且将集合的监听项挂载到监听设备,无论以前是否挂载过)

POLL模型

#include <poll.h>

用户自定义长度结构体数组作为POLL监听集合

struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

API:

int poll(struct pollfd * fds , nfds_t nfds , int timeout ); //timeout(工作模式):-1 阻塞监听 >0 定时阻塞 0非阻塞

返回值:成功时,poll () 返回一个非负值,该值是pollfds中其revents字段已设置为非零值(指示事件或错误)的元素数。返回值为0表示系统调用在任何文件描述符被读取之前超时。出错时返回 -1,并设置 errno以指示错误。

POLL模型服务器demo

#include <arpa/inet.h>  // 大小端转换
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>  // socket套接字
#include <unistd.h>

#define _SERVER_PORT 8080
#define BACK_LOG 128
#define IP_SIZE 16
#define BUFFER_SIZE 1600

int server_net_init(void) {
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(_SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //设置本机任意IP
    // inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr);
    // //转换并设置自定义IP
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ==
        -1) {
        perror("bind Error");
        exit(0);
    }
    listen(server_fd, BACK_LOG);
    printf("TCP Server Waiting for Connect\n");
    return server_fd;
}

int server_recv_response(int sockfd) {
    char recv_buffer[BUFFER_SIZE];
    bzero(recv_buffer, BUFFER_SIZE);
    ssize_t recvlen;
    if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) {
        printf("%s", recv_buffer);
        send(sockfd, recv_buffer, recvlen, 0);
        bzero(recv_buffer, BUFFER_SIZE);
    }
    if (recvlen == -1) {
        perror("recv error");
        exit(-1);
    } else if (recvlen == 0) {
        printf("client exit\n");
        return -1;
    }
    return 0;
}

// poll模型
void poll_starting(int sockfd) {
    int readycode;                     // 就绪量
    struct pollfd listen_array[1024];  // POLL监听集合
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen;
    char client_ip[IP_SIZE];
    bzero(client_ip, IP_SIZE);

    // init
    listen_array[0].fd = sockfd;
    listen_array[0].events = POLLIN;
    for (int i = 1; i < 1024; i++) {
        listen_array[i].fd = -1;
        listen_array[i].events = POLLIN;
    }

    printf("POLL Server Running ...\n");

    while (1) {
        readycode = poll(listen_array, 1024, -1);  // 阻塞监听sockfd读取事件
        while (readycode) {                        // 循环处理就绪事件
            if (listen_array[0].revents == POLLIN) {  // server_fd就绪
                addrlen = sizeof(client_addr);
                if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr,
                                        &addrlen)) > 0) {
                    printf(
                        "TCP Server Accept Success:client ip[%s] client "
                        "prot[%d] \n",
                        inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
                                  client_ip, IP_SIZE),
                        ntohs(client_addr.sin_port));
                    for (int i = 1; i < 1024; i++) {
                        if (listen_array[i].fd == -1) {
                            listen_array[i].fd = client_fd;
                            break;
                        }
                    }
                    listen_array[0].revents = 0;
                } else {
                    perror("accept error");
                    exit(0);
                }
            } else {  // client_fd就绪
                for (int i = 1; i < 1024; i++) {
                    if (listen_array[i].fd != -1) {
                        if (listen_array[i].revents == POLLIN) {
                            if (server_recv_response(listen_array[i].fd) ==
                                -1) {
                                close(listen_array[i].fd);
                                listen_array[i].fd = -1;
                            }
                            listen_array[i].revents = 0;
                            break;
                        }
                    }
                }
            }
            --readycode;
        }
    }
}

int main(void) {
    int sfd = server_net_init();  // 服务端网络初始化
    poll_starting(sfd);           // 阻塞等到连接

    close(sfd);

    return 0;
}

POLL模型优点:

  • 针对sockfd的监听设置更为灵活,可以针对不同的sockfd设置不同的监听项,相比select可以监听的事件更为丰富,选择更多
  • poll监听大小没有select模型的1024的数量限制,但是poll采用的依然是轮询监听机制
  • poll采用events和revents两个参数将传入传出分离

POLL模型的缺点和select一致

 EPOLL模型

EPOLL模型服务器demo

#include <arpa/inet.h>  // 大小端转换
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>  // socket套接字
#include <unistd.h>

#define _SERVER_PORT 8080
#define BACK_LOG 128
#define IP_SIZE 16
#define BUFFER_SIZE 1600
#define EPOLLSIZE 1e5

int server_net_init(void) {
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(_SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //设置本机任意IP
    // inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr);
    // //转换并设置自定义IP
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ==
        -1) {
        perror("bind Error");
        exit(0);
    }
    listen(server_fd, BACK_LOG);
    printf("TCP Server Waiting for Connect\n");
    return server_fd;
}

int server_recv_response(int sockfd) {
    char recv_buffer[BUFFER_SIZE];
    bzero(recv_buffer, BUFFER_SIZE);
    ssize_t recvlen;
    if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) {
        printf("%s", recv_buffer);
        // 回传
        send(sockfd, recv_buffer, recvlen, 0);
        bzero(recv_buffer, BUFFER_SIZE);
    }
    if (recvlen == -1) {
        perror("recv error");
        exit(-1);
    } else if (recvlen == 0) {
        printf("client exit ...\n");
        return -1;
    }
    return 0;
}

// epoll模型
void epoll_starting(
    int sockfd) {  // 文件描述符, 默认情况下进程打开的最大文件描述符数量为1024
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen;
    char client_ip[IP_SIZE];
    bzero(client_ip, IP_SIZE);

    int readycode;                                   // 就绪量
    struct epoll_event ready_array[(int)EPOLLSIZE];  // 就绪等待队列

    // init server_fd
    struct epoll_event node;
    node.data.fd = sockfd;
    node.events = EPOLLIN;

    //创建监听树
    int epfd = epoll_create(EPOLLSIZE);
    int i;

    // 第一次添加节点, 添加server_fd
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &node);

    printf("EPOLL Server Running ...\n");
    while (1) {
        readycode = epoll_wait(epfd, ready_array, EPOLLSIZE,
                               -1);  // 阻塞监听sock读取事件
        while (readycode) {          // 循环处理就绪事件
            i = 0;
            // 辨别就绪
            if (ready_array[i].data.fd == sockfd) {
                addrlen = sizeof(client_addr);
                if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr,
                                        &addrlen)) > 0) {
                    printf(
                        "TCP Server Accept Success:client ip[%s] client "
                        "prot[%d] \n",
                        inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
                                  client_ip, IP_SIZE),
                        ntohs(client_addr.sin_port));
                    node.data.fd = client_fd;
                    node.events = EPOLLIN;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &node);
                } else {
                    perror("accept error");
                    exit(0);
                }
            } else {  // clientfd就绪
                if ((server_recv_response(ready_array[i].data.fd)) == -1) {
                    close(ready_array[i].data.fd);  // 关闭连接
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ready_array[i].data.fd,
                              NULL);  // 删除监听节点
                }
            }
            --readycode;
            ++i;
        }
    }
}

int main(void) {
    int sfd = server_net_init();  // 服务端网络初始化

    epoll_starting(sfd);  // 阻塞等到连接

    close(sfd);

    return 0;
}

EPOLL模型相比其他模型的优势:

  1. epoll模型没有轮询限制,理论上epoll可以监听系统上最大的描述符数量,也不会有多余的开销
  2. epollm没有多余的拷贝开销和挂载开销,保证每个监听节点只拷贝一次挂载一次
  3. epoll不仅会返回监听数量还会返回就绪节点,用户直接处理即可
  4. epoll设置监听比较灵活,可以对不同的sock设置不同的事件监听,可监听的事件也比较丰富

EPOLL监听能力较为出色,但是处理能力而言与select,poll没有区别

posted on 2022-05-01 11:08  SocialistYouth  阅读(74)  评论(0编辑  收藏  举报