17.优于select的epoll

news2024/11/25 21:44:47

优于select的epoll

epoll 理解及应用

select复用方法其实由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端(当然,硬件性能不同,差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。

基于select的I/O复用技术速度慢的原因

第12章曾经实现过基于select的I/O复用服务器端,很容易从代码上分析出不合理的设计,最主要的两点如下。

  • 调用select函数后常见的针对所有文件描述符的循环语句。
  • 每次调用select函数时都需要向该函数传递监视对象信息。

上述两点可以从第12章示例echo selectserv.c的第45、49行及第54行代码得到确认。调用select 函数后,并不是把发生变化的文件描述符单独集中到一起,而是通过观察作为监视对象的fd_set 变量的变化,找出发生变化的文件描述符(示例echo selectserv.c的第54、56行),因此无法避免针对所有监视对象的循环语句。而且,作为监视对象的fd set变量会发生变化,所以调用select函数前应复制并保存原有信息(参考echo selectserv.c的第45行),并在每次调用select函数时传递新的监视对象信息。

各位认为哪些因素是提高性能的更大障碍?是调用select函数后常见的针对所有文件描述符对象的循环语句?还是每次需要传递的监视对象信息?

只看代码的话很容易认为是循环。但相比于循环语句,更大的障碍是每次传递监视对象信息。因为传递监视对象信息具有如下含义:

“每次调用select函数时向操作系统传递监视对象信息。”

应用程序向操作系统传递数据将对程序造成很大负担,而且无法通过优化代码解决,因此将成为性能上的致命弱点。

“那为何需要把监视对象信息传递给操作系统呢?”

有些函数不需要操作系统的帮助就能完成功能,而有些则必须借助于操作系统。假设各位定义了四则运算相关函数,此时无需操作系统的帮助。但select函数与文件描述符有关,更准确地说,是监视套接字变化的函数。而套接字是由操作系统管理的,所以select函数绝对需要借助于操作系统才能完成功能。select函数的这一缺点可以通过如下方式弥补:

“仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”

这样就无需每次调用select函数时都向操作系统传递监视对象信息,但前提是操作系统支持这种处理方式(每种操作系统支持的程度和方式存在差异)。Linux的支持方式是epoll,Windows 的支持方式是IOCP。

select也有优点

知道这些内容后,有些人可能对select函数感到失望,但大家应当掌握select函数。本章的epoll 方式只在Linux下提供支持,也就是说,改进的I/O复用模型不具有兼容性。相反,大部分操作系统都支持select函数。只要满足或要求如下两个条件,即使在Linux平台也不应拘泥于epoll。

  • 服务器端接入者少。
  • 程序应具有兼容性。

实际并不存在适用于所有情况的模型。各位应理解好各种模型的优缺点,并具备合理运用这些模型的能力。

实现epol时必要的函数和结构体

能够克服select函数缺点的epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。

  • 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。
  • 调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。

下面介绍epoll服务器端实现中需要的3个函数,希望各位结合epoll函数的优点理解这些函数的功能。

  • epoll_create:创建保存epoll文件描述符的空间。
  • epoll ctl:向空间注册并注销文件描述符。
  • epoll_wait:与select函数类似,等待文件描述符发生变化。

select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。

此外,为了添加和删除监视对象文件描述符,select方式中需要FD_SET、FD_CLR函数。但在epoll方式中,通过epoll ctl函数请求操作系统完成。最后,select方式下调用select函数等待文件描述符的变化,而epoll中调用epoll_wait函数。还有,select方式中通过fi_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体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_data_t data;
};

声明足够大的epoll event结构体数组后,传递给epollwait函数时,发生变化的文件描述符信息将被填入该数组。因此,无需像select函数那样针对所有文件描述符进行循环。

以上就是epoll中需要的函数和结构体。实际上,只要有select程序的编写经验,epoll程序的编写就并不难。接下来给出这些函数的详细说明。

epoll_create

epoll是从Linux的2.5.44版内核(操作系统的核心模块)开始引入的,所以使用epoll前需要验证Linux内核版本。但各位使用的Linux内核基本都是2.6以上的版本,所以这部分可以忽略。若有人怀疑自己的Linux版本过低,可以通过如下命令验证:

cat /proc/sys/kernel/osrelease

下面仔细观察epoll_create函数。

#include <sys/epoll.h>
int epoll_create(int size);
// 成功时返回 epoll文件描述符,失败时返回-1。
// size epoll实例的大小。

调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程",但有些情况下名称不同,需要稍加注意。通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议。换言之,size并非用来决定epoll例程的大小,而仅供操作系统参考。

