select源码分析
select()
函数是从SYSCALL_DEFINE5(select, ...)
开始. 可以简单的将SYSCALL_DEFINEx
理解为系统定义的系统函数, 如果想了解 SYSCALL_DEFINE 可以看一下.
具体的执行流程是 :
- 将时间定义从用户空间复制到内核空间中, 进行时间片的设置, 如果为0或不合法就设置为默认值, 否则就设置为传入的时间.
- 调用
core_sys_select
函数, 以实现等待消息到来, 轮询等主要操作 - 调用
timeval_compare
返回执行完剩余的时间 - 最后将返回的时间使用
copy_to_user
复制到用户空间
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, fd_set __user *, exp, struct timeval __user *, tvp)
{
s64 timeout = -1;
struct timeval tv;
int ret;
if (tvp)
{
// 将数据从用户空间拷贝到内核空间的tv中
if (copy_from_user(&tv, tvp, sizeof(tv)))
return -EFAULT;
...
}
// 设置 fds 结构的参数并且等待消息的到来, 或者时间片没有结束调度程序
ret = core_sys_select(n, inp, outp, exp, &timeout);
// 设置时间片
if (tvp)
{
...
// 返回执行完后剩余时间
if (timeval_compare(&rtv, &tv) >= 0)
rtv = tv;
if (copy_to_user(tvp, &rtv, sizeof(rtv)))
...
}
return ret;
}
core_sys_select
因为select的主要功能都是do_select
函数, 这里我们先分析一下关于core_sys_select函数.
- 获取文件文件描述符表并存放在
fdtable
中 - 为
fdtable
读, 写, 错误分配空间, 并初始化 - 将select传入的
readfds
,writefds
,errorfds
参数从用户空间复制到内核空间的fdtable
对应的读, 写, 错误中. 这里需要解释一下, fdselect主要是保存之后要将来的信号返回给用户空间的. - 调用
do_select
, 轮询等待消息的到来, 并且将消息的文件描述符保存在fds结构体中 - 将
fds
保存的读, 写, 错误集合从内核空间复制到用户空间
// 参数满足 int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout);
// typedef __kernel_fd_set fd_set
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,fd_set __user *exp, s64 *timeout)
{
fd_set_bits fds;
void *bits;
int ret, max_fds;
unsigned int size;
struct fdtable *fdt;
/*
#define FRONTEND_STACK_ALLOC 256
#define SELECT_STACK_ALLOC FRONTEND_STACK_ALLOC
*/
// 计算出来有32位, 所以数组的大小也定义的是32
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
// 文件描述符表
fdt = files_fdtable(current->files);
...
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
// 实现将数据用户空间复制到内核空间中, 这是是把数据从inp复制到内核的fds空间中
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;
// res初始化为0
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
// 调用do_select函数, 检验, 等待消息到来, 并且do_select函数会把到来消息的文件描述符存放在fds结构体中
ret = do_select(n, &fds, timeout);
...
// 实现将数据从内核空间复制到内核空间中
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;
...
return ret;
}
在这里可以看出来core_sys_select函数也是只是分配空间, 复制到内核中, 我们还没有看到阻塞等待消息的代码, 所以重要的还是在do_select
函数中. 可以看到在最后core_sys_select调用了do_select
函数, 等待消息的到来.
关于stack_fds
这个数组, 要存放的是6个位图, 分别对应用户态传入的存放监听读、写、异常三个操作的文件描述符集合,以及这三个操作在select执行过后需要返回的三个集合。这是 select 的机制,每次执行 select() 之后,函数把“就绪”的文件描述符留下,返回。下一次,再次执行 select() 时,需要重新把需要监听的文件描述符传入。
do_select
不过分析函数前, 先看看三个待会会用到的宏定义. POLLIN_SET
是检查输入消息, 同理POLLOUT_SET检查输出, POLLEX_SET检查错误.
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)
好了, 现在可以开始分析do_select源码.
- 获得系统能支持的最大文件描述符
- 调用
poll_initwait
函数设置回调消息到来时的回调函数 - 轮询, 不断的检查链表中有没有消息到来
- 没有消息就直接启动调度程序, 等待一定的时间片返回进程重复判断消息到来
- 有消息到来, 或者设置的时间到达后, 退出轮询, 判断消息的类型(输入, 输出, 还是错误), 并保存其消息的文件描述符
- 将进程设置为运行态, 清除集合, 返回
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
struct poll_wqueues table;
poll_table *wait;
int retval, i;
...
// 获取最大文件描述符
retval = max_select_fd(n, fds);
...
n = retval;
// 初始化等待队列, 并且设置回调函数, 有就绪文件描述符是就执行回调
poll_initwait(&table);
wait = &table.pt;
if (!*timeout)
wait = NULL;
retval = 0;
for (;;)
{
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
long __timeout;
// 进程设置为可中断的
set_current_state(TASK_INTERRUPTIBLE);
// select的三个参数, 读, 写, 错误. 以及三个返回参数.
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;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
// 确定该位有设置好等待返回的进程描述符, 如果没有参数继续自增, 进程等待, 有设置好的描述符, 就执行下一个for循环
all_bits = in | out | ex;
if (all_bits == 0)
{
i += __NFDBITS;
continue;
}
// 每次遍历一位, 也就是遍历一个文件描述符
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1)
{
int fput_needed;
// 是否超过最大设置的文件描述符
if (i >= n)
break;
// 判断是哪一位的有消息到来
if (!(bit & all_bits))
continuea;
// 从文件描述符中获取文件结构体
file = fget_light(i, &fput_needed);
if (file)
{
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
// 调用文件所对应的具体方法, 具体操作, 比如是POLLIN操作, 检测了文件是否就绪,而且还把当前进程加入等待队列,如果该文件描述符就绪,则会触发回调,以及唤醒该进程
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) // 与掩码mask相与, 判断是否是输入操作, 同时检测是否该文件描述符设置了输入操作
{
res_in |= bit; // 返回的res_op设置为POLLIN
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;
cond_resched();
}
wait = NULL;
// 如果时间到了或者有事件到来
if (retval || !*timeout || signal_pending(current))
break;
...
// 没有消息到来, 并且时间没有结束, 那么就进行调度程序, 等待时间片结束后返回到进程, 重新判断消息是否到来, 重复操作
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
// 将进程设置为运行态
__set_current_state(TASK_RUNNING);
// 释放页中的所有数据
// 所以我们在不断轮询的时侯, 每次都会重新为 fds 赋值, 因为每次操作完的时侯都会将传入的 fds 修改, 清除, 所以我们需要重复为 fds 恢复
poll_freewait(&table);
return retval;
}
这里也就可以看出select的效率, 时间复杂度居然是O(n3), 随着等待的文件描述符越来越多, 那么等待的时间也就越长, 而且等待的数据也是很有限, 对于一个服务器来说这是在太少了. 剩下的操作已经在注释中写的很清楚了. 希望对读者有用.
总结
我总结了一下函数的调用历程, 这样调理也就更加的清楚了.
select()函数首先是将参数时间调用copy_from_user将其从用户空间复制到内核空间中, 然后对时间片进行设置(如果时间<=0 或非法将设置为默认一直等待), 然后调用core_sys_select
函数, 分配一个fds
的数据结构来保存关于传入参数in, out, ex集合的数据. 接着就调用了函数do_select
, 先是在设置的时间段进行等待, 遍历所有传入的文件描述符, 如果有消息到来就直接返回给用户空间, 没有的消息, 就先进行进程调度, 同时设置一个回调时间, 在时间片结束后又回调到该进程, 继续判断消息的到来, 还是没有消息就重复调度, 直到有消息到来. 消息到来后, 先是遍历所有的消息, 确定是集合中的消息. 是文件描述符集合中的消息, 就保存该文件描述符, 退出轮询, 并且将文件集合清除, 只保留一个描述符, 然后将描述符返回到core_sys_select
, 然后该函数将返回的描述符从内核空间复制到用户空间, 通过FD_ISSET
来进行确认. 最后select将剩余的时间返回.