Linux系统编程--网络socket编程 之 I/O多路复用(epoll)服务器编程



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

如今,几乎所有的网站都是用epoll模型,虽然不是完全相同,但是其本质都是epoll的工作原理;

与 select 和 poll 相比较,当连接的客户端逐渐庞大,文教描述符越来越多,select 与 poll
的性能明显下降,这是因为他们每一次工作都需要将文件描述符从用户态拷贝到内核态,并且每一次都要遍历数组中的所有文件描述符等,epoll 在poll 之上加强改进,之所以能得到广泛引用,是因为他的设计以及工作原理,可以满足客户端再多也不会应影响他的性能。


水平触发与边缘出发

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

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

ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。

由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll函数

epoll并不是一个函数,而是几个函数的总称, epoll 取其精华去其糟粕,在 poll 上改进,共使用 3个 函数进行操作,那么他是怎么做到无论文件描述符再多也不会导致性能下降的呢?

1.epoll_create()

#include <sys/epoll.h>

int epoll_create(int size);

成功则返回一个 epoll 对像,用于传给epoll的其他函数,失败返回-1,错误信息被创建;

 系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。参数size指定了我们想要通过epoll实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。从Linux2.6.8版以来,size参数被忽略不用。作为函数返回值,epoll_create()返回了代表新创建的epoll实例的文件描述符。这个文件描述符在其他几个epoll系统调用中用来表示epoll实例。当这个文件描述符不再需要时,应该通过close()来关闭。当所有与epoll实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。该系统调用执行的任务同epoll_create()一样,但是去掉了无用的参数size,并增加了一个可用来修改系统调用行为的flags参数。目前只支持一个flag标志:EPOLL_CLOEXEC,它使得内核在新的文件描述符上启动了执行即关闭标志。

2.epoll_ctl()

#include <sys/epoll.h>

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

成功返回 0 失败返回 -1,错误被创建;

第一个参数 epfd 就是刚刚使用 epoll_create 创建的epoll实例或者说是epoll对象;
第二个参数 op 是这个函数要执行的操作,他有下面三种:

(1)EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误;

(2)EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误;

(3)EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图移除一个不在epfd的兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除;

第三个参数使我们要进行操作的文件描述符;
第四个参数,就是epoll的亮点,ev是指向结构体epoll_event的指针,结构体的定义如下:


typedef union epoll_data
{
 void *ptr; /* Pointer to user-defind data */
 int fd; /* File descriptor */
 uint32_t u32; /* 32-bit integer */
 uint64_t u64; /* 64-bit integer */
} epoll_data_t;

struct epoll_event
{
 uint32_t events; /* epoll events(bit mask) */
 epoll_data_t data; /* User data */
};

结构体中,第一个 events 是我们保存这个文件描述符的待发生事件的集合;
第二个成员 data,是一个联合,就是用来存储fd的;

3.epoll_wait

#include <sys/epoll.h>

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

成功则返回准备就绪的文件描述符个数,0表示超时,出错则返回 1,错误信息被创建;

第一个参数epfd是epoll_create()的返回值;
第二个参数evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请;
第三个参数maxevents指定所evlist数组里包含的元素个数;
第四个参数timeout用来确定epoll_wait()的阻塞行为。

数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。注意,data字段是唯一可获知同这个事件相关的文件描述符的途径。因此,当我们调用epoll_ctl()将文件描述符添加到感兴趣列表中时,应该要么将ev.date.fd设为文件描述符号,要么将ev.date.ptr设为指向包含文件描述符号的结构体。

当我们调用epoll_ctl()时可以在ev.events中指定的位掩码以及由epoll_wait()返回的evlist[].events中的值如下所示:

常量 说明 作为 epoll_ctl()的输入 作为epoll_wait()的返回
EPOLLIN 可读取非高优先级数据
EPOLLPRI 可读取高优先级数据
EPOLLRDHUP socket对端关闭(始于Linux 2.6.17)
EPOLLOUT 普通数据可写
EPOLLET 采用边沿触发事件通知
EPOLLONESHOT 在完成事件通知之后禁用检查
EPOLLERR 有错误发生
POLLHUP 出现挂断

