【Linux网络编程】多路转接I/O(一)select,poll

发布于:2025-06-25 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

一,I/O多路转接之select

1,初识select

2,select函数原型 

3,理解select执行过程

4,socket特点

 5,select缺点

 6,代码示例

二, I/O多路转接之poll

1,poll的作用和定位

2,认识poll接口

3,poll的优点

4,poll的缺点

5,代码示例

总结


 

核心概念:多路转接I/O允许单线程同时监控多个文件描述符(fd),当某个fd就绪(可读/可写/异常)时通知程序处理。相比于多线程/多进程方案,能显著降低资源消耗。 

一,I/O多路转接之select

1,初识select

首先IO=等+拷贝,其中等待是等底层数据准备好了,然后通过拷贝拿到上层使用。IO工作慢的原因就在于等待时间太长,为了提高效率,我们需要减少等待所占的时间。

  • 我们借助select系统调用来完成等待的工作,一次等待(select)传入多个文件描述符,也就是让select去帮我们监视这些文件描述符的状态变化。
  • 程序会停在 select这里等待,知道被监视的文件描述符有一个或多个 发生了状态变化。状态变化可以理解为某个事件就绪了(比如可写/可读/异常等)。

 

2,select函数原型 

select函数原型如下:

      #include <sys/select.h>

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

 

参数解释:

(1)参数nfds是需要监视的文件描述符中,最大文件描述符值+1;

(1)readfds,writefds,exceptfds:分别对应需要检测的可读文件文件描述符的集合,可写文件描述符的集合,异常文件描述符的集合。它们的类型是fd_set。它的结构如下:

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

eg:0000 0000,将该位图设置为0001 1111,表示select要关心0,1,2,3,4这些文件描述符的某个事件。如果将该位图放到select的第二个参数,表示关心读事件是否就绪,放到第三个参数表示关心写事件是否就绪,放到第四个参数表示异常是否就绪。

操作系统不允许我们直接对这个位图进行修改,使用提供了一组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 的全部位

(3)参数timeout为timeval结构体类型,用来设置select的等待时间。

 timeval结构:

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

参数timeout取值:

NULL:则表示select将一直阻塞,直到某个文件描述符上发生了事件才会返回。

特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。

 


select函数返回值:

执行成功则返回文件描述符状态已改变的个数(也就是那些文件描述符对应的事件就绪了)。

如果返回0代表已超过timeout时间,没有就绪的文件描述符。

当有错误发生时则返回-1,然后errno被设置。

错误值可能为:

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

3,理解select执行过程

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

(1)执行 fd_set set; FD_ZERO(&set);则 set 用位表示是 0000,0000。
(2)若 fd=5,执行 FD_SET(fd,&set);后 set 变为 0001,0000(第 5 位置为 1)
(3)若再加入 fd=2,fd=1,则 set 变为 0001,0011。
(4)执行 select(6,&set,0,0,0)阻塞等待。
(5)若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为0000,0011。注意:没有事件发生的 fd=5 被清空。

 以关心读事件为例:

可以看出:用户定义的一个 fd_set对象,内核是会进行修改的。用户通过修改后的fd_set对象,来判断哪些文件描述符就绪了。

4,socket特点

可监控的文件描述符个数取决于 sizeof(fd_set)的值。

  • 使用fd_set位图管理 FD

  • 支持跨平台(Linux/Windows)

  • 最大 FD 数受限(通常 1024)

  • 每次调用需重新设置 FD 集合

 5,select缺点

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

 6,代码示例

#include <sys/select.h>
#include <iostream>
#include <unistd.h>
#include <vector>

int main()
{
    // 关心读事件
    fd_set read_fds;
    int max_fd = STDIN_FILENO; // 标准输入 (fd=0),将来这里可以进行更改,比如换成网络套接字(监听套接字)
    // 使用辅助数组来保存要关心的fd
    std::vector<int> active_fds = {STDIN_FILENO};

    while (true)
    {
        // 清空集合
        FD_ZERO(&read_fds);
        // 将要关心的fd设置到集合中,同时求出最大的fd
        for (int fd : active_fds)
        {
            FD_SET(fd, &read_fds);
            if (fd > max_fd)
                max_fd = fd;
        }

        // 设置1秒超时
        timeval timeout{.tv_sec = 1, .tv_usec = 0};
        int ready = select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout);

        if (ready < 0)
        {
            //select出错
            perror("select error");
            break;
        }
        else if (ready == 0)
        {
            //select超时
            std::cout << "Timeout occurred!\n";
            continue;
        }

        // 检查就绪的FD
        for (int fd : active_fds)
        {
            if (FD_ISSET(fd, &read_fds))
            {
                //如果文件描述符是标准输入(将来这里更改成网络套接字)
                if (fd == STDIN_FILENO)
                {
                    std::string input;
                    std::getline(std::cin, input);
                    std::cout << "STDIN: " << input << "\n";
                }
                // 可扩展其他FD处理
                //......
            }
        }
    }
    return 0;
}

