IO多路复用之epoll

epoll

epoll的解释:当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制。

  1. 创建文件描述符
#include<sys/epoll.h>
int epoll_create(int size)
// size 表示要创建这个内核事件表的大小
  1. 操作内核事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event))
fd:要操作的文件描述符
op:操作方法
event:指定事件
struct epoll_event{
	__uint32_t events;  //  epoll事件
	epoll_data_t data;  //  用户数据
}

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
void *ptr:一个指针,可用于存储任何用户指定的数据类型的地址。
int fd:一个整数,通常用于存储文件描述符。指定事件所从属的目标文件描述符
uint32_t u32:一个32位无符号整数,用于存储用户指定的32位整数。
uint64_t u64:一个64位无符号整数,用于存储用户指定的64位整数
  1. 在超时事件内等待一组文件描述符上的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
  
// -------------------------------------
epfd:epoll 实例的文件描述符。
events:用于存储发生事件的 struct epoll_event 结构体数组。
maxevents:events 数组的大小,即最多返回多少个事件。
timeout:等待事件的超时时间,单位是毫秒。传递 -1 表示永久等待,传递 0 表示立即返回,传递正整数表示等待指定的毫秒数。

epoll_wait如果监听到事件,则将事件表中的所有就绪事件从内核事件表(由epfd指定)复制到由events指向的数组中

epoll对文件描述符的操作方式

ET:边缘触发(注册EPOLLET事件时)
而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait 调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数, 因此效率要比LT模式高。

  • ET是一次事件只会触发一次
  • socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
  • socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
  • 仅在状态变化时触发事件

LT:电平触发(默认工作模式)
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait 还会再次向应用程序通告此事件,直到该事件被处理。

  • LT是一次事件会触发多次
  • socket接收缓冲区不为空 有数据可读 读事件一直触发
  • socket发送缓冲区不满 可以继续写入数据 写事件一直触发

EPOLLONESHONT事件

一个线程读取一个socket上的数据,读取后处理但是socket上又有新的数据,然后会有另一个线程来读,就会造成两个线程读取一个socket
对注册该事件的fd,操作系统最多触发其注册的一个可读,可写或者异常事件,只触发一次。一旦socket被某个线程处理完毕,该线程就会立刻重置socket上的该事件,则下次可读时又会触发该事件。

代码案例

服务端功能 : 将接收的客户端的数据逆转发送给所有连接的客户端,采用ET模式,及对fd注册EPOLLET事件,不注册的话默认采用LT,同样的在注册事件是可以注册EPOLLONESHOT事件。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cassert>
#include<cstring> 
#include<algorithm>
#include<unistd.h>

#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>

constexpr int MAX_EVENTS = 10;

int main(){

    // 1. 创建服务器Socket
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置端口复用
    int opt = 1;
    if(setsockopt(server_socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) == -1){
        perror("socket opt failed");
        exit(EXIT_FAILURE);
    }

    // 3. 设置服务器端口号和ip
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // 4. 绑定ip和端口号
    if(bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1){
        perror("binding failed");
        exit(EXIT_FAILURE);
    }

    // 5. 监听
    if(listen(server_socket, 5) == -1){
        perror("listening failed");
        exit(EXIT_FAILURE);
    }


    // 6. epoll创建内核事件表
    int epoll_fd = epoll_create1(0);
    if(epoll_fd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    std::vector<int> clientSockets; //  存放所有客户端的socket

    // 7. 添加服务器Socket到epoll对象中, 并设置为边缘触发方式(只有epoll独有)
    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = server_socket;

    // 8. 将该事件注册到该内核事件表
    if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) == -1){
        perror("epoll_ctl");
        exit(EXIT_FAILURE);
    }

    while(true){
        struct epoll_event events[MAX_EVENTS];
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);  // 监听是否有事件
        if (num_events == -1) {
            perror("epoll_wait failed");
            exit(EXIT_FAILURE);
        }

        // 遍历到达的事件(和poll,select的差别)
        for(int i = 0; i < num_events; i++){

            // 新的客户端连接
            if(events[i].data.fd == server_socket){
                struct sockaddr_in client_addr;
                socklen_t client_len = sizeof(client_addr);
                int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_len);
                if (client_socket == -1) {
                    perror("Accept failed");
                    exit(EXIT_FAILURE);
                }

                std::cout << "New connection accepted from [" << inet_ntoa(client_addr.sin_addr)<<"]"<< std::endl;
                
                // 将新的客户端Socket添加到 epoll对象(并设置ET模式)
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_socket;

                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) == -1){
                    perror("epoll_ctl");
                    exit(EXIT_FAILURE);
                }

                clientSockets.push_back(client_socket);

            }
            else{

                // 读取客户端数据
                char buffer[2048];
                ssize_t byteRead = read(events[i].data.fd, buffer, sizeof(buffer)-1);

                if(byteRead <= 0){
                    // 客户端断开连接
                    if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr) == -1){
                        perror("epoll_ctl");
                        exit(EXIT_FAILURE);
                    }

                    close(events[i].data.fd);
                }
                else{
                    buffer[byteRead] = '\0';
                    std::cout<<"receive from ["<<events[i].data.fd<<" ] data is : "<<buffer<<std::endl;

                    // 发送数据给所有的客户端
                    std::string s(buffer);
                    std::reverse(s.begin(), s.end());
                    for(const auto &client : clientSockets){
                        std::cout<<"["<<client<<"] send data"<<std::endl;
                        send(client, s.data(), s.size(), 0);
                    }
                }
            }
        }
    }
  
}

