Linux中select的实现
一、sys_select
系统调用入口
#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. 内存对齐宏
#define _ROUND_UP(x,n) (((x)+(n)-1u) & ~((n)-1u))
#define ROUND_UP(x) _ROUND_UP(x,8LL)
这两个宏用于**向上舍入(Round Up)**一个数值到指定的对齐边界
#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
,整体效果是向上舍入)。
#define ROUND_UP(x) _ROUND_UP(x, 8LL)
- 功能:将
x
向上舍入到 8 的倍数(固定对齐边界) - 参数
x
:待舍入的数值
- 关键点
8LL
表示long long
类型的 8,避免整数溢出问题
1.1.1.工作原理(以 n=8
为例)
假设 n = 8
(二进制 1000
),则:
(n) - 1u = 7
(二进制0111
)~((n) - 1u) = ~7 = 0xFFFFFFF8
(假设 32 位系统,高位全 1,低位1111...1000
)
示例 1:x = 10
(x) + (n) - 1u = 10 + 7 = 17
(二进制10001
)17 & ~7 = 17 & 0xFFFFFFF8 = 16
(二进制10000)- 结果
16
是8
的倍数,且是 ≥10
的最小值
- 结果
1.1.2. 为什么要求 n
是 2 的幂次方?
- 掩码特性:
n = 2^k
时,n - 1
的二进制形式是k
个1
(如8-1=7
是0111
) - 取反掩码:
~(n-1)
的低位是k
个0
,高位是1
,适合用&
操作清除低位 - 非 2 的幂次方会失效:例如
n=10
时,n-1=9
(1001
),取反后无法正确对齐
1.2. 文件描述符位图计算宏
#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)的内存分配
#define FDS_BITPERLONG (8 * sizeof(long))
- 功能:计算一个
long
类型变量能表示的二进制位数 - 原理
sizeof(long)
返回long
类型的字节数(32 位系统通常是 4 字节,64 位系统是 8 字节)8 * sizeof(long)
将字节数转换为位数(1 字节 = 8 位)
#define FDS_LONGS(nr) (((nr) + FDS_BITPERLONG - 1) / FDS_BITPERLONG)
-
功能:计算存储
nr
个二进制位需要多少个long
类型的变量 -
参数
nr
:需要表示的二进制位数(如文件描述符的数量)
-
关键点
-
(nr) + FDS_BITPERLONG - 1
向上舍入到最近的
FDS_BITPERLONG
的倍数例如:
nr=33
,FDS_BITPERLONG=32
→33 + 31 = 64
-
#define FDS_BYTES(nr) (FDS_LONGS(nr) * sizeof(long))
- 功能:计算存储
nr
个二进制位所需的内存字节数 - 原理
FDS_LONGS(nr)
得到需要的long
数量* sizeof(long)
将long
数量转换为字节数
- 结果
- 例如:
nr=33
→FDS_LONGS(33)=2
→2 * 4 = 8
字节(32 位系统)
- 例如:
1.3. 内存分配函数
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
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. 函数代码分析
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
返回的错误)
计算位图字节数
nr = FDS_BYTES(nr);
- 作用:将文件描述符数量
nr
转换为位图所需的字节数
检查用户空间指针是否有效
if (ufdset) {int error;error = verify_area(VERIFY_WRITE, ufdset, nr); // 验证用户空间内存可写if (!error && __copy_from_user(fdset, ufdset, nr))error = -EFAULT;return error;
}
- 逻辑
if (ufdset)
- 如果用户传递了非
NULL
的ufdset
,则需要从用户空间拷贝数据 - 如果
ufdset
为NULL
,跳过拷贝,直接清空内核位图
- 如果用户传递了非
verify_area(VERIFY_WRITE, ufdset, nr)
- 作用:检查用户空间指针
ufdset
指向的nr
字节内存是否可写 - 如果不可写,返回错误(如
-EFAULT
)
- 作用:检查用户空间指针
__copy_from_user(fdset, ufdset, nr)
- 从用户空间
ufdset
拷贝nr
字节到内核空间fdset
- 如果拷贝失败(如用户空间内存非法),返回
-EFAULT
- 从用户空间
- 返回错误
- 如果
verify_area
或__copy_from_user
失败,返回错误码
- 如果
处理用户传递 NULL 的情况
memset(fdset, 0, nr); // 如果用户传递NULL,清空位图
return 0;
- 作用
- 如果
ufdset
为NULL
,表示用户不关心某些文件描述符集合 - 此时将内核位图
fdset
清零,表示不监听任何文件描述符
- 如果
2.2. set_fd_set()
- 写回结果到用户空间
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()
- 清空位图
static inline void zero_fd_set(unsigned long nr, unsigned long *fdset)
{memset(fdset, 0, FDS_BYTES(nr));
}
3. 系统调用主函数 sys_select()
3.1. 函数原型和变量声明
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
:最大文件描述符+1inp/outp/exp
:读/写/异常文件描述符集合tvp
:超时时间
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) 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. 超时时间处理
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;// 转换为jiffiesif ((unsigned long) sec < MAX_SELECT_SECONDS) {timeout = ROUND_UP(usec, 1000000/HZ); // 微秒对齐timeout += sec * (unsigned long) HZ; // 加上秒数}
}
- 默认设置为
MAX_SCHEDULE_TIMEOUT
(表示无限等待) - 如果用户提供了超时参数(
tvp != NULL
),则:- 从用户空间读取
struct timeval
(包含tv_sec
和tv_usec
) - 验证时间值的合法性(非负)
- 将时间转换为内核的
jiffies
单位(系统时钟滴答数)
- 从用户空间读取
- 处理边界条件(如超时值过大)
-
MAX_SELECT_SECONDS
是内核定义的最大允许秒数(防止jiffies
溢出) -
如果
sec
超过此值,直接保持MAX_SCHEDULE_TIMEOUT
(无限等待) -
ROUND_UP(usec, 1000000/HZ)
- 将微秒(
usec
)向上对齐到最近的jiffies
粒度 1000000/HZ
是1
个jiffies
对应的微秒数
- 将微秒(
-
sec * HZ
:将秒转换为jiffies
(HZ
是每秒时钟滴答数) -
timeout = 对齐后的微秒 + 秒数对应的jiffies
3.3. 参数验证
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
跳转到错误处理路径
获取进程的最大文件描述符限制
/* max_fdset can increase, so grab it once to avoid race */
max_fdset = current->files->max_fdset;
关键注释说明:max_fdset
可能会增加,所以一次性获取以避免竞态条件
if (n > max_fdset)n = max_fdset; // 限制不超过进程的最大fd
作用:将用户请求的检查范围限制在进程实际拥有的文件描述符范围内
3.4. 内存分配和位图布局
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个位图的空间
错误处理
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); // 输出异常结果
内存布局示意图
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. 数据拷贝和初始化
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);
- 先拷贝读集合
fds.in
- 如果成功,拷贝写集合
fds.out
- 如果前两步成功,拷贝异常集合
fds.ex
- 任何一步失败立即跳转到错误处理
结果位图初始化
- 确保结果位图初始状态为全0,只记录实际就绪的文件描述符。
3.6. 核心选择逻辑
ret = do_select(n, &fds, &timeout);
参数说明:
n
:要检查的文件描述符数量(经过之前验证和限制后的值)&fds
:指向fd_set_bits
结构的指针,包含6个位图&timeout
:指向超时时间的指针(传入传出参数)
3.7. 更新剩余时间
if (tvp && !(current->personality & STICKY_TIMEOUTS)) {time_t sec = 0, usec = 0;if (timeout) {sec = timeout / HZ; // 计算剩余秒数usec = timeout % HZ; // 计算剩余jiffiesusec *= (1000000/HZ); // 转换为微秒}put_user(sec, &tvp->tv_sec); // 写回用户空间put_user(usec, &tvp->tv_usec);
}
条件1:tvp
不为 NULL
if (tvp) // 用户传递了timeval结构指针
tvp
是用户空间的struct timeval
指针- 如果用户调用
select(n, inp, outp, exp, NULL)
,则tvp = NULL
,不更新超时时间
条件2:检查 STICKY_TIMEOUTS 标志
!(current->personality & STICKY_TIMEOUTS)
STICKY_TIMEOUTS 含义:
// 在 personality 标志中
#define STICKY_TIMEOUTS 0x400000 // 保持超时时间不变
行为差异:
- 没有 STICKY_TIMEOUTS:更新剩余时间(默认行为)
- 有 STICKY_TIMEOUTS:保持原始超时时间不变(特殊应用需要)
时间转换计算
time_t sec = 0, usec = 0;
if (timeout) {sec = timeout / HZ; // 计算剩余秒数usec = timeout % HZ; // 计算剩余jiffiesusec *= (1000000/HZ); // 转换为微秒
}
时间单位转换关系
内核内部: timeout (jiffies) ↓ 转换
用户空间: sec (秒) + usec (微秒)
转换公式详解
sec = timeout / HZ; // 整数除法,得到整秒数
usec = timeout % HZ; // 取余数,得到剩余的jiffies数
usec = usec * (1000000/HZ); // 将jiffies转换为微秒
写回用户空间
put_user(sec, &tvp->tv_sec); // 写回秒数
put_user(usec, &tvp->tv_usec); // 写回微秒数
3.8. 信号处理和结果返回
if (ret < 0)goto out;
if (!ret) { // 超时返回ret = -ERESTARTNOHAND;if (signal_pending(current)) // 检查是否有信号 pendinggoto 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;
错误情况分析
// ret < 0 表示do_select执行过程中发生错误
可能的错误码:
- -EBADF // 无效的文件描述符
- -EFAULT // 内存访问错误
- -ENOMEM // 内存分配失败
- -EINTR // 被信号中断(在do_select内部处理)
处理方式:直接跳转到 out
标签进行资源清理并返回错误码
超时返回的特殊处理
if (!ret) { // 超时返回ret = -ERESTARTNOHAND;if (signal_pending(current)) // 检查是否有信号 pendinggoto out;ret = 0;
}
步骤1:预设信号重启标志
ret = -ERESTARTNOHAND;
步骤2:检查是否有待处理信号
if (signal_pending(current))goto out;
signal_pending(current) 作用:
- 检查当前进程是否有未处理的信号
- 如果有信号,保持
ret = -ERESTARTNOHAND
并跳转 - 如果没有信号,继续执行
步骤3:纯超时情况
ret = 0; // 没有信号,纯超时返回
结果拷贝回用户空间
3.9. 资源清理
out:select_bits_free(bits, size); // 释放位图内存
out_nofds:return ret;
资源清理并返回错误码
4. 完整执行流程图
二、do_select
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(¤t->files->file_lock);retval = max_select_fd(n, fds);spin_unlock(¤t->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. 函数原型和初始化
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. 计算最大有效文件描述符
spin_lock(¤t->files->file_lock);
retval = max_select_fd(n, fds);
spin_unlock(¤t->files->file_lock);if (retval < 0)return retval;
n = retval;
作用:确定实际需要检查的文件描述符范围
- 使用自旋锁保护文件描述符表
max_select_fd
找到位图中设置的最高文件描述符- 减少不必要的遍历
3. 初始化轮询结构
poll_initwait(&table);
wait = &table.pt;
if (!__timeout)wait = NULL;
retval = 0;
关键设置:
poll_initwait(&table)
:初始化等待队列,设置回调函数为__pollwait
- 如果超时为0(非阻塞),设置
wait = NULL
,不注册等待队列
4. 主轮询循环
for (;;) {unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;set_current_state(TASK_INTERRUPTIBLE);
4.1. 设置进程状态
set_current_state(TASK_INTERRUPTIBLE);
- 将进程状态设置为可中断睡眠
- 为调用
schedule_timeout()
做准备
4.2. 获取位图指针
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字遍历
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. 内层循环:按位遍历
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. 文件操作和轮询检查
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);
关键调用:
mask = (*f_op->poll)(file, retval ? NULL : wait);
- 调用设备驱动的
poll
方法 - 如果已经有就绪的
fd
(retval > 0
),传递wait = NULL
,避免重复注册
8. 事件检查和结果记录
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++;
}
事件集合定义:
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)
9. 保存结果到输出位图
if (res_in)*rinp = res_in;
if (res_out)*routp = res_out;
if (res_ex)*rexp = res_ex;
结果回填:
- 只设置实际就绪的位
- 保持其他位为0
10. 退出条件检查
wait = NULL;
if (retval || !__timeout || signal_pending(current))break;
if(table.error) {retval = table.error;break;
}
__timeout = schedule_timeout(__timeout);
退出条件:
retval
:有就绪的文件描述符!__timeout
:超时时间为0signal_pending(current)
:有信号等待处理table.error
:轮询过程中发生错误
__timeout = schedule_timeout(__timeout);
- 让出CPU,进入睡眠状态
- 返回剩余的超时时间
- 会被设备唤醒或超时唤醒
12. 清理和返回
__set_current_state(TASK_RUNNING);
poll_freewait(&table);*timeout = __timeout;
return retval;
资源清理:
- 恢复进程状态
- 释放等待队列资源
- 返回剩余超时时间和就绪
fd
数量
完整执行流程图
三、max_select_fd
实际需要检查的最大文件描述符
#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. 宏定义和函数原型
#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字
/* 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的掩码
// ~0UL 表示对0按位取反,得到全1的unsigned long
32位系统: ~0UL = 0xFFFFFFFF
步骤2:n & (__NFDBITS-1)
- 计算在最后一个字中的位数
// __NFDBITS-1 创建掩码,用于取模运算
32位系统: __NFDBITS=32, __NFDBITS-1=31 (0x1F)// n & (__NFDBITS-1) 等价于 n % __NFDBITS
步骤3:~0UL << (n & (__NFDBITS-1))
- 左移并取反
// 先左移,然后取反得到有效的掩码
字索引计算
n /= __NFDBITS;
打开文件描述符位图定位
open_fds = current->files->open_fds->fds_bits+n;
// open_fds 指向最后一个需要检查的long字
// 由于我们从后往前遍历,所以先处理最后一个字
3. 检查最后一个不完整的long字
if (set) {set &= BITS(fds, n);if (set) {if (!(set & ~*open_fds))goto get_max;return -EBADF;}
}
有效性检查:
set &= BITS(fds, n)
:只保留用户关心的位set & ~*open_fds
:检查是否有未打开的文件描述符- 如果所有关心的
fd
都是打开的,跳转到计算最大值 - 如果有未打开的
fd
,返回-EBADF
(Bad file descriptor)
检查逻辑示例:
用户关心的位: 00000111 (关注fd 32,33,34)
进程打开的fd: 00000101 (只有fd 32,34打开)
未打开的fd: 00000010 (fd 33未打开) → 返回 -EBADF
4. 反向遍历完整的long字
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. 计算最大文件描述符
get_max:do {max++;set >>= 1;} while (set);max += n * __NFDBITS;
}
最大值计算:
do-while
循环:计算当前long字中最高位的设置位置max += n * __NFDBITS
:加上基础偏移量
四、测试select
#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.编译执行验证
编译
gcc select_test.c -o select_test
执行
./select_test
验证,预期输入输出
请输入文字,程序将在5秒超时内等待输入...等待输入(5秒超时)...test
检测到输入!
读取到 5 字节: test等待输入(5秒超时)...超时!没有检测到输入。等待输入(5秒超时)...