Linux中select的实现

一、sys_select系统调用入口

c 复制代码
#define _ROUND_UP(x,n) (((x)+(n)-1u) & ~((n)-1u))
#define ROUND_UP(x) _ROUND_UP(x,8LL)
#define FDS_BITPERLONG	(8*sizeof(long))
#define FDS_LONGS(nr)	(((nr)+FDS_BITPERLONG-1)/FDS_BITPERLONG)
#define FDS_BYTES(nr)	(FDS_LONGS(nr)*sizeof(long))
static void *select_bits_alloc(int size)
{
        return kmalloc(6 * size, GFP_KERNEL);
}
static void select_bits_free(void *bits, int size)
{
        kfree(bits);
}
static inline
int get_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
        nr = FDS_BYTES(nr);
        if (ufdset) {
                int error;
                error = verify_area(VERIFY_WRITE, ufdset, nr);
                if (!error && __copy_from_user(fdset, ufdset, nr))
                        error = -EFAULT;
                return error;
        }
        memset(fdset, 0, nr);
        return 0;
}
static inline unsigned long __must_check
set_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
        if (ufdset)
                return __copy_to_user(ufdset, fdset, FDS_BYTES(nr));
        return 0;
}
static inline
void zero_fd_set(unsigned long nr, unsigned long *fdset)
{
        memset(fdset, 0, FDS_BYTES(nr));
}
asmlinkage long
sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timeval __user *tvp)
{
        fd_set_bits fds;
        char *bits;
        long timeout;
        int ret, size, max_fdset;

        timeout = MAX_SCHEDULE_TIMEOUT;
        if (tvp) {
                time_t sec, usec;

                if ((ret = verify_area(VERIFY_READ, tvp, sizeof(*tvp)))
                    || (ret = __get_user(sec, &tvp->tv_sec))
                    || (ret = __get_user(usec, &tvp->tv_usec)))
                        goto out_nofds;

                ret = -EINVAL;
                if (sec < 0 || usec < 0)
                        goto out_nofds;

                if ((unsigned long) sec < MAX_SELECT_SECONDS) {
                        timeout = ROUND_UP(usec, 1000000/HZ);
                        timeout += sec * (unsigned long) HZ;
                }
        }

        ret = -EINVAL;
        if (n < 0)
                goto out_nofds;

        /* max_fdset can increase, so grab it once to avoid race */
        max_fdset = current->files->max_fdset;
        if (n > max_fdset)
                n = max_fdset;

        /*
         * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
         * since we used fdset we need to allocate memory in units of
         * long-words.
         */
        ret = -ENOMEM;
        size = FDS_BYTES(n);
        bits = select_bits_alloc(size);
        if (!bits)
                goto out_nofds;
        fds.in      = (unsigned long *)  bits;
        fds.out     = (unsigned long *) (bits +   size);
        fds.ex      = (unsigned long *) (bits + 2*size);
        fds.res_in  = (unsigned long *) (bits + 3*size);
        fds.res_out = (unsigned long *) (bits + 4*size);
        fds.res_ex  = (unsigned long *) (bits + 5*size);

        if ((ret = get_fd_set(n, inp, fds.in)) ||
            (ret = get_fd_set(n, outp, fds.out)) ||
            (ret = get_fd_set(n, exp, fds.ex)))
                goto out;
        zero_fd_set(n, fds.res_in);
        zero_fd_set(n, fds.res_out);
        zero_fd_set(n, fds.res_ex);

        ret = do_select(n, &fds, &timeout);

        if (tvp && !(current->personality & STICKY_TIMEOUTS)) {
                time_t sec = 0, usec = 0;
                if (timeout) {
                        sec = timeout / HZ;
                        usec = timeout % HZ;
                        usec *= (1000000/HZ);
                }
                put_user(sec, &tvp->tv_sec);
                put_user(usec, &tvp->tv_usec);
        }

        if (ret < 0)
                goto out;
        if (!ret) {
                ret = -ERESTARTNOHAND;
                if (signal_pending(current))
                        goto out;
                ret = 0;
        }

        if (set_fd_set(n, inp, fds.res_in) ||
            set_fd_set(n, outp, fds.res_out) ||
            set_fd_set(n, exp, fds.res_ex))
                ret = -EFAULT;

out:
        select_bits_free(bits, size);
out_nofds:
        return ret;
}

1. 宏定义和辅助函数

1.1. 内存对齐宏

c 复制代码
#define _ROUND_UP(x,n) (((x)+(n)-1u) & ~((n)-1u))
#define ROUND_UP(x) _ROUND_UP(x,8LL)

这两个宏用于**向上舍入(Round Up)**一个数值到指定的对齐边界

