I/O 多路转接select、poll

目录

I/O多路转接之select

[select 函数原型](#select 函数原型)

理解select执行过程

(1)初始化

[(2)添加 fd=5](#(2)添加 fd=5)

[(3)再加入 fd=2, fd=1](#(3)再加入 fd=2, fd=1)

[(4)调用 select](#(4)调用 select)

(5)事件发生

关键理解

socket就绪条件

select服务器

代码在做什么(运行主线)

select的优点

select的缺点

select的适用场景

I/O多路转接之poll

poll初识

poll函数

poll服务器

代码在做什么(运行主线)

[和 select 的核心差异](#和 select 的核心差异)

poll的优点

poll的缺点


I/O多路转接之select

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

  • select系统调用可以让我们的程序同时监视多个文件描述符的上的事件是否就绪。
  • select的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select才会成功返回并将对应文件描述符的就绪事件告知调用者。

select 函数原型

select 的函数原型如下:

cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数解释:

  • nfds:需要监视的文件描述符中,最大的文件描述符值+1。
  • readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
  • writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
  • exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
  • timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。

参数timeout的取值:

  • NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:selec调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
  • 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

select调用失败时,错误码可能被设置为:

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

常见的程序片段如下

cs 复制代码
fd_set readset;
FD_ZERO(&readset);
FD_SET(fd, &readset);

int n = select(fd + 1, &readset, NULL, NULL, NULL);
if (n > 0 && FD_ISSET(fd, &readset)) {
    /* ... 可读 ... */
}

为什么要传nfds,nfds是最大文件描述符值+1?

select内部使用 位图(bitmap) 来表示监视的文件描述符集合。

  • fd_set 实际上就是一个位数组,每一位对应一个文件描述符。

  • 下标 i 的位是否为 1,表示是否需要监视文件描述符 i

为什么要传递nfds?

select 并不会自动知道你设置的 fd_set 的最大有效范围。

为了避免在遍历时扫描整个 fd_set(可能很大),你需要告诉内核:

"我监视的文件描述符范围是 0 到 nfds-1"。

这样 select就只会检查前 nfds 个文件描述符对应的位,提升效率。

为什么是最大文件描述符 + 1?

  • 设最大文件描述符是 maxfd

  • 由于文件描述符从 0 开始计数,所以需要扫描到 maxfd 这一位。

  • 因此,nfds = maxfd + 1

例子:

  • 你监视的文件描述符集合是 {3, 5, 8},最大值为 8。

  • 为了让 select 检查到第 8 位,必须传入 nfds = 9

fd_set结构

cpp 复制代码
typedef struct
{
    /* XPG4.2 requires this member name. Otherwise avoid the name
       from the global namespace. */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;

其实这个结构就是一个整数数组,更严格的说,是一个"位图".使用位图中对应的位来表 示要监视的文件描述符.

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

cpp 复制代码
#define FD_ZERO(set) \
    memset (set, 0, sizeof (fd_set))

#define FD_SET(fd, set) \
    (__FDS_BITS(set)[(fd) / __NFDBITS] |= ((__fd_mask)1 << ((fd) % __NFDBITS)))

#define FD_CLR(fd, set) \
    (__FDS_BITS(set)[(fd) / __NFDBITS] &= ~((__fd_mask)1 << ((fd) % __NFDBITS)))

#define FD_ISSET(fd, set) \
    ((__FDS_BITS(set)[(fd) / __NFDBITS] & ((__fd_mask)1 << ((fd) % __NFDBITS))) != 0)
cpp 复制代码
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的全部位

timeval结构

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

cpp 复制代码
struct timeval {
    long tv_sec;   /* 秒 (seconds) */
    long tv_usec;  /* 微秒 (microseconds) */
};

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set 中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.

假设:

  • fd_set 长度 = 1字节 (8 bit)

  • 每一 bit 对应一个文件描述符(fd),bit=1 表示监视该 fd,bit=0 表示不监视。

(1)初始化

cs 复制代码
FD_ZERO(&set);

内存状态:

cs 复制代码
set = 0000 0000

→ 没有任何 fd 被监视。

(2)添加 fd=5

cs 复制代码
FD_SET(5, &set);

内存状态:

cs 复制代码
set = 0010 0000

→ 第 5 位被置 1,表示监视 fd=5。

(3)再加入 fd=2, fd=1

cs 复制代码
FD_SET(2, &set);
FD_SET(1, &set);

内存状态:

cpp 复制代码
set = 0010 0110

对应的 bit 标注:

cpp 复制代码
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
 0    0    1    0    0    1    1    0

→ 表示监视 fd=5, fd=2, fd=1。

(4)调用 select

cs 复制代码
select(6, &set, NULL, NULL, NULL);
  • 参数 6 = 最大 fd (5) + 1

  • 意味着 select 检查 fd=0~5 这些位置。

  • 阻塞等待,直到其中至少一个 fd 可读。

(5)事件发生

假设 fd=1, fd=2 都有可读事件。

  • select 返回值 = 2(就绪 fd 的数量)。

  • 返回时 set 被内核修改,仅保留发生事件的 fd 位:

cs 复制代码
set = 0000 0110

→ fd=1, fd=2 就绪;fd=5 因为没有事件,被清空(bit=0)。

关键理解

  • fd_set 本质就是一段 位图内存 ,通过 FD_SET/FD_CLR/FD_ISSET 操作位。

  • select 返回时,会 覆盖原有的 fd_set,只保留就绪的文件描述符。

  • 所以如果要多次调用 select,需要每次重新设置 fd_set

socket就绪条件

读就绪

select 通知某个 socket 可读时,可能有以下几种情况:

接收缓冲区中的字节数 ≥ SO_RCVLOWAT

  • SO_RCVLOWAT 默认值是 1。

  • 意味着至少有一个字节可读,可以无阻塞 read/recv,并返回 >0。

对端正常关闭连接

  • TCP 通信中,如果对端调用了 close()shutdown(SHUT_WR),本端再 read 时会返回 0。

  • 这表示 读到 EOF(End Of File)。

监听 socket 上有新的连接请求

  • 监听 socket(listen 过的 fd)在有新的客户端连接请求到来时,会变为可读。

  • 此时调用 accept() 就能返回一个新的连接。

socket 上有未处理的错误

  • 读 socket 会立刻返回错误,错误原因存于 errno

  • 可通过 getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, ...) 获取。

写就绪

当 socket 被标记为 可写时,意味着:

发送缓冲区可用空间 ≥ SO_SNDLOWAT

  • SO_SNDLOWAT = 2048(不同实现可能不同)。
  • 可以无阻塞地 write/send 至少这一数量的字节。

对端关闭连接或发生错误

  • 写操作可能立即返回错误(如 EPIPEECONNRESET)。

非阻塞 connect 成功建立连接

  • 对非阻塞 socket 调用 connect 后,会立即返回 EINPROGRESS。

  • 当写就绪时,说明三次握手已完成,可以用 getsockopt(SO_ERROR) 判断是否成功。

异常就绪

当 socket 被标记为 异常就绪时,通常是:

带外数据(OOB data)到达

  • TCP 的紧急数据(urgent data)会触发异常就绪。

  • 通过 recv(..., MSG_OOB) 接收。

某些错误条件

  • 某些实现会把特定错误也作为异常事件通知。

select服务器

Socket类

TCP------socket

cpp 复制代码
namespace SocketModule
{
    using namespace LogModule;
    const static int gbacklog = 16;
    // 模版方法模式
    // 基类socket, 大部分方法,都是纯虚方法
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t port) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        //virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual int Accept(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual int Connect(const std::string &server_ip, uint16_t port) = 0;
        virtual int Fd() = 0;
    public:
        void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }
        void BuildTcpClientSocketMethod()
        {
            SocketOrDie();
        }
        // void BuildUdpSocketMethod()
        // {
        //     SocketOrDie();
        //     BindOrDie();
        // }
    };

    const static int defaultfd = -1;

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(defaultfd)
        {
        }
        TcpSocket(int fd) : _sockfd(fd)
        {
        }
        ~TcpSocket() {}
        void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "socket error";
                exit(SOCKET_ERR);
            }
            int opt = 1;
            setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            LOG(LogLevel::INFO) << "socket success: " << _sockfd;
        }
        void BindOrDie(uint16_t port) override
        {
            InetAddr localaddr(port);
            int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::INFO) << "bind success";
        }
        void ListenOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::INFO) << "listen success";
        }
        int Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = ::accept(_sockfd, CONV(peer), &len);
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "accept warning ...";
                return -1; // TODO
            }
            return fd;
            // client->SetAddr(peer);
            // return std::make_shared<TcpSocket>(fd);
        }
        // n == read的返回值
        int Recv(std::string *out) override
        {
            // 流式读取,不关心读到的是什么
            char buffer[4096*2];
            ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                *out += buffer; // 故意
            }
            return n;
        }
        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }
        void Close() override //??
        {
            if (_sockfd >= 0)
                ::close(_sockfd);
        }
        int Connect(const std::string &server_ip, uint16_t port) override
        {
            InetAddr server(server_ip, port);
            return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
        }
        int Fd()
        {
            return _sockfd;
        }
    private:
        int _sockfd; // _sockfd , listensockfd, sockfd;
    };

    // class UdpSocket : public Socket
    // {
    // };
}