操作系统将完全忽略传递给epoll_create的参数

Linux 2.6.8之后的内核将完全忽略传入 epoll_create 函数的 size 参数,因为内核会根据情况调整 epoll例程的大小。但撰写本书时Linux版本未达到2.6.8,因此无法在忽略size参数的情况下编写程序。

epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符。也就是说,该函数返回的文件描述符主要用与于区分epoll 例程。需要终止时,与其他文件描述符相同,也要调用close函数。

epoll_ctl

生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_ctl函数。

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
// 成功时返回0,失败时返回-1。
// epfd         用于注册监视对象的epoll例程的文件描述符。
// op           用于指定监视对象的添加、删除或更改等操作。
// fd           需要注册的监视对象文件描述符。
// event        监视对象的事件类型。

与其他epoll函数相比,该函数多少有些复杂,但通过调用语句就很容易理解。假设按照如下形式调用epoll_ctl函数:

epoll_ctl(A, EPOLL_CTL_ADD, B, C);

第二个参数EPOLL_CTL_ADD意味着"添加",因此上述语句具有如下含义:

“向epoll A中注册文件描述符B,主要目的是监视参数C中的事件。”

再介绍一个调用语句。

epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);

上述语句中第二个参数EPOLL_CTL_DEL指"删除",因此该语句具有如下含义:

“从epoll A中删除文件描述符B。”

从上述调用语句中可以看到,从监视对象中删除时,不需要监视类型(事件信息),因此向第四个参数传递NULL。接下来介绍可以向epoll_ctl第二个参数传递的常量及含义。

  • EPOLL_CTLADD:将文件描述符注册到epoll例程。
  • EPOLL_CTL_DEL:从epoll例程中删除文件描述符。
  • EPOLL_CTLMOD:更改注册的文件描述符的关注事件发生情况。

关于EPOLL CTL MOD常量稍后讲解(即使我不讲大家也自然能明白)。如前所述,向epoll_ctl的第二个参数传递EPOLL_CTL_DEL时,应同时向第四个参数传递NULL。但Linux2.6.9 之前的内核不允许传递NULL。虽然被忽略掉,但也应传递epoll_event结构体变量的地址值(本书示例将传递NULL)。其实这是Bug,但也没必要因此怀疑epoll的功能,因为我们使用的标准函数中也存在Bug。

下面讲解各位不太熟悉的epoll_ctl函数的第四个参数,其类型是之前将过的epoll_event结构体指针。

“啊?不是说epoll_event用于保存发生变化的(发生事件)的文件描述符吗?”

当然!如前所述,epoll_event结构体用于保存发生事件的文件描述符集合。但也可以在epoll 例程中注册文件描述符时,用于注册关注的事件。函数中epoll_event结构体的定义并不显眼,因此通过调用语句说明该结构体在epoll_ctl函数中的应用。

struct epoll_event event;
event.events=EPOLLIN; //发生需要读取数据的情况(事件)时
event.data.fd=sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);

上述代码将sockfd注册到epoll例程epfd中,并在需要读取数据的情况下产生相应事件。接下来给出epoll_event的成员events中可以保存的常量及所指的事件类型。

  • EPOLLIN:需要读取数据的情况。
  • EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。
  • EPOLLPRI:收到OOB数据的情况。
  • EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。
  • EPOLLERR:发生错误的情况。
  • EPOLLET:以边缘触发的方式得到事件通知。
  • EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。

可以通过位或运算同时传递多个上述参数。关于“边缘触发”稍后将单独讲解,目前只需记住EPOLLIN即可。

epoll_wait

最后介绍与select函数对应的epoll_wait函数,epoll相关函数中默认最后调用该函数。

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// 成功时返回发生事件的文件描述符数,失败时返回-1。
// epfd         表示事件发生监视范围的epoll例程的文件描述符。
// events       保存发生事件的文件描述符集合的结构体地址值。
// maxevents    第二个参数中可以保存的最大事件数。
// timeout      以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。

该函数的调用方式如下。需要注意的是,第二个参数所指缓冲需要动态分配。

epoll_event* ep_events = new epoll_event[EPOLL_SIZE];
int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
// ...
delete[] ep_events;

调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。

基于epoll的回声服务器端

以上就是基于epoll技术实现服务器端的所有理论说明,接下来给出基于epoll的回声服务器端示例。我通过更改第12章的echo_selectserv.c实现了该示例。当然,从头开始写也与下面给出的内容类似。但通过更改select示例理解二者差异将更有利于学习。

#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

