非阻塞访问原理——poll(select)

尽管等待队列可以实现阻塞执行,但是用户可以通过描述符属性O_NONBLOCK来明确指定不阻塞,所以对应的驱动程序也应该满足这一行为,该标志通过filp中的f_flags标志位O_NONBLOCK来指示。阻塞操作是缺省的,除非指定了O_NONBLOCK:
  • 如果一个进程调用 read 但是没有数据可用(尚未), 这个进程必须阻塞. 这个进程在有数据达到时被立刻唤醒, 并且那个数据被返回给调用者, 即便小于在给方法的 count 参数中请求的数量。
  • 如果一个进程调用 write 并且在缓冲中没有空间, 这个进程必须阻塞, 并且它必须在一个与用作 read 的不同的等待队列中。当一些数据被写入硬件设备, 并且在输出缓冲中的空间变空闲, 这个进程被唤醒并且写调用成功, 尽管数据可能只被部分写入如果在缓冲只没有空间给被请求的 count 字节。

这 2 句都假定有输入和输出缓冲; 实际上, 几乎每个设备驱动都有。要求有输入缓冲是为了避免丢失到达的数据, 当无人在读时。相反, 数据在写时不能丢失, 因为如果系统调用不能接收数据字节, 它们保留在用户空间缓冲。即便如此, 输出缓冲几乎一直有用, 这可以减少应为等待而造成的进程切换时间损失,可从硬件挤出更多的性能。

一个阻塞读取的例子如下,首先会测试代表资源的布尔量是否为真,如果不是则直接返回EAGAIN,当然这也意味着gen_rtc_irq_data会在其他事件(比如中断处理程序)中将被改变。对于write来说,如果可以承载写出数据的资源不可得,也应该直接返回EAGAIN。


 

点击(此处)折叠或打开

    drivers/char/genrtc.c
      if (file->f_flags& O_NONBLOCK &&!gen_rtc_irq_data)
              return -EAGAIN;
      retval = wait_event_interruptible(gen_rtc_wait,
                      (data = xchg(&gen_rtc_irq_data, 0)));


poll和select

阻塞的读写操作是当前进程不得不停下来,等待事件的触发,那么只要一个描述符进程就不得不停下来,这是非常糟糕的事情!使用非阻塞的I/O可以通过poll,select和epoll系统调用来同时监听多个描述符,也即描述符集。poll和select功能相同,由不同的开发版本引入,epoll在2.5.45内核版本中加入,它使被监听的文件描述符多达几千个。


 

点击(此处)折叠或打开

    #include <poll.h>
    int poll(struct pollfd*fds, nfds_t nfds,int timeout);
    /* 驱动中的poll原型*/
    unsigned int (*poll)(struct file *, struct poll_table_struct*);


 

驱动的poll方法提供了用户空间的侦听动作。它负责两方面:
  • 在一个或多个可指示查询状态变化的等待队列上调用 poll_wait。如果没有文件描述符可用作 I/O, 内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符.
  • 返回一个位掩码, 描述可能不必阻塞就立刻进行的操作。

一个整体的poll调用过程如下,其中括号中的函数为辅助函数:

点击(此处)折叠或打开

do_sys_poll->(poll_select_set_timeout)do_sys_poll->(poll_initwait)do_poll(poll_freewait)->do_pollfd->.poll->poll_wait


 

为了理清实现过程,从poll的系统调用开始追踪:


点击(此处)折叠或打开

    fs/select.c
    SYSCALL_DEFINE3(poll, struct pollfd __user*, ufds, unsignedint, nfds,
            long, timeout_msecs)
    {
        struct timespec end_time, *to =NULL;
        int ret;
        if (timeout_msecs>= 0){
            to =&end_time;
            poll_select_set_timeout(to, timeout_msecs/ MSEC_PER_SEC,
                NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
        }
        ret = do_sys_poll(ufds, nfds,to);
        if (ret== -EINTR) {
            struct restart_block *restart_block;
            ......
            ret = -ERESTART_RESTARTBLOCK;
        }
        return ret;
    }


  • poll_select_set_timeout根据当前时间计算超时时间,存放入一个 struct timespec结构体实例to中。显然如果没有设置时间timeout_msecs,to指向NULL。
  • do_sys_poll实现实际的查询功能。接下来将对它展开分析。
  • 如果do_sys_poll返回-EINTR,则意味着poll操作被信号打断,返回ERESTART_RESTARTBLOCK,由用户注册的信号如果设置了SA_RESTART,则可以在处理完用户注册的信号处理程序后,继续执行。
do_sys_poll中首先把用户空间的struct pollfd拷贝到用户空间的struct poll_list类型的链表中,这链表的头定义在栈空间,而其他成员则通过kmalloc在内核空间动态分配,这可以提高对该表的访问效率。接着该函数执行如下代码:

点击(此处)折叠或打开

    poll_initwait(&table);
        fdcount = do_poll(nfds, head,&table, end_time);
        poll_freewait(&table);


每次针对poll的系统调用都会初始化一个类型为poll_wqueues的挑选队列:

点击(此处)折叠或打开

    linux/poll.h
    struct poll_wqueues {
        poll_table pt;
        struct poll_table_page *table;
        struct task_struct *polling_task;
        int triggered;
        int error;
        int inline_index;
        struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
    };


poll_initwait用于初始化该挑选队列,其中的polling_task成员被赋值为当前进程的task_struct:也即current。

点击(此处)折叠或打开

    void poll_initwait(struct poll_wqueues*pwq)
    {
        init_poll_funcptr(&pwq->pt, __pollwait);
        pwq->polling_task= current;
        pwq->triggered= 0;
        pwq->error= 0;
        pwq->table= NULL;
        pwq->inline_index= 0;
    }


接着调用do_poll,其中参数nfds为用户传入的整数,代表传入的pollfd的数量,而head即为拷贝后的poll_list链表,table是挑选队列,而end_time就是超时时间。do_poll对poll_list链表进行循环处理处理,对于单个fd,则调用do_pollfd进行处理。另外注意到在一次遍历之后一旦返现do_pollfd的返回值不为0,则说明该描述符可操作,计入count,如果count不为0或者超时则直接跳出循环,并返回活跃描述符的计数。

点击(此处)折叠或打开

    for (walk = list; walk!= NULL; walk = walk->next){
                struct pollfd * pfd,* pfd_end;
                pfd = walk->entries;
                pfd_end = pfd + walk->len;
                for (; pfd != pfd_end; pfd++){                
                    if (do_pollfd(pfd, pt)){
                        count++;
                        pt = NULL;
                    }
                }
            }
            ......
            pt = NULL;
            if (!count){
                count = wait->error;
                if (signal_pending(current))
                    count = -EINTR;
            }
            if (count|| timed_out)
                break;    
            ......    
            if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE,to, slack))
                timed_out = 1;