SelectServer类

cpp 复制代码
class SelectServer
{
    const static int size = sizeof(fd_set) * 8;
    const static int defaultfd = -1;

public:
    SelectServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
    {
        _listensock->BuildTcpSocketMethod(port);
        for (int i = 0; i < size; i++)
        {
            _fd_array[i] = defaultfd;
        }
        _fd_array[0] = _listensock->Fd();
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = defaultfd;
            for (int i = 0; i < size; i++)
            {
                if (_fd_array[i] == defaultfd)
                    continue;
                // 1. 每次select之前,都要对rfds进行重置!
                FD_SET(_fd_array[i], &rfds);
                // 2. 最大fd,一定是变化的
                if (maxfd < _fd_array[i])
                {
                    maxfd = _fd_array[i]; // 更新出最大fd
                }
            }
            PrintFd();

            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case -1:
                LOG(LogLevel::ERROR) << "select error";
                break;
            case 0:
                LOG(LogLevel::INFO) << "time out...";
                break;
            default:
                // 有事件就绪,就不仅仅是新连接到来了吧?读事件就绪啊?
                LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
                Dispatcher(rfds); // 处理就绪的事件啊!
                break;
            }
        }
        _isrunning = false;
    }

    // 事件派发器
    void Dispatcher(fd_set &rfds)
    {
        // 就不仅仅是新连接到来了吧?读事件就绪啊?
        // 指定的文件描述符,在rfds里面,就证明该fd就绪了
        for (int i = 0; i < size; i++)
        {
            if (_fd_array[i] == defaultfd)
                continue;
            if (FD_ISSET(_fd_array[i], &rfds))
            {
                // fd_array[i] 上面一定是读就绪了
                // listensockfd 新连接到来,也是读事件就绪啊
                // sockfd 数据到来,读事件就绪啊
                if (_fd_array[i] == _listensock->Fd())
                {
                    // listensockfd 新连接到来
                    Accepter();
                }
                else
                {
                    // 普通的读事件就绪
                    Recver(_fd_array[i], i);
                }
            }
        }
    }
    // 链接管理器
    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client);
        if (sockfd >= 0)
        {
            LOG(LogLevel::INFO) << "get a new link, sockfd: "
                                << sockfd << ", client is: " << client.StringAddr();
            int pos = 0;
            for (; pos < size; pos++)
            {
                if (_fd_array[pos] == defaultfd)
                    break;
            }
            if (pos == size)
            {
                LOG(LogLevel::WARNING) << "select server full";
                close(sockfd);
            }
            else
            {
                _fd_array[pos] = sockfd;
            }
        }
    }

    // IO处理器
    void Recver(int fd, int pos)
    {
        char buffer[1024];
        ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "Client Say@" << buffer << std::endl;
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "clien quit...";
            // 1. 不要让select在关系这个fd了
            _fd_array[pos] = defaultfd;

            // 2. 关闭fd
            close(fd);
        }
        else
        {
            LOG(LogLevel::ERROR) << "recv error";
            // 1. 关闭fd
            close(fd);
            // 2. 不要让select在关心这个fd了
            _fd_array[pos] = defaultfd;
        }
    }

    void Stop()
    {
        _isrunning = false;
    }
    void PrintFd()
    {
        std::cout << "_fd_array[]: ";
        for (int i = 0; i < size; i++)
        {
            if (_fd_array[i] == defaultfd)
                continue;
            std::cout << _fd_array[i] << " ";
        }
        std::cout << "\r\n";
    }
    ~SelectServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _fd_array[size];
};

