I/O多路复用(一)select与poll的内核实现简述

select与poll是比较常用的多路IO复用,在处理高并发情况下使用的更多的是epoll。这篇文章主要介绍select与poll的内核实现,而epoll将在下一次的文章中介绍。

一、先谈谈文件操作函数与IO多路复用的驱动实现

1、文件操作函数

我们知道在linux(unix)系统中,一切皆是文件。而在linux环境编程中,我们要读写或操作一个文件时,我们首先需要通过open函数打开该文件获取到文件的描述符,文件描述符是一个无符号整形的数值。无论我们是打开磁盘上的文件,亦或者是打开设备还是网络套接字,最终返回给我们的都是一个文件描述符,这也恰是我喜爱linux的原因之一。

当我们使用open函数时,返回给我们的那个文件描述符其实只是一个索引,这个索引指向当前进程拥有的文件指针数组。在内核中,进程往往使用task_struct数据结构表示,其中的成员files就是当前进程打开文件信息,files中的fd_array就是打开的文件指针数组,而open函数返回给我们的就是指向该数组中我们打开文件的那个指针。

QQ截图20180228111943.png

QQ截图20180228111943.png

打开的文件在内存中被抽象成file结构,而对于文件的读写等操作实现将会调用该结构中的file_operations成员,file_operations是由多个成员是函数指针的结构体,其中用户空间的read最终将会调用它的read函数,write将会调用它的write函数,等等,这里我们需要着重注意的是其中的poll函数。具体内容如下。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*open) (struct inode *, struct file *);
        ...
}

我们想在用户空间对一个文件使用多路I/O复用,那么我们需要实现该文件驱动的poll函数。对于一般的磁盘文件而言,这些函数都已在文件系统驱动中实现,而对于自己定义的设备,我们需要自己实现poll函数。

2、I/O多路复用的驱动实现

poll函数的原型如下,无论是调用selectpoll还是epoll,最终都会调用该函数。

unsigned int (*poll) (struct file *filp, poll_table *wait);

其中filp是当前文件指针,而wait是内核中处理poll等待队列的结构。要实现poll函数,我们只要先声明当前设备的读写等状态的等待队列,然后将该等待队列和filpwait传入到下面这个函数中即可,当设备状态改变时可唤醒对应的等待队列。具体内核中如何实现后文将会介绍。

void poll_wait (struct file *, wait_queue_head_t *, poll_table *);

二、select的内核实现

select函数的实现在内核源码select.c文件中。其中当我们调用select函数时,首先调用的是系统调用SYSCALL_DEFINE5函数,而该函数调用的是core_sys_select函数。

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

    ret = core_sys_select(n, inp, outp, exp, to);
    return ret;
}

core_sys_select中,将select传入的文件描述符集合统一存入了fds变量中,然后调用do_select函数。

int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timespec *end_time)
{
    fd_set_bits 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;

    zero_fd_set(n, fds.res_in);
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);

    ret = do_select(n, &fds, end_time);

    return ret;
}

do_select函数中,是这些描述符处理的地方。这里先使用poll_initwait主要是对上文的poll_wait函数进行赋值,然后遍历每一个描述符,调用文件驱动中的poll函数。

int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{

    /*这个函数时初始化poll_wait函数的*/
    poll_initwait(&table);

    /*遍历每一个描述符*/
    for (;;) {  
        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
            for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {

                struct fd f;
                f = fdget(i);

                if (f.file) {

                    /*调用驱动中实现的poll函数*/
                    f_op = f.file->f_op;
                    mask = DEFAULT_POLLMASK;
                    if (f_op->poll) {
                        mask = (*f_op->poll)(f.file, wait);
                    }

                    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;
                    }
                    /* got something, stop busy polling */
                    if (retval) {
                        can_busy_loop = false;
                        busy_flag = 0;

                    } else if (busy_flag & mask)
                        can_busy_loop = true;
                }

            }
        }
        wait->_qproc = NULL;

        /*如果有一个可读就跳出循环*/
        if (retval || timed_out || signal_pending(current))
            break;

        /*设置定时器睡眠*/
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
            timed_out = 1;
    }

    poll_freewait(&table);

    return retval;
}

poll_initwait函数主要是将其中的pwq->pt赋值为__pollwait,而pwq->pt我们发现在poll_wait函数中被使用,即poll_wait最终调用的就是__pollwait