constexpr size_t BUF_SIZE = 1024;
constexpr size_t EPOLL_SIZE = 64;

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 0;
    }

    // 创建套接字
    int servSock = socket(PF_INET, SOCK_STREAM, 0);
    if (servSock == -1) {
        std::cout << "socket 错误" << std::endl;
        return 0;
    }

    // 初始化地址信息
    sockaddr_in servAdr;
    std::memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAdr.sin_port = htons(std::atoi(argv[1]));

    // 绑定地址信息
    int stu = bind(servSock, (sockaddr*)&servAdr, sizeof(servAdr));
    if (stu == -1) {
        close(servSock);
        std::cout << "bind 错误" << std::endl;
        return 0;
    }

    // 进入监听状态
    stu = listen(servSock, 5);
    if (stu == -1) {
        close(servSock);
        std::cout << "listen 错误" << std::endl;
        return 0;
    }

    // 创建epoll
    int epFd = epoll_create(EPOLL_SIZE);
    if (epFd == -1) {
        close(servSock);
        std::cout << "epoll_create 错误" << std::endl;
        return 0;
    }

    // 初始化事件类型
    epoll_event epEvt;
    epEvt.events = EPOLLIN; // 类型指定为EPOLLIN
    epEvt.data.fd = servSock; // 目标套接字为servSock
    // 注册epoll事件
    epoll_ctl(epFd, EPOLL_CTL_ADD, servSock, &epEvt);

    char buf[BUF_SIZE] = { 0 };
    // 动态创建epoll_event数组,保存发生变化的事件
    epoll_event* evtArr = new epoll_event[EPOLL_SIZE];

    while (true) {
        // 返回事件数量
        int epCnt = epoll_wait(epFd, evtArr, EPOLL_SIZE, -1);
        if (epCnt == -1) {
            std::cout << "epoll_wait 错误" << std::endl;
            break;
        }

        // 遍历已触发的事件
        for (int i = 0; i < epCnt; ++i) {
            int sockId = evtArr[i].data.fd;
            // 如果变化的事件套接字等于服务端套接字,说明有新的请求
            if (sockId == servSock) {
                sockaddr_in clntAdr;
                socklen_t szAdr = sizeof(clntAdr);
                int clntSock = accept(servSock, (sockaddr*)&clntAdr, &szAdr);
                // 注册接收的套接字到epoll
                epEvt.events = EPOLLIN;
                epEvt.data.fd = clntSock;
                epoll_ctl(epFd, EPOLL_CTL_ADD, clntSock, &epEvt);

                std::string adrStr = inet_ntoa(clntAdr.sin_addr);
                std::cout << "接收" << adrStr << "的请求:" << clntSock << std::endl;
            } else {
                int strLen = read(sockId, buf, BUF_SIZE);
                if (strLen == 0) {
                    // 说明客户端断开连接
                    epoll_ctl(epFd, EPOLL_CTL_DEL, sockId, nullptr);
                    close(sockId);
                    std::cout << "客户端:" << sockId << "断开连接" << std::endl;
                } else {
                    buf[strLen] = { 0 };
                    std::cout << "接收客户端" << sockId << "的数据:" << buf << std::endl;
                    write(sockId, buf, strLen);
                }
            }
        }
    }

    close(epFd);
    close(servSock);

    delete[] evtArr;

    return 0;
}


之前解释过关键代码,而且程序结构与select方式没有区别,故省略代码说明。如果有些地方难以理解,说明未掌握本章之前的内容和select模型,建议复习。结合我的说明和select示例理解上述代码也是一种很好的学习方式。上述示例可以结合任意回声客户端运行,而且运行结果与其他回声服务器端/客户端程序没有差别,故省略。

条件触发和边缘触发

有些人学习epoll时往往无法正确区分条件触发(Level Trigger)和边缘触发(Edge Trigger),但只有理解了二者区别才算完整掌握epoll。有些人学习epoll时往往无法正确区分条件触发(Level Trigger)和边缘触发(Edge Trigger),但只有理解了二者区别才算完整掌握epoll。

条件触发和边缘触发的区别在于发生事件的时间点

首先给出示例帮助各位理解条件触发和边缘触发。观察如下对话,可以通过对话内容理解条件触发事件的特点。

  • 儿子:“妈妈,我收到了5000元压岁钱。”

  • 妈妈:“恩,真棒!”

  • 儿子:"我给隔壁家秀熙买了炒年糕,花了2000元。

  • 妈妈:“恩,做得好!”

  • 儿子:“妈妈,我还买了玩具,剩下500元。”

  • 妈妈:“用完零花钱就只能挨饿喽!”

  • 儿子:“妈妈,我还留着那500元没动,不会挨饿的。”

  • 妈妈:“恩,很明智嘛!”

  • 儿子:“妈妈,我还留着那500元没动,我要攒起来。”

  • 妈妈:“恩,加油!”

