IO多路转接:select、poll、epoll

目录

非阻塞读取

fcntl函数

I/O多路转接之select

select函数

fd_set结构

select的模拟实现

select的优缺点

I/O多路转接之poll

poll函数

struct pollfd结构体

poll函数的使用示例

poll的模拟实现

poll的优缺点

I/O多路转接之epoll

epoll的三个系统调用

epoll的工作原理

epoll的模拟实现

epoll的优点

epoll的工作方式

对比LT和ET


Linux中五种IO模型(阻塞、非阻塞、多路转接、信号驱动、异步IO)中,只有前三种比较常用,后两种不太常用,所以下面只学习前三种IO模型

首先IO = 等 + 数据拷贝,在网络通信时大部分时间都在等,等IO类事件就绪,一旦就绪了,我们就可以从内核拷贝到用户,所以我们为了提高IO效率,也就是为了减少等的比重,也就是让我们单位时间内,拷贝的数据量变得更多,IO的效率也就更高了

同步异步IO的区别其实就是:是否有参与到 等 + 拷贝 的这个流程中来

对于读来讲:底层有数据
对于写来讲:底层有空间

就叫做IO类事件就绪

非阻塞读取

说起阻塞读取,我们最常见的就是0号文件描述符标准输入了,如下所示:

此时运行代码:

我若是不输入内容,就会一直阻塞式等待,这就是阻塞,输入一行内容就打印一行内容:


如果想设置文件描述符为非阻塞式等待,需要用到fcntl函数:

fcntl函数

fcntl就是对文件描述符进行指定命令的操作,如果失败返回值是-1

而我们设置文件描述符为非阻塞,也并不想影响其他的文件描述符选项 ,所以第三个参数是在原有的基础上,按位或新的标记位

获取标志位,第二个参数填F_GETFL

设置标志位,第二个参数填F_SETFL,后面是可变参数,想设置什么就往后面加


此时将函数改为如下所示:

运行结果如下:

当没有数据时,就会打印"当前0号fd数据没有就绪, 请重复试试"
当输入数据时,就会打印输入的数据

这就叫做非阻塞读取

非阻塞读取的意义就是,当我们的线程走到这里时:

如果返回的错误码是:EWOULDBLOCK或是EAGAIN,就表示并不是出错了,只是数据还没有就绪

如果是EINTR,就表示被其他信号锁中断了,也并不是出错

所以如果发现返回的信号是上述的两种情况,就continue继续,并不是出错

非阻塞读取就可以让当前进程做其他的事情,而阻塞读取就只能被挂起,什么也干不了


I/O多路转接之select

多路转接是给我们提供更高效的方案,一次等待多个文件描述符,下面是select的主要工作:

  • 帮用户进行一次等待多个文件sock
  • 当哪些文件sock就绪了,select就要通知用户,对应就绪的sock有哪些,然后用户再调用recv/recvfrom/read等进行数据读取

select函数

 select函数的函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select需要包含头文件:sys/select.h 

函数参数:

nfds:需要监视的文件描述符中,最大的文件描述符值+1

readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪

writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪

exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪

timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间

参数timeout的取值:

timeout的类型struct timeval是一个结构体

其中tv_sec是秒,表示可以获取到秒级别的时间戳
tv_usec是微秒,表示可以获取到微秒级别的时间戳

struct timeval的使用如下所示,gettimeofday的第一个参数就是struct timeval类型的:

结果为: 

timeout作为输入型参数的含义:

select等待多个fd,等待策略可以选择:
①阻塞式       
nullptr
②非阻塞式     {0, 0}
③可以设置timeout时间,时间内阻塞,时间到,立马返回     {5, 0}

设置为nullptr,表示阻塞式
设置为{0, 0},表示非阻塞式
设置为{5, 0},表示5秒内阻塞,时间到后立马返回

timeout作为输出型参数的含义:

其中第三点,传入{5, 0}表示5秒内阻塞,但是在等待时间内假设过了2秒,有fd就绪,此时就可以表示它的输出性,此时这个参数中保存的就是3秒,表示距离下次timeout还剩多长时间

