文章目录

  • 前言
  • 一、IO多路转接select
  • 初始select
  • select函数原型
  • 关于fd_set结构
  • 关于timeval结构
  • select使用示例
  • select优缺点
  • 二、IO多路转接poll
  • poll函数接口
  • socket就绪的条件
  • poll的优点
  • poll的缺点
  • poll示例
  • 三、IO多路转接之epoll
  • epoll初始
  • epoll的相关系统调用
  • epoll_create
  • epoll_ctl
  • epoll_wait
  • epoll工作原理
  • epoll的优点(对比于select)
  • epoll的工作方式
  • 水平触发Level Triggered 工作模式
  • 边缘触发Edge Triggered工作模式
  • 对比LT和ET
  • 理解ET模式和非阻塞文件描述符
  • epoll的使用场景
  • epoll示例: epoll服务器(LT模式)
  • 总结

前言

  • 掌握select编程模型,能够实现select版本的TCP服务器.
  • 掌握poll编程模型,能够实现poll版本的TCP服务器.
  • 掌握epoll的编程模型,能够实现epoll版本的TCP服务器.
  • epoll的LT模式和ET模式.
  • 理解select和epoll的优缺点对比.

提示:以下是本篇文章正文内容,下面案例可供参考

一、IO多路转接select

多路转接天然的是让我们可以依次等待多个文件描述符.

什么叫做文件描述符状态的变化 —>1.可读 2.可写 3.异常

初始select

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序坚实多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或者多个发生了状态变化;

select函数原型

如下所示:

IO多路复用之select/poll/epoll_服务器


参数解释:

  • 参数ndfs是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应与需要检测的可读文件描述符的集合,可写文件描述符的集合和异常文件描述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间.

参数timeout取值:

  • NULL:表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回.

关于fd_set结构

IO多路复用之select/poll/epoll_c++_02

fd_set是一种位图结构.**比特位的位置代表fd的编号,比特位的内容代表(就绪/未就绪)**的概念.

提供了一组fd_set的接口,来比较方便的操作位图.

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
 int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
 void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
 void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

查看fd_set的大小

IO多路复用之select/poll/epoll_linux_03


因为fd_set是位图结构,求出来的结果是128字节,所以对于bit为就是1024.

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0.

IO多路复用之select/poll/epoll_网络_04


函数返回值

  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测.

错误值可能为:

  • EBADF文件描述词为无效的或该文件已关闭
  • EINTR此调用被信号所中断
  • EINVAL参数n为负值.
  • ENOMEM核心内存不足,

select使用示例

makefile

SelectServer:SelectServer.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm SelectServer

Sock.hpp

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <fstream>
using namespace std;

