Linux高级I/O:多路转接 epoll LT


全文约 7184 字,预计阅读时长: 21分钟


epoll的相关系统调用

按照man手册的说法: 是为处理大批量句柄而作了改进的poll. 它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44) .它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

  • epoll 有3个相关的系统调用:epoll_createepoll_ctlepoll_wait
  • int epoll_create(int size); :创建一个epoll的句柄。句柄:具有标识唯一资源的标识符。
    • 自从linux2.6.8之后,size参数是被忽略的。用完之后, 必须调用close()关闭。(服务启动起来以后几乎不会关闭。)

epoll_ctl

  • epoll的事件注册函数:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    • 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
    • 第一个参数是epoll_create()的返回值(epoll的句柄).
    • 第二个参数表示动作,用三个宏来表示. (增加,修改、删除)
      • 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 */
} __EPOLL_PACKED;
  • events可以是以下几个宏的集合:
    • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
    • EPOLLOUT : 表示对应的文件描述符可以写;
    • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);TCP的紧急指针。
    • EPOLLERR : 表示对应的文件描述符发生错误;
    • EPOLLHUP : 表示对应的文件描述符被挂断;
    • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
    • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.

epoll_wait

  • 收集在epoll监控的事件中已经发送的事件:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 参数解释;
    • events是分配好的epoll_event结构体数组.
    • epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存). maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
    • 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
  • 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.

epoll工作原理

  • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
struct eventpoll
{ 
 .... 
 /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
 struct rb_root rbr; 
 /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ 
 struct list_head rdlist; 
 .... 
}; 
  • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
  • epoll中,对于每一个事件,都会建立一个epitem结构体.
struct epitem
{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
}
  • 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
  • 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).

总结一下, epoll的使用过程就是三部曲:

  • 调用epoll_create创建一个epoll句柄;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪;

epoll的优点

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响。
  • 没有数量限制: 文件描述符数目无上限.
  • 支持线程安全

epoll工作方式:LT 和 ET

  • epoll有2种工作方式:水平触发(LT)边缘触发(ET)

水平触发Level Triggered 工作模式。epoll默认状态下就是LT工作模式.

  • epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
    • 例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
  • 支持阻塞读写和非阻塞读写

边缘触发Edge Triggered工作模式

  • 如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式
    • epoll检测到socket上事件就绪时, 必须立刻处理.
      • 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了。也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
    • ET的性能比LT性能更高( epoll_wait 返回的次数少了很多)。 Nginx默认采用ET模式使用epoll.
    • 只支持非阻塞的读写

问:为什么只支持非阻塞的读写

答:客户端发来10K数据,服务端ET只通知一次,而服务端的上层读了一半儿。服务端向客户端索要数据,而客户端表示要给刚才的10K数据一个应答,才会继续传送数据。而且一个文件默认的I/O方式是阻塞I/O。在应用层不知道这个就绪的读事件来了多少数据。虽然可以采用循环的方式检测,但总归有最后一次没数据可读。此时进程服务会被挂起。所以必须采用非阻塞进行轮询检测读取。


LT vs ET

LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完。

ET 相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。另一方面, ET 的代码复杂程度更高了.

  • LT:只要底层有数据,就会向上层进行一直通知,直至上层将数据读完。
  • ET:只在底层数据:从无到有,从有到多的时候,才会向上层进行通知就绪,否则,一律不通知。即便上层去了一部分数据,没取完。

epoll的使用场景、惊群问题

  • epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反。
    • 对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
    • 例如, 典型的一个需要处理上万个客户端的服务器。
    • 例如,各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
  • 如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接。这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型。
  • epoll的惊群问题: 接着就遇到了“惊群”现象:当listen_fd有新的accept()请求过来,操作系统会唤醒所有子进程(因为这些进程都epoll_wait()同一个listen_fd,操作系统又无从判断由谁来负责accept,索性干脆全部叫醒……),但最终只会有一个进程成功accept,其他进程accept失败。外国IT友人认为所有子进程都是被“吓醒”的,所以称之为Thundering Herd(惊群)。

简易的LT模式:epoll服务器

祖传的sock。hpp

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<strings.h>
using std::cerr;
using std::cin;
using std::cout;
using std::endl;

namespace ns_sock
{
    enum
    {
        SOCK_ERR = 2,
        BIND_ERR,
        LISTEN_ERR
    };

    const int g_backlog = 5;