上述例子是检查标准输入的可读事件是否就绪,可以扩展到网络套接字,这部分代码不在这里展示,可以在下面的仓库链接中查看,该部分代码是使用select多路转接实现的以一个echo server:

Select_Server · 小鬼/linux学习 - 码云 - 开源中国

通过select多路转接,就可以在单线程下,处理多个网络请求。

二, I/O多路转接之poll

1,poll的作用和定位

poll只负责等,一次可以等多个fd,事件就绪,就可以对上层进行事件通知。和select一样。

2,认识poll接口

       #include <poll.h>

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

 第三个参数表示超时时间。

 第一个参数是一个数组,类型为struct pollfd,第二个参数表示元素的个数。结构如下:

         struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

 这是一张事件位图,第一个成员填写要关心哪个文件描述符,第二个和第三个是事件,表示关心哪些事件。

events和revents的取值:

 上面的值都是通过宏定义出来的,其中只有一个比特位为1,events|=POLLIN,表示关心读事件,events|=POLLIN|POLLOUT表示 关心读事件和写事件。通过按位或运算,告诉内核要关心哪些事件。

上层用户在调用的时候,fd和events有效:用户告诉内核,你要帮我关心fd上的events事件。

poll成功返回时,fd和revents有效:内核告诉用户,你要我关心的fd上的events事件,已经就绪了,存储在revents中。

3,poll的优点

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

pollfd结构包含了要监视的events和已经就绪的events,不再使用select"参数-值"的传递方式,将二者分离了,使用起来更方便。

poll并没有最大的数量限制(但是数量过大后,性能也会下降)

Linux 专有(Windows 不支持)

4,poll的缺点

和select一样,poll返回后,需要轮询pollfd来获取就绪的文件描述符。

每次调用poll,都需要将大量的pollfd结构从用户拷贝到内核中。

同时连接的大量客户端在某一时刻,可能有很少的处于就绪状态,因此随着监视的文件描述符的增多,其效率也会下降。

5,代码示例

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

int main()
{
    std::vector<pollfd> poll_fds;
    poll_fds.push_back({STDIN_FILENO, POLLIN, 0}); // 监控标准输入

    while (true)
    {
        int ready = poll(poll_fds.data(), poll_fds.size(), 1000); // 1秒超时

        if (ready < 0)
        {
            perror("poll error");
            break;
        }
        else if (ready == 0)
        {
            std::cout << "Timeout occurred!\n";
            continue;
        }

        for (auto &pfd : poll_fds)
        {
            //检查并处理标准输入事件
            if (pfd.revents & POLLIN)
            {
                if (pfd.fd == STDIN_FILENO)
                {
                    std::string input;
                    std::getline(std::cin, input);
                    std::cout << "STDIN: " << input << "\n";
                }
                // 可扩展其他FD处理
            }
            //检查并处理标准输出事件
            //......

            //检查并处理其他事件
            //.......

            //检查并处理错误事件
            if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
            {
                std::cerr << "Error on fd: " << pfd.fd << "\n";
                // 通常移除失效FD
            }
        }
    }
    return 0;
}

同理,可以扩展到网络部分 ,使用poll实现多路转接,仓库连接:

Poll_Server · 小鬼/linux学习 - 码云 - 开源中国

总结

二者对比

特性 Select Poll
最大 FD 数 受限(FD_SETSIZE) 无限制
性能(FD 量大时) O(n) 扫描 O(n) 扫描
事件类型 仅读/写/异常 更丰富(POLLRDNORM 等)
平台支持 跨平台 Linux 专用
FD 重用 每次需重置 fd_set 可复用 pollfd 数组

 使用建议:

  • Select:FD 数量少(<1024),需跨平台

  • Poll:FD 数量多,仅需支持 Linux

注意:两种模型都采用 水平触发(LT) 模式,即只要 FD 处于就绪状态,每次调用都会返回该 FD


网站公告

今日签到

点亮在社区的每一天
去签到