Linux-5.0.1源码
1. 函数调用过程
arch/x86/entry/entry_64_compat.S:257: entry_SYSCALL_compat()
arch/x86/entry/common.c:397: do_fast_syscall_32()
arch/x86/entry/common.c:326: do_syscall_32_irqs_on()
fs/select.c:1295: __ia32_compat_sys_select()
fs/select.c:1295: __se_compat_sys_select()
fs/select.c:1299: __do_compat_sys_select()
fs/select.c:1273: do_compat_select()
fs/select.c:1320: compat_core_sys_select()
fs/select.c:526: do_select() // 核心代码
2. select()函数原型
int select(
int nfsd, /* 最大的文件描述符 */
fd_set* readfds, /* 读事件集合 */
fd_set* writefds, /* 写事件集合 */
fd_set* exceptfds, /* 异常事件集合 */
struct timeval* timeout /* 定时时间 */
);
3. 源码剖析
1. do_compat_select()
该函数所做的事情:
-
如果设置了定时时间,则拷贝到内核空间中的变量中
-
调用compat_core_sys_select()函数,进行下一步处理
-
在上述函数调用完,将定时剩余的时间复制回用户空间
2. compat_core_sys_select()
该函数所做的事情:
-
将用户空间的fd_set(输入、输出、异常)的集合复制到内核空间中
-
然后调用do_select(),该函数执行select主要逻辑
-
在上述函数调用完,将结果复制回用户空间中的变量
3. do_select()
该函数所做的事情:
-
初始化poll_wqueues
-
死循环,[0, nfds), 从0开始到nfds.遍历其中每个文件描述符是否有事件,并且判断该文件描述符是否是用户感兴趣.
如果感兴趣,则放到结果集合,并递增返回值(发生的事件数量)
-
如果有事件发生或超时,又或者收到信号,则退出死循环.
-
如果没有事件发生,有定时则定时睡眠或忙等待,等待fd设备唤醒自己.然后再继续遍历看感兴趣的fd是否有事件发生.
4. 函数细节
4.1 do_compat_select()
static int do_compat_select(int n,
compat_ulong_t __user *inp, /* 用户空间的读事件集合 */
compat_ulong_t __user *outp, /* 用户空间的写事件集合 */
compat_ulong_t __user *exp, /* 用户空间的异常事件集合 */
struct old_timeval32 __user *tvp /* 用户空间的定时 */)
{
struct timespec64 end_time, *to = NULL;
struct old_timeval32 tv;
int ret;
/*
1. 永久等待 tvp = nullptr
2. 不等待 tvp->tv_sec = 0 && tvp->tv_usec = 0
3. 等待指定时间 tvp->tv_sec != 0 || tvp->tv_usec != 0
*/
if (tvp) // 如果设置了超时时间
{
// 将用户空间的tv变量拷贝到内核空间中的tvp变量(防止用户将其改变)
if (copy_from_user(&tv, tvp, sizeof(tv)))
return -EFAULT;
to = &end_time;
// 设置超时时间
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
return -EINVAL;
}
// 主线,处理位图并进行select
ret = compat_core_sys_select(n, inp, outp, exp, to);
// 将定时剩余的时间复制回用户空间中
ret = poll_select_copy_remaining(&end_time, tvp, PT_OLD_TIMEVAL, ret);
return ret;
}
4.2 poll_select_copy_remaining()
static int poll_select_copy_remaining(
struct timespec64 *end_time, /* 内核空间变量,用于存储剩余的定时时间 */
void __user *p, /* 用户空间的定时时间 */
enum poll_time_type pt_type, /* */
int ret)
{
struct timespec64 rts;
if (!p)
return ret;
if (current->personality & STICKY_TIMEOUTS)
goto sticky;
// 如果没有设置超时时间,则返回
if (!end_time->tv_sec && !end_time->tv_nsec)
return ret;
ktime_get_ts64(&rts); // 获取当前时间
rts = timespec64_sub(*end_time, rts); // 计算定时剩余时间
if (rts.tv_sec < 0)
rts.tv_sec = rts.tv_nsec = 0;
// 以下根据不同类型,将定时剩余时间复制到用户空间的定时时间变量中
switch (pt_type)
{
case PT_TIMEVAL:
{
struct timeval rtv;
if (sizeof(rtv) > sizeof(rtv.tv_sec) + sizeof(rtv.tv_usec))
memset(&rtv, 0, sizeof(rtv));
// 将剩余定时时间复制回用户空间
rtv.tv_sec = rts.tv_sec;
rtv.tv_usec = rts.tv_nsec / NSEC_PER_USEC;
if (!copy_to_user(p, &rtv, sizeof(rtv)))
return ret;
}
break;
case PT_OLD_TIMEVAL:
{
struct old_timeval32 rtv;
rtv.tv_sec = rts.tv_sec;
rtv.tv_usec = rts.tv_nsec / NSEC_PER_USEC;
if (!copy_to_user(p, &rtv, sizeof(rtv)))
return ret;
}
break;
case PT_TIMESPEC:
if (!put_timespec64(&rts, p))
return ret;
break;
case PT_OLD_TIMESPEC:
if (!put_old_timespec32(&rts, p))
return ret;
break;
default:
BUG();
}
sticky:
if (ret == -ERESTARTNOHAND)
ret = -EINTR;
return ret;
}
4.3 compat_core_sys_select()
函数原型
static int compat_core_sys_select(
int n, /* nfds,最大文件描述符数量 */
compat_ulong_t __user *inp, /* 用户空间的读文件集合 */
compat_ulong_t __user *outp, /* 用户空间的写文件集合 */
compat_ulong_t __user *exp, /* 用户空间的异常集合 */
struct timespec64 *end_time /* 终止时间 */)
函数内定义的变量
fd_set_bits fds; // 用于存储输入和输出的文件描述符集合
void *bits; // 指向分配的位图内存,用于默认位图内存不够情况下分配多余内存
int size, max_fds, ret = -EINVAL;
struct fdtable *fdt;// 文件描述符表的指针
/*
* 我们需要将用户空间传进来的inset、outset、exset拷贝到内核空间,并且
* 需要等容量的空间来存储结果集,之后会将结果集的内容写回到用户空间。
* 我们先在栈上分配一块缓冲区,用于缓存输入集以及结果集,如果缓存的
* 空间大小不够,那么再使用kmalloc()动态分配,优先使用栈缓存而不用动态
* 内存可以加快访问...
*/
long stack_fds[SELECT_STACK_ALLOC / sizeof(long)]; // 256 / 4 = 64
获取当前进程打开的文件描述符值是什么,如果参数nfds大于这个值那么则进行修正.
所以,nfds传入多大的值都无所谓,内部会进行修正.
if (n < 0)
goto out_nofds; // 则返回-EINVAL值
/* max_fds 可能会增加,因此请抓取一次以避免竞争 */
// 读临界区,确保读取数据是安全的
rcu_read_lock();
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds; // 获取当前进程的最大文件描述符数量
rcu_read_unlock();
// 根据当前文件描述符表能表示的最大文件描述符对输入参数n进行修正
if (n > max_fds)
n = max_fds;
判断使用栈空间的stack_fds数组是否空间足够,如果不够则使用堆内存.该数组也就是内核空间中的fd_set集合.将用户空间的fd_set集合复制到这个数组里面了
/*
* n个bits至少需要size个long才能装下(之后我们使用long表示bits段)
* 为了存储输入集与结果集,我们需要6*size个long的存储空间
* 如果我们在栈上分配的那个缓冲区够用,那么就用它;而如果空间
* 容纳不下的话,那么我们只好kmalloc()动态分配内存了...
*/
size = FDS_BYTES(n); // (((n) + 31) / 32) * 4.计算n个文件描述符集合所需的字节数
bits = stack_fds;
if (size > sizeof(stack_fds) / 6)
{
bits = kmalloc_array(6, size, GFP_KERNEL); // 分配6 * size内存空间
ret = -ENOMEM; // 设置返回值为 -ENOMEM
if (!bits) // 如果不能分配空间则返回 -ENOMEM
goto out_nofds;
}
// 初始化位图指针
/*
* 将数组划分为6份...
* inset、outset、exset
* res_inset、res_outset、res_exset
*/
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 = compat_get_fd_set(n, inp, fds.in)) ||
(ret = compat_get_fd_set(n, outp, fds.out)) ||
(ret = compat_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);
然后调用核心函数do_select(),做select的内部逻辑
// ↓---------------------------------------------↓
ret = do_select(n, &fds, end_time); // *** 执行select操作,传递集合和超时时间 ***
// ↑---------------------------------------------↑
判断do_select函数返回值并做相应处理.并将结果复制回用户空间变量中.如果有分配堆空间,则释放
if (ret < 0)
goto out;
if (!ret) // 返回值等于0
{
ret = -ERESTARTNOHAND; // 需要重新调用select()
if (signal_pending(current)) // 表示当前进程有挂起信号
goto out;
ret = 0; // 没有挂起信号则设置为0
}
// 将结果集合复制到用户空间的变量中
if (compat_set_fd_set(n, inp, fds.res_in) ||
compat_set_fd_set(n, outp, fds.res_out) ||
compat_set_fd_set(n, exp, fds.res_ex))
ret = -EFAULT;
out:
if (bits != stack_fds)
kfree(bits);
out_nofds:
return ret;
}
4.4 do_select(),接下来就是重中之重,这个函数内部细节理解了,select也就理解怎么做的了.
函数原型
static int do_select(
int n, /* nfds,最大文件描述符数量 */
fd_set_bits *fds, /* 感兴趣的事件集合 */
struct timespec64 *end_time /* 定时终止时间 */)
定义的变量
ktime_t expire, *to = NULL; // 记录超时时间
struct poll_wqueues table; // 用于管理等待队列
poll_table *wait; // 指向table.pt,用于传递到poll()函数
int retval, i, timed_out = 0; // timed_out:表示是否超时
u64 slack = 0; // 表示时间精度的宽限
// 表示是否启用了忙循环
__poll_t busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
unsigned long busy_start = 0; // 记录忙循环的开始时间
上述定义了table,该变量是个poll_wqueues结构体.作用是,如果要阻塞进程,将进程添加到描述符表标识的所有文件的等待队列中,以便任意一个文件可进行非阻塞IO操作时唤醒进程
设置当前进程的最大描述符值
rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
初始化poll_wqueues,计算时间精度的宽限
/*
* 注意:
* poll_table被封装在了poll_wqueues结构体中,以便之后向资源
* 注册监听的时候,能够用poll_table得到对应的poll_wqueues
*
* 初始化poll_wqueues
* 1. 初始化poll_wqueues中的poll_table:
* * 设置监听注册函数为__pollwait
* * 设置想要监听的事件为所有事件(没必要,之后会修改)
* 2. 设置polling_task指向当前进程PCB
* 重点:资源注册函数为__pollwait
*/
poll_initwait(&table); // 初始化poll_wqueues结构体
wait = &table.pt; // wait指向table.pt,设置等待队列指针
// 如果没有设置超时时间的秒或纳秒值
if (end_time && !end_time->tv_sec && !end_time->tv_nsec)
{
wait->_qproc = NULL; // 不设置_qproc,表示立即超时
timed_out = 1; // 为1表示超时, 0表示未超时
}
if (end_time && !timed_out) // 如果设置了超时时间,并且未超时
slack = select_estimate_accuracy(end_time); // 计算时间精度的宽限
retval = 0;
接下来就是主要逻辑,死循环判断感兴趣的文件描述符是否有想要的事件发生
for (;;)
{
// 结果: 输入、输出、异常集合; 输入、输出、异常集合
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
bool can_busy_loop = false; // 是否可以进入忙等待循环
// 将指针指向fds中的相应集合
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
// 遍历参数n次
for (i = 0; i < n; ++rinp, ++routp, ++rexp)
{
// 用于存储当前文件描述符集合位图和结果的变量
unsigned long in, out, ex, all_bits, bit = 1, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
__poll_t mask;
// 从输入、输出、异常集合中读取位图
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex; // 如果位图全部为0
if (all_bits == 0)
{
i += BITS_PER_LONG; // 跳过当前循环块
continue;
}
// 遍历位图
for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1)
{
struct fd f;
if (i >= n)
break; // 遍历到最大文件描述符集合数量则跳出循环
if (!(bit & all_bits))
continue; // 如果当前位不再all_bits中,则跳过该位
/* 通过current->files->fdt->fd[i]获得当前进程中描述符i
* 对应文件的struct file...同时,增加struct file的引用
* 计数,防止在获取struct file之后,它被异步删除...
*/
f = fdget(i); // 根据i作为文件描述符获取文件描述符信息
/* 因为没有rdlock加锁,因此当前进程中描述符i对应的文件可能已经
* 被异步关闭。这就是为什么需要判断file是否为空的原因...
*/
if (f.file) // f.file存储对象的有关信息
{
// 设置等待key(key被设置为POLLIN、POLLOUT、POLLEX,根据当前
wait_key_set(wait, in, out, bit, busy_flag);
// --------------------- 主要函数 ---------------------
/* 注意:mask = POLLIN | POLLOUT | POLLRDNORM | POLLWRNORM; */
mask = vfs_poll(f.file, wait); // 检查文件描述符上f.file上
// --------------------- 核心!!! ---------------------
fdput(f); // 释放文件描述符
// mask & POLLIN_SET: 是否设置了*POLLIN*标志,表示是否有*可
// in & bit: 并且输入集合是否设置了当前位对应的文件描述符
if ((mask & POLLIN_SET) && (in & bit)) // 条件成立表示发生
{
res_in |= bit; // 在*输入结果*集合中将当前位(对应的文
retval++; // 表示有一个文件描述符发生了*可读*事件
wait->_qproc = NULL; // 结束当前等待队列的处理
}
// mask & POLLOUT_SET: 是否设置了*POLLOUT*标志,表示是否有*
// out & bit: 并且输出集合是否设置了当前位对应的文件描述符
if ((mask & POLLOUT_SET) && (out & bit))
{
res_out |= bit; // 在*输出结果*集合中将当前位(对应的文
retval++; // 表示有一个文件描述符发生了*可写*事件
wait->_qproc = NULL;
}
// mask & POLLEX_SET: 是否设置了*POLLEX*标志,表示是否有*异
// ex & bit: 并且*异常集合*是否设置了当前位对应的文件描述符
if ((mask & POLLEX_SET) && (ex & bit))
{
res_ex |= bit;
retval++;
wait->_qproc = NULL;
}
if (retval) // 检查是否有事件发生
{
can_busy_loop = false; // 如果有则停止忙等待循环
busy_flag = 0;
}
else if (busy_flag & mask) // 如果设置了忙等待标志
can_busy_loop = true; // 则进入忙等待
}
}
// 更新结果位图,将输入、输出、异常复制到rinp、routp、rexp
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
cond_resched(); // 重新调度CPU,以防止长时间占用CPU
}
wait->_qproc = NULL;
// 如果有事件发生、超时或收到信号,则退出循环
if (retval || timed_out || signal_pending(current))
break;
if (table.error) // 如果等待表中有错误fan
{
retval = table.error; // 设置返回值并退出循环
break;
}
// 如果允许忙等待并且不需要重新调度
if (can_busy_loop && !need_resched())
{
if (!busy_start) // 如果忙等待尚未启动
{
// 则记录当前事件并继续循环
busy_start = busy_loop_current_time();
continue;
}
// 如果已经启动了忙等待,并且超时时间未到,则继续循环
if (!busy_loop_timeout(busy_start))
continue;
}
busy_flag = 0; // 以上条件不满足,则重置忙等待标志
// 如果是第一次循环,并且设置了超时时间
if (end_time && !to)
{
// 超时时间转换为ktime_t,并设置to指向到期值
expire = timespec64_to_ktime(*end_time);
to = &expire;
}
// 设置超时并调度等待队列
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
timed_out = 1;
}
poll_freewait(&table); // 释放等待队列资源
上面的逻辑是,从0开始到nfds遍历每个文件描述符.然后获取该文件描述符对应的文件信息.
然后判断用户是否读该文件描述符感兴趣,并且用户是否对该文件描述符发生的具体事件感兴趣(比如读、写、异常).如果都感兴趣,那么将结果集合相应位置为1.
遍历完,如果有事件发生、超时或收到信号,则退出循环
如果没有事件发生并且设置了超时事件,那么则睡眠用户设置的事件.或者忙等待,等待设备IO通过事件唤醒.
主要函数是调用vfs_poll
// --------------------- 主要函数 ---------------------
/* 注意:mask = POLLIN | POLLOUT | POLLRDNORM | POLLWRNORM; */
mask = vfs_poll(f.file, wait); // 检查文件描述符上f.file上的事件,返回掩码
// --------------------- 核心!!! ---------------------
static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
// 如果没有在驱动层注册相应的poll函数,则返回DEFAULT_POLLMASK
// 内核认为这个可能性不大,毕竟既然应用层有poll的需求,驱动层就应该提供相应的poll函数
if (unlikely(!file->f_op->poll))
return DEFAULT_POLLMASK; // 则返回默认的事件掩码DEFAULT_POLLMASK
// 调用驱动层的poll函数(一般被命名为xxx_poll),返回值就是xxx_poll的返回值
return file->f_op->poll(file, pt);
}
5. 总结
-
将用户空间的定时事件、fd_set集合都复制到了内核空间
-
从0到用户进程打开的最大文件描述符号,全部遍历一遍
-
获取文件描述符是否有事件发生,然后判断是否用户对这个文件描述符感兴趣,并且对这个事件也感兴趣.如果感兴趣则将结果集合相应位置为1
-
如果没有事件发生,睡眠用户设置的时间.如果没有设置,则忙等待,等待fd唤醒自己.
-
如果有事件则跳出循环体,并返回已发生的事件数量
这里耗费性能的地方有:
用户空间->内核空间 数据复制
内核空间->用户空间 数据复制
然后遍历无用的自己不感兴趣的文件描述符, 时间复杂度为 O(n)
6. IO多路复用
IO多路复用就是可以在单个线程或单个进程同时监视多个文件描述符. 复用的的核心在于将多个IO操作复用到一个或少量的线程或进程中进行管理.
7. IO多路复用和自己实现的相比有何好处
自己实现方式:
- 创建多个进程或线程来监听
- 非阻塞读写监听的轮询
- 异步IO与信号事件触发
7.1 IO复用带来的好处
-
实现复杂度
IO多路复用: 使用简单,只需要调用系统提供的API就可以实现了
自己实现: 实现复杂,需要处理进程或线程创建、管理、同步等问题.还要编写代码代码来处理文件描述符的非阻塞读写和轮询
-
性能和资源消耗
IO多路复用: 资源消耗相对较低,只需要内核来处理
自己实现: 进程或线程的创建和切换开销大,消耗更多的系统资源(内存或CPU事件).需要管理大量进程和线程,增加了系统的复杂性和资源消耗