网络编程:epoll

原理

select 的几个缺点:
1)每次调用select,都需要把fd集合从用户空间拷贝到内核空间,这个开销在fd很多时会很大
2)每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也会很大
3)select支持的文件描述符数量太小了,默认是1024

在调用接口上,select和poll都只提供了一个函数——select或者poll函数。而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(就绪链表是否为空)。
对于第三个缺点,epoll没有这个限制,它所 支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,具体可以cat /proc/sys/fs/file-max查看,在1GB内存的机器上大约是10万左右。

epoll的回调机制:

/* 
 * This is the callback that is used to add our wait queue to the 
 * target file wakeup lists. 
 */  
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,  
                 poll_table *pt)  
{  
    struct epitem *epi = ep_item_from_epqueue(pt);  
    struct eppoll_entry *pwq;  
  
    if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {  
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);  
        pwq->whead = whead;  
        pwq->base = epi;  
        add_wait_queue(whead, &pwq->wait);  
        list_add_tail(&pwq->llink, &epi->pwqlist);  
        epi->nwait++;  
    } else {  
        /* We have to signal that an error occurred */  
        epi->nwait = -1;  
    }  
}  

其中init_waitqueue_func_entry的实现如下:

static inline void init_waitqueue_func_entry(wait_queue_t *q,  
                    wait_queue_func_t func)  
{  
    q->flags = 0;  
    q->private = NULL;  
    q->func = func;  
}  

可以看到,总体上和select实现是类似的,只不过它是创建了一个epoll_entry结构pwq,pwq->wait的func成员被设置成了回调函数ep_poll_callback(而不是default_wake_function,所以这里并不会有唤醒操作而只是执行回调函数),private成员被设置成了NULL。最后把pwq->wait链入到whead中(也就是设备等待队列中)。这样,当设备等待队列中的进程被唤醒时,就会调用ep_poll_callback了。

epoll的流程:
当epoll_wait时,它会判断就绪链表中有没有就绪的fd,如果没有,则把current进程加入到一个等待队列(file->private_data->wq)中,并在一个while(1)循环中判断就绪队列是否为空,并结合schedule_timeout实现睡一会。如果current进程在睡眠中,设备就绪了,就会调用回调函数。在回调函数中,会把就绪的fd放到就绪链表,并唤醒等待队列(file->private_data->wq)中的current进程,这样epoll_wait又能继续执行下去了。

API

epoll 不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更为强劲的 edge-triggered(边缘触发)机制
使用 epoll 进行网络程序的编写,需要三个步骤,分别是 epoll_createepoll_ctl epoll_wait

  • epoll_create:用于创建一个epoll实例
int epoll_create(int size);
int epoll_create1(int flags); 
返回值: 若成功返回一个大于0的值,表示epoll实例;若返回-1表示出错

size参数:用来告知内核期望监控的文件描述字大小,然后内核使用这部分的信息来初始化内核数据结构。现在,对size设置为一个大于0的整数就 可以
flags参数:输入flags为0,则和epoll_create一样,内核自动忽略

  • epoll_ctl :往这个epoll实例中添加删除监控的事件
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
        返回值: 若成功返回0;若返回-1表示出错

第一个参数epfd:调用epoll_create创建的epoll实例描述字,可简单理解为epoll的句柄
第二个参数op:表示增加/删除一个监控事件,有三个选项可供选择:

  • EPOLL_CTL_ADD: 向 epoll 实例注册文件描述符对应的事件;
  • EPOLL_CTL_DEL:向 epoll 实例删除文件描述符对应的事件;
  • EPOLL_CTL_MOD: 修改文件描述符对应的事件。
    第三个参数fd:注册的事件的文字描述符,比如一个监听套接字
    第四个参数event:表示注册的事件类型,并且可以在这个结构体里设置用户需要的数据,其中最为常见的是使用联合结构里的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 */
 };

重点看一下这几种事件类型:

  • EPOLLIN:表示对应的文件描述字可以读;
  • EPOLLOUT:表示对应的文件描述字可以写;
  • EPOLLRDHUP:表示套接字的一端已经关闭,或者半关闭;
  • EPOLLHUP:表示对应的文件描述字被挂起;
  • EPOLLET:设置为 edge-triggered,默认为 level-triggered。
  • epoll_wait:调用者进程被挂起,在等待内核I/O事件的分发
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1.
  • 第一个参数epfd是 epoll 实例描述字,也就是 epoll 句柄。
  • 第二个参数events返回给用户空间需要处理的 I/O 事件,这是一个数组,数组的大小由 epoll_wait 的返回值决定,这个数组的每个元素都是一个需要待处理的 I/O 事件,其中 events 表示具体的事件类型,事件类型取值和 epoll_ctl 可设置的值一样,这个 epoll_event 结构体里的 data 值就是在 epoll_ctl 那里设置的 data,也就是用户空间和内核空间调用时需要的数据。
  • 第三个参数maxevents是一个大于 0 的整数,表示 epoll_wait 可以返回的最大事件值。
  • 第四个参数timeout是 epoll_wait 阻塞调用的超时值,如果这个值设置为 -1,表示不超时;如果设置为 0 则立即返回,即使没有任何 I/O 事件发生