从上述对话可以看出,儿子从收到压岁钱开始一直向妈妈报告,这就是条件触发的原理。如果将上述对话中的儿子(儿子的钱包)换成输入缓冲,压岁钱换成输入数据,儿子的报告换成事件,则可以发现条件触发的特性。我将其整理如下:

“条件触发方式中,只要输入缓冲有数据就会一直通知该事件。”

例如,服务器端输入缓冲收到50字节的数据时,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务器端读取20字节后还剩30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。接下来通过如下对话介绍边缘触发的事件特性。

  • 儿子:“妈妈,我收到了5000元压岁钱。”

  • 妈妈:“恩,再接再厉。”

  • 儿子:“……”

  • 妈妈:“说话呀!压岁钱呢?不想回答吗?”

从上述对话可以看出,边缘触发中输入缓冲收到数据时仅注册1次该事件。即使输入缓冲中还留有数据,也不会再进行注册。

掌握条件触发的事件特性

接下来通过代码了解条件触发的事件注册方式。下列代码是稍微修改之前的echo_epollserv.c 示例得到的。epoll默认以条件触发方式工作,因此可以通过该示例验证条件触发的特性。

#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

constexpr size_t BUF_SIZE = 4;
constexpr size_t EPOLL_SIZE = 64;

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 0;
    }

    // 创建套接字
    int servSock = socket(PF_INET, SOCK_STREAM, 0);
    if (servSock == -1) {
        std::cout << "socket 错误" << std::endl;
        return 0;
    }

    // 初始化地址信息
    sockaddr_in servAdr;
    std::memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAdr.sin_port = htons(std::atoi(argv[1]));

    // 绑定地址信息
    int stu = bind(servSock, (sockaddr*)&servAdr, sizeof(servAdr));
    if (stu == -1) {
        close(servSock);
        std::cout << "bind 错误" << std::endl;
        return 0;
    }

    // 进入监听状态
    stu = listen(servSock, 5);
    if (stu == -1) {
        close(servSock);
        std::cout << "listen 错误" << std::endl;
        return 0;
    }

    // 创建epoll
    int epFd = epoll_create(EPOLL_SIZE);
    if (epFd == -1) {
        close(servSock);
        std::cout << "epoll_create 错误" << std::endl;
        return 0;
    }

    // 初始化事件类型
    epoll_event epEvt;
    epEvt.events = EPOLLIN; // 类型指定为EPOLLIN
    epEvt.data.fd = servSock; // 目标套接字为servSock
    // 注册epoll事件
    epoll_ctl(epFd, EPOLL_CTL_ADD, servSock, &epEvt);

    char buf[BUF_SIZE] = { 0 };
    // 动态创建epoll_event数组,保存发生变化的事件
    epoll_event* evtArr = new epoll_event[EPOLL_SIZE];

    size_t cntWait = 1;
    while (true) {
        // 返回事件数量
        int epCnt = epoll_wait(epFd, evtArr, EPOLL_SIZE, -1);
        if (epCnt == -1) {
            std::cout << "epoll_wait 错误" << std::endl;
            break;
        }

        std::cout << "epoll_wait次数:" << cntWait++ << std::endl;
        // 遍历已触发的事件
        for (int i = 0; i < epCnt; ++i) {
            int sockId = evtArr[i].data.fd;
            // 如果变化的事件套接字等于服务端套接字,说明有新的请求
            if (sockId == servSock) {
                sockaddr_in clntAdr;
                socklen_t szAdr = sizeof(clntAdr);
                int clntSock = accept(servSock, (sockaddr*)&clntAdr, &szAdr);
                // 注册接收的套接字到epoll
                epEvt.events = EPOLLIN;
                epEvt.data.fd = clntSock;
                epoll_ctl(epFd, EPOLL_CTL_ADD, clntSock, &epEvt);

                std::string adrStr = inet_ntoa(clntAdr.sin_addr);
                std::cout << "接收" << adrStr << "的请求:" << clntSock << std::endl;
            } else {
                int strLen = read(sockId, buf, BUF_SIZE);
                if (strLen == 0) {
                    // 说明客户端断开连接
                    epoll_ctl(epFd, EPOLL_CTL_DEL, sockId, nullptr);
                    close(sockId);
                    std::cout << "客户端:" << sockId << "断开连接" << std::endl;
                } else {
                    buf[strLen] = { 0 };
                    std::cout << "接收客户端" << sockId << "的数据:" << buf << std::endl;
                    write(sockId, buf, strLen);
                }
            }
        }
    }

    close(epFd);
    close(servSock);

    delete[] evtArr;

    return 0;
}