函数返回值:

表示就绪的fd的个数,至少只要有一个fd数据就绪 或 空间就绪,就可以进行返回了


fd_set结构

fd_set被称为文件描述符集,本质是一个位图结构,用位图中对应的位来表示要监视的文件描述符

调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中

这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对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是一个固定大小位图,直接决定了select能同时关心的fd的个数是有上限的

而fd_set的大小为1024byte,所以一个select服务器同时能监管的文件描述符总数是1024:

结果为:

之所以*8是因为,sizeof算的是字节数,字节数*8才是比特位数


第二、三、四个参数的类型都是fd_set,下面具体说明第二个参数fd_set *readfds,其他两个参数以此类推即可

参数readfds:

a.输入时:用户告诉内核,我的比特位中,比特位的位置表示文件描述符值,比特位的内容表示是否关心
例如:0000 1010,就代表关心1和3号文件描述符的读事件

b.输出时:内核告诉用户,我是OS,用户你让我关心的多个fd有结果了,比特位的位置表示文件描述符值,比特位的内容表示是否就绪
例如:0000 1000,就代表3号文件描述符的读事件已经就绪了,所以后续用户可以直接读取3号文件描述符,而不会被阻塞

由上述对于输入输出型参数readfds的解释,我们可以得出以下结论:

①用户和内核都会修改同一个位图结构
②这个参数用一次之后,一定需要进行重新设定

同样的道理,writefds和exceptfds与readfds的含义是一样的,都是通过位图置0置1,使得内核和用户相互传递信息


select的模拟实现

select的模拟实现中只完成读取功能,写入和异常不做处理,在模拟实现epoll中会写完整

select的一般编写代码的模式:

需要有一个第三方的数组,用来保存所有合法的fd
不断循环:
while(true)
{
        遍历数组,更新出最大值
        遍历数组,添加所有需要关心的fd到_fd_set位图中
        调用select事件检测
        遍历数组,找到就绪的事件,根据就绪事件,完成对应的动作
        ①Accepter ②Recver
}

select的模拟实现代码如下,具体细节都在代码中的注释中有体现:

Sock.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "Log.hpp"

class Sock
{
private:
    const static int gbacklog = 20;

public:
    Sock()
    {}

    // 创建套接字
    static int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            // logMessage(FATAL, "create socket error,%d:%s", errno, strerror(errno));
            exit(2);
        }
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        // 文件描述符012默认打开,所以再创建就是3
        // logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }

    // bind
    static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // bind  文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 4字节ip->转为网络
        // binf失败
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            // logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    //监听
    static void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            // 监听失败
            // logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
            exit(4);
        }
        // logMessage(NORMAL, "init server success");
    }

    //获取连接(server端)
    static int Accept(int listensock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int serversock = accept(listensock, (struct sockaddr *)&src, &len);
        if (serversock < 0)
        {
            // 获取连接失败
            // logMessage(ERROR, "accept error,%d : %s", errno, strerror(errno));
            return -1;
        }
        //拿到客户端的IP和port
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return serversock;
    }

    //连接函数(client端,发起连接请求)
    static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);//主机->网络
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());//inet_addr->点分十进制->4字节IP

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }

    ~Sock()
    {}
};

Log.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "Log.hpp"

class Sock
{
private:
    const static int gbacklog = 20;

public:
    Sock()
    {}

    // 创建套接字
    static int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            // logMessage(FATAL, "create socket error,%d:%s", errno, strerror(errno));
            exit(2);
        }
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        // 文件描述符012默认打开,所以再创建就是3
        // logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }

    // bind
    static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // bind  文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 4字节ip->转为网络
        // binf失败
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            // logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    //监听
    static void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            // 监听失败
            // logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
            exit(4);
        }
        // logMessage(NORMAL, "init server success");
    }

    //获取连接(server端)
    static int Accept(int listensock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int serversock = accept(listensock, (struct sockaddr *)&src, &len);
        if (serversock < 0)
        {
            // 获取连接失败
            // logMessage(ERROR, "accept error,%d : %s", errno, strerror(errno));
            return -1;
        }
        //拿到客户端的IP和port
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return serversock;
    }

    //连接函数(client端,发起连接请求)
    static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);//主机->网络
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());//inet_addr->点分十进制->4字节IP

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }

    ~Sock()
    {}
};