c 复制代码
#define _ROUND_UP(x, n) (((x) + (n) - 1u) & ~((n) - 1u))
  • 功能 :将 x 向上舍入到最接近的 n 的倍数。
  • 参数
    • x:待舍入的数值(通常是整数)
    • n:对齐边界(必须是 2 的幂次方,如 1, 2, 4, 8, 16...)
  • 关键点
    • (n) - 1u:计算 n 的掩码(例如 n=8 时,7 的二进制是 0111
    • ~((n) - 1u):对掩码取反(n=8 时,得到 111...1000
    • (x) + (n) - 1u:确保即使 x 不是 n 的倍数,也能通过加法进位
    • & ~((n) - 1u):用掩码清除低位,实现向下舍入到最近的 n 的倍数(但因为前面加了 (n)-1,整体效果是向上舍入)。
c 复制代码
#define ROUND_UP(x) _ROUND_UP(x, 8LL)
  • 功能 :将 x 向上舍入到 8 的倍数(固定对齐边界)
  • 参数
    • x:待舍入的数值
  • 关键点
    • 8LL 表示 long long 类型的 8,避免整数溢出问题
1.1.1.工作原理(以 n=8 为例)

假设 n = 8(二进制 1000),则:

  1. (n) - 1u = 7(二进制 0111
  2. ~((n) - 1u) = ~7 = 0xFFFFFFF8(假设 32 位系统,高位全 1,低位 1111...1000

示例 1:x = 10

  1. (x) + (n) - 1u = 10 + 7 = 17(二进制 10001
  2. 17 & ~7 = 17 & 0xFFFFFFF8 = 16(二进制10000)
    • 结果 168 的倍数,且是 ≥ 10 的最小值
1.1.2. 为什么要求 n 是 2 的幂次方?
  • 掩码特性n = 2^k 时,n - 1 的二进制形式是 k1(如 8-1=70111
  • 取反掩码~(n-1) 的低位是 k0,高位是 1,适合用 & 操作清除低位
  • 非 2 的幂次方会失效 :例如 n=10 时,n-1=91001),取反后无法正确对齐

1.2. 文件描述符位图计算宏

c 复制代码
#define FDS_BITPERLONG  (8*sizeof(long))        // 每个long的位数(32位系统=32,64位系统=64)
#define FDS_LONGS(nr)   (((nr)+FDS_BITPERLONG-1)/FDS_BITPERLONG)  // 需要的long数量
#define FDS_BYTES(nr)   (FDS_LONGS(nr)*sizeof(long))              // 需要的字节数

这三个宏用于高效管理位图(bitmap)的内存分配

c 复制代码
#define FDS_BITPERLONG (8 * sizeof(long))
  • 功能 :计算一个 long 类型变量能表示的二进制位数
  • 原理
    • sizeof(long) 返回 long 类型的字节数(32 位系统通常是 4 字节,64 位系统是 8 字节)
    • 8 * sizeof(long) 将字节数转换为位数(1 字节 = 8 位)
c 复制代码
#define FDS_LONGS(nr) (((nr) + FDS_BITPERLONG - 1) / FDS_BITPERLONG)
  • 功能 :计算存储 nr 个二进制位需要多少个 long 类型的变量

  • 参数

    • nr:需要表示的二进制位数(如文件描述符的数量)
  • 关键点

    复制代码
      (nr) + FDS_BITPERLONG - 1

    向上舍入到最近的FDS_BITPERLONG的倍数

    例如:nr=33FDS_BITPERLONG=3233 + 31 = 64

c 复制代码
#define FDS_BYTES(nr) (FDS_LONGS(nr) * sizeof(long))
  • 功能 :计算存储 nr 个二进制位所需的内存字节数
  • 原理
    • FDS_LONGS(nr) 得到需要的 long 数量
    • * sizeof(long)long 数量转换为字节数
  • 结果
    • 例如:nr=33FDS_LONGS(33)=22 * 4 = 8 字节(32 位系统)

1.3. 内存分配函数

c 复制代码
static void *select_bits_alloc(int size)
{
    return kmalloc(6 * size, GFP_KERNEL);  // 分配6个位图的空间
}

static void select_bits_free(void *bits, int size)
{
    kfree(bits);
}
  • 作用 :为 select 的位图分配内存
  • 参数
    • size:单个位图的大小(通常由 FDS_BYTES(nr) 计算得出,即 nr 个文件描述符所需的字节数)
  • 返回值
    • 返回指向分配内存的指针(void*),失败时返回 NULL

2. 用户空间数据拷贝函数

2.1. get_fd_set() - 从用户空间读取fd_set

c 复制代码
static inline int get_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
    nr = FDS_BYTES(nr);
    if (ufdset) {
        int error;
        error = verify_area(VERIFY_WRITE, ufdset, nr);  // 验证用户空间内存可写
        if (!error && __copy_from_user(fdset, ufdset, nr))
            error = -EFAULT;
        return error;
    }
    memset(fdset, 0, nr);  // 如果用户传递NULL,清空位图
    return 0;
}
2.1.1. 函数代码分析
c 复制代码
static inline int get_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
  • 作用 :将用户空间的文件描述符位图(ufdset)拷贝到内核空间(fdset),并进行安全性检查
  • 参数
    • nr:文件描述符的数量(或位图的比特数)
    • ufdset:用户空间的位图指针(void __user * 表示用户空间地址)
    • fdset:内核空间的位图指针(用于存储拷贝结果)
  • 返回值
    • 0:成功
    • -EFAULT:用户空间内存访问失败(如非法地址)
    • 其他错误码(如 verify_area 返回的错误)

计算位图字节数

c 复制代码
nr = FDS_BYTES(nr);
  • 作用 :将文件描述符数量 nr 转换为位图所需的字节数

检查用户空间指针是否有效

c 复制代码
if (ufdset) {
    int error;
    error = verify_area(VERIFY_WRITE, ufdset, nr);  // 验证用户空间内存可写
    if (!error && __copy_from_user(fdset, ufdset, nr))
        error = -EFAULT;
    return error;
}
  • 逻辑
    1. if (ufdset)
      • 如果用户传递了非 NULLufdset,则需要从用户空间拷贝数据
      • 如果 ufdsetNULL,跳过拷贝,直接清空内核位图
    2. verify_area(VERIFY_WRITE, ufdset, nr)
      • 作用:检查用户空间指针 ufdset 指向的 nr 字节内存是否可写
      • 如果不可写,返回错误(如 -EFAULT
    3. __copy_from_user(fdset, ufdset, nr)
      • 从用户空间 ufdset 拷贝 nr 字节到内核空间 fdset
      • 如果拷贝失败(如用户空间内存非法),返回 -EFAULT
    4. 返回错误
      • 如果 verify_area__copy_from_user 失败,返回错误码

处理用户传递 NULL 的情况

c 复制代码
memset(fdset, 0, nr);  // 如果用户传递NULL,清空位图
return 0;
  • 作用
    • 如果 ufdsetNULL,表示用户不关心某些文件描述符集合
    • 此时将内核位图 fdset 清零,表示不监听任何文件描述符

2.2. set_fd_set() - 写回结果到用户空间

c 复制代码
static inline unsigned long __must_check
set_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
    if (ufdset)
        return __copy_to_user(ufdset, fdset, FDS_BYTES(nr));
    return 0;
}

如果 ufdset 不为 NULL,从内核空间 fdset 拷贝 nr 字节到用户空间 ufdset

2.3. zero_fd_set() - 清空位图

c 复制代码
static inline void zero_fd_set(unsigned long nr, unsigned long *fdset)
{
    memset(fdset, 0, FDS_BYTES(nr));
}

3. 系统调用主函数 sys_select()

3.1. 函数原型和变量声明

c 复制代码
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp, 
                          fd_set __user *exp, struct timeval __user *tvp)
{
    fd_set_bits fds;
    char *bits;
    long timeout;
    int ret, size, max_fdset;

参数

  • n:最大文件描述符+1
  • inp/outp/exp:读/写/异常文件描述符集合
  • tvp:超时时间
c 复制代码
typedef struct {
    long unsigned int *in;
    long unsigned int *out;
    long unsigned int *ex;
    long unsigned int *res_in;
    long unsigned int *res_out;
    long unsigned int *res_ex;
} fd_set_bits;
gdb 复制代码
(gdb) ptype struct timeval
type = struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
}
(gdb) ptype time_t
type = long int
(gdb) ptype suseconds_t
type = long int

3.2. 超时时间处理

c 复制代码
timeout = MAX_SCHEDULE_TIMEOUT;  // 默认无限等待
if (tvp) {
    time_t sec, usec;

    // 从用户空间读取时间值
    if ((ret = verify_area(VERIFY_READ, tvp, sizeof(*tvp)))
        || (ret = __get_user(sec, &tvp->tv_sec))
        || (ret = __get_user(usec, &tvp->tv_usec)))
            goto out_nofds;

    ret = -EINVAL;
    if (sec < 0 || usec < 0)
        goto out_nofds;

    // 转换为jiffies
    if ((unsigned long) sec < MAX_SELECT_SECONDS) {
        timeout = ROUND_UP(usec, 1000000/HZ);  // 微秒对齐
        timeout += sec * (unsigned long) HZ;    // 加上秒数
    }
}
  1. 默认设置为 MAX_SCHEDULE_TIMEOUT(表示无限等待)
  2. 如果用户提供了超时参数(tvp != NULL),则:
    • 从用户空间读取 struct timeval(包含 tv_sectv_usec
    • 验证时间值的合法性(非负)
    • 将时间转换为内核的 jiffies 单位(系统时钟滴答数)
  3. 处理边界条件(如超时值过大)
  • MAX_SELECT_SECONDS 是内核定义的最大允许秒数(防止 jiffies 溢出)

  • 如果 sec 超过此值,直接保持 MAX_SCHEDULE_TIMEOUT(无限等待)

c 复制代码
ROUND_UP(usec, 1000000/HZ)
  • 将微秒(usec)向上对齐到最近的 jiffies 粒度

  • 1000000/HZ1jiffies 对应的微秒数

  • sec * HZ:将秒转换为 jiffiesHZ 是每秒时钟滴答数)

  • timeout = 对齐后的微秒 + 秒数对应的jiffies

3.3. 参数验证

c 复制代码
ret = -EINVAL;
if (n < 0)
    goto out_nofds;

/* max_fdset can increase, so grab it once to avoid race */
max_fdset = current->files->max_fdset;
if (n > max_fdset)
    n = max_fdset;  // 限制不超过进程的最大fd

作用:验证用户传递的文件描述符数量参数

  • n 是用户传递的参数,表示要检查的文件描述符范围(0 到 n-1)
  • 如果 n < 0,返回 -EINVAL(Invalid argument)错误
  • 使用 goto out_nofds 跳转到错误处理路径

获取进程的最大文件描述符限制

c 复制代码
/* max_fdset can increase, so grab it once to avoid race */
max_fdset = current->files->max_fdset;

关键注释说明max_fdset 可能会增加,所以一次性获取以避免竞态条件

c 复制代码
if (n > max_fdset)
    n = max_fdset;  // 限制不超过进程的最大fd

作用:将用户请求的检查范围限制在进程实际拥有的文件描述符范围内

3.4. 内存分配和位图布局

c 复制代码
ret = -ENOMEM;
size = FDS_BYTES(n);  // 计算单个位图大小
bits = select_bits_alloc(size);  // 分配6个位图的空间
if (!bits)
    goto out_nofds;

// 设置6个位图的指针
fds.in      = (unsigned long *)  bits;        // 输入读集合
fds.out     = (unsigned long *) (bits +   size);  // 输入写集合  
fds.ex      = (unsigned long *) (bits + 2*size);  // 输入异常集合
fds.res_in  = (unsigned long *) (bits + 3*size);  // 输出读结果
fds.res_out = (unsigned long *) (bits + 4*size);  // 输出写结果
fds.res_ex  = (unsigned long *) (bits + 5*size);  // 输出异常结果
  • size = FDS_BYTES(n):计算单个位图所需字节数
  • 6 * size:一次性分配6个位图的空间

错误处理

c 复制代码
if (!bits)
    goto out_nofds;  // 内存分配失败,跳转到错误处理

位图指针设置

c 复制代码
// 设置6个位图的指针
fds.in      = (unsigned long *)  bits;        // 输入读集合
fds.out     = (unsigned long *) (bits +   size);  // 输入写集合  
fds.ex      = (unsigned long *) (bits + 2*size);  // 输入异常集合
fds.res_in  = (unsigned long *) (bits + 3*size);  // 输出读结果
fds.res_out = (unsigned long *) (bits + 4*size);  // 输出写结果
fds.res_ex  = (unsigned long *) (bits + 5*size);  // 输出异常结果

内存布局示意图

复制代码
bits指针 (起始地址)
│
├── [0, size)        : fds.in      输入读集合
├── [size, 2*size)   : fds.out     输入写集合  
├── [2*size, 3*size) : fds.ex      输入异常集合
├── [3*size, 4*size) : fds.res_in  输出读结果
├── [4*size, 5*size) : fds.res_out 输出写结果
└── [5*size, 6*size) : fds.res_ex  输出异常结果

3.5. 数据拷贝和初始化

c 复制代码
if ((ret = get_fd_set(n, inp, fds.in)) ||
    (ret = get_fd_set(n, outp, fds.out)) ||
    (ret = get_fd_set(n, exp, fds.ex)))
        goto out;

// 清空结果位图
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
  1. 先拷贝读集合 fds.in
  2. 如果成功,拷贝写集合 fds.out
  3. 如果前两步成功,拷贝异常集合 fds.ex
  4. 任何一步失败立即跳转到错误处理

结果位图初始化

  • 确保结果位图初始状态为全0,只记录实际就绪的文件描述符。

3.6. 核心选择逻辑

c 复制代码
ret = do_select(n, &fds, &timeout);

参数说明

  • n:要检查的文件描述符数量(经过之前验证和限制后的值)
  • &fds:指向 fd_set_bits 结构的指针,包含6个位图
  • &timeout:指向超时时间的指针(传入传出参数)

3.7. 更新剩余时间

c 复制代码
if (tvp && !(current->personality & STICKY_TIMEOUTS)) {
    time_t sec = 0, usec = 0;
    if (timeout) {
        sec = timeout / HZ;      // 计算剩余秒数
        usec = timeout % HZ;     // 计算剩余jiffies
        usec *= (1000000/HZ);    // 转换为微秒
    }
    put_user(sec, &tvp->tv_sec);    // 写回用户空间
    put_user(usec, &tvp->tv_usec);
}

条件1:tvp 不为 NULL

c 复制代码
if (tvp)  // 用户传递了timeval结构指针
  • tvp 是用户空间的 struct timeval 指针
  • 如果用户调用 select(n, inp, outp, exp, NULL),则 tvp = NULL,不更新超时时间

条件2:检查 STICKY_TIMEOUTS 标志

c 复制代码
!(current->personality & STICKY_TIMEOUTS)

STICKY_TIMEOUTS 含义

c 复制代码
// 在 personality 标志中
#define STICKY_TIMEOUTS    0x400000  // 保持超时时间不变

行为差异

  • 没有 STICKY_TIMEOUTS:更新剩余时间(默认行为)
  • 有 STICKY_TIMEOUTS:保持原始超时时间不变(特殊应用需要)

时间转换计算

c 复制代码
time_t sec = 0, usec = 0;
if (timeout) {
    sec = timeout / HZ;      // 计算剩余秒数
    usec = timeout % HZ;     // 计算剩余jiffies
    usec *= (1000000/HZ);    // 转换为微秒
}

时间单位转换关系

复制代码
内核内部: timeout (jiffies) 
         ↓ 转换
用户空间: sec (秒) + usec (微秒)

转换公式详解

c 复制代码
sec = timeout / HZ;          // 整数除法,得到整秒数
usec = timeout % HZ;         // 取余数,得到剩余的jiffies数
usec = usec * (1000000/HZ);  // 将jiffies转换为微秒

写回用户空间

c 复制代码
put_user(sec, &tvp->tv_sec);    // 写回秒数
put_user(usec, &tvp->tv_usec);  // 写回微秒数

3.8. 信号处理和结果返回

c 复制代码
if (ret < 0)
    goto out;
if (!ret) {  // 超时返回
    ret = -ERESTARTNOHAND;
    if (signal_pending(current))  // 检查是否有信号 pending
        goto out;
    ret = 0;
}

// 将结果拷贝回用户空间
if (set_fd_set(n, inp, fds.res_in) ||
    set_fd_set(n, outp, fds.res_out) ||
    set_fd_set(n, exp, fds.res_ex))
    ret = -EFAULT;

错误情况分析

c 复制代码
// ret < 0 表示do_select执行过程中发生错误
可能的错误码:
- -EBADF    // 无效的文件描述符
- -EFAULT   // 内存访问错误
- -ENOMEM   // 内存分配失败
- -EINTR    // 被信号中断(在do_select内部处理)

处理方式 :直接跳转到 out 标签进行资源清理并返回错误码

超时返回的特殊处理

c 复制代码
if (!ret) {  // 超时返回
    ret = -ERESTARTNOHAND;
    if (signal_pending(current))  // 检查是否有信号 pending
        goto out;
    ret = 0;
}

步骤1:预设信号重启标志

c 复制代码
ret = -ERESTARTNOHAND;

步骤2:检查是否有待处理信号

c 复制代码
if (signal_pending(current))
    goto out;

signal_pending(current) 作用

  • 检查当前进程是否有未处理的信号
  • 如果有信号,保持 ret = -ERESTARTNOHAND 并跳转
  • 如果没有信号,继续执行

步骤3:纯超时情况

c 复制代码
ret = 0;  // 没有信号,纯超时返回

结果拷贝回用户空间

3.9. 资源清理

c 复制代码
out:
    select_bits_free(bits, size);  // 释放位图内存
out_nofds:
    return ret;

资源清理并返回错误码

4. 完整执行流程图

有信号 无信号 错误 错误 失败 错误 错误 用户调用select 读取超时时间 参数验证 计算位图大小 分配6个位图内存 设置位图指针布局 拷贝用户fd_set到内核 清空结果位图 调用do_select核心逻辑 更新剩余时间 检查信号和超时 设置ERESTARTNOHAND 设置返回码 拷贝结果回用户空间 释放内存资源 返回结果 跳转out_nofds 跳转out

二、do_select

c 复制代码
int do_select(int n, fd_set_bits *fds, long *timeout)
{
        struct poll_wqueues table;
        poll_table *wait;
        int retval, i;
        long __timeout = *timeout;

        spin_lock(&current->files->file_lock);
        retval = max_select_fd(n, fds);
        spin_unlock(&current->files->file_lock);

        if (retval < 0)
                return retval;
        n = retval;

        poll_initwait(&table);
        wait = &table.pt;
        if (!__timeout)
                wait = NULL;
        retval = 0;
        for (;;) {
                unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

                set_current_state(TASK_INTERRUPTIBLE);

                inp = fds->in; outp = fds->out; exp = fds->ex;
                rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

                for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
                        unsigned long in, out, ex, all_bits, bit = 1, mask, j;
                        unsigned long res_in = 0, res_out = 0, res_ex = 0;
                        struct file_operations *f_op = NULL;
                        struct file *file = NULL;

                        in = *inp++; out = *outp++; ex = *exp++;
                        all_bits = in | out | ex;
                        if (all_bits == 0) {
                                i += __NFDBITS;
                                continue;
                        }

                        for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
                                if (i >= n)
                                        break;
                                if (!(bit & all_bits))
                                        continue;
                               file = fget(i);
                                if (file) {
                                        f_op = file->f_op;
                                        mask = DEFAULT_POLLMASK;
                                        if (f_op && f_op->poll)
                                                mask = (*f_op->poll)(file, retval ? NULL : wait);
                                        fput(file);
                                        if ((mask & POLLIN_SET) && (in & bit)) {
                                                res_in |= bit;
                                                retval++;
                                        }
                                        if ((mask & POLLOUT_SET) && (out & bit)) {
                                                res_out |= bit;
                                                retval++;
                                        }
                                        if ((mask & POLLEX_SET) && (ex & bit)) {
                                                res_ex |= bit;
                                                retval++;
                                        }
                                }
                        }
                        if (res_in)
                                *rinp = res_in;
                        if (res_out)
                                *routp = res_out;
                        if (res_ex)
                                *rexp = res_ex;
                }
                wait = NULL;
                if (retval || !__timeout || signal_pending(current))
                        break;
                if(table.error) {
                        retval = table.error;
                        break;
                }
                __timeout = schedule_timeout(__timeout);
        }
        __set_current_state(TASK_RUNNING);

        poll_freewait(&table);

        /*
         * Up-to-date the caller timeout.
         */
        *timeout = __timeout;
        return retval;
}

1. 函数原型和初始化

c 复制代码
int do_select(int n, fd_set_bits *fds, long *timeout)
{
    struct poll_wqueues table;
    poll_table *wait;
    int retval, i;
    long __timeout = *timeout;

参数

  • n:文件描述符数量
  • fds:包含6个位图的结构体指针
  • timeout:超时时间指针(用于返回剩余时间)

2. 计算最大有效文件描述符

c 复制代码
spin_lock(&current->files->file_lock);
retval = max_select_fd(n, fds);
spin_unlock(&current->files->file_lock);

if (retval < 0)
    return retval;
n = retval;

作用:确定实际需要检查的文件描述符范围

  • 使用自旋锁保护文件描述符表
  • max_select_fd 找到位图中设置的最高文件描述符
  • 减少不必要的遍历

3. 初始化轮询结构

c 复制代码
poll_initwait(&table);
wait = &table.pt;
if (!__timeout)
    wait = NULL;
retval = 0;

关键设置

  • poll_initwait(&table):初始化等待队列,设置回调函数为 __pollwait
  • 如果超时为0(非阻塞),设置 wait = NULL,不注册等待队列

4. 主轮询循环

c 复制代码
for (;;) {
    unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

    set_current_state(TASK_INTERRUPTIBLE);

4.1. 设置进程状态

c 复制代码
set_current_state(TASK_INTERRUPTIBLE);
  • 将进程状态设置为可中断睡眠
  • 为调用 schedule_timeout() 做准备

4.2. 获取位图指针

c 复制代码
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
  • inp/outp/exp:输入位图(用户关心的fd
  • rinp/routp/rexp:输出位图(就绪的fd

5. 外层循环:按long字遍历

c 复制代码
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
    unsigned long in, out, ex, all_bits, bit = 1, mask, j;
    unsigned long res_in = 0, res_out = 0, res_ex = 0;
    struct file_operations *f_op = NULL;
    struct file *file = NULL;

    in = *inp++; out = *outp++; ex = *exp++;
    all_bits = in | out | ex;
    if (all_bits == 0) {
        i += __NFDBITS;
        continue;
    }

按字处理优化

  • 一次处理一个 unsigned long(32或64位)
  • all_bits = in | out | ex:合并所有关心的位
  • 如果整个字都为0,直接跳过,提高效率

6. 内层循环:按位遍历

c 复制代码
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
    if (i >= n)
        break;
    if (!(bit & all_bits))
        continue;

位遍历逻辑

  • j:在当前字中的位索引
  • i:全局文件描述符编号
  • bit:当前位的掩码(1 << j)
  • 跳过未设置的位,只检查用户关心的fd

7. 文件操作和轮询检查

c 复制代码
file = fget(i);
if (file) {
    f_op = file->f_op;
    mask = DEFAULT_POLLMASK;
    if (f_op && f_op->poll)
        mask = (*f_op->poll)(file, retval ? NULL : wait);
    fput(file);

关键调用

c 复制代码
mask = (*f_op->poll)(file, retval ? NULL : wait);
  • 调用设备驱动的 poll 方法
  • 如果已经有就绪的fdretval > 0),传递 wait = NULL,避免重复注册

8. 事件检查和结果记录

c 复制代码
if ((mask & POLLIN_SET) && (in & bit)) {
    res_in |= bit;
    retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
    res_out |= bit;
    retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
    res_ex |= bit;
    retval++;
}

事件集合定义

c 复制代码
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)

9. 保存结果到输出位图

c 复制代码
if (res_in)
    *rinp = res_in;
if (res_out)
    *routp = res_out;
if (res_ex)
    *rexp = res_ex;

结果回填

  • 只设置实际就绪的位
  • 保持其他位为0

10. 退出条件检查

c 复制代码
wait = NULL;
if (retval || !__timeout || signal_pending(current))
    break;
if(table.error) {
    retval = table.error;
    break;
}
__timeout = schedule_timeout(__timeout);

退出条件

  1. retval:有就绪的文件描述符
  2. !__timeout:超时时间为0
  3. signal_pending(current):有信号等待处理
  4. table.error:轮询过程中发生错误
c 复制代码
__timeout = schedule_timeout(__timeout);
  • 让出CPU,进入睡眠状态
  • 返回剩余的超时时间
  • 会被设备唤醒或超时唤醒

12. 清理和返回

c 复制代码
__set_current_state(TASK_RUNNING);
poll_freewait(&table);

*timeout = __timeout;
return retval;

资源清理

  • 恢复进程状态
  • 释放等待队列资源
  • 返回剩余超时时间和就绪fd数量

完整执行流程图

是 否 否 是 是 否 是 否 满足条件 不满足 开始do_select 计算最大有效fd 初始化poll_wqueues 设置wait指针 进入主循环 设置进程状态为TASK_INTERRUPTIBLE 外层循环: 按long字遍历 整个字为0? 跳过这个字 内层循环: 按位遍历 位被设置? 跳过这位 fget获取文件结构 调用f_op->poll 检查事件并记录结果 还有位? 保存结果到输出位图 还有字? 检查退出条件 退出循环 schedule_timeout睡眠 恢复进程状态 poll_freewait清理 返回结果

三、max_select_fd实际需要检查的最大文件描述符

c 复制代码
#define BITS(fds, n)    (*FDS_IN(fds, n)|*FDS_OUT(fds, n)|*FDS_EX(fds, n))

static int max_select_fd(unsigned long n, fd_set_bits *fds)
{
        unsigned long *open_fds;
        unsigned long set;
        int max;

        /* handle last in-complete long-word first */
        set = ~(~0UL << (n & (__NFDBITS-1)));
        n /= __NFDBITS;
        open_fds = current->files->open_fds->fds_bits+n;
        max = 0;
        if (set) {
                set &= BITS(fds, n);
                if (set) {
                        if (!(set & ~*open_fds))
                                goto get_max;
                        return -EBADF;
                }
        }
        while (n) {
                open_fds--;
                n--;
                set = BITS(fds, n);
                if (!set)
                        continue;
                if (set & ~*open_fds)
                        return -EBADF;
                if (max)
                        continue;
get_max:
                do {
                        max++;
                        set >>= 1;
                } while (set);
                max += n * __NFDBITS;
        }

        return max;
}

1. 宏定义和函数原型

c 复制代码
#define BITS(fds, n) (*FDS_IN(fds, n)|*FDS_OUT(fds, n)|*FDS_EX(fds, n))

static int max_select_fd(unsigned long n, fd_set_bits *fds)
{
    unsigned long *open_fds;
    unsigned long set;
    int max;

宏定义

  • BITS(fds, n):合并第n个long字中的所有输入位图(读+写+异常)

2. 处理最后一个不完整的long字

c 复制代码
/* handle last in-complete long-word first */
set = ~(~0UL << (n & (__NFDBITS-1)));
n /= __NFDBITS;
open_fds = current->files->open_fds->fds_bits+n;
max = 0;

步骤1:~0UL - 创建全1的掩码

c 复制代码
// ~0UL 表示对0按位取反,得到全1的unsigned long
32位系统: ~0UL = 0xFFFFFFFF

步骤2:n & (__NFDBITS-1) - 计算在最后一个字中的位数

c 复制代码
// __NFDBITS-1 创建掩码,用于取模运算
32位系统: __NFDBITS=32, __NFDBITS-1=31 (0x1F)

// n & (__NFDBITS-1) 等价于 n % __NFDBITS

步骤3:~0UL << (n & (__NFDBITS-1)) - 左移并取反

c 复制代码
// 先左移,然后取反得到有效的掩码

字索引计算

c 复制代码
n /= __NFDBITS;

打开文件描述符位图定位

c 复制代码
open_fds = current->files->open_fds->fds_bits+n;
c 复制代码
// open_fds 指向最后一个需要检查的long字
// 由于我们从后往前遍历,所以先处理最后一个字

3. 检查最后一个不完整的long字

c 复制代码
if (set) {
    set &= BITS(fds, n);
    if (set) {
        if (!(set & ~*open_fds))
            goto get_max;
        return -EBADF;
    }
}

有效性检查

  1. set &= BITS(fds, n):只保留用户关心的位
  2. set & ~*open_fds:检查是否有未打开的文件描述符
  3. 如果所有关心的fd都是打开的,跳转到计算最大值
  4. 如果有未打开的fd,返回 -EBADF(Bad file descriptor)

检查逻辑示例:

c 复制代码
用户关心的位:   00000111  (关注fd 32,33,34)
进程打开的fd:   00000101  (只有fd 32,34打开)
未打开的fd:     00000010  (fd 33未打开) → 返回 -EBADF

4. 反向遍历完整的long字

c 复制代码
while (n) {
    open_fds--;
    n--;
    set = BITS(fds, n);
    if (!set)
        continue;
    if (set & ~*open_fds)
        return -EBADF;
    if (max)
        continue;

反向遍历逻辑

  • 从高位向低位遍历(从最后一个完整long字开始)
  • open_fds--:移动到前一个long字
  • n--:减少long字索引
  • 检查每个long字的有效性
  • 如果已经找到最大值(max != 0),继续检查但不重新计算

5. 计算最大文件描述符

c 复制代码
get_max:
    do {
        max++;
        set >>= 1;
    } while (set);
    max += n * __NFDBITS;
}

最大值计算

  • do-while 循环:计算当前long字中最高位的设置位置
  • max += n * __NFDBITS:加上基础偏移量

四、测试select

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

int main() {
    fd_set readfds;
    struct timeval tv;
    int ret;
    char buffer[1024];

    printf("=== select 系统调用测试程序 ===\n");
    printf("程序将监控标准输入(stdin)\n");
    printf("请输入文字,程序将在5秒超时内等待输入...\n\n");

    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&readfds);
        
        // 将标准输入(文件描述符0)添加到读集合
        FD_SET(STDIN_FILENO, &readfds);

        // 设置5秒超时
        tv.tv_sec = 5;
        tv.tv_usec = 0;

        printf("等待输入(5秒超时)...");
        fflush(stdout);

        // 调用select系统调用
        ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);

        if (ret == -1) {
            // 错误情况
            perror("select错误");
            break;
        } else if (ret == 0) {
            // 超时情况
            printf("超时!没有检测到输入。\n\n");
            continue;
        } else {
            // 有文件描述符就绪
            if (FD_ISSET(STDIN_FILENO, &readfds)) {
                printf("检测到输入!\n");
                
                // 读取输入
                ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
                if (bytes_read > 0) {
                    buffer[bytes_read] = '\0';  // 添加字符串结束符
                    printf("读取到 %zd 字节: %s", bytes_read, buffer);
                    
                    // 检查退出条件
                    if (strncmp(buffer, "quit", 4) == 0 || strncmp(buffer, "exit", 4) == 0) {
                        printf("退出程序。\n");
                        break;
                    }
                } else if (bytes_read == 0) {
                    printf("标准输入已关闭。\n");
                    break;
                } else {
                    perror("读取错误");
                }
            }
            printf("\n");
        }
    }

    return 0;
}

1.编译执行验证

编译

shell 复制代码
gcc select_test.c -o select_test

执行

shell 复制代码
./select_test

验证,预期输入输出

text 复制代码
请输入文字,程序将在5秒超时内等待输入...

等待输入(5秒超时)...test
检测到输入!
读取到 5 字节: test

等待输入(5秒超时)...超时!没有检测到输入。

等待输入(5秒超时)...
相关推荐
bst@微胖子3 小时前
Harmony中EventHub实现发布订阅
linux·运维·ubuntu
爱奥尼欧3 小时前
【Linux】系统部分——线程安全与线程的单例模式
linux·安全·单例模式
尹蓝锐5 小时前
在学校Linux服务器上配置go语言环境
linux·运维·服务器
用户31187945592186 小时前
Fedora 38 安装 perl-JSON RPM 包步骤(含依赖问题解决及附安装包)
linux
ARTHUR-SYS6 小时前
基于Kali linux 安装pyenv及简单使用方法及碰到的问题
linux·运维·chrome
南山鹤1666 小时前
中型规模生产架构部署详细步骤
linux
IvanCodes6 小时前
十六、Linux网络基础理论 - OSI模型、TCP/IP协议与IP地址详解
linux·网络·tcp/ip
shylyly_6 小时前
Linux-> TCP 编程2
linux·服务器·网络·tcp/ip·松耦合·command程序
upgrador7 小时前
操作系统命令:Linux与Shell(Operating System & Command Line, OS/CLI)目录导航、文件操作与日志查看命令实践
linux·ubuntu·centos