至此我们可以看到,在我们调用select函数时,最终调用的是设备驱动中的poll函数。poll函数中为设备的读写等状态分别声明了等待队列,并将等待队列传递给了__pollwait函数,而__pollwait函数把当前进程加入等待队列中,然后就去睡眠了。当超时或者被唤醒时便会查看是否有描述符状态发生改变。

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;
}

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);
}

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;
    entry->filp = get_file(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的内核实现

poll系统调用的实现与select相似。首先poll函数直接调用的是SYSCALL_DEFINE3函数,在改函数中直接调用的是do_sys_poll函数。

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
        int, timeout_msecs)
{
    ret = do_sys_poll(ufds, nfds, to);
    return ret;
}

do_sys_poll可分为三个部分,第一个部分是将用户空间传来的描述符集合拷贝到内核空间,第二部分与select函数类似给poll_wait函数赋值,最后调用do_poll函数对描述符集合进行操作。

int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
        struct timespec *end_time)
{

/*1.将描述符添加到工作链表中,这里的工作链表的第一个节点为head,内存较小,如果head不够存储才会继续申请大块的内存*/
    /*ken: POLL_STACK_ALLOC=256*/
    long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
    struct poll_list *const head = (struct poll_list *)stack_pps;
    struct poll_list *walk = head;
    unsigned long todo = nfds;

    len = min_t(unsigned int, nfds, N_STACK_PPS);
    for (;;) {
        walk->next = NULL;
        walk->len = len;
        if (!len)
            break;

        /*将用户空间中的描述符一部分拷贝到工作链表结点中,循环拷贝,直到拷贝完为止*/
        if (copy_from_user(walk->entries, ufds + nfds-todo,
                    sizeof(struct pollfd) * walk->len))
            goto out_fds;

        todo -= walk->len;
        if (!todo)
            break;

        /*如果剩余的描述符需要的空间比POLLFD_PER_PAGE小则申请需要的空间即可,否则申请POLLFD_PER_PAGE*/
        len = min(todo, POLLFD_PER_PAGE);
        size = sizeof(struct poll_list) + sizeof(struct pollfd) * len;
        walk = walk->next = kmalloc(size, GFP_KERNEL);
        if (!walk) {
            err = -ENOMEM;
            goto out_fds;
        }
    }

/*2.初始化poll_wait函数*/
    poll_initwait(&table);

/*3.调用do_poll函数*/
    fdcount = do_poll(nfds, head, &table, end_time);

    return err;
}

do_poll函数比较简单,遍历工作链表中的描述符并调用do_pollfd函数,do_pollfd函数最终调用了驱动中的poll函数将改进程加入到该设备的等待队列中,其余操作与select类似。

static int do_poll(unsigned int nfds,  struct poll_list *list,
           struct poll_wqueues *wait, struct timespec *end_time)
{
    for (;;) {

        /*遍历所有的文件描述符*/
        for (walk = list; walk != NULL; walk = walk->next) {
            pfd = walk->entries;
            pfd_end = pfd + walk->len;
            for (; pfd != pfd_end; pfd++) {

                /*这里需要注意,当返回有效描述符时,就把pt->_qproc赋值
                  为NULL,即poll_wait函数为NULL,是因为已经存在有效的
                  描述符了,可以返回不必等待了,不需要把剩下的描述符都加
                  入等待队列中
                 */
                if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {
                    count++;
                    pt->_qproc = NULL;
                    /* found something, stop busy polling */
                    busy_flag = 0;
                    can_busy_loop = false;
                }
            }
        }
        /*
         * All waiters have already been registered, so don't provide
         * a poll_table->_qproc to them on the next loop iteration.
         */
        pt->_qproc = 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;
    }
    return count;
}

static inline unsigned int do_pollfd()
{
    unsigned int mask;
    int fd;

    mask = 0;
    fd = pollfd->fd;
    if (fd >= 0) {
        struct fd f = fdget(fd);
        mask = POLLNVAL;
        if (f.file) {
            mask = DEFAULT_POLLMASK;
            if (f.file->f_op->poll) {
                pwait->_key = pollfd->events|POLLERR|POLLHUP;
                pwait->_key |= busy_flag;

                /*ken: 将其加入描述符的等待队列中*/
                mask = f.file->f_op->poll(f.file, pwait);
                if (mask & busy_flag)
                    *can_busy_poll = true;
            }
            /* Mask out unneeded events. */
            mask &= pollfd->events | POLLERR | POLLHUP;
            fdput(f);
        }
    }
    pollfd->revents = mask;

    return mask;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值