运行服务器

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
    svr->Start();

    return 0;
}

代码在做什么(运行主线)

1、初始化

  • SelectServer(int port)

    • _listensock 创建监听 socket,并完成 bind/ listen(由 BuildTcpSocketMethod 内部实现)。

    • defaultfd(-1)_fd_array 清空;把监听 fd 放在 _fd_array[0]

  • size = sizeof(fd_set) * 8:把监视的"容量上限"设成 fd_set 能容纳的位数(见下文"注意点")。

2、事件循环 (Start)

  • 每轮:

    • 清零 rfdsFD_ZERO(&rfds)

    • 遍历 _fd_array,把所有有效 fd 用 FD_SET 加入 rfds,同时计算 maxfd

    • select(maxfd + 1, &rfds, nullptr, nullptr, nullptr) 阻塞等待读事件

    • 根据返回值分流;若有事件就绪,调用 Dispatcher(rfds)

3、事件派发 (Dispatcher)

  • 再次遍历 _fd_array

    • FD_ISSET(fd, &rfds) 为真 → 该 fd 读就绪

    • 如果是监听 fd → 调 Accepter() 接新连接。

    • 否则是普通连接 → 调 Recver(fd, pos) 读数据。

4、接入管理 (Accepter)

  • accept() 成功得到 sockfd

    • _fd_array 的空位(值 == -1)塞进去;满了就关掉新连接并告警。

