epoll概念理解

epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 poll,是 Linux 下多路复用 IO接口 select/poll 的增强版本.

在 linux 的网络编程中,很长时间都在使用 select 来做事件触发。在 2.6 内核中,有一种替换它的机制,就是 epoll。
select 与 epoll 区别概述
1、函数使用上:epoll 使用一组函数来完成任务,而不是单个函数
2、效率:select 使用轮询来处理,随着监听 fd 数目的增加而降低效率。
而 epoll 把用户关心的文件描述符事件放在内核里的一个事件表中,只需要一个额外的文件描述符来标识内核中的这个事件表即可。
/*
函数名 :int epoll_create(int size)
参  数 :int size        -- 监听的数目
返回值 :int fd            -- 一个额外的文件描述符, 来标识内核事件表
说  明 :创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
*/
int epoll_create(int size);

epoll_create1() 是 Linux 系统中用于创建一个 epoll 实例 的系统调用,它是 epoll_create() 的增强版本,提供了更灵活的控制选项。
#include <sys/epoll.h>
int epoll_create1(int flags);
/*
函数名 :epoll_ctl 
参  数 :int epfd                  -- 要操作的内核事件表的文件描述符,即epoll_create 的返回值
参  数 :int op                    -- 指定操作类型,操作类型有三种:
                                    ->EPOLL_CTL_ADD:往内核事件表中注册指定fd 相关的事件
                                    ->EPOLL_CTL_MOD:修改指定 fd 上的注册事件
                                    ->EPOLL_CTL_DEL:删除指定 fd 的注册事件
参  数 :int fd                    -- 要操作的文件描述符,也就是要内核事件表中监听的fd
参  数 :struct epoll_event        -- 要监听的事件类型,epoll_event 结构指针类型。
返回值 :int ret                    -- 成功时返回 0,失败则返回 -1,并设置 errno
说  明 :epoll 的事件注册函数,用来操作内核事件表
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
函数名 :epoll_wait; 
参  数 :int epfd                   -- epoll_create 的返回值
参  数 :struct epoll_event*        -- 内核事件表中得到的检测事件集合
参  数 :int maxevents              -- 最大size
参  数 :int timeout                -- 超时时间
返回值 :int ret                    -- 成功时返回就绪的文件描述符的个数,失败返回 -1 并设置 errno
说  明 :等待事件的发生,它在一段超时时间之内等待一组文件描述符上的事件,epoll_wait 函数如果检测到事件,
        就将所有就绪的事件从内核事件表(epfd 参数决定)中复制到第二个参数 events 指向的数组中。
*/
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
 } epoll_data_t;

 struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};


Epoll events 对应的宏:

EPOLLIN:    表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:    表示对应的文件描述符可以写;
EPOLLPRI:    表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:    表示对应的文件描述符发生错误;
EPOLLHUP:    表示对应的文件描述符被挂断;
EPOLLET:     将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
 
epoll 工作模式
LT(level trigger) 模式是默认模式
  LT模式:电平触发,当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。
    下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
ET(edge trigger)
    边沿触发:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。
    如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
  ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。
    epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

单线程 epoll + ET 模式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_PORT     8000
#define MAX_EVENTS      1024            // epoll 最大监听事件数
#define BUFFER_SIZE     2048            // 缓冲区大小
#define MAX_CONNECTIONS 10000           // 最大连接数(用于 epoll_create1)

// 设置文件描述符为非阻塞
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) flags = 0;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int server_fd, epoll_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    struct epoll_event ev, events[MAX_EVENTS];

    // 1. 创建监听 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置端口复用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(SERVER_PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 1024) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 设置监听 socket 为非阻塞(accept 时需要)
    set_nonblocking(server_fd);

    // 2. 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        perror("epoll_create1 failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 将 server_fd 添加到 epoll 监听可读事件(边缘触发 ET 模式)
    ev.events = EPOLLIN | EPOLLET;        // EPOLLET: 边缘触发
    ev.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) < 0) {
        perror("epoll_ctl: server_fd add failed");
        close(server_fd);
        close(epoll_fd);
        exit(EXIT_FAILURE);
    }

    printf("epoll Server started on port %d\n", SERVER_PORT);
    printf("Listening for connections (ET mode)...\n");

    // 3. 主事件循环
    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 永久阻塞等待
        if (nfds < 0) {
            if (errno == EINTR) continue;  // 被信号中断,继续
            perror("epoll_wait failed");
            break;
        }

        // 处理所有就绪事件
        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;

            // 1. 如果是监听 socket 就绪 → 接受新连接
            if (fd == server_fd && (events[i].events & EPOLLIN)) {
                while (1) {
                    client_len = sizeof(client_addr);
                    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);

                    if (client_fd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            // 所有连接已 accept 完毕
                            break;
                        } else {
                            perror("accept error");
                            break;
                        }
                    }

                    // 设置客户端 socket 为非阻塞
                    set_nonblocking(client_fd);

                    // 将新客户端加入 epoll 监听(ET 模式)
                    ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;  // 可读 + ET + 单次通知
                    ev.data.fd = client_fd;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == 0) {
                        char client_ip[INET_ADDRSTRLEN];
                        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
                        printf("🔗 Accepted client: %s:%d (fd=%d)\n", 
                               client_ip, ntohs(client_addr.sin_port), client_fd);
                    } else {
                        perror("epoll_ctl: client_fd add failed");
                        close(client_fd);
                    }
                }
            }
            // 2. 如果是客户端 socket 就绪 → 处理数据
            else if (events[i].events & (EPOLLIN | EPOLLERR | EPOLLHUP)) {
                char buffer[BUFFER_SIZE];
                ssize_t count;

                while ((count = recv(fd, buffer, sizeof(buffer) - 1, 0)) > 0) {
                    buffer[count] = '\0';
                    printf("Received %zd bytes from fd %d: %s", count, fd, buffer);

                    // 回显
                    if (send(fd, buffer, count, 0) < 0) {
                        perror("send failed");
                        break;
                    }
                    printf("Sent %zd bytes back to fd %d\n", count, fd);
                }

                // recv 返回 0:客户端关闭连接
                if (count == 0) {
                    printf("Client fd %d disconnected.\n", fd);
                } 
                // 错误
                else if (count < 0) {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                        perror("recv error");
                    }
                }

                // 从 epoll 删除并关闭
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
                printf("Closed client fd %d\n", fd);
            }
        }
    }

    close(server_fd);
    close(epoll_fd);
    return 0;
}
posted @ 2018-11-18 21:10  osbreak  阅读(620)  评论(0)    收藏  举报