【后端教程】Linux select 一网打尽

1

原型

int select (int __nfds, fd_set *__restrict __readfds,

如你所知,select是IO多种复用的一种实现,它将需要监控的fd分为读,写,异常三类,使用fd_set表示,当其返回时要么是超时,要么是有至少一种读,写或异常事件发生。

2

相关数据结构

FD_SET

FD_SET是select最重要的数据结构了,其在内核中的定义如下:

typedef __kernel_fd_set     fd_set;

我们来简化下,fd_set是一个struct, 其内部只有一个 由16个元素组成的unsigned long数组,这个数组一共可以表示16 × 64 = 1024位, 每一位用来表示一个 fd, 这也就是 select针对读,定或异常每一类最多只能有 1024个fd 限制的由来。

3

相关宏

下面这些宏定义在内核代码fs/select.c中:

扫描二维码关注公众号,回复: 11069508 查看本文章
  • FDS_BITPERLONG: 返回每个long有多少位,通常是64bits
#define FDS_BITPERLONG    (8*sizeof(long))
  • FDS_LONGS(nr): 获取 nr 个fd 需要用几个long来表示
#define FDS_LONGS(nr) (((nr)+FDS_BITPERLONG-1)/FDS_BITPERLONG)
  • FD_BYTES(nr): 获取 nr 个fd 需要用 多少个字节来表示
#define FDS_BYTES(nr) (FDS_LONGS(nr)*sizeof(long))

下面这些宏可以在gcc源码中找到:

  • FD_ZERO: 初始化一个fd_set
#define __FD_ZERO(s) \

将上面所说的由16个元素组成的unsigned long数组每一个元素都设为 0;

  • __FD_SET(d, s): 将一个fd 赋值到 一个 fd_set
#define __FD_SET(d, s) \

分三步:

a. __FD_ELT(d): 确定赋值到数组的哪一个元素

 #define   __FD_ELT(d) \

其中 #define __NFDBITS (8 * (int) sizeof (__fd_mask)) , 即__NFDBITS = 64

这里实现使用了__builtin_constant_p针对常量作了优化,我也没有太理解常量与非常量实现方案有什么不同,我们暂时忽略这个细节看本质。

本质就是 一个 unsigned long有64位,直接 __d / __NFDBITS取模就可以确定用数组的哪一个元素了;

b. __FD_MASK(d): 确定赋值到一个 unsigned long的哪一位

#define   __FD_MASK(d)    ((__fd_mask) (1UL << ((d) % __NFDBITS)))

直接 (d) % __NFDBITS)取余后作为 1 左移的位数即可

c. |= :用 位或 赋值即可;

4

在内核中的实现

调用层级

image.gif

系统调用入口位置

位于fs/select.c中

SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,

入口函数 kern_select

static int kern_select(int n, fd_set __user *inp, fd_set __user *outp,

做三件事:

a. 如果设置了超时,首先准备时间戳 timespec64;

b. 调用 core_sys_select,这个是具体的实现,我们下面会重点介绍

c. poll_select_finish:作的主要工作就是更新用户调用select时传进来的 超时参数tvp,我列一下关键代码:

    ktime_get_ts64(&rts);

可以看到先获取当前的时间戳,然后通过timespec64_sub和传入的时间戳(接中传入的是超时时间,实现时会转化为时间戳)求出差值,将此差值传回给用户,即返回了剩余的超时时间。所以这个地方是个小陷阱,用户在调用select时,需要每次重新初始化这个超时时间。

通过core_sys_select实现

这个函数主要功能是在实现真正的select功能前,准备好 fd_set ,即从用户空间将所需的三类 fd_set 复制到内核空间。从下面的代码中你会看到对于每次的 select系统调用,都需要从用户空间将所需的三类 fd_set 复制到内核空间,这里存在性能上的损耗。

int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,

代码中的注释很清晰,我们这里简单过一下,分五步:

  • 规范化select系统调用传入的第一个参数 n
/* max_fds can increase, so grab it once to avoid race */

这个n是三类不同的fd_set中所包括的fd数值的最大值 + 1, linux task打开句柄从0开始,不加1的话可能会少监控fd.

用户在使用时可以有个偷懒的,就是将这个n设置 为 FD_SETSIZE,通常 是1024, 这将监控的范围扩大到了上限,但实际上远没有这么多fd需要监控,浪费资源。

linux man中的解释如下:

nfds should be set to the highest-numbered file descriptor in any of the three sets, plus 1. The indicated file descriptors in each set are checked, up to this limit (but see BUGS).

  • 计算内核空间所需要的fd_set的空间, 内核态需要三个fd_set来容纳用户态传递过来的参数,还需要三个fd_set来容纳select调用返回后生成的三个fd_set, 即一共是6个fd_set
   long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];