默认情况下,一旦通过epoll_ctl()的EPOLL_CTL_ADD操作将文件描述符添加到epoll实例的兴趣列表中后,它会保持激活状态(即,之后对epoll_wait()的调用会在描述符处于就绪态时通知我们)直到我们显示地通过epoll_ctl()的EPOLL_CTL_DEL操作将其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给epoll_ctl()的ev.events中指定EPOLLONESHOT标志。如果指定了这个标志,那么在下一个epoll_wait()调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的epoll_wait()调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后用过调用epoll_ctl()的EPOLL_CTL_MOD操作重新激活对这个文件描述符的检查。

下面是我个人对epoll的一个理解,实际上epoll是通过一颗红黑树与一个链表来完成的,而我先通过简单的图解省略了中间的链表,将红黑树表示为下图,有助于理解;

图解

作为一个服务器,当我们创建的epoll实例后,首先将一个 struct epoll_event 的结构体 event 的成员赋值

 event.events = EPOLLIN;   //我们等待他的读事件;
 
 event.data.fd = listenfd;   //将用于监听的listenfd套接字赋值给该结构体;

这个结构体大致可以这样理解:
在这里插入图片描述

使用 epoll_ctl() 函数将这个结构体加入到感兴趣的文件描述符中:

在这里插入图片描述
这个listenfd包含了listenfd以及待发生的事件;

这永远是服务器的第一步,等待第一个客户端的连接,使用 epoll_wait() 函数,传入参数,监听epoll实例中的文件描述符,第一个事件就是客户端连接,所以是listenfd 发生事件:

在这里插入图片描述
将发生事件的描述符加入数组,程序将会遍历数组,对数组中的文件描述符进行操作,这里是listenfd,所以,程序会调用accept() 函数来接收新的客户端,并将客户端的信息加入结构体,使用函数 epoll_ctl() 将其加入到 epoll实例中;
当多个客户端都已连接成功,epoll实例应该是这样:
在这里插入图片描述程序阻塞 : epoll_wait(epfd,event_array,MAX_EVENTS,-1);
因为每次都不同的文件描述符发生,所以该函数会在返回之前,将用于存储调皮的文件描述符的数组清空;

假设,同时,有客户端发来新的连接请求,并且已连接的 client1,client3,client4发来发来数据可读,则
epoll_wait() 会将调皮的文件描述符写入数组:
在这里插入图片描述
这里的各个文件描述符其实是按一颗红黑树来存储的。发现了吗,我们并不需要每一次都遍历整个数组,这是因为,我们在调用epoll_ctl时将感兴趣的文件描述符加入到红黑树的同时,会注册一个回调函数,当该文件描述符准备就绪时,该回调函数就会将其放入到链表中,链表再将数据返回给数组,而只需遍历发生事件的数组即可;假设epoll中存储了10万个客户端的文件描述符,但是这10万个客户端并不会同一时刻同时访问(除非恶意攻击),我们每次只需要遍历发生了事件的套接字即可,不需要遍历所有描述符;

epoll服务器代码

/*********************************************************************************
 *      Copyright:  (C) 2020 Xiao yang IoT System Studio
 *                  All rights reserved.
 *
 *       Filename:  epoll_server.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(03/11/2020)
 *         Author:  Lu Xiaoyang <[email protected]>
 *      ChangeLog:  1, Release initial version on "03/11/2020 09:15:02 PM"
 *                 
 ********************************************************************************/
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>
#include <getopt.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <sys/types.h>    
#include <sys/socket.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/resource.h>
#include <arpa/inet.h>
#include <time.h>

#define  MAX_EVENTS 512
static inline void print_usage(char *progname);
int server_Be_ready(char *servip,int serv_port);