客户端

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<cassert>

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>


int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd != -1);

    struct sockaddr_in saddr;
    memset(&saddr,0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port=htons(8080);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = connect(sockfd,(struct sockaddr*)&saddr, sizeof(saddr));
    assert(res != -1);

    while(true){

		std::cout<<"input you want to send message ...."<<std::endl;
		std::string msg;
		std::getline(std::cin, msg);

		if(msg == "end") break;
		send(sockfd, msg.data(), msg.size(), 0);  // recv的msg最大就是send的msg的大小
		recv(sockfd, msg.data(), msg.size(), 0);
		
		std::cout<<"receive data is : "<<msg<<std::endl;
	}

    close(sockfd);
}

三种IO多路复用区别

这 3 组 系 统 调 用 都 能 同 时 监 听 多 个 文 件 描 述 符 ,它 们 将 等 待 由 timeout 参 数 指 定 的 超 时 时 间 , 直 到 一 个 或 者 多 个 文 件 描 述 符 上 有 事 件 发 生 时 返 回 , 返 回 值 是 就 绪 的 文 件 描 述 符 的 数 量 , 返 回 0 表 示 没 有 事 件 发 生 。

我 们 从 事 件 集 、 最 大 支 持 文 件 描 述 符 数 、 工 作 模 式 和 具 体 实 现 等 四 个 方 面 进 一 步 比 较 它 们 的 异 同 , 以 明 确 在 实 际 应 用 中 应 该 选 择 使 用 哪 个 ( 或 哪 些 )。

事件集

这 3 组 函 数 都 通 过 某 种 结 构 体 变 量 来 告 诉 内 核 监 听 哪 些 文 件 描 述 符 上 的 哪 些 事 件 , 并 使 用 该 结 构 体 类 型 的 参 数 来 获 取 内 核 处 理 的 结 果 ,

select 的 参 数 类 型 fd_set 没 有 将 文 件 描 述 符 和 事 件 绑 定 , 它 仅 仅 是 一 个 文 件 描 述 符 集 合, 因 此 select 需 要 提 供 3 个 这 种 类 型 的 参 数 来 分 别 传 人 和 输 出 可 读 、 可 写 及 异 常 等 事 件 。 这 一 方 面 使 得 select 不 能 处 理 更 多 类 型 的 事 件 , 另 一 方 面 由 于 内 核 对 fd_set 集 合 的 在 线 修 改 , 应 用 程 序 下 次 调 用 select 前 不 得 不 重 置 这 3 个 fd 集 合 。