上述示例与之前的echo_epollserv.c之间的差异如下。

  • 将调用read函数时使用的缓冲大小缩减为4个字节(第2行)
  • 插入验证epoll_wait函数调用次数的语句(第50行)

减少缓冲大小是为了阻止服务器端一次性读取接收的数据。换言之,调用read函数后,输入缓冲中仍有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出"return epoll wait"字符串。前提是条件触发的工作方式与我的描述一致。接下来观察运行结果。该程序同样可以结合第4章的echo client.c运行。

发送一个较长的字符串,会发现,epoll_wait被调用了好多次。

从运行结果中可以看出,每当收到客户端数据时,都会注册该事件,并因此多次调用epoll_wait函数。下面将上述示例改成边缘触发方式,需要做一些额外的工作。但我希望通过最小的改动验证边缘触发模型的事件注册方式。将上述示例的第57行改成如下形式运行服务器端和客户端(不会单独提供这方面的源代码,需要各位自行更改):

event.events = EPOLLIN|EPOLLET;

更改后可以验证如下事实:

"从客户端接收数据时,仅输出1次’return epoll_wait’字符串,这意味着仅注册1次事件。"虽然可以验证上述事实,但客户端运行时将发生错误。大家是否遇到了这种问题?能否自行分析原因?虽然目前不必对此感到困惑,但如果理解了边缘触发的特性,应该可以分析出错误原因。

select模型是条件触发还是边缘触发?

select模型是以条件触发的方式工作的,输入缓冲中如果还剩有数据,肯定会注册事件。各位若感兴趣,可以自行编写示例验证select模型的工作方式。

由于缓冲区只有4字节,读不完,但是边缘触发只会触发一次,导致缓冲区始终无法清空

边缘触发的服务器端实现中必知的两点

这部分我的理解

使用非阻塞函数,可以配合错误类型判断缓冲区是否读完。
阻塞和非阻塞函数都需要循环才能读完缓冲区
如果使用阻塞函数,当缓冲区读完时,strLen不会为0,就没法退出读取循环。
非阻塞函数的strLen也不会为0,但是会有一个EAGAIN错误值,通过判断此错误值,就可知道缓冲区已经读完了

下面讲解边缘触发服务器端的实现方法。在此之前,我希望说明如下2点,这些是实现边缘触发的必知内容。

  • 通过errno变量验证错误原因。
  • 为了完成非阻塞(Non-blocking)I/O,更改套接字特性。

Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息,Linux声明了如下全局变量:

int errno;

为了访问该变量,需要引入error.h头文件,因为此头文件中有上述变量的extern声明。另外,每种函数发生错误时,保存到errno变量中的值都不同,没必要记住所有可能的值。学习每种函数的过程中逐一掌握,并能在必要时参考即可。本节只介绍如下类型的错误:

“read函数发现输入缓冲中没有数据可读时返回-1,同时在ermo中保存EAGAIN常量。”

稍后通过示例给出errno的使用方法。下面讲解将套接字改为非阻塞方式的方法。Linux提供更改或读取文件属性的如下方法(曾在第13章使用过)。

#include <fcntl.h>
int fcntl(int filedes, int cmd, ...);
// 成功时返回cmd参数相关值,失败时返回-1。
// filedes          属性更改目标的文件描述符。
// cmd              表示函数调用的目的。

从上述声明中可以看到,fcntl具有可变参数的形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性(int型)。反之,如果传递F_SETFL,可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下2条语句。

int flag=fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);

通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read&write函数时,无论是否存在数据,都会形成非阻塞文件(套接字)。fcntl函数的适用范围很广,各位既可以在学习系统编程时一次性总结所有适用情况,也可以每次需要时逐一掌握。

实现边缘触发的回声服务器端

之所以介绍读取错误原因的方法和非阻塞模式的套接字创建方法,原因在于二者都与边缘触发的服务器端实现有密切联系。首先说明为何需要通过errno确认错误原因。

“边缘触发方式中,接收数据时仅注册1次该事件。”

就因为这种特点,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据。因此需要验证输入缓冲是否为空。

“read函数返回-1,变量errno中的值为EAGAIN时,说明没有数据可读。”

既然如此,为何还需要将套接字变成非阻塞模式?边缘触发方式下,以阻塞方式工作的read &write函数有可能引起服务器端的长时间停顿。因此,边缘触发方式中一定要采用非阻塞read&write函数。接下来给出以边缘触发方式工作的回声服务器端示例。

