I/O多路复用-epoll

epoll

​ epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率[1]。剩下的部分是对man手册的翻译.

简介

I/O event notification facility I/O 事件通知工具

epoll API监视多个文件描述符以查看是否可以在其中任何一个上进行 I/O操作。 epoll API 可以用作边缘触发或水平触发的接口,并且可以很好地扩展到大量监视的文件描述符。 提供了以下系统调用来创建和管理 epoll 实例:

  • epoll_create(2) 创建一个新的 epoll 实例并返回一个引用该实例的文件描述符。
  • 通过 epoll_ctl(2) 注册需要被监视的文件描述符。 注册在 epoll 实例上的文件描述符的集合有时称为 epoll 集。
  • epoll_wait(2) 等待 I/O 事件,如果当前没有可用的事件,则阻塞调用线程。

水平触发和边缘触发

epoll 事件分发接口既可以作为边缘触发(edge-triggered, ET),也可以作为级别触发(level-triggered , LT)。

边缘触发模式仅在受监视的文件描述符发生更改时才传递事件。向被监听文件描述符对应的输入缓冲区写入数据后会产生可读事件, epoll_wait(2) 会返回,如果读操作没有消耗整个缓冲区数据, 那么下次epoll_wait(2)可能会永久阻塞. 使用 EPOLLET(边缘触发)标志将rfd文件描述符添加到 epoll 接口.
使用 epoll 作为边缘触发接口的建议方法:使用非阻塞文件描述符和仅在 read(2)write(2) 返回 EAGAIN 后等待事件

水平触发模式是只要读缓冲区有内容或写缓冲区有空间epoll_wait(2) 会返回. 默认使用这种模式.

epoll_create(2)

#include <sys/epoll.h>

int epoll_create(int size);

方法介绍
epoll_create()创建一个新的 epoll(7) 实例。 从 Linux 2.6.8 开始,size 参数被忽略,但必须大于零;
epoll_create() 返回一个引用新 epoll实例的文件描述符。 此文件描述符用于所有后续对 epoll 接口的调用。 当不再需要时,应使用 close(2) 关闭 epoll_create() 返回的文件描述符。 当所有引用 epoll 实例的文件描述符都已关闭时,内核会销毁该实例并释放相关资源以供重用。

返回值
成功时,这些系统调用返回一个非负的文件描述符。 出错时,返回 -1,并设置 errno 以指示错误。

epoll_ctl(2)

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

方法介绍
​ 这是一个系统调用. 该系统调用对文件描述符 epfd 引用的 epoll(7) 实例执行控制操作。控制操作是在 epoll(7) 实例上对目标文件描述符 fd 执行操作op

参数

epfd: epoll(7) 实例对应的文件描述符

op: 表示被操作的文件描述符fd是添加到epoll, 还是从epoll实例上移除, 还是修改epoll实例对文件描述符fd监听的事件. 分别对应下面三个

//在文件描述符epfd所引用的epoll实例上注册目标文件描述符fd,并将事件事件与链接到 fd 的内部文件相关联。
EPOLL_CTL_ADD  
//更改与目标文件描述符 fd 关联的事件事件。
EPOLL_CTL_MOD
//从 epfd 引用的 epoll 实例中移除(注销)目标文件描述符 fd。该事件被忽略并且可以为 NULL。
EPOLL_CTL_DEL

struct epoll_event *event : epoll实例要监听文件描述符fd的哪些事件

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_event结构体中的events 成员是一个位掩码,通过位的或运算or(|) 将以下零个或多个可用事件类型组合在一起:
EPOLLIN 关联文件fd可用于 read(2) 操作。
EPOLLOUT 关联文件fd可用于 write(2) 操作。
EPOLLPRI 文件描述符存在异常情况。请参阅 poll(2) 中对 POLLPRI 的讨论。

epoll_wait(2)

等待epoll实例的I/O事件

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

方法介绍

epoll_wait() 系统调用等待文件描述符 epfd 引用的 epoll(7) 实例上的事件。事件指向的内存区域将包含可供调用者使用的事件。

参数

epfd : epoll实例的文件描述符

maxevents : epoll_wait() 最多返回 maxeventsmaxevents 参数必须大于零。

timeout : 指定epoll_wait()将阻塞的毫秒数。传入 -1 epoll_wait()无限期阻塞直到epoll管理的fd可读或可写,而指定超时时间为零会导致epoll_wait()立即返回,即使没有可用的事件。传入其他值该方法将阻塞,直到:

* 一个文件描述符传递一个事件;
* 调用被信号处理程序中断;或者
* 超时到期。

events : I/O准备好的文件描述符将在方法返回时添加到这个数组中

返回值
成功时,epoll_wait() 返回为I/O准备好的文件描述符的数量,如果在请求的超时毫秒内没有文件描述符准备好,则返回零。 发生错误时,epoll_wait() 返回 -1 并正确设置 errno

代码实例

/*************************************************************************
	使用epoll实现的服务端
    > File Name: epoll_server.c
	> Author: 
	> Mail: 
	> Created Time: Sat 31 Jul 2021 09:55:45 AM CST
 ************************************************************************/

#include <stdio.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include "../log/log.h"
#define MAX_EVENTS 50
#define BUF_SIZE 1024

//创建接受链接的socket
int createListenSocket() {
    //创建socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    bzero(&addr, sizeof addr);
    addr.sin_port = htons(8000);
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_family = AF_INET;
    //绑定
    int restBind = bind(lfd, (const struct sockaddr *) &addr, sizeof(addr));
    if (restBind < 0) {
        close(lfd);
        perror(strerror(errno));
        exit(EXIT_FAILURE);
    }
    int opt = 1;
    //设置为可复用
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    socklen_t len = sizeof(addr);
    //监听
    int restListen = listen(lfd, 128);
    if (restListen < 0) {
        close(lfd);
        perror(strerror(errno));
        exit(EXIT_FAILURE);
    }
    return lfd;
}

int main() {

    LOG(GREEN"Debug\n");
    int lfd = createListenSocket();

    struct epoll_event ev, events[MAX_EVENTS];

    //创建epoll实例
    int efd = epoll_create(1);
    if (efd < 0) {
        exit(EXIT_FAILURE);
    }
    //设置为非阻塞
    int old_option = fcntl(lfd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(lfd, F_SETFL, new_option);
    //添加到epoll实例
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = lfd;
    epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &ev);
    while (1) {
        int ret = epoll_wait(efd, events, MAX_EVENTS, -1);
        if (ret < 0) {
            exit(EXIT_FAILURE);
        }
        for (int i = 0; i < ret; ++i) {
            struct epoll_event event = events[i];
            int fd = event.data.fd;
            if (event.data.fd == lfd) {
                //有新的链接
                struct sockaddr_in cliaddr;
                bzero(&cliaddr, sizeof cliaddr);
                socklen_t len = sizeof(cliaddr);
                //获取到为新链接创建的socket的文件描述符
                int cfd = accept(event.data.fd, (struct sockaddr *) &cliaddr, &len);
                char buf[16] = {0};
                //printf("accept new connect, ip:%s, port:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, buf, 16),ntohs(cliaddr.sin_port));
                //设为非阻塞
                int oldOpt = fcntl(cfd, F_GETFL);
                int newOpt = oldOpt | O_NONBLOCK;
                fcntl(cfd, F_SETFL, newOpt);

                struct epoll_event ev;
                ev.data.fd = cfd;
                ev.events = EPOLLIN | EPOLLET;
                //添加epoll实例
                epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
            } else {
                if(!event.events & EPOLLIN){
                    continue;
                }
                char buf[BUF_SIZE];
                while (1) {
                    memset(buf, 0, BUF_SIZE);
                    int len = recv(event.data.fd, buf, BUF_SIZE - 1, 0);
                    if (len < 0) {
                        //在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable
                        //EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
                        if ((errno == EAGAIN) || errno == EWOULDBLOCK) {
                            //printf("-------\n\n");
                            break;
                        }
                        perror("recv ");
                        exit(EXIT_FAILURE);
                    } else if (len == 0) {
                        if(epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL)==-1){
                            printf("file descriptor:%d\n",event.data.fd);
                            perror("EPOLL_CTL_DEL ");
                            exit(1);
                        }
                        close(event.data.fd);
                        break;
                    } else {
                        printf("%s", buf);
                        write(fd,buf,len);
                        //printf("file descriptor %d get %d bytes of content: %s", event.data.fd,  len, buf);
                    }
                }
            }
        }
    }
    return 0;
}

参考文章:
[1]百度百科

posted @ 2021-08-08 21:12  hello_zhao  阅读(167)  评论(0)    收藏  举报