poll 的 参 数 类 型 pollfd 则 多 少 “ 聪 明 ” 一 些 。 它 把 文 件 描 述 符 和 事 件 都 定 义 其 中 , 任 何 事 件 都 被 统 一 处 理, 从 而 使 得 编 程 接 口 简 洁 得 多 。 并 且 内 核 每 次 修 改 的 是 pollfd 结 构 体 的 revents 成 员 , 而 events 成 员 保 持 不 变, 因 此 下 次 调 用 poll时 应 用 程 序 无 须 重 置 pollfd 类 型 的 事 件 集 参 数 ,由 于 每 次 和 poll 调 用 都 返 回 整 个 用 户 注 册 的 事 件 集 合 ( 其 中 包 括 就 绪 的 和 未 就 绪 的 ) , 所 以 应 用 程 序 索 引 就 绪 文 件 描 述 符 的 时 间 复 杂 度 为 0 (n)。

epoll 则 采 用 与 和 poll 完 全 不 同 的 方 式 来 管 理 用 户 注 册 的 事 件 。 它 在 内 核 中 维 护 一 个 事 件 表 , 并 提 供 了 一 个 独 立 的 系 统 调 用 epoll_ctl 来 控 制 往 其 中 添 加 、 删 除 、 修 改 事 件 。 这 样 , 每 次 epoll_ wait 调 用 都 直 接 从 该 内 核 事 件 表 中 取 得 用 户 注 册 的 事 件 , 而 无 须 反 复 从 用 户 空 间 读 入 这 些 事 件 ,epoll_wait 系 统 调 用 的 events 参 数 仅 用 来 返 回 就 绪 的 事 件 , 这 使 得 应 用 程 序 索 引 就 绪 文 件 描 述 符 的 时 间 复 杂 度 达 到 O(1) 。

最 大 支 持 文 件 描 述 符 数

poll 和 epoll_wait 分 别 用 nfds 和 maxevents 参 数 指 定 最 多 监 听 多 少 个 文 件 描 述 符 和 事 件 。 这 两 个 数 值 都 能 达 到 系 统 允 许 打 开 的 最 大 文 件 描 述 符 数 目 。 即 65535 (cat/proc/sys/fs/file- max)。而 select 允 许 监 听 的 最 大 文 件 描 述 符 数 量 通 常 有 限 制(一般是1024) 。 虽 然 用 户 可 以 修 改 这 个 限 制 , 但 这 可 能 导 致 不 可 预 期 的 后 果 。

工 作 模 式

select和poll都 只 能 工 作 在 相 对 低 效 的 LT 模 式 , 而 epoll则 可 以 工 作 在 ET 高 效 模 式 。 并 且 epoll 还 支 持 EPOLLONESHOT 事 件 。 该 事 件 能 进 一 步 减 少 可 读 、 可 写 和 异 常 等 事 件 被 触 发 的 次 数

具 体 实 现

从 实 现 原 理 上 来 说 , select 和 poll 采 用 的 都 是 轮 询 的 方 式 , 即 每 次 调 用 都 要 扫 描 整 个 注 册 文 件 描 述 符 集 合 , 并 将 其 中 就 绪 的 文 件 描 述 符 返 回 给 用 户 程 序 , 因 此 它 们 检 测 就 绪 事 件 的 算 法 的 时 间 复 杂 度 是 O (n)
epoll_wait 则 不 同 , 它 采 用 的 是 回 调 的 方 式 。 内 核 检 测 到 就 绪 的 文 件 描 述 符 时 , 将 触 发 回 调 数 ,回 调 函 数 就 将 该 文 件 描 述 符 上 对 应 的 事 件 插 人 内 核 就 绪 事 件 队 列 。 内 核 最 后 在 适 当 的 时 机 将 该 就 绪 事 件 队 列 中 的 内 容 拷 贝 到 用 户 空 间 。 因 此 epoll_wait 无 须 轮 询 整 个 文 件 描 述 符 集 合 来 检 测 哪 些 事 件 已 经 就 绪 , 其 算 法 时 间 复 杂 度 是 O(1)。 但 是 , 当 活 动 连 接 比 较 多 的 时 候 , epoll_watt 的 效 率 未 必 比 select 和 poll 高 , 因 为 此 时 回 调 函 数 被 触 发 得 过 于 頻 繁 。 所 以 epoll_wait 适 用 于 连 接 数 量 多 , 但 活 动 连 接 较 少 的 情 况 。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值