找回密码
 立即注册
首页 业界区 业界 聊一聊 .NET在Linux下的IO多路复用select和epoll ...

聊一聊 .NET在Linux下的IO多路复用select和epoll

哈妙思 前天 20:35
一:背景

1. 讲故事

在windows平台上,相信很多人都知道.NET异步机制是借助了Windows自带的 IO完成端口 实现的异步交互,那在 Linux 下.NET 又是怎么玩的呢?主要还是传统的 select,poll,epoll 的IO多路复用,在 coreclr源代码中我们都能找到它们的影子。

  • select & poll
在平台适配层的 pal.cpp 文件中,有这样的一句话。
  1. #if HAVE_POLL
  2. #include <poll.h>
  3. #else
  4. #include "pal/fakepoll.h"
  5. #endif  // HAVE_POLL
复制代码
简而言之就是在不支持 poll 的linux版本中使用 select(fakepoll) 模拟,参考代码如下:
1.png


  • epoll
同样的在 linux 中你也会发现很多,截图如下:
2.png

二:select IO多路复用

1. select 解读

在没有 select 之前,我们需要手工管理多句柄的收发,在使用select IO多路复用技术之后,这些多句柄管理就由用户转交给linux系统了,这个也可以从核心的 select 函数看出。
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
复制代码

  • readfds,writefds,exceptfds
这三个字段依次监视着哪些句柄已成可读状态,哪些句柄已成可写状态,哪些句柄已成异常状态,那技术上是如何实现的呢?在libc 中定义了一个 bit 数组,刚好文件句柄fd值作为 bit数组的索引,linux 在内核中只需要扫描 __fds_bits 中哪些位为1 即可找到需要监控的句柄。
  1. /* fd_set for select and pselect.  */
  2. typedef struct
  3.   {
  4.     /* XPG4.2 requires this member name.  Otherwise avoid the name
  5.        from the global namespace.  */
  6. #ifdef __USE_XOPEN
  7.     __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
  8. # define __FDS_BITS(set) ((set)->fds_bits)
  9. #else
  10.     __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
  11. # define __FDS_BITS(set) ((set)->__fds_bits)
  12. #endif
  13.   } fd_set;
复制代码

  • nfds,timeout
为了减少扫描范围,提高程序性能,需要用户指定一个最大的扫描值到 nfds 上。后面的timeout即超时时间。
2. select 的一个小例子

说了再多还不如一个例子有说服力,我们使用 select 机制对 Console 控制台句柄 (STDIN_FILENO) 进行监控,一旦有数据进来立马输出,参考代码如下:
  1. #include <stdio.h>
  2. #include <sys/select.h>
  3. #include <unistd.h>
  4. int main()
  5. {
  6.     fd_set readfds;
  7.     struct timeval timeout;
  8.     char buf[256];
  9.     printf("Enter text (press Ctrl+D to end):\n");
  10.     while (1)
  11.     {
  12.         FD_ZERO(&readfds);
  13.         FD_SET(STDIN_FILENO, &readfds);
  14.         timeout.tv_sec = 5; // 5秒超时
  15.         timeout.tv_usec = 0;
  16.         int ready = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
  17.         if (ready == -1)
  18.         {
  19.             perror("select");
  20.             break;
  21.         }
  22.         else if (ready == 0)
  23.         {
  24.             printf("\nTimeout (5秒无输入).\n");
  25.             break;
  26.         }
  27.         else if (FD_ISSET(STDIN_FILENO, &readfds))
  28.         {
  29.             // 使用 fgets 逐行读取
  30.             if (fgets(buf, sizeof(buf), stdin) != NULL)
  31.             {
  32.                 printf("You entered: %s", buf); // 输出整行(包含换行符)
  33.             }
  34.             else
  35.             {
  36.                 printf("\nEnd of input (Ctrl+D pressed).\n");
  37.                 break;
  38.             }
  39.         }
  40.     }
  41.     return 0;
  42. }
复制代码
3.png

稍微解释下代码逻辑。
  1. /* Standard file descriptors.  */
  2. #define        STDIN_FILENO        0        /* Standard input.  */
  3. #define        STDOUT_FILENO        1        /* Standard output.  */
  4. #define        STDERR_FILENO        2        /* Standard error output.  */
复制代码

  • 将 STDIN_FILENO=0 塞入到可读句柄监控 (readfds) 中。
  • 数据进来之后 select 被唤醒,执行后续逻辑。
  • 通过 FD_ISSET 判断 bit=0 的位置(STDIN_FILENO)是否可用,可用的话读取数据。