5、I/O 处理 (Recver)

  • recv()

    • n > 0:打印收到的数据;

    • n == 0:对端正常关闭 → close(fd),并把 _fd_array[pos] 置回 -1;

    • n < 0:错误 → 关闭并清空该槽位。

整体模型:用一个 数组保存活跃 fd ;每轮把它们装进 fd_set,交给 select 挑出读就绪的;回来后只处理这些就绪 fd。

select的优点

  • 可以同时等待多个文件描述符,并且只负责等待,实际的IO操作由accept、read、write等接口来完成,这些接口在进行IO操作时不会被阻塞。
  • select同时等待多个文件描述符,因此可以将"等"的时间重叠,提高了IO的效率。

当然,这也是所有多路转接接口的优点。

select的缺点

  • 每次调用select,都需要手动设置fd集合,从接口使用角度来说也非常不便。
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
  • select可监控的文件描述符数量太少。

select可监控的文件描述符个数

调用select函数时传入的readfds、writefds以及exceptfds都是fd_set结构的,fd_set结构本质是一个位图,它用每一个比特位来标记一个文件描述符,因此select可监控的文件描述符个数是取决于fd_set类型的比特位个数的。

其实select可监控的文件描述符个数就是1024个。

select的适用场景

多路转接接口select、poll和epoll,需要在一定的场景下使用,如果场景选择的不适宜,可能会适得其反。

  • 多路转接接口一般适用于多连接,且多连接中只有少部分连接比较活跃。因为少量连接比较活跃,也就意味着几乎所有的连接在进行IO操作时,都需要花费大量时间来等待事件就绪,此时使用多路转接接口就可以将这些等的事件进行重叠,提高IO效率。
  • 对于多连接中大部分连接都很活跃的场景,其实并不适合使用多路转接。因为每个连接都很活跃,也就意味着任何时刻每个连接上的事件基本都是就绪的,此时根本不需要动用多路转接接口来帮我们进行等待,毕竟使用多路转接接口也是需要花费系统的时间和空间资源的。