#include <arpa/inet.h>
#include <cstring>
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

constexpr size_t BUF_SIZE = 4;
constexpr size_t EPOLL_SIZE = 64;

// 封装函数,设置套接字非阻塞
void setNoBlockingMode(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 0;
    }

    // 创建套接字
    int servSock = socket(PF_INET, SOCK_STREAM, 0);
    if (servSock == -1) {
        std::cout << "socket 错误" << std::endl;
        return 0;
    }

    // 初始化地址信息
    sockaddr_in servAdr;
    std::memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAdr.sin_port = htons(std::atoi(argv[1]));

    // 绑定地址信息
    int stu = bind(servSock, (sockaddr*)&servAdr, sizeof(servAdr));
    if (stu == -1) {
        close(servSock);
        std::cout << "bind 错误" << std::endl;
        return 0;
    }

    // 进入监听状态
    stu = listen(servSock, 5);
    if (stu == -1) {
        close(servSock);
        std::cout << "listen 错误" << std::endl;
        return 0;
    }

    // 创建epoll
    int epFd = epoll_create(EPOLL_SIZE);
    if (epFd == -1) {
        close(servSock);
        std::cout << "epoll_create 错误" << std::endl;
        return 0;
    }

    // 初始化事件类型
    epoll_event epEvt;
    epEvt.events = EPOLLIN | EPOLLET; // 类型指定为EPOLLIN
    epEvt.data.fd = servSock; // 目标套接字为servSock
    // 注册epoll事件
    epoll_ctl(epFd, EPOLL_CTL_ADD, servSock, &epEvt);

    char buf[BUF_SIZE] = { 0 };
    // 动态创建epoll_event数组,保存发生变化的事件
    epoll_event* evtArr = new epoll_event[EPOLL_SIZE];

    size_t cntWait = 1;
    while (true) {
        // 返回事件数量
        int epCnt = epoll_wait(epFd, evtArr, EPOLL_SIZE, -1);
        if (epCnt == -1) {
            std::cout << "epoll_wait 错误" << std::endl;
            break;
        }

        std::cout << "epoll_wait次数:" << cntWait++ << std::endl;
        // 遍历已触发的事件
        for (int i = 0; i < epCnt; ++i) {
            int sockId = evtArr[i].data.fd;
            // 如果变化的事件套接字等于服务端套接字,说明有新的请求
            if (sockId == servSock) {
                sockaddr_in clntAdr;
                socklen_t szAdr = sizeof(clntAdr);
                int clntSock = accept(servSock, (sockaddr*)&clntAdr, &szAdr);
                // 将接收的套接字设置为非阻塞
                setNoBlockingMode(clntSock);
                // 注册接收的套接字到epoll
                epEvt.events = EPOLLIN | EPOLLET;
                epEvt.data.fd = clntSock;
                epoll_ctl(epFd, EPOLL_CTL_ADD, clntSock, &epEvt);

                std::string adrStr = inet_ntoa(clntAdr.sin_addr);
                std::cout << "接收" << adrStr << "的请求:" << clntSock << std::endl;
            } else {
                // 读取循环
                while (true) {
                    int strLen = read(sockId, buf, BUF_SIZE);
                    if (strLen == 0) {
                        // 说明客户端断开连接
                        epoll_ctl(epFd, EPOLL_CTL_DEL, sockId, nullptr);
                        close(sockId);
                        std::cout << "客户端:" << sockId << "断开连接" << std::endl;
                        break;
                    } else if (strLen < 0) {
                        if (errno == EAGAIN) {
                            // 发生错误且错误值为EAGAIN时,说明读取完毕
                            std::cout << "套接字:" << sockId << "读取完毕" << std::endl;
                        } else {
                            std::cout << "套接字:" << sockId << "读取错误" << std::endl;
                        }
                        break;
                    } else {
                        buf[strLen] = { 0 };
                        std::cout << "接收客户端" << sockId << "的数据:" << buf << std::endl;
                        write(sockId, buf, strLen);
                    }
                }
            }
        }
    }

    close(epFd);
    close(servSock);

    delete[] evtArr;

    return 0;
}


条件触发和边缘触发孰优孰劣

我们从理论和代码的角度充分理解了条件触发和边缘触发,但仅凭这些还无法理解边缘触发相对于条件触发的优点。边缘触发方式下可以做到如下这点:“可以分离接收数据和处理数据的时间点!”

虽然比较简单,但非常准确有力地说明了边缘触发的优点。关于这句话的含义,大家以后开发不同类型的程序时会有更深入的理解。现阶段给出如下情景帮助大家理解,如图17-1所示。