如果大家对 select 底层代码感兴趣,可以看下 linux 的 do_select 简化实现,大量的遍历逻辑(bit)。
  1. static noinline_for_stack int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
  2. {
  3.         for (;;) {
  4.                 unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
  5.                 bool can_busy_loop = false;
  6.                 inp = fds->in; outp = fds->out; exp = fds->ex;
  7.                 rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
  8.                 for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
  9.                         in = *inp++; out = *outp++; ex = *exp++;
  10.                         all_bits = in | out | ex;
  11.                         for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
  12.                                 mask = select_poll_one(i, wait, in, out, bit,busy_flag);
  13.                                 if ((mask & POLLIN_SET) && (in & bit)) {
  14.                                         res_in |= bit;
  15.                                         retval++;
  16.                                         wait->_qproc = NULL;
  17.                                 }
  18.                                 if ((mask & POLLOUT_SET) && (out & bit)) {
  19.                                         res_out |= bit;
  20.                                         retval++;
  21.                                         wait->_qproc = NULL;
  22.                                 }
  23.                                 if ((mask & POLLEX_SET) && (ex & bit)) {
  24.                                         res_ex |= bit;
  25.                                         retval++;
  26.                                         wait->_qproc = NULL;
  27.                                 }
  28.                         }
  29.                 }
  30.                 if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
  31.                         timed_out = 1;
  32.         }
  33.         return retval;
  34. }
复制代码
三:epoll IO多路复用

1. epoll 解读

现在主流的软件(Redis,Nigix) 都是采用 epoll,它解决了select低效的遍历,毕竟数组最多支持1024个bit位,一旦句柄过多会影响异步读取的效率。epoll的底层借助了。

  • 红黑树:对句柄进行管理,复杂度为 O(logN)。
  • 就绪队列:一旦句柄变得可读或可写,内核会直接将句柄送到就绪队列。
libc中使用 epoll_wait 函数监视着就绪队列,一旦有数据立即提取,复杂度 O(1),其实这个机制和 Windows 的IO完成端口 已经很靠近了,最后配一下参考代码。
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/epoll.h>
  5. #include <fcntl.h>
  6. #include <errno.h>
  7. #define MAX_EVENTS 10   // 最大监听事件数
  8. #define TIMEOUT_MS 5000 // epoll_wait 超时时间(毫秒)
  9. int main()
  10. {
  11.     int epoll_fd, nfds;                        // epoll 文件描述符和返回的事件数
  12.     struct epoll_event ev, events[MAX_EVENTS]; // epoll 事件结构体
  13.     char buf[256];
  14.     // 创建 epoll 实例
  15.     epoll_fd = epoll_create1(0);
  16.     if (epoll_fd == -1)
  17.     {
  18.         perror("epoll_create1");
  19.         exit(EXIT_FAILURE);
  20.     }
  21.     // 配置并添加标准输入到 epoll 监听
  22.     ev.events = EPOLLIN;       // 监听文件描述符的可读事件(输入)
  23.     ev.data.fd = STDIN_FILENO; // 监听标准输入(文件描述符 0)
  24.     if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1)
  25.     {
  26.         perror("epoll_ctl: STDIN_FILENO");
  27.         exit(EXIT_FAILURE);
  28.     }
  29.     printf("Enter text line by line (press Ctrl+D to end):\n");
  30.     // 主循环:监听事件
  31.     while (1)
  32.     {
  33.         // 等待事件发生或超时
  34.         nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, TIMEOUT_MS);
  35.         if (nfds == -1)
  36.         {
  37.             perror("epoll_wait");
  38.             break;
  39.         }
  40.         else if (nfds == 0)
  41.         {
  42.             printf("\nTimeout (5秒无输入).\n");
  43.             break;
  44.         }
  45.         // 处理所有触发的事件
  46.         for (int n = 0; n < nfds; ++n)
  47.         {
  48.             if (events[n].data.fd == STDIN_FILENO)
  49.             {
  50.                 // 使用 fgets 逐行读取输入
  51.                 if (fgets(buf, sizeof(buf), stdin) != NULL)
  52.                 {
  53.                     printf("You entered: %s", buf);
  54.                 }
  55.                 else
  56.                 {
  57.                     // 输入结束(用户按下 Ctrl+D)
  58.                     printf("\nEnd of input (Ctrl+D pressed).\n");
  59.                     break;
  60.                 }
  61.             }
  62.         }
  63.     }
  64.     close(epoll_fd);
  65.     return 0;
  66. }
复制代码
4.png

四:总结

说了这么多,文尾总结下目前主流的 epoll 和 iocp 各自的特点。
特性epoll (Linux)IOCP (Windows)模型事件驱动 (Reactor)完成端口 (Proactor)核心思想通知可读写事件通知I/O操作完成适用场景高并发网络编程高并发I/O操作编程复杂度较低较高网络I/O性能极佳(百万级连接)优秀磁盘I/O支持有限完善CPU利用率高中内存开销低中
5.jpg

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册