多连接中只有少量连接是比较活跃的,比如聊天工具,我们登录QQ后大部分时间其实是没有聊天的,此时服务器端不可能调用一个read函数阻塞等待读事件就绪。

多连接中大部分连接都很活跃,比如企业当中进行数据备份时,两台服务器之间不断在交互数据,这时的连接是特别活跃的,几乎不需要等的过程,也就没必要使用多路转接接口了。

I/O多路转接之poll

poll初识

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

  • poll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,和select的定位是一样的,适用场景也是一样的。

poll函数

poll函数的函数原型如下:

复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明:

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

参数timeout的取值:

  • -1:poll调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:poll调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll检测后都会立即返回。
  • 特定的时间值:poll调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后poll进行超时返回。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

poll调用失败时,错误码可能被设置为:

  • EFAULT:fds数组不包含在调用程序的地址空间中。
  • EINTR:此调用被信号所中断。
  • EINVAL:nfds值超过RLIMIT_NOFILE值。
  • ENOMEM:核心内存不足。

struct pollfd结构

cpp 复制代码
struct pollfd {
    int   fd;         /* 需要监视的文件描述符 */
    short events;     /* 关心的事件 */
    short revents;    /* 实际发生的事件 (由内核填充) */
};

struct pollfd结构当中包含三个成员:

  • fd:特定的文件描述符,若设置为负值则忽略events字段并且revents字段返回0。
  • events:需要监视该文件描述符上的哪些事件。
  • revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪。

events和revents的取值:

事件 描述 是否可作为输入 是否可作为输出
POLLIN 数据(包括普通数据和优先数据)可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读(Linux 不支持)
POLLPRI 高优先级数据可读,比如 TCP 带外数据
POLLOUT 数据(包括普通数据和优先数据)可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLRDHUP TCP 连接被对方关闭,或者对方关闭了写操作(GNU 扩展)
POLLERR 错误
POLLHUP 挂起(例如管道写端关闭后,读端会收到该事件)
POLLNVAL 文件描述符未打开

这些取值实际都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。

cs 复制代码
#define POLLIN      0x0001   /* 普通或优先数据可读 */
#define POLLPRI     0x0002   /* 高优先级数据可读 */
#define POLLOUT     0x0004   /* 普通或优先数据可写 */
#define POLLERR     0x0008   /* 错误 */
#define POLLHUP     0x0010   /* 挂起 */
#define POLLNVAL    0x0020   /* 无效的请求: fd 未打开 */

#define POLLRDNORM  0x0040   /* 普通数据可读 */
#define POLLRDBAND  0x0080   /* 优先数据可读 */
#define POLLWRNORM  0x0100   /* 普通数据可写 */
#define POLLWRBAND  0x0200   /* 优先数据可写 */

#define POLLRDHUP   0x2000   /* 对方关闭写(GNU 扩展) */
  • 因此在调用poll函数之前,可以通过或运算符将要监视的事件添加到events成员当中。
  • 在poll函数返回后,可以通过与运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。

