IO 多路复用

本文详细介绍了I/O多路复用技术,包括selectAPI的使用、poll的特性与API示例,以及epoll的概念、原理和工作模式,展示了如何在服务器编程中利用这些机制高效管理多个连接请求。
摘要由CSDN通过智能技术生成

1. 概述

        I/O 多路复用是一种在单个线程中处理多个I/O操作的机制。

        I/O 多路复用通过操作系统提供的特定系统调用, 在一个线程中监听多个I/O事件。当其中任何一个I/O事件就绪(可读、可写或异常)时,线程就会被唤醒,并且可以针对就绪的事件执行相应的操作,而不需要阻塞等待。

1.1. select

1.2. select API

#include <sys/select.h>
struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout);

函数参数:
nfds:委托内核检测的这三个集合中最大的文件描述符 + 1
    内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件

readfds:文件描述符的集合,内核只检测这个集合中文件描述符对应的读缓冲区
    传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据

writefds:文件描述符的集合,内核只检测这个集合中文件描述符对应的写缓冲区
    传入传出参数,如果不需要使用这个参数可以指定为 NULL

exceptfds:文件描述符的集合,内核检测集合中文件描述符是否有异常状态
    传入传出参数,如果不需要使用这个参数可以指定为 NULL

timeout:超时时长,用来强制解除 select () 函数的阻塞的
    NULL:函数检测不到就绪的文件描述符会一直阻塞。
    等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回 0
    不等待:函数不会阻塞,直接将该参数对应的结构体初始化为 0 即可。
    
函数返回值:
    大于 0:成功,返回集合中已就绪的文件描述符的总个数
    等于 -1:函数调用失败
// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0        
void FD_CLR(int fd, fd_set *set);

// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);

// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);

// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);

1.3. select 流程图

1.4. select 示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_sockets[MAX_CLIENTS], max_clients = MAX_CLIENTS;
    fd_set readfds;
    int max_sd, sd, activity, i, valread;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // Create server socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Set socket options
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // Bind server socket to address and port
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(9999);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // Listen
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // Initialize client sockets
    for (i = 0; i < max_clients; i++) {
        client_sockets[i] = 0;
    }

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

        // Add client sockets to set
        for (i = 0; i < max_clients; i++) {
            sd = client_sockets[i];
            if (sd > 0) {
                FD_SET(sd, &readfds);
            }
            if (sd > max_sd) {
                max_sd = sd;
            }
        }

        // Wait for activity on any of the sockets
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            perror("select error");
        }

        // If activity on server socket, it's a new connection
        if (FD_ISSET(server_fd, &readfds)) {
            if ((sd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }

            // Add new socket to array of client sockets
            for (i = 0; i < max_clients; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = sd;
                    break;
                }
            }
        }

        // Check client sockets for activity
        for (i = 0; i < max_clients; i++) {
            sd = client_sockets[i];
            if (FD_ISSET(sd, &readfds)) {
                // If client is sending data
                if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {
                    // Client disconnected
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Host disconnected, ip %s, port %d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    // Echo received data back to client
                    send(sd, buffer, valread, 0);
                }
            }
        }
    }

    return 0;
}

        poll 实现 IO多路复用的机制本质上和 select 是一样的,那么下面我们主要从 poll 的特点、poll 的 API、以及实现一个简单的 poll 的代码示例来更好的理解并使用它。

2. poll

2.1. poll 的特点

相同点:

1. 内核对文件描述符的集合检测的方式都是线性轮询的方式

2. 文件描述符集合都需要频繁在内核和用户态之间进行拷贝

不同点:

1. 参数接口不同,参数被封装到结构体中作为 poll 的参数。

2. select 检测的文件描述符上限是 1024,poll 没有限制。

2.2. poll API

#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
    int   fd;         /* 委托内核检测的文件描述符 */
    short events;     /* 委托内核检测文件描述符的什么事件 */
    short revents;    /* 文件描述符实际发生的事件 -> 传出 */
};

struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数:
    fds: 这是一个 struct pollfd 类型的数组,里边存储了待检测的文件描述符的信息,这个数组中有三个成员:
        fd:委托内核检测的文件描述符
        events:委托内核检测的 fd 事件(输入、输出、错误),每一个事件有多个取值
        revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果

    nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数 1 数组的元素总个数)
    
    timeout: 指定 poll 函数的阻塞时长
        -1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
        0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
        大于 0:阻塞指定的毫秒(ms)数之后,解除阻塞
    