实践

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>

#define SERV_PORT 43211
#define LISTENQ 1024
#define MAXEVENTS 128

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

void make_nonblocking(int fd)
{
    fcntl(fd, F_SETFL, O_NONBLOCK);
}


int tcp_nonblocking_server_listen(int port)
{
    int listen_fd;
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    make_nonblocking(listen_fd);

    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if(rt1 < 0)
    {
        perror("bind error\n");
        return -1;
    }

    int rt2 = listen(listen_fd, LISTENQ);
    if(rt2 < 0)
    {
        perror("listen failed");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);
    return listen_fd;

}


int main(int argc, char *argv[])
{
    int listen_fd, socket_fd;
    int n, i;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;


    listen_fd = tcp_nonblocking_server_listen(SERV_PORT);

    //为epoll创建实例
    efd = epoll_create1(0);
    if(efd == -1)
    {
        perror("epoll create failed");
        return -1;
    }

    event.data.fd = listen_fd;
    event.events = EPOLLIN | EPOLLET;
    // 调用epoll_ctl将监听字对应的I/O事件进行注册,有新的连接建立,就可以感知,采用edge-triggered边缘触发
    if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
    {
        perror("epoll_ctl add listen fd failed");
        return -1;
    }

    // Buffer where events are returned
    events = calloc(MAXEVENTS, sizeof(event));

    while(1)
    {
        // 调用epoll_wait函数分发I/O事件,当epoll_wait成功返回后,通过遍历返回的event数组,就可知道发生的I/O事件
        n = epoll_wait(efd, events, MAXEVENTS, -1);
        printf("epoll_waite wakeup\n");
        for(i = 0; i < n; i++)
        {
            if((events[i].events & EPOLLERR) ||
                (events[i].events & EPOLLHUP) ||
                (!events[i].events & EPOLLIN))
                {
                    fprintf(stderr, "epoll error\n");
                    close(events[i].data.fd);
                    continue;
                }
                else if(listen_fd == events[i].data.fd)
                {
                    struct sockaddr_storage ss;
                    socklen_t slen = sizeof(ss);
                    int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
                    if(fd < 0)
                    {
                        perror("accept failed");
                        return -1;
                    }
                     else
                    {
                        // accept建立连接,并将该连接设置为非阻塞,在调用epoll_ctl把已连接套接字对应的可读事件
                        // 注册到epoll实例中,这里使用了event_data里面的fd字段,将连接套接字存储器中
                        make_nonblocking(fd);
                        event.data.fd = fd;
                        event.events = EPOLLIN | EPOLLET;// edge-triggered
                        if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
                        {
                            perror("epoll_ctl add connection fd failed");
                            return -1;
                        }
                    }
                    continue;
                }
                else
                {
                    socket_fd = events[i].data.fd;
                    printf("get event on socket fd == %d\n",socket_fd);
                    while(1)
                    {
                        char buf[512];
                        if((n = read(socket_fd, buf, sizeof(buf))) < 0)
                        {
                            if(errno != EAGAIN)
                            {
                                perror("read error");
                                close(socket_fd);
                            }
                            break;
                        }
                        else if(n == 0)
                        {
                            close(socket_fd);
                            break;
                        }
                        else 
                        {
                            for(i = 0;i < n; ++i)
                            {
                                buf[i] = rot13_char(buf[i]);
                            }
                            if(write(socket_fd, buf, n) < 0)
                            {
                                perror("write error");
                                return -1;
                            }
                        }
                    }
                }
               
        }
    }
    free(events);
    close(listen_fd);
    return 0;
}

运行结果

边缘触发vs水平触发

条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
一般认为,边缘触发的效率比条件触发的效率要高
边缘触发:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>

#define SERV_PORT 43211
#define LISTENQ 1024
#define MAXEVENTS 128

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

void make_nonblocking(int fd)
{
    fcntl(fd, F_SETFL, O_NONBLOCK);
}


int tcp_nonblocking_server_listen(int port)
{
    int listen_fd;
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    make_nonblocking(listen_fd);

    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if(rt1 < 0)
    {
        perror("bind error\n");
        return -1;
    }

    int rt2 = listen(listen_fd, LISTENQ);
    if(rt2 < 0)
    {
        perror("listen failed");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);
    return listen_fd;

}


