epoll,解决C10K问题的关键

epoll是event poll的意思。因为涉及到用户态wait获取到内核返回的读写就绪事件之后、去主动到内核缓冲区获取数据、所以本质上是属于同步非阻塞io模型。linux上边真正的异步非阻塞io模型还没提供、windows倒是有了,但是服务器端仍然是linux的天下,所以现在真正事实上的标准高性能网络io模型仍然是epoll。它也是解决c10k问题的关键突破。

一、epoll在linux中的定义和系统调用

//用户数据载体、用来用户态和内核态交换数据

typedef union epoll_data {
    
    
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
//fd装载入内核的载体

struct epoll_event {
    
    
    uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

//三个主要api

int epoll_create(int size);  

//在内核空间创建epoll句柄epfd,就是新建一个多路复用器(struct eventpoll对象)然后返回它的文件描述符。可以类比java nio里的selector.open()方法。

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

//向epfd指向的内核空间里边的多路复用器epoll添加/删除(int op)一个fd以及感兴趣的事件event。这个event是epoll_event类型,里边封装了epoll_data用户态的数据、以及事件类型。可以类比一下java nio里边的register()方法。

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

//这个就是需要用户态程序去轮询的方法了,可以查到就绪的事件、内核会通过这个方法告知用户态程序就绪的events。一般来说用户态程序拿到一批就绪的事件以后会遍历、然后逐个判断属于哪种事件、比如是connect还是read/write,然后执行不同的逻辑。类比java nio里的selector.selectedKeys()。

//epoll官方例子

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Set up listening socket, 'listen_sock' (socket(),bind(), listen()) */
epollfd = epoll_create(10); //创建多路复用器epollfd
if(epollfd == -1) {
    
    
    perror("epoll_create");
    exit(EXIT_FAILURE);
} 
ev.events = EPOLLIN;
ev.data.fd = listen_sock;

if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    
     //向创建好的多路复用器epollfd添加server socket, 然后指定其event为EPOLLIN即监听客户端连接请求
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
} 
for(;;) {
    
     //自旋
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); //从多路复用器epollfd中查询已就绪的events
    if (nfds == -1) {
    
    
        perror("epoll_pwait");
        exit(EXIT_FAILURE);
    } 
    for (n = 0; n < nfds; ++n) {
    
    
        if (events[n].data.fd == listen_sock) {
    
    
            //主监听socket有新连接,connect事件
            conn_sock = accept(listen_sock,(struct sockaddr *) &local, &addrlen);
            if (conn_sock == -1) {
    
    
                perror("accept");
                exit(EXIT_FAILURE);
            } 
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) {
    
    
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
    
    
            //已建立连接的可读写句柄,read/write事件
            do_use_fd(events[n].data.fd);
        }
    }
}

【文章福利】小编推荐自己的linuxC/C++语言交流群:832218493,整理了一些个人觉得比较好的学习书籍、视频资料共享在里面,有需要的可以自行添加哦!~!
在这里插入图片描述

二、`C10K问题由来

随着互联网的普及,应用的用户群体几何倍增长,此时服务器性能问题就出现。最初的服务器是基于进程/线程模型。新到来一个TCP连接,就需要分配一个进程。假如有C10K,就需要创建1W个进程,可想而知单机是无法承受的。那么如何突破单机性能是高性能网络编程必须要面对的问题,进而这些局限和问题就统称为C10K问题,最早是由Dan Kegel进行归纳和总结的,并且他也系统的分析和提出解决方案。

三、C10K问题的本质

C10K问题的本质上是操作系统的问题。对于Web 1.0/2.0时代的操作系统,传统的同步阻塞I/O模型处理方式都是requests per second。当创建的进程或线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞,进程/线程上下文切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质。

可见, 解决C10K问题的关键就是尽可能减少这些CPU资源消耗。

四、C10K问题的解决方案

从网络编程技术的角度来说,主要思路:

每个连接分配一个独立的线程/进程
同一个线程/进程同时处理多个连接
每个进程/线程处理一个连接
该思路最为直接,但是申请进程/线程是需要系统资源的,且系统需要管理这些进程/线程,所以会使资源占用过多,可扩展性差

每个进程/线程同时处理 多个连接(I/O多路复用)
select方式:使用fd_set结构体告诉内核同时监控那些文件句柄,使用逐个排查方式去检查是否有文件句柄就绪或者超时。该方式有以下缺点:文件句柄数量是有上线的,逐个检查吞吐量低,每次调用都要重复初始化fd_set。
poll方式:该方式主要解决了select方式的2个缺点,文件句柄上限问题(链表方式存储)以及重复初始化问题(不同字段标注关注事件和发生事件),但是逐个去检查文件句柄是否就绪的问题仍然没有解决。
epoll方式:该方式可以说是C10K问题的killer,他不去轮询监听所有文件句柄是否已经就绪。epoll只对发生变化的文件句柄感兴趣。其工作机制是,使用"事件"的就绪通知方式,通过epoll_ctl注册文件描述符fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd, epoll_wait便可以收到通知, 并通知应用程序。而且epoll使用一个文件描述符管理多个描述符,将用户进程的文件描述符的事件存放到内核的一个事件表中, 这样数据只需要从内核缓存空间拷贝一次到用户进程地址空间。而且epoll是通过内核与用户空间共享内存方式来实现事件就绪消息传递的,其效率非常高。但是epoll是依赖系统的(Linux)。
异步I/O以及Windows,该方式在windows上支持很好,这里就不具体介绍啦。

五、LT模式和ET模式的简单理解

level triggered水平触发和edge triggered边缘触发,指的是epoll在socket读写上的两种通知方式。

1、LT 是默认的模式,支持阻塞和非阻塞socket、epoll_wait获取到内核拷贝来的就绪事件之后、用户态程序如果没有处理完、下次仍然会在调用epoll_wait的时候通知给用户态程序、数据不会丢失因为反复提醒、更加安全。
这里边read的话只要有数据就通知就绪可读了,但是write的话,一般来说socket空闲了、写缓冲区不满就会提醒写就绪、也就是反复的提醒某个socket可写。但此时用户态程序可能并没有对这个socket的写的需求,大量连接的时候这也是个不小的开销,所以一般是在没有数据要发送的时候,由用户态程序把对应的fd写事件从epoll列表里去掉,需要的时候再加进去。

2、ET模式是高速模式,只支持非阻塞socket,如果用户态程序对事件没有处理完,那下一次epoll_wait调用就不会继续通知了。对用户态读写处理的逻辑容错提出了更高的要求、但因为没有反复通知、所以性能更高。简单来说ET模式只在socket的读写状态发生变化的时候通知、状态不变则不通知,比如读缓冲区由无数据到有数据通知read事件、写缓冲区由满到未满通知可写write事件。
通过前面的对比可以看到LT模式比较安全并且代码编写也更清晰,但是ET模式属于高速模式,在处理大高并发场景使用得当效果更好,具体选择什么根据自己实际需要和团队代码能力来选择。

猜你喜欢

转载自blog.csdn.net/lingshengxueyuan/article/details/111589828