class Sock
{
public:
    static const int gBackLog = 20;
    static int Socket()
    {
        // 1.创建socket
        int _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0)
        {
            exit(1);
        }
        int opt = 1;
        setsockopt(_listenSock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT, &opt,sizeof opt);
    }
    static void Bind(int socket, uint16_t _port)
    {
        // 2.bind绑定
        // 2.1填充服务器
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        // 2.2本地socket信息,写入_sock对应的内核区域
        if (bind(socket, (const sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }
    }
    static void Listen(int socket)
    {
        // 3.监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(socket, gBackLog) < 0)
        {
            exit(3);
        }
        // 允许别人来连接你了
    }
    static int Accept(int socket,string* clientip,uint16_t* clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int serviceSock = accept(socket, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            return -1;
        }
        if(clientport) *clientport = ntohs(peer.sin_port);
        if(clientip) *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

SelectServer.cc

#include <iostream>
#include <sys/select.h>
#include <string>
#include "Sock.hpp"

using namespace std;

// 保存历史上所有的合法fd
#define NUM (sizeof(fd_set) * 8)
int fdsArray[NUM] = {0};
#define DFL -1

static void Usage(string process)
{
    cerr << "\nUsage: " << process << "port\n"
         << endl;
}

static void showArray(int arr[], int num)
{
    cout << "当前合法sock list # ";
    for (int i = 0; i < num; ++i)
    {
        if (arr[i] == DFL)
            continue;
        else
            cout << arr[i] << " ";
    }
    cout << endl;
}

// redfds : 现在包含的就是已经就绪的sock
static void HandlerEvent(int listensock, fd_set &readfds)
{
    for (int i = 0; i < NUM; ++i)
    {
        if (fdsArray[i] == DFL)
            continue;
        if (i == 0 && fdsArray[i] == listensock)
        {
            // 如何得知那些fd上面的事件就绪了呢?
            if (FD_ISSET(listensock, &readfds))
            {
                // 具有了一个新链接
                cout << "已经有一个新链接到来了,需要进行获取了..." << endl;
                string ip;
                uint16_t port;
                int sock = Sock::Accept(listensock, &ip, &port); // 这里不会阻塞
                if (sock < 0)
                    return;
                cout << "获取新链接成功 : " << ip << " : " << port << " sock: " << sock << endl;
                // read/wirte --- 不能调用,因为你read不知道底层数据是否就绪!!!
                // secelt知道! 想办法把新的fd托管给select
                int i = 0;
                for (; i < NUM; ++i)
                {
                    if (fdsArray[i] == DFL)
                        break;
                }
                if (i == NUM)
                {
                    cerr << "我的服务器已经到了最大的上限了,无法在承载更多的连接了..." << endl;
                    close(sock);
                }
                else
                {
                    // 将sock添加到select中,进一步的监听就绪事件了!
                    fdsArray[i] = sock;
                    showArray(fdsArray, NUM);
                }
            }
        }
        else
        {
            // 处理普通sock的IO事件!
            if (FD_ISSET(fdsArray[i], &readfds))
            {
                // 一定是一个合法的普通的IO类sock就绪了
                // read/recv读取即可
                char buffer[1024];
                ssize_t s = recv(fdsArray[i], buffer, sizeof buffer, 0);
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[" << fdsArray[i] << "]# " << buffer << endl;
                }
                else if (s == 0)
                {
                    cout << "client[" << fdsArray[i] << "] quit,server close: " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL; // 取出对该文件描述符的select的事件监听
                }
                else
                {
                    cout << "client[" << fdsArray[i] << "] quit,server close: " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL; // 取出对该文件描述符的select的事件监听
                    showArray(fdsArray, NUM);
                }
            }
        }
    }
}

// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    // fd_set fds; // fa_set是用位图表示多个fd的.
    // cout<<sizeof(fd_set)<<endl;

    int listensock = Sock::Socket();
    Sock::Bind(listensock, 8080);
    Sock::Listen(listensock);

    for (int i = 0; i < NUM; ++i)
        fdsArray[i] = DFL;
    fdsArray[0] = listensock;

    while (true)
    {
        int maxFd = -1;
        fd_set readfds;
        FD_ZERO(&readfds);
        for (int i = 0; i < NUM; ++i)
        {
            if (fdsArray[i] == DFL) // 过滤不合法的fd
                continue;
            FD_SET(fdsArray[i], &readfds); // 添加所有的合法的fd到readfds中,方便select统一进行就绪监听
            if (maxFd < fdsArray[i])       // 更新最大值
                maxFd = fdsArray[i];
        }

        struct timeval timeout = {5, 0};
        // 如何看待监听socket, 获取新链接的, 本质是需要先三次握手!
        // 前提是给我发送syn -? 建立连接的本质,其实也是IO,一个建立好的连接,我们成为读时间就绪!
        // accept: 等+"数据拷贝"
        // string ip;
        // uint16_t port;
        // int sock = Sock::Accept(listensock,&ip,&port);

        // 编写多路转接代码时,必须先保证条件就绪了,才能调用IO类函数!
        int n = select(maxFd + 1, &readfds, nullptr,
                       nullptr, &timeout);

        switch (n)
        {
        case 0:
            cout << "time out ..." << (unsigned long long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            // 等待成功
            // 1.刚启动的时候,只有一个fd,listenscok
            // 2.server 运行的时候,sock才会慢慢变多
            // 3.select 使用位图,采用输入输出型参数的方式,来进行 内核<->用户 信息的传递.
            // 每一次调用select,都需要对历史数据和sock进行重新设置!
            // 4.listensock, 永远都要被设置进readfds中!
            // 5.select就绪的时候,可能是listen就绪,也可能是普通的IO sock就绪!!
            HandlerEvent(listensock, readfds);
            break;
        }
    }

    return 0;
}

select编码特征

  1. select之前要进行所有参数的重置.之后要遍历所有合法fd进行事件监测.
  2. select需要用户自己维护第三方数组,来保存所有的合法fd,方便select批量处理.
  3. 一旦特点的fd事件就绪,本次的读取或者写入不会被阻塞.

select优缺点

优点:占用资源少,并且高效.对比之前的多线程,多进程.

缺点:每一次都要进行大量的重置工作,效率比较低.
每一次能够检测的fd数量是有上限的.
每一次都需要内核到用户,用户到内核传递位图参数,有较为大量的数据拷贝
select编码特别不方便,需要用户自己维护数组
select底层需要遍历的方式,检测所需要检测的fd

二、IO多路转接poll

poll函数接口

IO多路复用之select/poll/epoll_#include_05

参数说明:

  • fds是一个poll函数监听的结构列表.每一个元素中,包含了三部分内容:文件描述符,监听事件集合,返回的事件集合.
  • nfds表示fds数组的长度.
  • timeout表示poll函数的超时时间,单位是毫秒(ms).

events和revents的取值

IO多路复用之select/poll/epoll_c++_06


返回结果

  • 返回值小于0,表示出错.
  • 返回值等于0,表示poll函数等待超时.
  • 返回值大于0,表示poll由于监听的文件描述符就绪而返回.

socket就绪的条件

和select相同.

poll的优点

不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.

  • pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式.接口使用比select更方便.
  • poll并没有最大文件描述符数量限制(但是数量过大后性能还是会下降).

poll的缺点

poll监听的文件描述符数目增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的文件描述符.
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  • 同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降.

poll示例

makefile

PollServer:PollServer.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm PollServer
#include <iostream>
#include <poll.h>
#include <string>
#include "Sock.hpp"

using namespace std;

// 保存历史上所有的合法fd
#define NUM 1024
struct pollfd fdsArray[NUM] = {0};
#define DFL -1


// 意义:不光是网络sock,本地的文件描述符也可以被托管给多路转接,那么后面的文件操作,管道等也可以直接对接到多路转接!!!


static void Usage(string process)
{
    cerr << "\nUsage: " << process << "port\n"
         << endl;
}

static void showArray(int arr[], int num)
{
    cout << "当前合法sock list # ";
    for (int i = 0; i < num; ++i)
    {
        if (arr[i] == DFL)
            continue;
        else
            cout << arr[i] << " ";
    }
    cout << endl;
}

// redfds : 现在包含的就是已经就绪的sock
static void HandlerEvent(int listensock)
{
    for (int i = 0; i < NUM; ++i)
    {
        if (fdsArray[i].fd == DFL)
            continue;
        if (i == 0 && fdsArray[i].fd == listensock)
        {
            // 如何得知那些fd上面的事件就绪了呢?
            if (fdsArray[i].revents & POLLIN)
            {
                // 具有了一个新链接
                cout << "已经有一个新链接到来了,需要进行获取了..." << endl;
                string ip;
                uint16_t port;
                int sock = Sock::Accept(listensock, &ip, &port); // 这里不会阻塞
                if (sock < 0)
                    return;
                cout << "获取新链接成功 : " << ip << " : " << port << " sock: " << sock << endl;
                // read/wirte --- 不能调用,因为你read不知道底层数据是否就绪!!!
                // secelt知道! 想办法把新的fd托管给select
                int i = 0;
                for (; i < NUM; ++i)
                {
                    if (fdsArray[i].fd == DFL)
                        break;
                }
                if (i == NUM)
                {
                    cerr << "我的服务器已经到了最大的上限了,无法在承载更多的连接了..." << endl;
                    close(sock);
                }
                else
                {
                    // 将sock添加到select中,进一步的监听就绪事件了!
                    fdsArray[i].fd = sock;
                    fdsArray[i].events = POLLIN;
                    fdsArray[i].revents = 0;
                }
            }
        }
        else
        {
            // 处理普通sock的IO事件!
            if (fdsArray[i].revents & POLLIN)
            {
                // 一定是一个合法的普通的IO类sock就绪了
                // read/recv读取即可
                char buffer[1024];
                ssize_t s = recv(fdsArray[i].fd, buffer, sizeof buffer, 0);
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[" << fdsArray[i].fd << "]# " << buffer << endl;
                }
                else if (s == 0)
                {
                    cout << "client[" << fdsArray[i].fd << "] quit,server close: " << fdsArray[i].fd << endl;
                    close(fdsArray[i].fd);
                    fdsArray[i].fd = DFL; // 取出对该文件描述符的select的事件监听
                }
                else
                {
                    cout << "client[" << fdsArray[i].fd << "] quit,server error: " << fdsArray[i].fd << endl;
                    close(fdsArray[i].fd);
                    fdsArray[i].fd = DFL; // 取出对该文件描述符的select的事件监听
                }
            }
        }
    }
}

// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    // fd_set fds; // fa_set是用位图表示多个fd的.
    // cout<<sizeof(fd_set)<<endl;

    int listensock = Sock::Socket();
    Sock::Bind(listensock, 8080);
    Sock::Listen(listensock);

    for (int i = 0; i < NUM; ++i)
    {
        fdsArray[i].fd = DFL;
        fdsArray[i].events = 0;
        fdsArray[i].revents = 0;
    }
    fdsArray[0].fd = listensock;
    fdsArray[0].events = POLLIN; // 只关心读事件
    int timeout = 1000;
    while (true)
    {
        int n = poll(fdsArray,NUM,timeout);

        switch (n)
        {
        case 0:
            cout << "time out ..." << (unsigned long long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            HandlerEvent(listensock);
            break;
        }
    }

    return 0;
}

Sock.hpp的代码和select当时的代码相同!!!

那么现在的多路转接还有什么问题?

  1. 链接多的时候,select和poll都是基于对多个fd进行遍历检测,来识别事件的,链接多的时候,一定会引起遍历周期的增加.
  2. 对于事件(用户告诉内核,内核通知用户),需要使用的数据结构(数组),需要由程序员自己维护.

三、IO多路转接之epoll

epoll初始

按照man手册的说法:是为处理大批量句柄而作了改进的poll.

他是在2.5.44内核中引进的(epoll(4) is a nwe API introduced in Linux kernel 2.5.44).

他几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路IO就绪通知方法.

epoll的相关系统调用

epoll有三个相关的系统调用.
无论有多个接口,核心工作:只负责等!

epoll_create

IO多路复用之select/poll/epoll_#include_07


创建一个epoll的句柄.

  • 自从linux2.6.8以后,size参数是被忽略的,参数只要大于0即可.
  • 用完之后,必须调用close关闭.

epoll_ctl

IO多路复用之select/poll/epoll_#include_08


epoll的事件注册函数.

  • 他不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型.
  • 第一个参数是epoll_create()的返回值(epoll的句柄).
  • 第二个参数表示动作,用三个宏来表示.
  • 第三个参数是需要监听的fd.
  • 第四个参数是告诉内核需要监听什么事.

第二个参数的取值:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中删除一个fd;

struct epoll_event的结构如下:

IO多路复用之select/poll/epoll_c++_09

epoll_data_t的结构如下:

IO多路复用之select/poll/epoll_网络_10

events可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读(这里应该表示有外带数据的到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT : 只监听一次事件,当监听完这次事件事件之后,如果还需要监听这个socket的话,需要再次把这个socket加入到EPOLL队列中.

epoll_wait

IO多路复用之select/poll/epoll_c++_11

收集在epoll监控的事件中已经发送的事件:

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

epoll工作原理

操作系统如何得知,网络中的数据到来了呢?

网卡先得到数据会向CPU发送硬件中断,调用OS预设的中断函数,负责从外设进行数据拷贝.

epoll函数针对特定的一个或者多个fd,设定对应的回调机制;当fd缓冲区有数据的时候,进行回调.

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关.

....
/* 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/* 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
  • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
  • 这些事件都会挂载在红黑树中,如此重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是O(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的优点(对比于select)

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

注意!!!
网上有些博客说,epoll中使用了内存映射机制

  • 内存映射机制:内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝内存这样的额外性能开销.

这种说法是不准确的.我们定义的struct epoll_event是我们在用户空间中分配好的内存.势必还是需要将内核的数据拷贝到这个用户空间的内存中的.

epoll的工作方式

epoll有两种工作方式-水平触发(LT)和边缘触发(ET).

加入有这样一个例子

  • 我们已经把一个tcp socket添加到epoll描述符.
  • 这个时候socket的另一端被写入了2KB的数据.
  • 调用epoll_wait,并且他会返回,说明他已经准备好读取操作.
  • 然后调用read,制度去了1KB数据.
  • 继续调用epoll_wait.

水平触发Level Triggered 工作模式

epoll默认状态下就是LT工作模式.

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

边缘触发Edge Triggered工作模式

如果我们在第一步将socket添加到epoll描述符的时候使用了EPOLLET表示,epoll进入ET工作模式.

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

select和poll其实也是工作在LT模式下,epoll既可以支持LT,也可以支持ET.

对比LT和ET

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

相当于一个文件描述符就绪以后,不会反复被提示就绪,看起来就比LT更高效一些.但是在LT情况下如果也能做到每次就绪的文件描述符都立即处理,不让这个就绪被重复提示的话,其实性能也是一样的.

另一方面,ET的代码复杂程度更高了!

理解ET模式和非阻塞文件描述符

使用ET模式的epoll,需要将文件描述设置为非阻塞.这个不是接口上的要求,而是"工程实践"上的要求.

假设这样的场景:服务器接收到一个10K的请求,会向客户端返回一个应答数据.如果客户端收不到应答,不会发送第二个10K请求.

IO多路复用之select/poll/epoll_服务器_12


如果服务器写的代码是阻塞式的read,并且一次只read1K的数据的话(read不能保证一次就把所有的数据都读出来,参考man手册的说明,可能信号被打断),剩下的9K数据就会待在缓冲区中.

IO多路复用之select/poll/epoll_#include_13

此时由于epoll是ET模式,并不会认为文件描述符就绪.epoll_wait就不会再次返回.剩下的9K数据会一直在缓冲区中.直到下一次客户端再给服务器写数据.epoll_wait才能返回.

但是问题来了.

  • 服务器只读到1K数据,要10K读完才会给客户端返回响应数据.
  • 客户端要读到服务器的响应,才会发送下一个请求.
  • 客户端发送下一个请求,epoll_wait才会返回,才能去读缓冲区中剩余的数据.

IO多路复用之select/poll/epoll_#include_14


所以为了解决上述问题(阻塞read不一定能一下把完整的请求读完),于是就可以使用非阻塞轮询的方式来读缓冲区,保证一定能把完整的请求都读出来.

如果是LT没这个问题.只要缓冲区中的数据没读完,就能够让epoll_wait返回文件描述符读就绪.

epoll的使用场景

epoll的高性能,是有一定的特定场景的.如果场景选择不适宜的话,epoll的性能可能适得其反.

  • 对于多连接,且多个连接中只有一部分连接比较活跃时,比较适合epoll.

例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll.

如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不合适.具体要根据需求和场景特点来决定使用哪种IO模型.

epoll示例: epoll服务器(LT模式)

makefile

main:main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm main

log.hpp

/*
 * @Author: hulu 2367598978@qq.com
 * @Date: 2022-11-28 16:18:12
 * @LastEditors: hulu 2367598978@qq.com
 * @LastEditTime: 2022-12-05 11:47:11
 * @FilePath: /udp/log.hpp
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#pragma once
#include<cstdio>
#include<ctime>
#include<cstdarg>
#include<cassert>
#include<cstdlib>
#include<cstring>
#include<cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEBUG     0
#define NOTICE    1
#define WARINING  2
#define FATAL     3

const char* log_level[]={"DEBUG","NOTICE","WARINING","FATAL"};

#define LOGFIFE "ServerTcp.log"

class Log
{
public:
    Log():_logFd((-1))
    {}
    ~Log()
    {
        if(_logFd!=-1)
        {
            fsync(_logFd);//将操作系统中的数据尽快刷盘
            close(_logFd);
        }

    }
    void enable()
    {
        _logFd=open(LOGFIFE,O_WRONLY|O_APPEND|O_CREAT,0666);
        assert(_logFd!=-1);
        dup2(_logFd,0);
        dup2(_logFd,1);
        dup2(_logFd,2);
    }
private:
    int _logFd;
};

//logMessage(DEBUG,"%d",10);
void logMessage(int level,const char* format,...)
{
    assert(level>=DEBUG);
    assert(level<=FATAL);
    char logInfo[1024];
    char* name=getenv("USER");
    va_list ap; //ap--->char*
    va_start(ap,format);

    vsnprintf(logInfo,sizeof(logInfo)-1,format,ap);

    va_end(ap); //ap=NULL

    FILE* out=(level==FATAL)?stderr:stdout;
    fprintf(out,"%s | %u | %s | %s\n",\
    log_level[level],(unsigned int)time(nullptr),\
    name==nullptr?"unknow":name,logInfo);
    fflush(out);//将C缓冲区中的数据刷新到OS
    fsync(fileno(out)); // 将OS中的数据尽快刷盘
}

Sock.hpp

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <fstream>
using namespace std;

class Sock
{
public:
    static const int gBackLog = 20;
    static int Socket()
    {
        // 1.创建socket
        int _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0)
        {
            exit(1);
        }
        int opt = 1;
        setsockopt(_listenSock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT, &opt,sizeof opt);
        return _listenSock;
    }
    static void Bind(int socket, uint16_t _port)
    {
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        // 2.2本地socket信息,写入_sock对应的内核区域
        if (bind(socket, (const sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }
    }
    static void Listen(int socket)
    {
        // 3.监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(socket, gBackLog) < 0)
        {
            exit(3);
        }
        // 允许别人来连接你了
    }
    static int Accept(int socket,string* clientip,uint16_t* clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int serviceSock = accept(socket, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            return -1;
        }
        if(clientport) *clientport = ntohs(peer.sin_port);
        if(clientip) *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

EpollServer.hpp

#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <string>
#include<functional>
#include "log.hpp"
#include "Sock.hpp"
using namespace std;

#define NUM 1024

class EpollServer
{
public:
    using func_t = function<int(int)>;
    EpollServer(uint16_t port,func_t func)
        : _port(port),_func(func)
    {
    }
    ~EpollServer()
    {
        if (_listenSock != -1)
            close(_listenSock);
        if (_epFd != -1)
            close(_epFd);
    }
    void InitEpollServer()
    {
        _listenSock = Sock::Socket();

        Sock::Bind(_listenSock, _port);

        Sock::Listen(_listenSock);

        // 这里直接使用原生接口
        _epFd = epoll_create(NUM);
        if (_epFd < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(DEBUG, "创建监听套接字成功:%d", _listenSock);
        logMessage(DEBUG, "创建epoll成功:%d", _epFd);
    }
    void Run()
    {
        // 1.先添加_listenSock
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listenSock;
        int n = epoll_ctl(_epFd, EPOLL_CTL_ADD, _listenSock, &ev);
        assert(n == 0);
        (void)n;

        struct epoll_event revs[NUM];
        int timeout = 1000;
        while (true)
        {
            // 关于n: 就绪的fd的个数,只需要进行将底层的就绪队列中节点,一次从0下标放入到revs中即可
            int n = epoll_wait(_epFd, revs, NUM, timeout);
            switch (n)
            {
            case 0:
                cout << "time out ..." << (unsigned long long)time(nullptr) << endl;
                break;
            case -1:
                cerr << errno << " : " << strerror(errno) << endl;
                break;
            default:
                HandlerEvents(revs, n);
                break;
            }
        }
    }
    void HandlerEvents(struct epoll_event revs[], int n)
    {
        for (int i = 0; i < n; ++i)
        {
            int sock = revs[i].data.fd;
            uint32_t event = revs[i].events;
            if (event & EPOLLIN) // 读就绪就绪
            {
                if (sock == _listenSock) // 监听socket就绪,获取新链接
                {
                    string ip;
                    uint16_t port;
                    int sockfd = Sock::Accept(_listenSock, &ip, &port);
                    if (sockfd < 0)
                    {
                        logMessage(WARINING, "%d:%s", errno, strerror(errno));
                        continue;
                    }
                    // 托管给Epoll
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = sockfd;
                    int n = epoll_ctl(_epFd, EPOLL_CTL_ADD, sockfd, &ev);
                    assert(n == 0);
                    (void)n;
                }
                else // 普通socket就绪,进行数据INPUT
                {
                    int n = _func(sock);
                    if(n == 0 || n<0)
                    {
                        // 文件描述符先移除在关闭
                        int x =epoll_ctl(_epFd,EPOLL_CTL_DEL,sock,nullptr);
                        assert(x == 0);
                        (void)x;
                        logMessage(DEBUG,"client quit: %d",sock);
                        close(sock);
                    }


                }
            }
            else
            {
            }
        }
    }

private:
    int _listenSock = -1;
    int _epFd = -1;
    uint16_t _port;
    func_t _func;
};

main.cc

#include "EpollServer.hpp"
#include <memory>

static void Usage(string process)
{
    cerr << "\nUsage: " << process << "\tport\n"
         << endl;
}

int myfunc(int sock)
{
    char buffer[NUM];
    ssize_t s = recv(sock, buffer,sizeof(buffer)-1,0); // 不会被阻塞
    if(s > 0)
    {
        buffer[s] = 0;
        logMessage(DEBUG,"client[%d] #:%s",sock,buffer);
    }
    return s;
}

// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    unique_ptr<EpollServer> epollServer(new EpollServer(atoi(argv[1]), myfunc));
    epollServer->InitEpollServer();
    epollServer->Run();
}

总结

对于IO多路复用的三个函数就介绍到这里了,下一篇博客我们基于ET模式下的epoll服务器,也加Reactor模式.
(本章完!)