epoll函数及原理

epoll是Linux内核提供的一种高效处理大批量文件描述符的机制,它是select/poll的增强版本。epoll提供了水平触发和边缘触发两种模式,且在内核中通过红黑树和准备就绪链表来维护句柄。epoll_ctl用于注册和管理监听事件,epoll_wait则用于等待并返回就绪事件。这种设计提高了在高并发场景下的系统CPU利用率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

epoll是Linux内核为处理大批量文件描述符而作了改进的epoll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll的接口如下:
1. int epoll_create(int size);
创建一个epoll的 句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll 后,必须调用close()关闭,否则可能导致fd被耗尽。

  1. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件,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);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。

epoll使用实例(从客户端收取消息):

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <poll.h>
#include <sys/epoll.h>

#define SIZE 100


int start_up(sockaddr_in &addr){

    int socketi = socket(PF_INET, SOCK_STREAM, 0);
    if (socketi < 0){
        printf("%d, %s\n", errno, strerror(errno));
        exit(1);
    }

    int flag = bind(socketi, (sockaddr*)&addr, sizeof(addr));
    if (flag < 0){
        printf("%d, %s\n", errno, strerror(errno));
        exit(2);
    }

    flag = listen(socketi, 5);
    if (flag < 0){
        printf("%d, %s\n", errno, strerror(errno));
        exit(2);
    }
    return socketi;
}//start_up



int main(int argc, char *argv[]){

    if (argc < 3 ){
        perror("can su error");
        exit(0);
    }

    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_family = PF_INET;


    int socketi = start_up(addr);
    /////////////////////////////////

    int epfd = epoll_create(1024);
    if (epfd < 0){
        perror("epfd error");
        exit(4);
    }
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = socketi;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD,  socketi,  &event) < 0){
        printf("%d, %s\n", errno, strerror(errno));
        exit(6);
    }

    int maxevents = 1;
    int timeout = 2000;

    struct sockaddr_in client;
    socklen_t addr_len = sizeof(client);

    char buf[100] = {0};
    while (1){

           struct epoll_event things[SIZE];
           int ret = epoll_wait(epfd, things, maxevents, timeout);

            for ( int i = 0; i < ret; i++){

                if (things[i].data.fd == socketi){

                        int connfd = accept(socketi,(sockaddr*)&client, &addr_len);
                        if (connfd < 0){
                            printf("%d, %s\n", errno, strerror(errno));
                            continue;
                        }//if
                        event.data.fd = connfd;

                        if (epoll_ctl(epfd, EPOLL_CTL_ADD,connfd,  &event) < 0){
                            printf("%d, %s\n", errno, strerror(errno));
                            close(connfd);
                        }
                        maxevents++;
                        printf("加入一个\n");
                        continue;
                }//if


                ssize_t size = read(things[i].data.fd, buf, 20);
                if (size <= 0){
                    event.data.fd = things[i].data.fd;
                   if ( epoll_ctl(epfd, EPOLL_CTL_DEL, things[i].data.fd, &event) < 0){
                        printf("del error\n");
                   }

                    maxevents--;
                    printf("client is quit\n");
                }//if

                else{
                    printf("client:%s\n", buf);
                }//else

            }//for




    }//while





    return 0;
}//main

以上信息参考:
http://blog.chinaunix.net/uid-24517549-id-4051156.html
http://blog.youkuaiyun.com/hdutigerkin/article/details/7517390
http://www.cnblogs.com/panfeng412/articles/2229095.html
http://blog.chinaunix.net/uid-17299695-id-3059110.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值