int main(int argc, char *argv[])
{
    int listen_fd, socket_fd;
    int n, i;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;


    listen_fd = tcp_nonblocking_server_listen(SERV_PORT);

    efd = epoll_create1(0);
    if(efd == -1)
    {
        perror("epoll create failed");
        return -1;
    }

    event.data.fd = listen_fd;
    event.events = EPOLLIN | EPOLLET;
    if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
    {
        perror("epoll_ctl add listen fd failed");
        return -1;
    }

    // Buffer where events are returned
    events = calloc(MAXEVENTS, sizeof(event));

    while(1)
    {
        n = epoll_wait(efd, events, MAXEVENTS, -1);
        printf("epoll_waite wakeup\n");
        for(i = 0; i < n; i++)
        {
            if((events[i].events & EPOLLERR) ||
                (events[i].events & EPOLLHUP) ||
                (!events[i].events & EPOLLIN))
                {
                    fprintf(stderr, "epoll error\n");
                    close(events[i].data.fd);
                    continue;
                }
                else if(listen_fd == events[i].data.fd)
                {
                    struct sockaddr_storage ss;
                    socklen_t slen = sizeof(ss);
                    int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
                    if(fd < 0)
                    {
                        perror("accept failed");
                        return -1;
                    }
                     else
                    {
                        make_nonblocking(fd);
                        event.data.fd = fd;
                        event.events = EPOLLIN | EPOLLET;// edge-triggered
                        if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
                        {
                            perror("epoll_ctl add connection fd failed");
                            return -1;
                        }
                    }
                    continue;
                }
                else
                {
                    socket_fd = events[i].data.fd;
                    printf("get event on socket fd == %d\n",socket_fd);
                }
               
        }
    }
    free(events);
    close(listen_fd);
    return 0;
}

执行效果:

可发现,边缘触发情况下,开启这个服务器程序,用 telnet 连接上,输入一些字符,我们看到,服务器端只从 epoll_wait 中苏醒过一次,就是第一次有数据可读的时候。
水平触发:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>

#define SERV_PORT 43211
#define LISTENQ 1024
#define MAXEVENTS 128

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

void make_nonblocking(int fd)
{
    fcntl(fd, F_SETFL, O_NONBLOCK);
}


int tcp_nonblocking_server_listen(int port)
{
    int listen_fd;
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    make_nonblocking(listen_fd);

    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if(rt1 < 0)
    {
        perror("bind error\n");
        return -1;
    }

    int rt2 = listen(listen_fd, LISTENQ);
    if(rt2 < 0)
    {
        perror("listen failed");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);
    return listen_fd;

}


int main(int argc, char *argv[])
{
    int listen_fd, socket_fd;
    int n, i;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;


    listen_fd = tcp_nonblocking_server_listen(SERV_PORT);

    efd = epoll_create1(0);
    if(efd == -1)
    {
        perror("epoll create failed");
        return -1;
    }

    event.data.fd = listen_fd;
    event.events = EPOLLIN | EPOLLET;
    if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
    {
        perror("epoll_ctl add listen fd failed");
        return -1;
    }

    // Buffer where events are returned
    events = calloc(MAXEVENTS, sizeof(event));

    while(1)
    {
        n = epoll_wait(efd, events, MAXEVENTS, -1);
        printf("epoll_waite wakeup\n");
        for(i = 0; i < n; i++)
        {
            if((events[i].events & EPOLLERR) ||
                (events[i].events & EPOLLHUP) ||
                (!events[i].events & EPOLLIN))
                {
                    fprintf(stderr, "epoll error\n");
                    close(events[i].data.fd);
                    continue;
                }
                else if(listen_fd == events[i].data.fd)
                {
                    struct sockaddr_storage ss;
                    socklen_t slen = sizeof(ss);
                    int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
                    if(fd < 0)
                    {
                        perror("accept failed");
                        return -1;
                    }
                     else
                    {
                        make_nonblocking(fd);
                        event.data.fd = fd;
                        event.events = EPOLLIN;// level-triggered
                        if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
                        {
                            perror("epoll_ctl add connection fd failed");
                            return -1;
                        }
                    }
                    continue;
                }
                else
                {
                    socket_fd = events[i].data.fd;
                    printf("get event on socket fd == %d\n",socket_fd);
                   
                }
               
        }
    }
    free(events);
    close(listen_fd);
    return 0;
}

效果:

然后按照同样的步骤来一次,观察服务器端,可看到,服务器端不断地从 epoll_wait 中苏醒,告诉我们有数据需要读取。

小结

epoll 通过改进的接口设计,避免了用户态 - 内核态频繁的数据拷贝,大大提高了系统性能。在使用 epoll 的时候,我们一定要理解条件触发和边缘触发两种模式。条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

posted @ 2022-03-27 11:09  牛犁heart  阅读(745)  评论(0编辑  收藏  举报