Linux下的I/O多路复用

在 I/O 多路复用中,epoll、poll 和 select 是常用的三种机制,它们都可以用于实现事件驱动的网络编程。

select

select 是 Unix 系统最早引入的 I/O 多路复用函数,它允许一个进程监视多个文件描述符,当其中任何一个文件描述符准备好进行 I/O 操作时,select 函数就会返回。

  • 优点:跨平台支持好,几乎所有的操作系统都支持 select。
  • 缺点:效率较低,因为在调用 select 函数后,内核需要遍历所有的文件描述符来检查状态变化,同时 select 函数有最大文件描述符数量的限制。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>

int main() {
    int server_socket, client_socket, max_sd, activity;
    struct sockaddr_in address;
    fd_set readfds;
    char buffer[1025];

    // 创建 TCP 套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8888);
    if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_socket, 5) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 接受连接
    client_socket = accept(server_socket, (struct sockaddr *)NULL, NULL);

    while (1) {
        FD_ZERO(&readfds);
        FD_SET(client_socket, &readfds);
        max_sd = client_socket;

        // 监视文件描述符变化
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        
        if ((activity < 0) && (errno != EINTR)) {
            printf("select error");
        }

        if (FD_ISSET(client_socket, &readfds)) {
            // 读取数据
            int valread = read(client_socket, buffer, 1024);
            if (valread == 0) {
                printf("客户端关闭连接\n");
                break;
            } else {
                buffer[valread] = '\0';
                printf("收到数据: %s\n", buffer);
            }
        }
    }

    close(server_socket);
    return 0;
}

poll

poll 是对 select 的改进,它也能够监视多个文件描述符,并在其中任何一个准备好进行 I/O 操作时返回。

  • 优点:没有最大文件描述符数量的限制,相比 select 更加灵活。
  • 缺点:效率仍然有限,因为调用 poll 函数后,内核必须遍历所有的文件描述符来检查状态变化。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <poll.h>

int main() {
    int server_socket, client_socket, i, activity;
    struct sockaddr_in address;
    struct pollfd fds[1];
    char buffer[1025];

    // 创建 TCP 套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8888);
    if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_socket, 5) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 接受连接
    client_socket = accept(server_socket, (struct sockaddr *)NULL, NULL);

    fds[0].fd = client_socket;
    fds[0].events = POLLIN;

    while (1) {
        activity = poll(fds, 1, -1);
        if (activity < 0) {
            perror("poll failed");
            break;
        }

        if (fds[0].revents & POLLIN) {
            int valread = read(client_socket, buffer, 1024);
            if (valread == 0) {
                printf("客户端关闭连接\n");
                break;
            } else {
                buffer[valread] = '\0';
                printf("收到数据: %s\n", buffer);
            }
        }
    }

    close(server_socket);
    return 0;
}

epoll

epoll 是 Linux 特有的高性能 I/O 多路复用机制,可以显著提升大规模并发连接的性能。

  • 优点:使用红黑树结构存储文件描述符,只有活跃的文件描述符才会被放入红黑树中,因此效率很高。同时,epoll 支持水平触发和边缘触发两种模式,可以更精确地控制事件通知。
  • 缺点:只能在 Linux 系统上使用,不具有跨平台特性。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

int main() {
    int server_socket, client_socket, i, activity;
    struct sockaddr_in address;
    struct epoll_event event, events[1];
    char buffer[1025];

    // 创建 TCP 套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8888);
    if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_socket, 5) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1 failed");
        exit(EXIT_FAILURE);
    }

    event.events = EPOLLIN;
    event.data.fd = server_socket;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) == -1) {
        perror("epoll_ctl failed");
        exit(EXIT_FAILURE);
    }

    while (1) {
        activity = epoll_wait(epoll_fd, events, 1, -1);
        if (activity < 0) {
            perror("epoll_wait failed");
            break;
        }

        for (i = 0; i < activity; i++) {
            if (events[i].data.fd == server_socket) {
                client_socket = accept(server_socket, (struct sockaddr *)NULL, NULL);
                event.events = EPOLLIN;
                event.data.fd = client_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) == -1) {
                    perror("epoll_ctl failed");
                    exit(EXIT_FAILURE);
                }
            } else {
                int valread = read(events[i].data.fd, buffer, 1024);
                if (valread == 0) {
                    printf("客户端关闭连接\n");
                    close(events[i].data.fd);
                } else {
                    buffer[valread] = '\0';
                    printf("收到数据: %s\n", buffer);
                }
            }
        }
    }

    close(server_socket);
    close(epoll_fd);
    return 0;
}

总的来说,select 和 poll 在处理大量并发连接时效率较低,而 epoll 则在 Linux 系统上表现更好,因此在高性能网络编程中更常用。选择合适的 I/O 多路复用机制取决于具体的应用场景和目标平台。

posted on 2024-03-20 19:51  未连接到互联网  阅读(8)  评论(0编辑  收藏  举报