main.cc

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

int main()
{
    unique_ptr<selectServer> svr(new selectServer());
    svr->Start();
    
    return 0;
}

selectServer.hpp

#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__

#include <sys/select.h>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>

#include "Sock.hpp"
#include "Log.hpp"

using namespace std;

#define BITS 8
#define NUM (sizeof(fd_set) * BITS)
#define FD_NONE -1

class selectServer
{
public:
    selectServer(const uint16_t& port = 8080):_port(port)
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(DEBUG, "create base socket success");
        // 将 _fd_array 数组初始化为-1
        // 再将 _fd_array[0] = _listensock
        for(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;
        _fd_array[0] = _listensock;
    }

    void Start()
    {

        while(true)
        {
            // struct timeval timeout = {0, 0};
            // 将_listensock添加到读文件描述符集rdfds中
            // FD_SET(_listensock, &rdfds);
            // nfds: 添加到select中的sock越来越多,所以每一次都会变化
            // 二三四个参数:输入输出型参数,每一次都是不一样的,所以每一次都要重新添加
            // timeout:输入输出型参数,每一次都要重置
            // 所以需要将合法的文件描述符保存起来,存到 _fd_array数组中

            DebugPrint();

            fd_set rdfds;
            FD_ZERO(&rdfds);
            int maxfd = _listensock;
            for(int i = 0; i < NUM; i++)
            {
                if(_fd_array[i] == FD_NONE) continue;
                FD_SET(_fd_array[i], &rdfds);
                if(maxfd < _fd_array[i]) maxfd = _fd_array[i];
            }

            int n = select(maxfd+1, &rdfds, nullptr, nullptr, nullptr);
            switch(n)
            {
            case 0:
                logMessage(DEBUG, "time out...");
                break;
            case -1:
                logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
                break;
            default:
                logMessage(DEBUG, "get a new link event...");
                HandlerEvent(rdfds);
                break;
            }
        }
    }

    ~selectServer()
    {
        if(_listensock >= 0)
            close(_listensock);
    }
private:
    void HandlerEvent(const fd_set& rdfds)
    {
        for(int i = 0; i < NUM; i++)
        {
            // 去掉不合法的fd
            if(_fd_array[i] == FD_NONE) continue;
            // 合法了不一定就绪,只有在文件描述符集 rdfds 中,才表示就绪
            if(FD_ISSET(_fd_array[i], &rdfds))
            {
                // 读事件就绪,连接事件到来(_listensock)
                if(_fd_array[i] == _listensock) Accepter();
                // 读事件就绪,INPUT事件到来: read / recv
                else Recver(i);
            }
        }
    }

    void Accepter()
    {
        string clientip;
        uint16_t clientport = 0;
        // 表示listensock上面的读事件就绪了,可以读取了,此时不会阻塞
        int sock = Sock::Accept(_listensock, &clientip, &clientport);
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        logMessage(DEBUG, "get a new link success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        // 这里不能直接read/recv,因为不能确定sock上的数据什么到来,此时就有可能会被阻塞
        // 所以这里也让select帮我们检测sock上是否有新的数据,如果有再通知我,此时就不会被阻塞了
        // 直接将 sock 放入数组 _fd_array 中即可
        int pos = 1;
        for(; pos < NUM; pos++)
        {
            if(_fd_array[pos] == FD_NONE) break;
        }
        if(pos == NUM)
        {
            // 文件描述符集已经满了
            logMessage(WARNING, "select server already full, close: %d", sock);
            close(sock);
        }
        else
        {
            // 找到了文件描述符集中值为 FD_NONE 的位置
            _fd_array[pos] = sock;
        }
    }

    void Recver(int i)
    {
        logMessage(DEBUG, "message in, get IO event: %d", _fd_array[i]);
        char buffer[1024];
        int n = recv(_fd_array[i], buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {   
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_array[i], buffer);
        }
        else if(n == 0)
        {
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[i]);
            // 1. 关闭不需要的描述符
            close(_fd_array[i]);
            // 2. 数组对应位置置为FD_NONE,select也不需要关心了
            _fd_array[i] = FD_NONE;
        }
        else
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[i], errno, strerror(errno));
            // 1. 关闭不需要的描述符
            close(_fd_array[i]);
            // 2. 数组对应位置置为FD_NONE,select也不需要关心了
            _fd_array[i] = FD_NONE;                    
        }
    }

    void DebugPrint()
    {
        cout << "_fd_array[]: ";
        for(int i = 0; i < NUM; i++)
        {
            if(_fd_array[i] == FD_NONE) continue;
            cout << _fd_array[i] << " ";
        }
        cout << endl;
    }

