I/O多路复用 select 源码解析

1. I/O多路复用介绍

       在介绍I/O多路复用之前,我们先看看一个最基本的socket client/server端是如何接收处理数据的:

       

       client: socket -> bind -> connect -> read/write -> close

       server: socket -> bind -> listen -> accept -> read/write -> close

       对于server端,这种方式逻辑简单实现容易,但是不足之处也比较明显,那就是只支持一个client的连接和数据收发。那如何支持多个client呢?很容易想到使用多线程,即每当accept一个新连接的时候,就创建一个新的线程,在新的线程里处理该连接对应socket的数据收发。但是多线程也有自己的问题,比如当client非常多时,系统需要创建和维护大量的线程,这对系统资源消耗比较大,同时对共享资源的访问需要各种加锁解锁等操作,影响系统效率。

       那有没有一种方法,只用一个线程就能支持多个client的数据收发交互呢?答案是肯定的,那就是I/O多路复用。I/O多路复用通过一种机制,使单线程可以监视多个文件描述符,一旦有一个或多个fd有事件发生就通知应用程序,然后进行相应fd的读写等操作。与多线程相比,I/O 多路复用的最大优势是系统开销小,不需要创建和管理大量的线程,提高系统效率。当前常见的多路复用机制有select, poll, epoll等,各个机制都有自己特点。本篇介绍select的实现机制,通过对源代码的解析,了解其实现机制,进而理解其优缺点,如为何select默认最大监听fd数量为1024个,为何每次调用select前需要重新注册fd监听事件和设置超时参数?带着这些疑问,我们开始下面的讲解。

 

2. 源码

a. 下载地址

        https://www.kernel.org/

        该网页上可以下载最新版本的linux kernel代码,本文以5.2.13为例讲解

b. 函数调用关系

        压缩包解压后,找到fs/select.c文件,该文件就是select函数和poll函数的代码实现,本篇只介绍select函数相关的实现

        select函数入口:

SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
		fd_set __user *, exp, struct timeval __user *, tvp)
{
	return kern_select(n, inp, outp, exp, tvp);
}

        其主要函数调用关系:select -> kern_select -> core_sys_select -> do_select

 

3. fd_set

a. 类型解析

      在开始select解析之前,首先介绍一个非常重要的数据结构fd_set,其定义在文件 include/linux/types.h:

typedef __kernel_fd_set		fd_set;

      我们转到源结构__kernel_fd_set,其定义在文件 include/uapi/linux/posix_types.h:

#undef __FD_SETSIZE
#define __FD_SETSIZE	1024

