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秒超时)...
相关推荐
宴之敖者、21 分钟前
Linux——\r,\n和缓冲区
linux·运维·服务器
LuDvei23 分钟前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ29 分钟前
【Linux 系统】进程间的通信方式
linux·服务器
Abona30 分钟前
C语言嵌入式全栈Demo
linux·c语言·面试
Lenyiin44 分钟前
Linux 基础IO
java·linux·服务器
The Chosen One9851 小时前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器
Kira Skyler1 小时前
eBPF debugfs中的追踪点format实现原理
linux
2501_927773072 小时前
uboot挂载
linux·运维·服务器
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [drivers][dma]dmapool
linux·笔记·学习
goxingman4 小时前
在 Linux 中查看磁盘运行占用(I/O 使用率)
linux·运维·chrome