函数返回值:
	失败: 返回 - 1
	成功:返回一个大于 0 的整数,表示检测的集合中已就绪的文件描述符的总个数

2.3. poll 使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <errno.h>

#define MAXFD 1024

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(2048);
    addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sockfd);
        return -1;
    }

    listen(sockfd, 10);

    struct pollfd events[MAXFD];
    events[0].fd = sockfd;
    events[0].events = POLLIN;

    for (int i = 1; i < MAXFD; ++i) {
        events[i].fd = -1;
    }

    char buff[1024] = {0};

    while (1) {
        int readynum = poll(events, MAXFD, -1);
        if (readynum < 0) {
            perror("poll");
            break;
        }

        for (int i = 0; i < MAXFD; ++i) {
            if (events[i].revents & POLLIN) {
                if (i == 0) {
                    int clientfd = accept(sockfd, NULL, NULL);
                    if (clientfd < 0) {
                        perror("accept");
                        continue;
                    }

                    // 添加新事件
                    for (int j = 1; j < MAXFD; ++j) {
                        if (events[j].fd == -1) {
                            events[j].fd = clientfd;
                            events[j].events = POLLIN;
                            break;
                        }
                    }

                } else {
                    // 接收数据
                    int n = recv(events[i].fd, buff, sizeof(buff), 0);
                    if (n == 0) {
                        // 连接关闭
                        printf("exit fd[%d]\n", i);
                        close(events[i].fd);
                        events[i].fd = -1;
                    } else if (n < 0) {
                        // 出错
                        perror("recv");
                    } else {
                        // 正常接收到数据
                        printf("fd[%d] recv: %s\n", i, buff);
                        send(events[i].fd, buff, n, 0);
                    }
                }
            }
        }
    }

    close(sockfd);
    return 0;
}

3. epoll

3.1. 概念

        epoll 全称 eventpoll, 基于事件触发的通知机制。当文件描述符读缓冲区不为空时,触发读事件。当文件描述符写缓冲区可写时,则触发写事件。

3.2. 原理

epoll 的核心数据结构是:一个红黑树和一个链表。

1. 红黑树用于存储文件描述符,方便快速索引到文件描述符。

2. 链表负责存放准备就绪的文件描述符。

3. 当调用 epoll_wait() 函数时,仅仅观察链表中是否有数据即可。

4. 链表的数据通过mmap的传递数据,减少复制的开销。

3.3. 特点

1. 不再通过遍历的方式,查找文件描述符。

2. 只需要传递链表中准备就绪的文件描述符即可。

3. 内核和用户空间通过共享内存传递数据。

3.4. epoll API

struct epoll_event {
    uint32_t events;  // 表示关注的事件类型,可以是 EPOLLIN、EPOLLOUT、EPOLLERR、EPOLLHUP 等组合。
    epoll_data_t data; // 用户数据,可以是文件描述符或指针等。
};

typedef union epoll_data {
    void    *ptr;      // 用于指定指针类型的用户数据
    int      fd;       // 用于指定文件描述符类型的用户数据
    uint32_t u32;      // 用于指定 32 位无符号整数类型的用户数据
    uint64_t u64;      // 用于指定 64 位无符号整数类型的用户数据
} epoll_data_t;

事件宏
 EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
 EPOLLOUT: 表示对应的文件描述符可以写;
 EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
 EPOLLERR: 表示对应的文件描述符发生错误;
 EPOLLHUP: 表示对应的文件描述符被挂断;
 EPOLLET:  将EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
 EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

#include <sys/epoll.h>

int epoll_create(int size);
//作用: 创建一个新的 epoll 实例
//参数:
    size 支持的最大文件描述符数目(Linux 2.6.8 之后这个参数已经被忽略, 大于 0 即可)
//返回值:
    成功:新的 epoll 文件描述符,用于后续的 epoll 操作
    失败:-1
---------------------------------------------------------------------------------
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//作用:控制 epoll 文件描述符上的事件
//参数:
    epfd: epoll 文件描述符,由 epoll_create 返回
    op: 指定操作类型:
        EPOLL_CTL_ADD //添加事件
        EPOLL_CTL_MOD //修改事件
        EPOLL_CTL_DEL //删除事件
    fd: 需要关注的文件描述符
    event:指定事件类型和其他控制参数的结构体