int main(int argc, char **argv)
{
    char                         buf[1024];
    char                        *progname;
    int                          listen_port;
    int                          i,j;
    int                          rv,ch;
    int                          epfd;
    int                          listenfd,clifd;
    int                          num = 0;
    int                          daemon_run = 0;
    struct epoll_event           event;
    struct epoll_event           event_array[1024] = {0};
    struct option                opts[] = {
        {"port",required_argument,NULL,'p'},
        {"daemon",no_argument,NULL,'d'},
        {"help",no_argument,NULL,'h'},
        {NULL,0,NULL,0}
    };

    progname = basename(argv[0]);

    while((ch = getopt_long(argc,argv,"p:dh",opts,NULL)) != -1)   //获取命令行参数
    {
        switch (ch)
        {
            case 'h':
                print_usage(progname);
                break;

            case 'p':
                listen_port = atoi(optarg);
                break;

            case 'd':
                daemon_run = 1;
                break;

            default:
                break;

        }
    }

    if(!listen_port)
    {
        print_usage(progname);
        return -1;
    }


    if(( listenfd = server_Be_ready(NULL,listen_port)) < 0)
    {
        printf("Create socket failure:%s\n",strerror(errno));
        return -1;
    }
    printf("Create listenfd[%d] ok!\n",listenfd);

    if(daemon_run)   //后台运行
    {
        daemon(0,0);
    }

    if((epfd = epoll_create(8)) < 0)   //创建epoll对象,里面参数任意
    {
        printf("epoll_create failure:%s\n",strerror(errno));
        return -3;
    }

    event.events = EPOLLIN;
    event.data.fd = listenfd;
    
    if((rv = epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&event)) < 0)   //添加感兴趣的文件描述符
    {
        perror("epoll_ctl() failure");
        return -4;
    }

    for( ; ; )
    {
        printf("Waitting ......\n");
        if((num = epoll_wait(epfd,event_array,MAX_EVENTS,-1)) < 0)
        {
            perror("epoll_wait() failure");
            break;
        }

        else if(num == 0)
        {
            printf("Time out\n");
            continue;
        }

        for(i = 0;i < num;i++)   //就绪的文件描述符,遍历检测,执行相应操作
        {
            if((event_array[i].events & EPOLLERR) || (event_array[i].events & EPOLLHUP))
            {
                printf("wait a error clinet[%d] : %s.\n",event_array[i].data.fd,strerror(errno));
                epoll_ctl(epfd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
                close(event_array[i].data.fd);
            }

            if(event_array[i].data.fd == listenfd)   //新客户端请求连接
            {
                if((clifd = accept(listenfd,(struct sockaddr *)NULL,NULL)) < 0)
                {
                    perror("accept new client failure");
                    continue;
                }

                event.events = EPOLLIN;
                event.data.fd = clifd;
                if(( rv = epoll_ctl(epfd,EPOLL_CTL_ADD,clifd,&event)) < 0)
                {
                    perror("epoll_ctl failure");
                    close(clifd);
                    continue;
                }
                printf("Accept new client[%d] in epfd\n",clifd);

            }
        
            else   //已连接的客户端发来数据
            {
                if(( rv = read(event_array[i].data.fd,buf,sizeof(buf))) <= 0)
                {
                    printf("Read to client[%d] failure:%s\n",event_array[i].data.fd,strerror(errno));
                    epoll_ctl(epfd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
                    close(event_array[i].data.fd);
                    continue;
                }
                printf("Read %d bytes data from client[%d]:%s\n",rv,event_array[i].data.fd,buf);
                
                for(j = 0;j < rv;j++)
                {
                    buf[j] = toupper(buf[j]);
                }

                if((rv = write(event_array[i].data.fd,buf,sizeof(buf))) <= 0)
                {
                    printf("Write to client[%d] failure:%s\n",event_array[i].data.fd,strerror(errno));
                    epoll_ctl(epfd, EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
                    close(event_array[i].data.fd);
                    continue;
                }

            }

        }   //for(i = 0;i < num;i++)
    }   //for( ; ; )
    
    return 0;
}

static inline void print_usage(char *progname)
{
    printf("-p(--port) input the port you'll bind\n");
    printf("-d(--daemon) the program will running at background\n");
    printf("-h(--help) print help massage\n");
    printf("For example: ./a.out -p 8888 -d\n");
    
    return;
}

int server_Be_ready(char *servip,int serv_port)
{
    int                    rv;
    int                    on = 1;
    int                    listenfd;
    struct sockaddr_in     servaddr;
    socklen_t              addrlen = sizeof(servaddr);

    if((listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
    {
        perror("socket faliure");
        return -1;
    }

    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    bzero(&servaddr,sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(serv_port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(listenfd,(struct sockaddr *)&servaddr,addrlen) < 0)
    {
        perror("bind failure");
        close(listenfd);
        return -1;
    }
    printf("Bind listenfd[%d] on port[%d] ok!\n",listenfd,serv_port);

    listen(listenfd,13);

    return listenfd;


}

epoll之所以高效

· select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。

· select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。

· select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。

· select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。

· epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符

发布了18 篇原创文章 · 获赞 37 · 访问量 5001

猜你喜欢

转载自blog.csdn.net/weixin_45121946/article/details/104834749