poll服务器

poll的工作流程和select是基本类似的,这里我们也实现一个简单poll服务器,该服务器也只是读取客户端发来的数据并进行打印。

PollServer类

cpp 复制代码
class PollServer
{
    const static int size = sizeof(fd_set) * 8;
    const static int defaultfd = -1;

public:
    PollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
    {
        _listensock->BuildTcpSocketMethod(port);
         for (int i = 0; i < size; i++)
        {
            _fds[i].fd = defaultfd;
            _fds[i].events = 0;
            _fds[i].revents = 0;
        }

        _fds[0].fd = _listensock->Fd();
        _fds[0].events = POLLIN;
    }
    void Start()
    {
         int timeout = -1;
        _isrunning = true;
        while (_isrunning)
        {
            PrintFd();
            int n = poll(_fds, size, timeout);
            // rfds: 0000 0000
            switch (n)
            {
            case -1:
                LOG(LogLevel::ERROR) << "poll error";
                break;
            case 0:
                LOG(LogLevel::INFO) << "poll time out...";
                break;
            default:
                // 有事件就绪,就不仅仅是新连接到来了吧?读事件就绪啊?
                LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
                Dispatcher(); // 处理就绪的事件啊!
                break;
            }
        }

        _isrunning = false;
    }

    // 事件派发器
    void Dispatcher()
    {
        // 就不仅仅是新连接到来了吧?读事件就绪啊?
        // 指定的文件描述符,在rfds里面,就证明该fd就绪了
        for (int i = 0; i < size; i++)
        {
            if (_fds[i].fd == defaultfd)
                continue;
            if (_fds[i].revents & POLLIN)
            {
                // fd_array[i] 上面一定是读就绪了
                // listensockfd 新连接到来,也是读事件就绪啊
                // sockfd 数据到来,读事件就绪啊
                if (_fds[i].fd == _listensock->Fd())
                {
                    // listensockfd 新连接到来
                    Accepter();
                }
                else
                {
                    // 普通的读事件就绪
                    Recver(i);
                }
            }
        }
    }
    // 链接管理器
    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client);
        if (sockfd >= 0)
        {
            LOG(LogLevel::INFO) << "get a new link, sockfd: "
                                << sockfd << ", client is: " << client.StringAddr();
            int pos = 0;
            for (; pos < size; pos++)
            {
                if (_fds[pos].fd == defaultfd)
                    break;
            }
            if (pos == size)
            {
                LOG(LogLevel::WARNING) << "select server full";
                close(sockfd);
            }
            else
            {
                _fds[pos].fd = sockfd;
                _fds[pos].events = POLLIN;   // ★ 必须:关心可读
                _fds[pos].revents = 0;
            }
        }
    }

    // IO处理器
    void Recver( int pos)
    {
        char buffer[1024];
        ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "Client Say@" << buffer << std::endl;
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "clien quit...";
             // 2. 关闭fd
            close(_fds[pos].fd);
            // 1. 不要让select在关系这个fd了
            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
        else
        {
            LOG(LogLevel::ERROR) << "recv error";
            // 2. 关闭fd
            close(_fds[pos].fd);
            // 1. 不要让select在关系这个fd了
            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
    }

    void Stop()
    {
        _isrunning = false;
    }
    void PrintFd()
    {
         std::cout << "_fds[]: ";
        for (int i = 0; i < size; i++)
        {
            if (_fds[i].fd == defaultfd)
                continue;
            std::cout << _fds[i].fd << " ";
        }
        std::cout << "\r\n";
    }
    ~PollServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    struct pollfd _fds[size];
    //int _fd_array[size];
};

代码在做什么(运行主线)

1、初始化(构造函数)

  • _listensock->BuildTcpSocketMethod(port):创建、绑定、监听端口。

  • _fds 整个数组清空:fd=-1events=0revents=0

  • 第 0 槽放入监听 fd,并设置关心事件为 POLLIN (监听 socket 的"可读"代表有新连接accept)。