这里有个小技巧,先从内核栈上分配空间,如果不够用,才使用 kvmalloc分配。

通过 size = FDS_BYTES(n);计算出单一一种fd_set所需字节数,

再能过 alloc_size = 6 * size; 即可计算出所需的全部字节数。

  • 初始化用作参数的和用作返回值的两类fd_set
if ((ret = get_fd_set(n, inp, fds.in)) ||

主要使用 copy_from_user和 memset来实现。

  • 真正实现部分 do_select, 我们在下面详讲

  • 返回结果复制回用户空间

if (set_fd_set(n, inp, fds.res_in) ||

这里又多了一次内核空间到用户空间的copy, 而且我们看到返回值也是用fd_set结构来表示,这意味着我们在用户空 间处理里也需要遍历每一位。

精华所在do_select

这里用到了Linux里一个很重要的数据结构 wait queue, 我们暂不打算展开来讲,先简单来说下其用法,比如我们在进程中read时经常要等待数据准备好,我们用伪码来写些流程:

// Read 代码

360私有云平台(HULK平台)管理着360公司90%以上的业务线,面对如此众多的服务器,如何进行管理?当然需要一套完善的工具来自动化。HULK平台的命令系统可以对批量机器执行脚本,命令系统的底层是基于SaltStack开发当然最大的问题在于机器部署的机房多。

do_select源码走读

  • 获取当前三类fd_set中最大的fd
    rcu_read_lock();

上面 n = retval中的 n, 即为三类fd_set中最大的fd, 也是下面要介绍的循环体的上限

  • 初始化用作wait queue中的 wait entry 的private数据结构
poll_initwait(&table);
  • 核心循环体

    我们讲注释写在这个代码块里

// 最外面是一个无限循环,它只有在poll到有效的事件,或者超时,或者有中断发生时,才会退出

简单总结如下:

a. 循环遍历每一个监控的fd;

b. 有下列情况之一则返回:

任意监控的fd上有事件发生

c. 如查无上述情况,将当前进程设置为 TASK_INTERRUPTIBLE, 并调用 schedule作进程切换;

d. 等待socket 事件发生,对应的socket将当前进程唤醒后,当前进程被再次调度切换回来,继续运行;

细心的你可能已经发现,这个有个影响效率的问题:即使只有一个监控中的fd有事件发生,当前进程就会被唤醒,然后要将所有监控的fd都遍历一边,依次调用vfs_poll来获取其有效事件,好麻烦啊~~~

5

vfs_poll 讲解

  • 调用层级

image

  • 作用
初始化wait entry, 将其加入到这个fd对应的socket的等待队列中
  • 加入等待队列时,最终会调用 fs_select.c中的 __pollwait
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,

6

总结

  • select调用中每类fd_set中最多容纳1024个fd;

  • 每次调用select都需要将三类fd_set从用户空间复制到内核空间;

  • wait queue是个好东西,select会被当前进程task加入到每一个监控的socket的等待队列;

  • select进程被唤醒后即使只有一个被监控的fd有事件发生,也会再次将所有的监控fd遍历一次;

  • 在遍历fd的过程中会调用cond_resched()来主动出让CPU, 作进程切换;

服务推荐

发布了0 篇原创文章 · 获赞 0 · 访问量 347

猜你喜欢

转载自blog.csdn.net/weixin_47143210/article/details/105644820