理解边缘触发

图17-1的运行流程如下。

  • 服务器端分别从客户端A、B、C接收数据。
  • 服务器端按照A、B、C的顺序重新组合收到的数据。
  • 组合的数据将发送给任意主机。

为了完成该过程,若能按如下流程运行程序,服务器端的实现并不难。

  • 客户端按照A、B、C的顺序连接服务器端,并依序向服务器端发送数据。
  • 需要接收数据的客户端应在客户端A、B、C之前连接到服务器端并等待。

但现实中可能频繁出现如下这些情况,换言之,如下情况更符合实际。

  • 客户端C和B正向服务器端发送数据,但A尚未连接到服务器端。
  • 客户端A、B、C乱序发送数据。
  • 服务器端已收到数据,但要接收数据的目标客户端还未连接到服务器端。

因此,即使输入缓冲收到数据(注册相应事件),服务器端也能决定读取和处理这些数据的时间点,这样就给服务器端的实现带来巨大的灵活性。

“条件触发中无法区分数据接收和处理吗?”

并非不可能。但在输入缓冲收到数据的情况下,如果不读取(延迟处理),则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加,服务器端能承受吗?这在现实中是不可能的(本身并不合理,因此是根本不想做的事)。

条件触发和边缘触发的区别主要应该从服务器端实现模型的角度谈论,因此希望各位不要提下面这种问题。如果理解了之前的讲解,应该有更好的提问。

“边缘触发是否更快?能快多少呢?”

从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/167972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IIC驱动中景园0.96寸OLED

驱动硬件介绍 1、驱动电压3.3到5,但是正点的也是这个芯片说用3.3 5会烧坏掉。 2、RST 上的低电平,将导致OLED 复位,在每次初始化之前,都应该复位一下 OLED 模块。而我们使用四线,里面就没有复位了 3、裸屏有多种接口方式(驱动芯片为SSD1306) 6800、8080 两种并行接口方…

Redis应用2(Redison)

不推荐使用application的配置方式,因为会替代spring内部的对于redis的配置方式 注意:如果redis数据库没有密码,不要使用 config.useSingleServer().setPassword("") 的形式,直接跳过setPassword()就可以,配置类写法如下: Configuration public class RedisConfig…

MySQL进阶——索引

一、索引及其分类 1.索引的概念 索引是一种特殊的文件&#xff0c;包含着对数据表中所有记录的引用指针通俗点说&#xff0c;索引就好比是一本书的目录&#xff0c;能加快数据库的查询速度例如需要遍历 200 条数据&#xff0c;在没有索引的情况下&#xff0c;数据库会遍历全部…

Spring之事务编程概述

目录 一&#xff1a;基本概念 搭建测试环境 基于xml声明式事务控制 二&#xff1a;事务相关配置 ​编辑 基于注解声明式事务控制 三&#xff1a;Spring事务角色 四&#xff1a;事务传播行为 五&#xff1a;案例&#xff1a;转账业务追加日志 一&#xff1a;基本概念 事…

Vue.nextTick核心原理

相信大家在写vue项目的时候&#xff0c;一定会发现一个神奇的api&#xff0c;Vue.nextTick。为什么说它神奇呢&#xff0c;那是因为在你做某些操作不生效时&#xff0c;将操作写在Vue.nextTick内&#xff0c;就神奇的生效了。那这是什么原因呢&#xff1f; 让我们一起来研究一…

手把手教你写Dockerfile以及测试

Dockerfile是什么&#xff1f; dockerfile就是用来构建docker镜像的构建文件,命令参数脚本。 如何使用Dockerfile&#xff1f; 1、编写一个Dockerfile文件2、docker build构建成 基础使用&#xff08;此处罗列一些我们经常用到的&#xff09; # 指定依赖镜像版本&#xff…

【附代码】十大主流聚类算法

准备工作安装必要的库pip install scikit-learn准备数据集使用 make _ classification ()函数创建一个测试二分类数据集。数据集将有1000个示例&#xff0c;每个类有两个输入要素和一个群集。这些群集在两个维度上是可见的&#xff0c;因此我们可以用散点图绘制数据&#xff0c…

第18章_JDBC

一、JDBC概述JDBC概述什么是JDBCJDBC&#xff08;Java DataBase Connectivity, Java数据库连接&#xff09; ,是一种用于执行SQL语句的Java API&#xff0c;为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成有了JDBC&#xff0c;程序员只需用JDBC API写一个…

夜深忽梦少年事,7年又一年,来看看95年那个小伙现在怎么样了