typedef struct {
	unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

...

#endif /* _LINUX_POSIX_TYPES_H */

       可以看到,fd_set本质是一个unsigned long型的数组,其数组长度与__FD_SETSIZE有关。这个宏定义有什么作用呢,这个值1024与select默认最大监听fd数量1024个有没有关系呢?

       我们知道,linux系统中一切皆是文件,如键盘,屏幕,串口,socket等等,每打开一个设备系统都会分配一个fd来指示该文件,其值从0开始。如进程启动的时候系统默认打开三个文件(0:stdin, 1:stdout, 2:stderr),因此我们可以直接从键盘读取输入数据或直接输出数据到屏幕,而不需要手动调用open()函数。当我们继续打开其他文件时,系统会为该文件分配新的fd,新的fd是该进程的文件描述符表中值最小的可用文件描述符,即每次分配的fd值自增++。Linux 中一个进程默认最多能打开1024(NR_OPEN_DEFAULT)个文件,因此默认fd取值范围为0-1023。

       select与fd又有什么关系呢?我们可以想到select函数定会有一个输入参数(后面可以看到有三个相关参数),用来保存需要监听的fd,那么这个参数是什么类型呢,如何能存储需要监听的多个fd呢?答案就是用1024个bit位来存储。由于fd范围是0-1023,我们可以以fd为索引,当需要监听该fd时,将对应索引的bit位置1,当不需要监听该fd时,将对应索引的bit位置0,这样就可以保存所有设置的监听描述符了。如果1024个bit位全部置1,表示最多可同时监听1024个fd,这就是为什么select默认最多只能监听1024个fd的原因了。那1024个bit以什么方式存在呢?为了后续处理效率,系统选用unsigned long型数组为载体来存储,即unsigned long fds[1024 / (8 * sizeof(long))]。如果我们将1024这个值用宏定义__FD_SETSIZE来指示,即为unsigned long fds[__FD_SETSIZE / (8 * sizeof(long))],是不是有些眼熟,这不就是最开始__kernel_fd_set/fd_set的定义嘛!由此我们知道,select就是使用unsigned long数组来存储监听的fd,数组中的每个unsigned long以及unsigned long 中的每个bit,都表示一个fd是否被监听。假设一个long 4个字节,则fd_set图示如下:

       上述过程我们发现有两个宏定义,__FD_SETSIZE 和 NR_OPEN_DEFAULT,那么这两个值与select有什么关系呢?NR_OPEN_DEFAULT 表示一个进程最多可以打开的文件个数,即约束了fd的取值为 0 - NR_OPEN_DEFAULT-1,而 __FD_SETSIZE 表示 select 用于存储fd所用的bit位个数。如果NR_OPEN_DEFAULT > __FD_SETSIZE, 则select可能无法监听所有打开的文件,即fd范围在 __FD_SETSIZE - NR_OPEN_DEFAULT之间的文件无法监听。如果NR_OPEN_DEFAULT < __FD_SETSIZE, 则select可以监听所有的文件,只是会有多余无用的bit位。两个宏定义默认值都为1024,如果修改需要重新编译系统,并且最好以sizeof(long)为单位进行修改,后面可以看到如果取值不合适,会影响select的执行效率。

b. FD_ZERO、FD_SET、FD_CLR、FD_ISSET

      了解了fd_set的结构和实现之后,再来看看跟它相关的几个常见的函数,主要就是对特定的bit位进行置1,置0操作,以及查询某个bit位当前是置1还是置0等:

void FD_ZERO(fd_set *fdset);          // fd_set中的所有bit位置0
void FD_SET(int fd, fd_set *fdset);   // fd_set中以文件描述符fd为索引的bit位置1
void FD_CLR(int fd, fd_set *fdset);   // fd_set中以文件描述符fd为索引的bit位置0
int FD_ISSET(int fd, fd_set *fdset);  // 查询fd_set中以fd值为索引的bit位是否置位,用于判断fd是否发生了事件

 

4. select 函数原型

       了解了fd_set的实现之后,我们就可以来看看select函数的原型声明了。其函数声明如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

a. nfds

       后面我们会分析到,select内部是通过循环判断fd_set中所有置1的bit位所对应的fd是否有事件发生,当最高置1的bit位判断完成后,循环就可以提前结束了,因为再往高位所有的bit位都置0了,也就是那些fd并没有被监听,因此可以提前退出循环提高效率。如下图所示:

       nfds参数即表示查询完所有置1的bit位所需要的循环次数,由于索引值就是fd的值,因此该参数取值通常为所有的监听描述符中最大值+1。当然直接取1024也没有问题,只是select内部可能会多做些无用的循环判断,稍稍影响些效率。

b. readfd/writefds/exceptfds

      三个监听文件描述符集合,可分别设置读/写/异常事件的监听描述符集合,如果所有fd均不需要监听某个时间则该集合可以设为NULL

c. timeout

      超时时间,通过s和us两个字段设置,它表示如果所有的fd都没有任何事件发生的时候,函数返回之前需要等待的时间。参数有三种取值对应不同的处理机制:

      1. NULL, 表示永远等待下去,直到有一个或多个fd发生了事件函数才会返回,返回值为所有fd发生的所有事件的个数之和

      2. 具体等待时间,即s和us至少一个不为0,表示当所有的fd都没有事件发生的时候,函数返回前需要等待的时间。在等待期间如果任何一个fd发生任何一个事件,则函数返回,返回值为所有fd发生的所有事件的个数之和;如果时间到了仍没有fd发生任何事件,则函数返回0

      3. 不等待,s和us均为0,即非阻塞方式,表示检查完所有监听的fd,不管有没有事件发生函数均立即返回。如果有事件发生,则返回值为所有fd发生的所有事件的个数之和;如果没有任何fd发生任何事件,则函数返回0

d. 返回值

       上面已经提到过了,返回值表示所有fd发生的所有事件个数之和。由于返回值仅仅是一个int值,因此select只能告诉我们总共有多少个事件发生了,但不能告诉我们是哪个fd发生了事件(可能是一个,多个,甚至全部),如果同一个fd发生了多个事件,也不能告诉我们具体发生的是哪些事件。所以当select返回后,用户只能通过FD_ISSET函数循环判断每个fd是否有事件发生,如果有事件发生再判断该fd发生的是可读、可写还是异常事件,然后再进行相应的读写操作。所以select具有O(n)的轮询复杂度,监听的fd越多循环时间就越长。后面当分析epoll的实现的时候,我们会发现epoll只返回发生事件的fd信息,用户可以直接进行读写操作,大大提高了效率

 

5. core_sys_select 解析

int core_sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timespec64 *end_time)
{
    ...
    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)]; // SELECT_STACK_ALLOC = 256
    size = FDS_BYTES(n); // n个fd,n个bit,使用long型数组存储,需要数组长度 size = (n+8*sizeof(long)-1)/8*(sizeof(long))
    bits = stack_fds;    // 内核预分配的long型数组空间,共256个字节,数组长度 256/4 = 64

    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; // 6个long型数组指针,分别用于保存输入和输出的in/out/ex描述符集合

    get_fd_set(n, inp, fds.in);
    get_fd_set(n, outp, fds.out);
    get_fd_set(n, exp, fds.ex);  // 用户监听的三组fd分别拷贝到内核态,每组拷贝size个long型长度

    zero_fd_set(n, fds.res_in);  // 清空用于保存结果的三组long型数组,每组清空size个long型长度
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);

    ret = do_select(n, &fds, end_time); // 循环查询所有fd的驱动poll函数,检查是否有可读可写事件,有则返回,无则进入睡眠等待超时或等待事件发生

    if (ret <= 0)
        return ret; // 返回0表示没有任何fd有事件发生,<0表示发生错误

    set_fd_set(n, inp, fds.res_in);
    set_fd_set(n, outp, fds.res_out);
    set_fd_set(n, exp, fds.res_ex); // 拷贝保存结果的三组long型数组数据到用户态

    return ret; // 返回所有fd发生的所有事件个数之和
}