private:
    uint16_t _port;
    int _listensock;
    int _fd_array[NUM]; // 数组保存合法的fd
};


#endif

select的优缺点

优点:
①相比于之前效率比较高,因为在单位时间内等的比重大大减少了,单位时间内任何文件描述符就绪的概率比之前大
②应用场景:有大量的连接,但是只有少量是活跃的,省资源

缺点:
①为了维护第三方数组,select服务器会充满大量数组
②每一次都要对select输出参数进行重新设定
③能够同时管理的fd的个数是有上限的(1024)
④因为几乎每一个参数都是输入输出型的,所以select一定会频繁的进行用户到内核、内核到用户的数据拷贝
⑤编码比较复杂 


I/O多路转接之poll

下面的poll和epoll都是针对于select的缺点进步的

poll主要针对了select的如下两个缺点:
①输入输出参数一体
②管理fd个数是有上限

poll针对于这两个缺点做以改进


poll与select一样,也是只负责等

用户告诉内核:你要帮我关心哪些fd上的哪些事件
内核告诉用户:哪些fd已经就绪了

poll函数的第一个和第二个参数就用于解决上述的两个问题,与select的输入输出参数不同的是,select的输入输出参数是位图结构,所以需要改动
而poll的参数是结构体,结构体中可以有很多成员,有些成员是解决第一个问题,有些成员是解决第二个问题的

poll函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数参数:

fds:struct pollfd的结构体的起始地址,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合
nfds:表示fds数组的长度
timeout:表示poll函数在多长时间后返回,单位是毫秒(ms)

参数timeout的取值:

大于0:每隔多少秒,timeout一次
0:以非阻塞的方式等
-1:以阻塞的方式等

函数返回值:

大于0:是多少就表示有多少个文件描述符就绪
等于0:说明超时了
小于0:表示函数调用失败,同时错误码会被设置

struct pollfd结构体

fd表示文件描述符
events:需要监视该文件描述符上的哪些事件
revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪

poll中,区分读、写、异常的方式是给events设置进POLLIN、POLLOUT、POLLERR就可以了

其中POLLIN、POLLOUT是宏


poll函数的使用示例

简易使用示例代码:

#include <iostream>
#include <poll.h>
#include <cstdio>
#include <unistd.h>

using namespace std;

int main()
{
    struct pollfd poll_fd;
    poll_fd.fd = 0;
    // 关心读事件就绪
    poll_fd.events = POLLIN;

    while(true)
    {
        int ret = poll(&poll_fd, 1, 1000);
        if(ret < 0)
        {
            perror("poll");
            continue;
        }
        else if(ret == 0)
        {
            cout << "poll timeout..." << endl;
            continue;
        }
        // 表示成功返回
        if(poll_fd.revents == POLLIN)
        {
            cout << "poll event ready!" << endl;
            char buff[1024] = {0};
            read(0, buff ,sizeof(buff)-1);
            cout << "stdin: " << buff << endl;
        }
    }
    return 0;
}

运行结果:

可以看到,在示例代码中,在while死循环中,让poll关注读事件,如果返回值小于等于0,表示没有就绪,此时继续循环
如果返回值大于0,表示就绪了,此时判断revents是否是POLLIN,如果是就表示读事件就绪,调用read接口读取内容,并打印出来


poll与select的模拟实现的代码非常相似,并且是在select的基础上做以改进,比select的视线更简单

poll的模拟实现

同样的Sock.hpp和Log.hpp与select一样,makefile和main.cc也是相差不大,下面只体现pollServer.hpp的代码实现:

#ifndef __POLL_SVR_H__
#define __POLL_SVR_H__

#include <poll.h>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>

#include "Sock.hpp"
#include "Log.hpp"

using namespace std;

#define FD_NONE -1

class PollServer
{
public:
    static const int nfds = 100;
public:
    PollServer(const uint16_t& port = 8080):_port(port), _nfds(nfds)
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(DEBUG, "create base socket success");

        _fds = new struct pollfd[_nfds];
        // 初始化 struct pollfd结构体
        for(int i = 0; i < _nfds; i++)
        {
            _fds[i].fd = FD_NONE;
            _fds[i].events = _fds[i].revents = 0;
        } 
        // 将_listensock 填入0号位置
        _fds[0].fd = _listensock;
        _fds[0].events = POLLIN;
        _timeout = 1000;

    }

    void Start()
    {
        while(true)
        {
            DebugPrint();
            int n = poll(_fds, _nfds, _timeout);
            switch(n)
            {
            case 0:
                logMessage(DEBUG, "time out...");
                break;
            case -1:
                logMessage(WARNING, "poll error: %d : %s", errno, strerror(errno));
                break;
            default:
                HandlerEvent();
                break;
            }
        }
    }

    ~PollServer()
    {
        if(_listensock >= 0)
            close(_listensock);
        if(_fds)
            delete []_fds;
    }
private:
    void HandlerEvent()
    {
        for(int i = 0; i < _nfds; i++)
        {
            // 去掉不合法的fd
            if(_fds[i].fd == FD_NONE) continue;
            // _fds[i].revents 中如果存在POLLIN,就说明读事件就绪
            if(_fds[i].revents & POLLIN)
            {
                // 读事件就绪,连接事件到来(_listensock)
                if(_fds[i].fd == _listensock) Accepter();
                // 读事件就绪,INPUT事件到来: read / recv
                else Recver(i);
            }
        }
    }

    void Accepter()
    {
        string clientip;
        uint16_t clientport = 0;
        // 表示listensock上面的读事件就绪了,可以读取了,此时不会阻塞
        int sock = Sock::Accept(_listensock, &clientip, &clientport);
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        logMessage(DEBUG, "get a new link success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        int pos = 1;
        for(; pos < _nfds; pos++)
        {
            if(_fds[pos].fd == FD_NONE) break;
        }
        if(pos == _nfds)
        {
            logMessage(WARNING, "poll server already full, close: %d", sock);
            close(sock);
        }
        else
        {
            _fds[pos].fd = sock;
            _fds[pos].events = POLLIN;
        }
    }

    void Recver(int i)
    {
        logMessage(DEBUG, "message in, get IO event: %d", _fds[i].fd);
        char buffer[1024];
        int n = recv(_fds[i].fd, buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {   
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fds[i].fd, buffer);
        }
        else if(n == 0)
        {
            logMessage(DEBUG, "client[%d] quit, me too...", _fds[i].fd);
            // 1. 关闭不需要的描述符
            close(_fds[i].fd);
            // 2. _fds对应位置置为FD_NONE,poll也不需要关心了
            _fds[i].fd = FD_NONE;
            _fds[i].events = 0;
        }
        else
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fds[i].fd, errno, strerror(errno));
            // 1. 关闭不需要的描述符
            close(_fds[i].fd);
            // 2. _fds对应位置置为FD_NONE,poll也不需要关心了
            _fds[i].fd = FD_NONE;                    
            _fds[i].events = 0;
        }
    }

    void DebugPrint()
    {
        cout << "_fd_array[]: ";
        for(int i = 0; i < _nfds; i++)
        {
            if(_fds[i].fd == FD_NONE) continue;
            cout << _fds[i].fd << " ";
        }
        cout << endl;
    }

