【Linux】I/O多路转接epoll

本文深入解析epoll的工作原理,探讨其相较于select和poll的优势。包括epoll的核心函数介绍、两种工作模式及其底层实现机制。

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

epoll函数

epoll的三个操作函数:

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

1. int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占⽤一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调⽤用close()关闭,否则可能导致fd被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,⽽是在这里先注册要监听的事件类型.

  • epfd:是epoll_create()的返回值。
  • op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
  • fd:是需要监听的fd(文件描述符)
  • epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:

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

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

等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
  LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
  
1、LT模式
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

2、ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

epoll底层实现

epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。
执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

epoll与poll和select相比

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

epoll的改进

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。

看select的缺点I/O多路转接select
epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。来解决这个问题

 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,**而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。**epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

实现一个epoll模型
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

//events.data.ptr的指向
typedef struct fd_buff
{
    int fd; //保存文件描述符
    char buf[10240];//读写信息

}fd_buff_t, *fd_buff_p;

//创建一个struct fd_buff
static void *alloc_fd_buf(int fd)
{
    fd_buff_p tmp = (fd_buff_p)malloc(sizeof(fd_buff_t));
    if(!tmp)
    {
        perror("malloc");
        return NULL;
    }
    tmp->fd = fd;
    return tmp;
}

//获取listen_sock
int get_listen(char *ip, short port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock <0)
    {
        perror("socket");
        exit(2);
    }
    int opt =1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr =inet_addr(ip);

    //bind
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(3);
    }
    //listen
    if(listen(sock,10)<0)
    {
        perror("listen");
        exit(4);
    }

    return sock;
}


int  epoll_server(int listen_sock)
{
    //1创建epoll模型
    int epoll_fd = epoll_create(256);

    if(epoll_fd < 0)
    {
        perror("epoll_create");
        close(listen_sock);
        return 5;
    }
    //2 添加监听读事件,
    struct epoll_event ev;//要监听的事件
    ev.events = EPOLLIN;
    ev.data.ptr = alloc_fd_buf(listen_sock);

    if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ev)<0)//事件注册函数
    {
        perror("epoll_ctl");
        return 6;
    }

    int nums = 0;
    //int timeout = -1;//?阻塞式等待
    int timeout = 1000;
    struct epoll_event ready_evs[64];

    while(1)
    {
        switch(nums = epoll_wait(epoll_fd,ready_evs,64,timeout))
        {
            case 0:
                printf("timeout....\n");
                break;
            case -1:
                perror("epoll_wait");
                break;
            default:
                {
                    //监听事件已经就绪
                    int i =0;
                    for(; i < nums; ++i)
                    {
                        fd_buff_p fp = (fd_buff_p)ready_evs[i].data.ptr;

                        //listen_sock监听读事件就绪
                        if(fp->fd == listen_sock && \
                                (ready_evs[i].events & EPOLLIN))
                        {
                            struct sockaddr_in client;
                            socklen_t len = sizeof(client);
                            int new_sock = accept(listen_sock,\
                                    (struct sockaddr*)&client,&len);
                            if(new_sock < 0)
                            {
                                perror("accept");
                                continue;
                            }
                            printf("get a client:%s %d\n",\
                    inet_ntoa(client.sin_addr),ntohs(client.sin_port));

                            //将new_sock的写事件添加到模型中
                            ev.events = EPOLLIN;
                            ev.data.ptr = alloc_fd_buf(new_sock);
                            epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&ev);

                        }
                        else if(fp->fd != listen_sock)
                        {
                            //其他监听文件描述符 读事件就绪               
                            if(ready_evs[i].events & EPOLLIN)
                            {
                                ssize_t s = read(fp->fd,fp->buf,sizeof(fp->buf));
                                if(s >0)
                                {
                                    fp->buf[s] = 0;
                                    printf("client say#:%s",fp->buf);
                                    //将其修改为写事件
                                    ev.events = EPOLLOUT;
                                    ev.data.ptr = fp;
                                    epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fp->fd,&ev);

                                }

                                else if(s <= 0)
                                {
                                    printf("client quit\n");
                                    close(fp->fd);
                                    epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fp->fd,NULL);
                                    free(fp);
                                }
                            }                       
                            else if(ready_evs[i].events & EPOLLOUT)
                            {
                                //其他监听文件描述符 写事件就绪
                                char * msg = "HTTP/1.0 200 OK\r\n\r\n<html><h2>hello epoll! </h2></html>";
                                write(fp->fd,msg,strlen(msg));
                                close(fp->fd);
                                epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fp->fd,NULL);
                                free(fp);
                            }
                        }
                    }
                }
             break;
        }
    }
    return 8;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        printf("Usge:%s [ip] [port]\n",argv[0]);
        return 1;
    }
    int listen_sock = get_listen(argv[1],atoi(argv[2]));

    epoll_server(listen_sock);
    return 0;
}
客户端浏览器连接服务器

这里写图片描述

这里写图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值