do_pollfd调用驱动提供的poll函数,如果没有则永远返回0。poll 返回位掩码, 它描述哪个操作可马上被实现;例如, 如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态。

点击(此处)折叠或打开

    static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
    {
        unsigned int mask;
        int fd;
        mask = 0;
        fd = pollfd->fd;
        if (fd>= 0){
            int fput_needed;
            struct file * file;
            file = fget_light(fd,&fput_needed);
            mask = POLLNVAL;
            if (file!= NULL) {
                mask = DEFAULT_POLLMASK;
                if (file->f_op&& file->f_op->poll){
                    if (pwait)
                        pwait->key= pollfd->events|
                                POLLERR | POLLHUP;
                    mask = file->f_op->poll(file, pwait);
                }
                /* Mask out unneeded events.*/
                mask &= pollfd->events| POLLERR | POLLHUP;
                fput_light(file, fput_needed);
            }
        }
        pollfd->revents= mask;
        return mask;
    }


可以惊奇的发现pwait参数就是挑选队列中的pt成员的指针,所以驱动的poll要实现加入设备等待队列的功能。poll_table结构, 作为poll 方法的第2个参数, 最终在内核中用来实现poll, select, 和 epoll 调用。驱动编写者不必要知道所有它的内容并且必须作为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它,并且可改变 poll 操作状态。驱动通过调用函数 poll_wait增加一个等待队列到 poll_table 结构:

点击(此处)折叠或打开

    poll_wait(filp,&wtqueue, wait);
    if (bool)
        return |= POLLIN| POLLRDNORM;/* readable*/


poll_wait实际上就是调用__pollwait添加poll_table成员到wtqueue。

点击(此处)折叠或打开

    static inline void poll_wait(struct file* filp, wait_queue_head_t* wait_address, poll_table*p)
    {
        if (p&& wait_address)
            p->qproc(filp, wait_address, p);
    }
    /* Add a new entry*/
    static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                    poll_table *p)
    {
        struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
        struct poll_table_entry *entry = poll_get_entry(pwq);
        if (!entry)
            return;
        get_file(filp);
        entry->filp= filp;
        entry->wait_address= wait_address;
        entry->key= p->key;
        init_waitqueue_func_entry(&entry->wait, pollwake);
        entry->wait.private= pwq;
        add_wait_queue(wait_address,&entry->wait);
    }


  • poll_get_entry用来分配struct poll_table_page结构,它的大小是一个页面,这表明它的尾部包含多个struct poll_table_entry结构体。如果已经分配过,并且POLL_TABLE_FULL判断poll_table_entry结构体未完全使用,则直接返回该表中的下一个poll_table_entry结构体指针。
  • 接下来初始化一个poll_table_entry,其中的wait_address成员总是指向等待队列头。
  • init_waitqueue_func_entry用来填充poll_table_entry中的等待队列成员,其中的唤醒函数被赋值为pollwake,而私有数据赋值为poll_wqueues,它是挑选队列的头部地址。
  • add_wait_queue将等待队列成员放入等待队列。

在经过__pollwait的一系列处理后,整个结构图看起来如下所示:

那么进程何时被休眠呢?注意do_poll中的poll_schedule_timeout函数,注意到它的第二个参数被设置为了TASK_INTERRUPTIBLE,它总是让进程休眠到超时(如果设置了超时)为止,并可被中断:


 

点击(此处)折叠或打开

    fs/select.c
    int poll_schedule_timeout(struct poll_wqueues*pwq, int state,
                 ktime_t *expires, unsigned long slack)
    {
        int rc =-EINTR;
        set_current_state(state);
        if (!pwq->triggered)
            rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
        __set_current_state(TASK_RUNNING);
        set_mb(pwq->triggered, 0);
        return rc;
    }


该函数首先设置当前进程的状态为TASK_INTERRUPTIBLE,然后通过schedule_hrtimeout_range进醒休眠,直到被唤醒或者超时,它的状态位被改为TASK_RUNNING继续运行。

所以通常应该在open中初始化等待队列,而在poll中添加进程到等待队列,而其他事件的处理程序(比如中断处理)中唤醒进程,这样poll就完成它的整个过程,select的过程与此类似
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值