//返回值:
    成功:0
    失败: -1
---------------------------------------------------------------------------------
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//作用:等待 epoll 文件描述符上的事件发生
//参数:
    epfd: epoll 文件描述符,由 epoll_create 返回
    events: 用于存放发生事件的结构体数组
    maxevents: 数组的大小,即最多处理多少个事件
    timeout: 等待事件的超时时间,以毫秒为单位。-1 阻塞、0 非阻塞
//返回值:
    成功:发生事件的文件描述符数量
    失败: -1
---------------------------------------------------------------------------------

3.5. epoll 的工作模式

3.5.1. ET 边缘触发

3.5.2. 工作原理

当被监听的文件描述符上的事件状态发生变化时,epoll 只通知一次,直到下一次状态变化再次通知。

也就是说,只有在文件描述符状态由无数据可读/写 变为有数据可读/写时才通知。

3.5.3. 设置方法

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 监听可读事件的边缘触发
// 或
ev.events = EPOLLOUT | EPOLLET; // 监听可写事件的边缘触发

3.5.4. 适用场景

1. 服务器监听 socket 上的连接,当有新连接到来时,epoll_wait 返回一次可读事件,服务器读取数据直至缓冲区为空,再次触发需要新的可读事件。

2. 从文件中读取数据到内存缓冲区,当文件可读时,epoll_wait 返回一次可读事件,读取文件内容直至读完,再次触发需要新的可读事件。

3. 通过管道进行进程间通信,当管道可读时,epoll_wait 返回一次可读事件,读取管道内容直至为空,再次触发需要新的可读事件.

3.6. LT 水平触发模式

3.6.1. 工作原理

当被监听的文件描述符上的事件状态发生变化时,epoll 会一直通知,直到状态恢复为无数据可读/写。

也就是说,只要文件描述符上有数据可读/写,就会一直通知。

3.6.2. 设置方法

struct epoll_event ev;
ev.events = EPOLLIN;  // 监听可读事件的水平触发
// 或
ev.events = EPOLLOUT; // 监听可写事件的水平触发

3.6.3. 适用场景

1. 一个多线程程序,每个线程负责处理一个文件的数据。主线程通过 epoll_wait 监听文件描述符上的可读事件,然后将任务派发给空闲的工作线程。线程读取文件数据直至完成,然后继续监听可读事件。

3.7. 代码示例

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

#define MAX_EVENTS 10
#define MAX_BUFF_SIZE 1024

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(2048);
    addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sockfd);
        return -1;
    }

    listen(sockfd, 10);

    // 创建 epoll 句柄
    int epollfd = epoll_create(MAX_EVENTS);
    if (epollfd == -1) {
        perror("epoll_create");
        close(sockfd);
        return -1;
    }

    // 添加监听套接字 sockfd 到 epoll
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = sockfd;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
        perror("epoll_ctl");
        close(epollfd);
        close(sockfd);
        return -1;
    }

    struct epoll_event events[MAX_EVENTS];

    char buff[MAX_BUFF_SIZE];

    while (1) {
        // 监听事件
        int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == sockfd) {
                // 有新连接
                int clientfd = accept(sockfd, NULL, NULL);
                if (clientfd == -1) {
                    perror("accept");
                    continue;
                }

                // 添加新连接到 epoll
                event.events = EPOLLIN;
                event.data.fd = clientfd;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &event) == -1) {
                    perror("epoll_ctl");
                    close(clientfd);
                    continue;
                }
            } else {
                // 已连接套接字可读
                int n = recv(events[i].data.fd, buff, sizeof(buff), 0);
                if (n <= 0) {
                    if (n == 0) {
                        // 连接关闭
                        printf("exit fd[%d]\n", events[i].data.fd);
                    } else {
                        // 出错
                        perror("recv");
                    }

                    // 从 epoll 中移除套接字
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    close(events[i].data.fd);
                } else {
                    // 正常接收到数据
                    printf("fd[%d] recv: %s\n", events[i].data.fd, buff);
                    send(events[i].data.fd, buff, n, 0);
                }
            }
        }
    }

    close(epollfd);
    close(sockfd);
    return 0;
}


 

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值