private:
    uint16_t _port;
    int _listensock;
    // poll的三个参数都设为成员变量
    struct pollfd* _fds;
    int _nfds;
    int _timeout;
};

#endif

poll的优缺点

优点:
①相比于之前效率比较高,同样在单位时间内等的比重大大减少了,单位时间内任何文件描述符就绪的概率比之前大
②应用场景:有大量的连接,但是只有少量是活跃的,同样节省资源
③输入输出参数是分离,不需要每次都进行重置
④poll参数级别,没有可以管理的fd的上限

缺点:
①poll服务器依旧需要不少的遍历,在用户层检测时间就绪,在内核检测fd就绪
②poll需要内核到用户的拷贝
③poll代码编写也比较复杂,但是相比于select容易一些


I/O多路转接之epoll

epoll也是系统提供的一个多路转接接口

epoll的三个系统调用

epoll_create函数

epoll_create函数用于创建一个epoll模型:

int epoll_create(int size);

参数:

size参数一般是被忽略的,但size的值必须设置为大于0的值,至于为什么不废弃,是因为需要向前向后兼容

返回值:

epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置


epoll_ctl函数

epoll_ctl函数用于向指定的epoll模型进行操作:

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

参数:

epfd:指定的epoll模型(epoll_create的返回值)
op:表示具体的动作,用三个宏来表示(增加、删除、修改)
fd:需要监视的文件描述符
event:需要监视该文件描述符上的哪些事件

op的取值:

EPOLL_CTL_ADD:增加
EPOLL_CTL_MOD:修改
EPOLL_CTL_DEL:删除

返回值:

函数调用成功返回0,调用失败返回-1,同时错误码会被设置


epoll_wait函数

epoll_wait函数用于收集监视的事件中已经就绪的事件

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

参数:

在特定的epfd中,获取已经就绪的事件,maxevents是events数组中的元素个数

events是输出型参数,内核告诉用户的已经就绪的事件

timeout与poll中的timeout是一样的含义

大于0:每隔多少秒,timeout一次
0:以非阻塞的方式等
-1:以阻塞的方式等

结构体epoll_event

有两个成员,分别是events和data

events是哪个事件就绪,例如EPOLLIN

data是一个联合体,每次只使用一个成员,这里我们使用fd成员

返回值:

返回已经就绪的文件描述符的个数

epoll返回的时候,会将就绪的event按照顺序放入events数组中,即从0下标开始,一共有返回值个
如果底层就绪的sock非常多,events装不下,那么一次拿不完就下一次再拿


epoll的工作原理

在学习epoll的工作原理之前,先想想前面所学习的select和poll的共识:

①无论是select和poll,都是需要用户自己维护一个数组,来进行保存特定的fd与特定的事件
②select和poll都需要遍历
③select和poll的工作模式:
        a. 用户告诉内核,需要你帮我关心哪些文件描述符上的哪些事件
        b. 内核告诉用户,哪些文件描述符上的哪些事件已经就绪

epoll模型:

当某一进程调用epoll_create函数时,Linux内核会创建一个epoll模型

红黑树与就绪队列:

红黑树:本质就是用户告诉内核,需要监视哪些文件描述符上的哪些事件,调用epll_ctl函数实际就是在对这颗红黑树进行对应的增删改操作
就绪队列:本质就是内核告诉用户,哪些文件描述符上的哪些事件已经就绪了,调用epoll_wait函数实际就是在从就绪队列当中获取已经就绪的事件

红黑树需要key值,而这里的fd就是一个天然的key值

回调机制:

对于select和poll来说,操作系统在监视多个文件描述符上的事件是否就绪时,需要让操作系统主动对这多个文件描述符进行轮询检测,这一定会增加操作系统的负担
而对于epoll来说,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中
当用户调用epoll_wait函数获取就绪事件时,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可