a. FDS_BYTES(n)

      我们先来看宏定义FDS_BYTES(n),经过多层宏定义调用,最终为:

define FDS_BYTES(n) = (n+8*sizeof(long)-1)/8*(sizeof(long))

      即n个bit位由long型数组来存储所需要的数组长度。我们以long为4个字节来说明,那么1-32个bit位只需要长度为1的long型数组,如果有33个bit位,则就需要长度为2的long型数组了。因此如前所述,当我们在给select传入第一个参数nfds的时候,该参数取值最好为所有监听fd中最大值+1,让系统分配能存下nfds个bit位的最小long型数组长度,如果直接传入1024,这里就会多分配无用的数组长度,后续也会多做无用的循环判断

b. stack_fds

       stack_fds就是系统预先分配的long型数组空间,用于存储所有需要监听的fd,以及用于存储查询结果,返回读,写,异常事件的查询结果。即long型数组空间所需长度为6*FDS_BYTES(n),如果nfds取值最大1024,即进程所有打开的1024个fd全部进行可读可写和异常监听,那么此时FDS_BYTES(n) = (1024+8*sizeof(long)-1)/8*(sizeof(long)) = 32,即存储监听可读事件的fd需要长度为32的long型数组,加上监听可写和异常事件的fd,以及返回结果的存储,总共需要长度为 32*6 = 192的long型数组,而long型数组stack_fds默认长度为 256/4 = 64,因此当这个默认长度存储不下时,就需要对数组长度进行扩展,扩展为6*FDS_BYTES(n)。源代码如下:

...
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) 
{
    /* Not enough space in on-stack array; must use kmalloc */
    ...
    alloc_size = 6 * size;
    bits = kvmalloc(alloc_size, GFP_KERNEL);
}

c. 函数流程图

       当long型数组stack_fds空间足够了之后,通过6个间距为FDS_BYTES(n)的long型指针,标记6段长度为FDS_BYTES(n)的long型数组空间,用来存储所有输入和输出的fd。函数处理流程如下图:

  1.  通过get_fd_set函数将需要监听可读可写和异常事件的fd拷贝到三个输入long型数组空间fds->in/out/ex,通过zero_fd_set先将三个输出long型数组空间fds->res_in/out/ex清空,用于保存查询结果
  2. 通过do_select函数循环查询所有fd发生的事件,如果返回值<0表示内部发生异常,如果=0表示没有事件发生,如果>0表示共发生了多少个事件

  3. 将查询结果赋值到三个输出long型数组空间fds->res_in/out/ex

  4. 通过set_fd_set将查询结果从输出long型数组空间fds->res_in/out/ex拷贝回用户输入的fd_set,然后返回发生的事件个数,并作为select函数的最终返回值

       由第4步可知,select返回监听结果是复用输入的fd_set的,因此当调用完一次select函数后,需要重新置位需要监听的fd。否则假设我们要监听5个fd,结果第一次select返回发现其中只有2个fd有事件发生,这时另外3个fd标志位是被清零了,如果下次调用select之前不重新置位,那么这3个fd就不会再进入查询循环中,即便有事件发生,select函数再也不会返回这些fd的结果了。因此在调用完select之后,一定要将需要监听的fd重新置位,具体可参考后面的示例程序

 

6. do_select 解析

       通过core_sys_select函数的解析,我们发现真正完成查询事件发生结果的是do_select函数。该函数首先获取三个输入long型数组,获取需要查询的fd,查询完成后,再将结果写入到三个输出long型数组中。下面我们通过5种场景,分别分析各个场景的处理机制:

1. 有fd收到数据

2. 没有fd收到数据,超时时间设为0,即不等待

3. 没有fd收到数据,超时时间设为NULL,即永远等待直到有fd收到数据

4. 没有fd收到数据,超时时间设为具体值,定时结束之前有fd收到数据

