select源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Function_Dou/article/details/80397791

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将剩余的时间返回.

猜你喜欢

转载自blog.csdn.net/Function_Dou/article/details/80397791