采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当事件就绪时会自动调用对应的回调函数进行处理

下面是总结的epoll的五个细节:
①文件描述符可以作为红黑树天然的key值
②用户只需要设置关系,获取结果即可,不用关心任何对fd和event的管理细节
③epoll高效是因为:

第一、底层是用的红黑树管理的,之前是数组管理的,所以增删改的效率高
第二、之前文件描述符是否就绪需要操作系统遍历,现在只需要就绪以后回调即可,所以操作系统并不需要浪费精力在文件描述符的时间监测上
第三、以前想要获取就绪的文件描述符,依旧需要操作系统遍历,现在epoll有就绪队列,只需要调用epoll_wait从就绪队列中获取就绪结点即可,可以以O(1)的方式直接监测队列是否有数据

④epoll底层只要有fd就绪了,OS会自己构建节点,插入到就绪队列中,上层只需要不断地从就绪队列中将数据拿走,就完成了获取就绪事件的任务
(生产者消费者模型,就绪队列本质是共享资源,epoll保证所有epoll接口是线程安全的,也就是进行了加锁)
⑤如果底层没有就绪事件,那么上层只能阻塞等待


epoll的模拟实现

同样只实现读取

main.cc:

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

using namespace std;

void Print(string request)
{
    cout << "change: " << request << endl;
}

int main()
{
    unique_ptr<EpollServer> svr(new EpollServer(Print));
    svr->Start();

    return 0;
}

Epoll.hpp(封装的三个epoll的函数):

#include <iostream>
#include <sys/epoll.h>

class Epoll
{
public:
    static const int gsize = 256; // 随便一个大于0的数都可以

public:
    static int CreateEpoll()
    {
        int epfd = epoll_create(gsize);
        if(epfd > 0) return epfd;
        exit(5);
    }

    static bool CtlEpoll(int epfd, int op, int sock, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(epfd, op, sock, &ev);
        return n == 0;
    }

    static int EpollWait(int epfd, struct epoll_event *events, int maxevents, int timeout)
    {
        return epoll_wait(epfd, events, maxevents, timeout);
    }
};

EpollServer.hpp:

#ifndef __EPOLL_SERVER_H__
#define __EPOLL_SERVER_H__

#include <iostream>
#include <sys/epoll.h>
#include <string>
#include <functional>
#include <cassert>
#include <unistd.h>

#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"

using namespace std;

class EpollServer
{
public:
    using func_t = function<void(string)>;
    static const int gmax = 100;
public:
    EpollServer(func_t HandlerRequest, const uint16_t& port = 8080)
            :_port(port), _maxevent(gmax), _HandlerRequest(HandlerRequest)
    {
        // 0. 申请对应的空间
        _events = new struct epoll_event[_maxevent];
        // 1. 创建listensock
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        // 2. 创建epoll模型
        _epfd = Epoll::CreateEpoll();
        logMessage(DEBUG, "init success, listensock: %d, epfd: %d", _listensock, _epfd);
        // 将listensock先添加到epoll中,让epoll帮我们管理起来
        if(!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN)) exit(6);
        logMessage(DEBUG, "add listensock to epoll success.");
    }

    void Accepter(int listensock)
    {
        string clientip;
        uint16_t clientport;
        int sock = Sock::Accept(listensock, &clientip, &clientport);
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        // 将新的sock,添加给epoll
        if(!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN)) return;
        logMessage(DEBUG, "add new sock : %d to epoll success", sock);
    }

    void Recver(int sock)
    {
        char buffer[1024];
        ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {
            // 假设读到了完整的报文
            buffer[n] = 0;
            // 处理数据
            _HandlerRequest(buffer);
        }
        else if(n == 0)
        {
            // 1. 先在epoll模型中去掉对 sock 的关心
            bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
            assert(res);
            (void)res;
            // 2. 再close文件
            close(sock);
            logMessage(NORMAL, "client %d quit, me too...", sock);
        }
        else
        {
            // 1. 先在epoll模型中去掉对 sock 的关心
            bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
            assert(res);
            (void)res;
            // 2. 再close文件
            close(sock);
            logMessage(NORMAL, "client recv %d error, close error sock", sock);            
        }
    }

    void HandlerEvents(int n)
    {
        assert(n > 0);
        for(int i = 0; i < n; i++)
        {
            uint32_t revents = _events[i].events;
            int sock = _events[i].data.fd;
            // 读事件就绪
            if(revents & EPOLLIN)
            {
                if(sock == _listensock) Accepter(_listensock); // listensock就绪
                else Recver(sock);                             // 一般的sock就绪 -- read
            }
        }
    }

    // 循环一次的函数
    void LoopOnce(int timeout)
    {
        int n = Epoll::EpollWait(_epfd, _events, _maxevent, timeout);
        switch(n)
        {
        case 0:
            logMessage(DEBUG, "timeout...");
            break;
        case -1:
            logMessage(WARNING, "epoll wait error: %s", strerror(errno));
            break;
        default:
            // 等待成功
            logMessage(DEBUG, "get a event");
            HandlerEvents(n);
            break;
        }
    }

    void Start()
    {
        int timeout = -1;
        while(true)
        {
            LoopOnce(timeout);
        }
    }

    ~EpollServer()
    {
        if(_listensock >= 0) close(_listensock);
        if(_epfd >= 0) close(_epfd);
        if(_events) delete[] _events;
    }