5. 没有fd收到数据,超时时间设为具体值,定时结束之前没有fd收到数据

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
    ...
    int timed_out = 0; 
    u64 slack = 0;

    // 注册_qproc函数,用于查询事件发生结果时将当前进程加入读写等待队列
    poll_initwait(&table);
    
    // 我们有如下5种场景,下面分析一下分别会有什么样的处理
    // 1. 有fd收到数据
    // 2. 没有fd收到数据,超时时间设为0,即不等待
    // 3. 没有fd收到数据,超时时间设为NULL,即永远等待直到有fd收到数据
    // 4. 没有fd收到数据,超时时间设为具体值,定时结束之前有fd收到数据
    // 5. 没有fd收到数据,超时时间设为具体值,定时结束之前没有fd收到数据

    // 对于场景2,超时时间设为0,设置timed_out = 1,该标志位用于场景2退出for (;;)循环
    if (end_time && !end_time->tv_sec && !end_time->tv_nsec) 
        timed_out = 1;

    // 对于场景4和5,超时时间设为具体值,则将超时时间结构体转为U64整数,便于后续定时器处理
    if (end_time && !timed_out) 
        slack = select_estimate_accuracy(end_time);

    retval = 0; // 统计所有fd发生的所有事件个数之和,也是select函数的返回值
    for (;;)    // 不同场景有不同的机制来退出该死循环
    {
        // 6个long型指针inp/outp/exp/rinp/routp/rexp,分别指向stack_fds中用于输入和输出的in/out/ex文件描述符集合
        inp   = fds->in;
        outp  = fds->out;
        exp   = fds->ex;
        rinp  = fds->res_in;
        routp = fds->res_out;
        rexp  = fds->res_ex;

        // 循环遍历所有的n个bit位
        for (i = 0; i < n; ++rinp, ++routp, ++rexp)
        {
            unsigned long in, out, ex, all_bits, bit = 1;
            unsigned long res_in = 0, res_out = 0, res_ex = 0;

            // n个bit位以long型为单位进行多组(即32个为一组)循环遍历
            in = *inp++; out = *outp++; ex = *exp++;
            all_bits = in | out | ex;
            if (all_bits == 0) { // 该组的32(BITS_PER_LONG)个fd都不需要监听in/out/ex事件,则调过该组循环
                i += BITS_PER_LONG;
                continue;
            }

            // 循环遍历一个组的32个bit位
            for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1)
            {
                // 当遍历的是最后一组时,该组fd个数可能不到32个了,则遍历完最后一个fd就可以提前退出该组循环
                if (i >= n)
                    break;

                // 该bit位对应的fd没有注册监听in/out/ex事件,则跳过该bit位循环
                if (!(bit & all_bits))
                    continue;

                f = fdget(i); // 通过fd值获取file结构

                // vfs_poll调用驱动程序 file->f_op->poll(file, pt),查询该fd发生的事件,驱动函数poll功能:
                // 1. 返回该fd发生的in/out/ex事件,用mask标识发生的事件
                // 2. 将当前进程加入到该fd的读写等待队列,当进程唤醒后再从等待队列中移除该进程相关的节点
                mask = vfs_poll(f.file, wait);

                // 当fd发生了in/out/ex事件,且用户设置监听该事件,则保存查询结果:
                // 1. 保存发生事件的fd值
                // 2. retval++,即所有fd发生的所有事件个数之和,即如果同一个fd发生2个事件,统计结果是+2而不是+1,这就是select返回值的意义
                if ((mask & POLLIN_SET) && (in & bit)) {
                    res_in |= bit;
                    retval++;
                    wait->_qproc = NULL;
                }
                if ((mask & POLLOUT_SET) && (out & bit)) {
                    res_out |= bit;
                    retval++;
                    wait->_qproc = NULL;
                }
                if ((mask & POLLEX_SET) && (ex & bit)) {
                    res_ex |= bit;
                    retval++;
                    wait->_qproc = NULL;
                }
            }

            // 循环遍历完该组的32个fd后,将发生事件的fd分别写到用于保存in/out/ex结果的三组long型数组中
            if (res_in)
                *rinp = res_in;
            if (res_out)
                *routp = res_out;
            if (res_ex)
                *rexp = res_ex;
        }

        // 场景1,有事件发生,retval不为0,退出for (;;)循环,函数返回事件发生个数之和
        // 场景2,没有事件发生,retval为0,但因为timed_out已被设为1,因此退出for (;;)循环,函数返回事件发生个数之和,即为0
        if (retval || timed_out || signal_pending(current)) 
            break;

        // 场景3,4,5,没有事件发生,则需等待
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
        {
                // 设置当前进程为睡眠状态
                set_current_state(TASK_INTERRUPTIBLE);

                // 场景3,没有事件发生,永远等待直到有任何fd收到数据
                if (!expires)
                    // 该进程移除运行队列,同时主动释放CPU控制权让调度器调度执行另一个进程
                    // 该进程将不再被调度,直到有fd收到数据时其驱动程序将该进程唤醒
                    schedule(); 
                    return -EINTR;

                // 场景4和5,没有事件发生,开启定时器等待特定时间
                // 设置定时时间
                hrtimer_set_expires_range_ns(&t.timer, *expires, delta);

                // 初始化定时器
                hrtimer_init_sleeper(&t, current);
                    t->timer.function = hrtimer_wakeup; // 设置超时回调函数hrtimer_wakeup
                    t->task = current; // 场景4,设置t->task指向当前进程,用于poll_schedule_timeout判断进程唤醒是因为有fd收到数据
                        // 如果直到定时器超时都没有收到数据,则触发超时回调函数
                        static enum hrtimer_restart hrtimer_wakeup(struct hrtimer *timer)
                            t->task = NULL; // 场景5,设置t->task为空,用于poll_schedule_timeout判断进程唤醒是因为超时
                            wake_up_process(task); // 唤醒当前进程

                // 启动定时器
                hrtimer_start_expires(&t.timer, mode);

                // 该进程移除运行队列,同时主动释放CPU控制权让调度器调度执行另一个进程
                // 该进程将不再被调度,直到有fd收到数据或者定时器超时将该进程唤醒
                schedule();

                set_current_state(TASK_RUNNING); // 进程被驱动程序或超时回调函数唤醒,退出睡眠状态

                // 根据t->task值判断进程唤醒是因为超时还是因为有fd收到数据
                // 场景4,超时前有fd收到数据,不触发回调函数,t->task没有被置空,poll_schedule_timeout返回-EINTR
                // 场景5,一直没有fd收到数据,触发回调函数,t->task被置空,poll_schedule_timeout返回0
                return !t.task ? 0 : -EINTR; 

            // 根据poll_schedule_timeout返回值判断进程唤醒是否因为超时,如果是则设置timed_out = 1
            timed_out = 1;
        }

        /*
        至此,我们完成了第一次for (;;)循环
        1. 对于场景1,第一次for (;;)循环就能退出死循环,返回事件发生个数之和
        2. 对于场景2,第一次for (;;)循环就能退出死循环,返回事件发生个数之和,此时和为0
        3. 对于场景3,4,5,在第一次for (;;)循环结束后并不能退出死循环,而是接着进入第二次循环,我们来分析在第二次循环的处理:
            3.1. 场景3,fd收到了数据后进入第二次for (;;)循环,回到场景1,退出死循环返回事件发生个数之和
            3.2. 场景4,fd收到了数据后进入第二次for (;;)循环,回到场景1,退出死循环返回事件发生个数之和
            3.3. 场景5,超时后timed_out被置1,进入第二次for (;;)循环后,回到场景2,退出死循环返回事件发生个数之和,此时和为0
        因此,对于所有场景,经过一次或者两次for (;;)循环后,均能退出死循环,返回事件发生个数之和
        */
    }

    poll_freewait(&table); // 进程唤醒后,移除所有fd的读写等待队列中该进程相关的表项

    return retval; // 返回所有fd发生的所有事件的个数之和
}

       主要的处理逻辑在注释中已做说明,do_select主要处理逻辑就是:循环遍历所有的n个bit位对应的fd,通过调用其驱动函数poll获取该fd是否有事件发生:

       对于场景1,第一次for (;;)循环就能退出死循环,返回事件发生个数之和

       对于场景2,第一次for (;;)循环就能退出死循环,返回事件发生个数之和,此时和为0

       对于场景3,fd收到了数据后进入第二次for (;;)循环,回到场景1,退出死循环返回事件发生个数之和

       对于场景4,fd收到了数据后进入第二次for (;;)循环,回到场景1,退出死循环返回事件发生个数之和

       对于场景5,超时后timed_out被置1,进入第二次for (;;)循环后,回到场景2,退出死循环返回事件发生个数之和,此时和为0

 

