一、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
),则:
(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. 文件描述符位图计算宏
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=33
,FDS_BITPERLONG=32
→33 + 31 = 64
c
#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. 内存分配函数
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;
}
- 逻辑
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 的情况
c
memset(fdset, 0, nr); // 如果用户传递NULL,清空位图
return 0;
- 作用
- 如果
ufdset
为NULL
,表示用户不关心某些文件描述符集合 - 此时将内核位图
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
:最大文件描述符+1inp/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; // 加上秒数
}
}
- 默认设置为
MAX_SCHEDULE_TIMEOUT
(表示无限等待) - 如果用户提供了超时参数(
tvp != NULL
),则:- 从用户空间读取
struct timeval
(包含tv_sec
和tv_usec
) - 验证时间值的合法性(非负)
- 将时间转换为内核的
jiffies
单位(系统时钟滴答数)
- 从用户空间读取
- 处理边界条件(如超时值过大)
-
MAX_SELECT_SECONDS
是内核定义的最大允许秒数(防止jiffies
溢出) -
如果
sec
超过此值,直接保持MAX_SCHEDULE_TIMEOUT
(无限等待)
c
ROUND_UP(usec, 1000000/HZ)
-
将微秒(
usec
)向上对齐到最近的jiffies
粒度 -
1000000/HZ
是1
个jiffies
对应的微秒数 -
sec * HZ
:将秒转换为jiffies
(HZ
是每秒时钟滴答数) -
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);
- 先拷贝读集合
fds.in
- 如果成功,拷贝写集合
fds.out
- 如果前两步成功,拷贝异常集合
fds.ex
- 任何一步失败立即跳转到错误处理
结果位图初始化
- 确保结果位图初始状态为全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(¤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. 函数原型和初始化
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(¤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. 初始化轮询结构
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
方法 - 如果已经有就绪的
fd
(retval > 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);
退出条件:
retval
:有就绪的文件描述符!__timeout
:超时时间为0signal_pending(current)
:有信号等待处理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;
}
}
有效性检查:
set &= BITS(fds, n)
:只保留用户关心的位set & ~*open_fds
:检查是否有未打开的文件描述符- 如果所有关心的
fd
都是打开的,跳转到计算最大值 - 如果有未打开的
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秒超时)...