private:
    int _listensock;
    uint16_t _port;
    int _epfd;                   // epoll模型
    struct epoll_event* _events; // 结构体数组
    int _maxevent;               // _events的容量
    func_t _HandlerRequest;      // Recver函数中的处理方法
};


#endif

epoll的优点

①接口使用简单,使用起来比较高效,不用每次循环设置关注的描述符
②数据拷贝轻量,只需要在合适的时候调用EPOLL_CTL_ADD,将文件描述符拷贝到内核中,操作并不频繁(select/poll每次都要循环拷贝)
③事件回调机制,使用回调的方式将就绪的文件描述符加入就绪队列中,调用epoll_wait时直接访问就绪队列就知道哪些文件描述符已经就绪,检测是否有文件描述符就绪的时间复杂度是O(1),因为本质只需要判断就绪队列是否为空即可
④没有数量限制,文件描述符数量无上限


epoll的工作方式

epoll有两种工作方式,分别是水平触发(LT)和边缘触发(ET)工作模式

水平触发 (LT)

由于在LT工作模式下,只要底层有事件就绪就会一直通知用户,因此当epoll检测到底层读事件就绪时,可以不立即进行处理,或者只处理一部分,因为只要底层数据没有处理完,下一次epoll还会通知用户事件就绪

select/poll/epoll默认状态下就是LT工作模式

支持阻塞读写和非阻塞读写

边缘触发 (ET)

只有底层就绪事件数量由无到有或由有到多发生变化的时候,epoll才会通知用户

只支持非阻塞的读写

之所以ET模式下只支持非阻塞,原因是:

ET的工作模式中,如果底层有数据就绪了,上层必须一次将数据读完,倒逼着程序员一次将接收缓冲区的数据取走,所以必须一直循环读,而循环读到最后一次时,必然会阻塞,从而导致程序挂起,为了避免这个问题,就必须将套接字设置为非阻塞的


对比LT和ET

ET模式更高效,理由如下:
①更少的返回次数
②ET模式会倒逼着程序员一次将接收缓冲区的数据取走,应用层尽快的取走了缓冲区的数据,那么在单位时间内,该模式下工作的服务器,就可以在一定程度上,给发送方发送一个更大的接收窗口,所以对方就有更大的滑动窗口,可以一次向我们发送更多的数据,提高IO吞吐

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

在ET模式下,一个文件描述符就绪之后,用户不会反复收到通知,看起来比LT更高效,但如果在LT模式下能够做到每次都将就绪的文件描述符立即全部处理,不让操作系统反复通知用户的话,其实LT和ET的性能也是一样的

ET的代码比较复杂一些


IO多路转接之select、poll、epoll到此结束啦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值