7. 设备驱动函数poll示例

       在上一节中,我们知道do_select函数通过调用驱动函数poll来获取fd发生的事件,那么驱动函数poll是如何知道该fd上有没有事件发生呢,以及如何将进程添加到自己的读写等待队列的呢?我们来分析一个例子,该设备用一个环形队列作为输入输出缓存,当环形队列还有空间时,则设置可写事件,当队列有数据时,则设置可读事件:

static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
    unsigned int mask = 0;
    ...

    poll_wait(filp, &dev->inq,  wait); // 将当前进程加入fd读写等待队列
    poll_wait(filp, &dev->outq, wait);
    if (dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM;   // 发生可读事件
    if (spacefree(dev))
        mask |= POLLOUT | POLLWRNORM;  // 发生可写事件

    return mask; // 返回fd发生的事件
}

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && p->_qproc && wait_address)
        p->_qproc(filp, wait_address, p); // 调用注册的_qproc函数
}

void poll_initwait(struct poll_wqueues *pwq)
{
    init_poll_funcptr(&pwq->pt, __pollwait); // 注册_qproc函数
    pwq->polling_task = current; // 赋值当前进程,用于将当前进程加入fd读写等待队列
    ...
}

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
    ...
    add_wait_queue(wait_address, &entry->wait); // 将包含当前进程的节点添加到fd读写等待队列
}

        驱动函数调用poll_wait将当前进程加入fd读写等待队列,poll_wait调用poll_initwait函数中注册的__pollwait函数,而在__pollwait函数中,通过调用add_wait_queue函数才真正的将当前进程添加到fd读写等待队列

 