2022年已到尾声&#xff0c;疫情也结束了&#xff0c;这三年太不容易了&#xff0c;今年也是一样在疫情的艰难的度过&#xff0c;就是做了两件事&#xff0c;防疫和上班&#xff0c;没什么可写的。但是在一个深夜晚上&#xff0c;想了很多以前的事&#xff0c;想想还是写一点东…

亚马逊云科技Amazon DeepRacer互联网行业全国冠军诞生

1月11日&#xff0c;首届亚马逊云科技Amazon DeepRacer自动驾驶赛车互联网行业全国总决赛圆满结束&#xff0c;从全国各地选拔出的9支冠军队伍齐聚滨海三亚&#xff0c;向总决赛的桂冠发起了冲击。 本次比赛沿袭了Amazon DeepRacer League全球赛事标准&#xff0c;使用了全新的…

Vue.js的this如何取到data和method里的属性?

本篇文章介绍的是Vue.js如何取到data和methods里的属性&#xff1f; 准备工作 克隆源码到本地 git clone https://github.com/vuejs/vue.git 下载完毕后&#xff0c;用vscode打开&#xff0c;目光移动到package.json的scripts属性&#xff0c;我们看到有dev和build&#xff0…

Golang -- openwechat发送消息、自动回复

开篇 马上就要到农历新年了&#xff0c;不妨写一段代码准时为好友们送上祝福。 该 Demo 使用开源项目 openwechat &#xff0c;实现获取好友列表、为好友发送消息、图片或文件&#xff0c;接收来自好友或群组的消息并设置自动回复等功能。 openwechat Github地址 openwechat 文…

CSS设置元素内边距(padding)、外边距(margin)

设置元素内边距padding 所有的 HTML 元素基本都是以矩形为基础。 每个 HTML 元素周围的矩形空间由三个重要的属性来控制&#xff1a; padding&#xff08;内边距&#xff09; margin&#xff08;外边距&#xff09; border&#xff08;边框&#xff09; padding控制着元素内容…

产品经理需要懂的专业术语有哪些?

不同的行业都有着不同的专业术语&#xff0c;掌握专业术语不仅是个人专业能力的体现&#xff0c;还可以进一步促进工作中的交流&#xff0c;提高工作效率。 1、工作类 BRD&#xff1a;商业文档&#xff0c;包含了商业几乎&#xff0c;产品背景&#xff0c;可行性说明&#xff…

Redis底层数据结构简介

目录 1、Redis存储结构 2、数据结构 2.1、简单动态字符串(SDS) 2.2.1、SDS数据结构 2.2.2、编码 2.2.3、SDS与C字符串对比 2.2、链表(Linkedlist) 2.2.1、链表数据结构(双向链表) 2.2.2、特性 2.3、跳表(Skiplist) 2.3.1、数据结构 2.3.2、特点 2.3.3、增删查操作…

宝元机床联网

一、设备信息确认 宝元数控在台湾也是做的比较早的数控系统品牌&#xff0c;13年被研华并购。 1、确认型号 宝元的数控面板关机情况下是没办法判断型号的&#xff0c;要在开机的一瞬间确认。 此系统为&#xff1a;M520 注&#xff1a;目前接触宝元系统基本上都含网口。 2、…

maven依赖设置

之前说过了可以通过依赖的方式将一个大程序分为多个小的模块&#xff0c;模块之间可以利用依赖链接在一起。 但是如果有多个依赖的情况下会怎么样呢&#xff1f; A依赖于B、C&#xff0c;而B、C又有各自的依赖&#xff0c;那么A是否依赖于B、C的依赖呢&#xff1f; 答案是是的…

OpenResty中Lua变量的使用

全局变量 在 OpenResty 里面&#xff0c;只有在 init_by_lua* 和 init_worker_by_lua* 阶段才能定义真正的全局变量。 这是因为其他阶段里面&#xff0c;OpenResty 会设置一个隔离的全局变量表&#xff0c;以免在处理过程污染了其他请求。 即使在上述两个可以定义全局变量的阶…

全息(CSDN_0009_20220919)

文章编号&#xff1a;CSDN_0009_20220919 目录 全息的广义概念 发展历程 全息摄影 全息投影 全息影像 全息应用 全息投影 全息的广义概念 反映物体在空间存在时的整个情况的全部信息。 特指一种技术&#xff0c;可以让从物体发射的衍射光能够被重现&#xff0c;其位置和…

uniapp获取微信openid - 微信提现 - 登录授权 - AndroidStudio离线打包微信登陆

效果图 主要步骤 (详细步骤有配图) 登录微信开放平台,获取AppID + AppSecrethttps://open.weixin.qq.com/