2、事件循环(Start()

  • timeout = -1poll 永久阻塞等待。

  • 每轮打印当前已管理的 fd(PrintFd() 仅做辅助观察)。

  • 调用 poll(_fds, size, timeout)

    • 返回 <0:错误;

    • 返回 0:超时(这里不会发生,因为 timeout=-1);

    • 返回 >0:有就绪事件 → Dispatcher() 分发处理。

3、事件分发(Dispatcher()

  • 遍历 _fds

    • 忽略空槽(fd==-1)。

    • 如果 revents & POLLIN

      • 若是监听 fd → Accepter()

      • 否则 → Recver(i) 读取客户端数据。

4、接入管理(Accepter()

  • accept() 成功得到 sockfd,寻找空槽位存入。

  • 注意 :新加入的 sockfd 需要设置 events = POLLIN 才会被 poll 监视(见下文改进点)。

5、I/O 处理(Recver(pos)

  • recv()

    • n > 0:打印数据;

    • n == 0:对端关闭 → 关闭 fd,清空该槽;

    • n < 0:错误 → 关闭 fd,清空该槽。

select 的核心差异

  • 集合表达方式不同

    • select 用位图(fd_set),每次调用都要重建位图;

    • poll结构体数组struct pollfd),每个元素独立声明"关心事件"和"返回事件"。

  • 容量限制

    • selectFD_SETSIZE 限制(常见 1024);

    • poll 没这个硬上限(由数组长度决定),但一次调用是 O(n) 扫描。

  • 就绪结果的承载

    • select改写 传入的 fd_set

    • poll 把结果写在每个元素的 revents 字段里,调用者读完即可(不必预清零,内核会填充)。

poll的优点

  • struct pollfd结构当中包含了events和revents,相当于将select的输入输出型参数进行分离,因此在每次调用poll之前,不需要像select一样重新对参数进行设置。
  • poll可监控的文件描述符数量没有限制。
  • 当然,poll也可以同时等待多个文件描述符,能够提高IO的效率。

说明一下:

  • 虽然代码中将fds数组的元素个数定义为1024,但fds数组的大小是可以继续增大的,poll函数能够帮你监视多少个文件描述符是由传入poll函数的第二个参数决定的。
  • 而fd_set类型只有1024个比特位,因此select函数最多只能监视1024个文件描述符。

poll的缺点

  • 和select函数一样,当poll返回后,需要遍历fds数组来获取就绪的文件描述符。
  • 每次调用poll,都需要把大量的struct pollfd结构从用户态拷贝到内核态,这个开销也会随着poll监视的文件描述符数目的增多而增大。
  • 同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
相关推荐
彩旗工作室2 小时前
用 Supabase 打造统一认证中心:为多应用提供单点登录(SSO)
服务器·前端·数据库
木子_lishk2 小时前
SpringBoot 不更改 pom.xml 引入各种 JDBC 驱动 jar 包
数据库·spring boot
大聪明-PLUS2 小时前
ARM Cortex-M:内存保护单元 (MPU) 发布
linux·嵌入式·arm·smarc
清风6666662 小时前
基于51单片机宠物喂食系统设计
数据库·单片机·毕业设计·51单片机·课程设计·宠物
莫克_Cheney2 小时前
Ubuntu 24.04 安装搜狗输入法完整教程
linux·运维·ubuntu
一语雨在生无可恋敲代码~3 小时前
RAG Day06 查询重建
数据库
对着晚风做鬼脸3 小时前
MySQL进阶知识点(八)---- SQL优化
数据库
半桔4 小时前
【网络编程】套接字入门:网络字节序与套接字种类剖析
linux·网络·php·套接字
nbsaas-boot4 小时前
使用 DuckDB 构建高性能 OLAP 分析平台
java·服务器·数据库