8. 进程唤醒

       我们思考一个问题,对于场景3和4,当进程处于睡眠状态的时候,如果这个时候任何一个fd上发生了事件,进程都将会唤醒,进而函数返回。这是如何实现的呢,为什么任何fd发生了事件都可以唤醒进程呢?

      上面分析驱动函数poll的功能的时候我们知道,在调用每个fd的poll函数的时候,会将当前进程加入到该fd的读写等待队列,当完成一次for (;;)循环之后,该进程就被加入到所有fd的读写等待队列了。当任意一个fd收到收据,都会唤醒自己读写等待队列里的所有进程,这就实现了当任何一个fd可读可写时就能立即唤醒进程的原理

       我们以一个串口设备为例,当硬件设备收到数据时,触发interrupt中断处理,经过一系列的处理,最终调用到receive_buf函数来唤醒进程:

serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push -> flush_to_ldisc -> disc->receive_buf

disc->receive_buf()
    if (waitqueue_active(&tty->read_wait))
        wake_up_interruptible(&tty->read_wait);

        在receive_buf函数中,首先判断是否有进程阻塞在自己的读写等待队列中,如果有则调用wake_up_interruptible函数唤醒这些进程。当进程唤醒后,select函数返回之前,通过调用poll_freewait()函数移除所有fd的读写等待队列中该进程相关的表项

 

9. select 示例程序

#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    fd_set rfds;
    struct timeval tv;
    int fd;

    mkfifo("test_fifo", 0666);
    fd = open("test_fifo", O_RDWR);
    while(1)
    {
        FD_SET(0, &rfds);   // 每次select调用之前都需要重新设置监听描述符和超时参数
        FD_SET(fd, &rfds);  
        tv.tv_sec = 3;
        tv.tv_usec = 0;

        int ret = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
        if (-1 == ret) // 异常
        {
            perror("select()");
        }
        else if (0 == ret) // 超时
        {
            printf("timeout\n");
        }
        else if (ret > 0) // 有fd收到数据
        {
            char buf[128] = {0};
            if (FD_ISSET(0, &rfds))  // 通过FD_ISSET判断是哪个fd发生了事件
            {
                read(0, buf, sizeof(buf));
                printf("stdin = %s\n", buf);
            }
            else if (FD_ISSET(fd, &rfds))
            { 
                read(fd, buf, sizeof(buf));
                printf("fifo = %s\n", buf);
            }
        }
    }
    return 0;
}

 

10. select 特点

优点

      a. select目前几乎在所有的平台上支持,有良好跨平台支持特性,在某些Unix系统上不支持poll()和epoll()

      b. select对于超时提供了更好的精度,微秒级,而poll是毫秒

缺点

       a. 每次调用 select,都需要把 fd 从用户态拷贝到内核态,然后在内核轮询遍历所有 fd,再把结果拷贝回用户态。返回后用户还需要轮询fd_set来知道哪些fd是有事件发生的,fd多的时候开销很大

       b. 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义重新编译内核的方式提升这一限制,但是这样也会造成效率的降低

       c. select返回结果时复用了用户注册监听事件的fd_set结构,因此每次调用select前需要重新注册监听事件。超时参数在返回时也是未定义的,每次调用select之前也需要重新设置超时参数

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值