目录

🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂 捐赠一下吧 🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂

BiscuitOS


IO 多路复用使用手册

本节基于丰富的实例案例来介绍 IO 多路复用的使用,实践案例已经在 BiscuitOS 适配,开发者可以参考部署流程在 BiscuitOS 直接实践案例:


IO 多路复用: Epoll 机制使用攻略

epoll 就是对 select 和 poll 的改进了, 它的核心思想是基于事件驱动来实现的,实现起来也并不难,就是给每个 fd 注册一个回调函数,当 fd 对应的设备发生 IO 事件时,就会调用这个回调函数,将该 fd 放到一个链表中,然后由客户端从该链表中取出一个个 fd,以此达到 O(1) 的时间复杂度. epoll 操作实际上对应着有三个函数: epoll_create、epoll_ctr 和 epoll_wait

epoll_create

epoll_create 相当于在内核中创建一个存放 fd 的数据结构。在 select 和 poll 方法中,内核都没有为 fd 准备存放其的数据结构,只是简单粗暴地把数组或者链表复制进来; 而 epoll 则不一样,epoll_create 会在内核建立一颗专门用来存放 fd 结点的红黑树,后续如果有新增的 fd 结点,都会注册到这个 epoll 红黑树上.

epoll_ctr

另一点不一样的是,select 和 poll 会一次性将监听的所有 fd 都复制到内核中,而 epoll 不一样,当需要添加一个新的fd时,会调用 epoll_ctr,给这个 fd 注册一个回调函数,然后将该 fd 结点注册到内核中的红黑树中。当该 fd 对应的设备活跃时,会调用该 fd 上的回调函数,将该结点存放在一个就绪链表中。这也解决了在内核空间和用户空间之间进行来回复制的问题.

epoll_wait

epoll_wait 的做法也很简单,其实直接就是从就绪链表中取结点,这也解决了轮询的问题,时间复杂度变成 O(1). BiscuitOS 提供了使用案例,其在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS/
# KERNEL_VERSION: 内核版本字段 e.g. 5.0
# ARCHITECTURE: 架构字段 e.g x86_64 or i386
make linux-${KERNEL_VERSION}-${ARCHITECTURE}_defconfig
make menuconfig

  [*] Package --->
      [*] IO --->
          [*] Epoll mechanism on BiscuitOS --->

make
cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-epoll-default/
# 下载案例源码
make download

BiscuitOS-epoll-default Gitee Source Code

BiscuitOS 独立模块部署手册

案例源码通过一个应用程序进行展示,在应用程序 39 行通过 open() 函数打开 “/dev/BiscuitOS” 节点,然后在 45 行调用 epoll_create() 函数存储 fd 的数据结构,然后在 49-50 行监听 fd 对应打开文件的 EPOLLIN 事件,程序接着在 51 行调用 epoll_ctl() 函数结合 EPOLL_CTL_ADD 标志将监听事件注册到 epoll 机制中,然后程序在 55 行使用 for() 死循环监听 fd 的 EPOLLIN 事件,程序在 56 行调用 epoll_wait() 函数在等待 epoll 事件,此时该函数最后一个参数为 -1 表示阻塞方式等待 EPOLL 事件。当程序等到一个 EPOLL 使劲之后,程序通过 60-68 行对 EPOLL 事件进行解析,只有 fd 的 EPOLLIN 事件才会跳转到 66 行,此时数据已经准备好,那么调用 read() 函数进行读取,此时可能不止以此 EPOLL 事件,可能多次。为了让 “/dev/BiscuitOS” 能够长生数据并上报 EPOLL 事件,这里使用了一个字符驱动进行模拟,其源码如下:

驱动由一个 MISC 驱动框架构成,在 MISC 驱动对应的 file_operations 数据结构中实现了 poll 接口,驱动通过一个定时器每 BISCUITOS_TIMER_PERIOD 调用一次定时器处理函数 BiscuitOS_timer_handler,该函数在 33 行将全局遍历 poll_size 设置为 20,以此模拟有数据到达,接着调用 wake_up_interruptible() 函数唤醒 BiscuitOS_wait 等待队列。而在 BiscuitOS_poll() 函数中,驱动在 55 行通过调用 poll_wait() 函数等待 BiscuitOS_wait 队列就绪,如果就绪,那么函数检查 poll_size 是否为非空,如果为非空那么表示数据到达了,那么将 mask 设置为 POLLIN, BiscuitOS_poll() 函数最后返回该值会触发一个 POLL 事件. 通过上面的模拟,驱动会定期向用户进程发送 POLL 事件. 案例代码很精简,那么接下来使用如下命令在 BiscuitOS 上实践案例代码:

cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-epoll-default/
# 编译源码
make
# 安装驱动
make install
# Rootfs 打包
make pack
# 运行 BiscuitOS
make run

# BiscuitOS 运行之后安装驱动模块
insmod /lib/modules/$(uname -r)/extra/BiscuitOS-epoll-default.ko

# 运行程序
BiscuitOS-epoll

可以看到当安装模块之后,运行 BiscuitOS-epoll 应用程序,程序每隔 2s 打印字符串 Epoll load data….,以此表示应用程序已经通过 epoll 监听到打开文件的 EPOLLIN 事件. 通过以上实践可以看到案例代码成功实现 IO 多路复用的 epoll 接口.


IO 多路复用: Poll 机制使用攻略

在早期计算机网络并不发达,所以并发网络请求并不会很高,select 模型也足够使用了,但是随着网络的高速发展,高并发的网络请求程序越来越多,而 select 模式下 fd_set 长度限制就开始成为了致命的缺陷。吸取了 select 的教训,poll 模式就不再使用数组的方式来保存自己所监控的 fd 信息了,poll 模型里面通过使用链表的形式来保存自己监控的 fd 信息,正是这样 poll 模型里面是没有了连接限制,可以支持高并发的请求。和 select 还有一点不同的是保存在链表里的需要监控的 fd 信息采用的是 pollfd 的文件格式,select 调用返回的 fd_set 是只包含了上次返回的活跃事件的 fd_set 集合,下一次调用 select 又需要把这几个 fd_set 清空,重新添加上自己感兴趣的 fd 和事件类型,而 poll 采用的 pollfd 保存着对应 fd 需要监控的事件集合,也保存了一个当返回于激活事件的 fd 集合。所以重新发请求时不需要重置感兴趣的事件类型参数。BiscuitOS 提供了使用案例,其在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS/
# KERNEL_VERSION: 内核版本字段 e.g. 5.0
# ARCHITECTURE: 架构字段 e.g x86_64 or i386
make linux-${KERNEL_VERSION}-${ARCHITECTURE}_defconfig
make menuconfig

  [*] Package --->
      [*] IO --->
          [*] Poll mechanism on BiscuitOS --->

make
cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-poll-default/
# 下载案例源码
make download

BiscuitOS-poll-default Gitee Source Code

BiscuitOS 独立模块部署手册

案例源码通过一个应用程序进行展示,在应用程序 33 行通过 open() 函数打开 “/dev/BiscuitOS” 节点, 然后在 39-40 行监听 fd 打开文件的 POLLIN, 函数接着在 42 行使用 for() 函数构建死循环,并在 43 行通过 poll() 函数监听所有文件的 POLLIN,并且使用 -1 参数表示阻塞方式监听。如果监听到 POLL,那么函数在 44 行检查是否为 fd 发送的 POLLIN,如果是函数进入 45 行分支调用 read() 函数从文件中读取已经准备号的数据. 为了让 “/dev/BiscuitOS” 能够长生数据并上报 EPOLL 事件,这里使用了一个字符驱动进行模拟,其源码如下:

驱动由一个 MISC 驱动框架构成,在 MISC 驱动对应的 file_operations 数据结构中实现了 poll 接口,驱动通过一个定时器每 BISCUITOS_TIMER_PERIOD 调用一次定时器处理函数 BiscuitOS_timer_handler,该函数在 33 行将全局遍历 poll_size 设置为 20,以此模拟有数据到达,接着调用 wake_up_interruptible() 函数唤醒 BiscuitOS_wait 等待队列。而在 BiscuitOS_poll() 函数中,驱动在 55 行通过调用 poll_wait() 函数等待 BiscuitOS_wait 队列就绪,如果就绪,那么函数检查 poll_size 是否为非空,如果为非空那么表示数据到达了,那么将 mask 设置为 POLLIN, BiscuitOS_poll() 函数最后返回该值会触发一个 POLL 事件. 通过上面的模拟,驱动会定期向用户进程发送 POLL 事件. 案例代码很精简,那么接下来使用如下命令在 BiscuitOS 上实践案例代码:

cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-poll-default/
# 编译源码
make
# 安装驱动
make install
# Rootfs 打包
make pack
# 运行 BiscuitOS
make run

# BiscuitOS 运行之后安装驱动模块
insmod /lib/modules/$(uname -r)/extra/BiscuitOS-poll-default.ko

# 运行程序
BiscuitOS-poll

可以看到当安装模块之后,运行 BiscuitOS-poll 应用程序,程序每隔 2s 打印字符串 Poll load data….,以此表示应用程序已经通过 poll 监听到打开文件的 EPOLLIN 上报. 通过以上实践可以看到案例代码成功实现 IO 多路复用的 poll 接口.


IO 多路复用: Select 机制使用攻略

select 方法本质其实就是维护了一个文件描述符(fd)数组,以此为基础,实现 IO 多路复用的功能。这个 fd 数组有长度限制,在 32 位系统中,最大值为 1024 个,而在 64 位系统中,最大值为 2048 个. select 方法被调用,首先需要将 fd_set 从用户空间拷贝到内核空间,然后内核用 poll 机制 (此 poll 机制非 IO 多路复用的那个 poll 方法) 直到有一个 fd 活跃,或者超时了方法返回。BiscuitOS 提供了使用案例,其在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS/
# KERNEL_VERSION: 内核版本字段 e.g. 5.0
# ARCHITECTURE: 架构字段 e.g x86_64 or i386
make linux-${KERNEL_VERSION}-${ARCHITECTURE}_defconfig
make menuconfig

  [*] Package --->
      [*] IO --->
          [*] Select mechanism on BiscuitOS --->

make
cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-select-default/
# 下载案例源码
make download

BiscuitOS-select-default Gitee Source Code

BiscuitOS 独立模块部署手册

案例源码通过一个应用程序进行展示,在应用程序 32 行通过 open() 函数打开 “/dev/BiscuitOS” 节点,然后在 38 行调用 for 构建死循环,在循环内部函数在 39 行调用 FD_ZERO() 函数重置 fds, 然后调用 FD_SET() 函数将 fd 添加到 fds 里,以此监听 fd。函数接着在 43-44 行初始化 tv 设置超时时间,这里设置 1s 超时一次。函数接着在 46 行调用 select() 函数进行监听,超时之后函数如果满足 55 行的条件,即 fd 打开文件有数据,那么函数进入 56 行分支调用 read() 函数读取数据. 为了让 “/dev/BiscuitOS” 能够长生数据并上报 EPOLL 事件,这里使用了一个字符驱动进行模拟,其源码如下:

驱动由一个 MISC 驱动框架构成,在 MISC 驱动对应的 file_operations 数据结构中实现了 poll 接口,驱动通过一个定时器每 BISCUITOS_TIMER_PERIOD 调用一次定时器处理函数 BiscuitOS_timer_handler,该函数在 33 行将全局遍历 poll_size 设置为 20,以此模拟有数据到达,接着调用 wake_up_interruptible() 函数唤醒 BiscuitOS_wait 等待队列。而在 BiscuitOS_poll() 函数中,驱动在 55 行通过调用 poll_wait() 函数等待 BiscuitOS_wait 队列就绪,如果就绪,那么函数检查 poll_size 是否为非空,如果为非空那么表示数据到达了,那么将 mask 设置为 POLLIN, BiscuitOS_poll() 函数最后返回该值会触发一个 POLL 事件. 通过上面的模拟,驱动会定期向用户进程发送 POLL 事件. 案例代码很精简,那么接下来使用如下命令在 BiscuitOS 上实践案例代码:

cd BiscuitOS/output/linux-${KERNEL_VERSION}-${ARCHITECTURE}/package/BiscuitOS-select-default/
# 编译源码
make
# 安装驱动
make install
# Rootfs 打包
make pack
# 运行 BiscuitOS
make run

# BiscuitOS 运行之后安装驱动模块
insmod /lib/modules/$(uname -r)/extra/BiscuitOS-select-default.ko

# 运行程序
BiscuitOS-select

可以看到当安装模块之后,运行 BiscuitOS-select 应用程序,程序每隔 2s 打印字符串 Select load data….,以此表示应用程序已经通过 select 监听到打开文件已经准备好数据. 通过以上实践可以看到案例代码成功实现 IO 多路复用的 select 接口.