    struct sock_package
    {
        static int SockCreate()
        {
            int sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                cerr << "socket  failed" << endl;
                exit(SOCK_ERR);
            }
            //设置可以立即重启绑定。
            int opt = 1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            return sock;
        }
        static void Bind(const int &sock_fd, uint16_t &port)
        {
            struct sockaddr_in local;
            bzero(&local,sizeof(local));
            local.sin_addr.s_addr=INADDR_ANY;
            local.sin_port=htons(port);
            local.sin_family=AF_INET;
            if(bind(sock_fd,(sockaddr*)&local,sizeof(local))<0)
            {
                cerr<<"Bind Error"<<endl;
                exit(BIND_ERR);
            }
        }
        static void Listen(const int& sock_fd)
        {
            if(listen(sock_fd,g_backlog)<0)
            {
                cerr<<"Listen Error"<<endl;
                exit(LISTEN_ERR);
            }
        }
    };

}

epoll。hpp

#include "sock.hpp"
#include <sys/epoll.h>
#include <string>
namespace ns_epoll
{
    using std::string;
    using namespace ns_sock;
    const int g_port = 8080;
    class epoll_sv
    {
    private:
        int _listen_sock;
        uint16_t _port;
        int _epfd;

    public:
        epoll_sv(int port = g_port) : _listen_sock(-1), _port(port), _epfd(-1)
        {
        }
        ~epoll_sv()
        {
            if (_listen_sock >= 0)
                close(_listen_sock);
            if (_epfd >= 0)
                close(_epfd);
        }
        void Init()
        {
            _listen_sock = sock_package::SockCreate();
            sock_package::Bind(_listen_sock, _port);
            sock_package::Listen(_listen_sock);

            _epfd = epoll_create(128);
            if (_epfd < 0)
            {
                cerr << " epoll_create error" << endl;
                exit(5);
            }
            cout << " Init  success ..." << endl;
        }

        void Hander(epoll_event revs[], int num)
        {
            for (size_t i = 0; i < num; i++)
            {
                int ret_sock = revs[i].data.fd;
                uint32_t ret_event = revs[i].events;
                //先去分是什么时间就绪,在区分是哪个文件fd
                if (ret_event & EPOLLIN) //新连接
                {
                    if (_listen_sock == ret_sock)
                    {
                        sockaddr_in peer;
                        socklen_t len = sizeof(peer);
                        int new_sock = accept(ret_sock, (sockaddr *)&(peer), &len);
                        if (new_sock < 0)
                        {
                            cout << "accept error .." << endl;
                            continue;
                        }
                        epoll_event new_even;
                        new_even.data.fd = new_sock;
                        new_even.events = EPOLLIN;
                        epoll_ctl(_epfd, EPOLL_CTL_ADD, new_sock, &new_even);
                        cout << "...new_sock added..." << endl;
                    }
                    else //其他的普通读事件就绪
                    {
                        char buffer[1024];
                        ssize_t s = recv(ret_sock, buffer, 1024, 0);
                        if (s > 0)
                        {
                            buffer[s] = 0;
                            cout << "client said # " <<buffer <<endl;
                            //重新设置该sock的读写事件
                            epoll_event new_ev;
                            new_ev.events = EPOLLIN | EPOLLOUT;
                            new_ev.data.fd = ret_sock;
                            epoll_ctl(_epfd, EPOLL_CTL_MOD, ret_sock, &new_ev);
                        }
                        else
                        {
                            cout << "client closed .." << endl;
                            close(ret_sock);
                            epoll_ctl(_epfd, EPOLL_CTL_DEL, ret_sock, nullptr);
                        }
                    }
                }
                if (ret_event & EPOLLOUT) //写事件就绪
                {
                    string wes = "i know..!";
                    send(ret_sock, wes.c_str(), wes.size(), 0);
                    //写事件按需设置,写完取消该文件的写事件检测。
                    epoll_event modev;
                    modev.data.fd = ret_sock;
                    modev.events = EPOLLIN;
                    epoll_ctl(_epfd, EPOLL_CTL_MOD, ret_sock, &modev);
                }
            }
        }

        void Loop()
        {
            int timeout = -1;
            epoll_event le1;
            le1.events = EPOLLIN;
            le1.data.fd = _listen_sock;
            epoll_ctl(_epfd, EPOLL_CTL_ADD, _listen_sock, &le1); //先将监听套接字插入_epd红黑树中。
            cout << "监听套接字加入成功,开始进入循环" << endl;
            //需要传入一个 epoll_event 的结构体数组充当就绪队列。数组的个数可以自定义。
#define NUM 10
            epoll_event revs[NUM];
            while (true)
            {
                int num = epoll_wait(_epfd, revs, NUM, timeout);
                switch (num)
                {
                case 0:
                    cout << "time out.." << endl;
                    break;
                case -1:
                    cerr << "epoll wait error" << endl;
                    break;
                default:
                    // cout<<"here we go"<<endl;
                    // sleep(1);
                    Hander(revs, num);
                    break;
                }
            }
        }
    };
}

---// epoll。cc
#include"epl.hpp"
using namespace ns_epoll;
int main()
{
    epoll_sv esv;
    esv.Init();
    esv.Loop();
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值