日期:2014-05-16  浏览次数:20841 次

linux 内核poll/select/epoll实现剖析

f_ops.poll和wait_queue


poll/select/epoll的实现都是基于文件提供的poll方法(f_op->poll),
该方法利用poll_table提供的_qproc方法向文件内部事件掩码_key对应的的一个或多个等待队列(wait_queue_head_t)上添加包含唤醒函数(wait_queue_t.func)的节点(wait_queue_t),并检查文件当前就绪的状态返回给poll的调用者(依赖于文件的实现)。
当文件的状态发生改变时(例如网络数据包到达),文件就会遍历事件对应的等待队列并调用回调函数(wait_queue_t.func)唤醒等待线程。

通常的file.f_ops.poll实现及相关结构体如下

struct file {
    const struct file_operations	*f_op;
    spinlock_t			f_lock;
    // 文件内部实现细节
    void			   *private_data;
#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head	f_ep_links;
    struct list_head	f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    // 其他细节....
};

// 文件操作
struct file_operations {
    // 文件提供给poll/select/epoll
    // 获取文件当前状态, 以及就绪通知接口函数
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    // 其他方法read/write 等... ...
};

// 通常的file.f_ops.poll 方法的实现
unsigned int file_f_op_poll (struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    wait_queue_head_t * wait_queue;

    //1. 根据事件掩码wait->key_和文件实现filep->private_data 取得事件掩码对应的一个或多个wait queue head
    some_code();

    // 2. 调用poll_wait 向获得的wait queue head 添加节点
    poll_wait(filp, wait_queue, wait);

    // 3. 取得当前就绪状态保存到mask
    some_code();

    return mask;
}

// select/poll/epoll 向文件注册就绪后回调节点的接口结构
typedef struct poll_table_struct {
    // 向wait_queue_head 添加回调节点(wait_queue_t)的接口函数
    poll_queue_proc _qproc;
    // 关注的事件掩码, 文件的实现利用此掩码将等待队列传递给_qproc
    unsigned long   _key;
} poll_table;
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);


// 通用的poll_wait 函数, 文件的f_ops->poll 通常会调用此函数
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && p->_qproc && wait_address) {
        // 调用_qproc 在wait_address 上添加节点和回调函数
        // 调用 poll_table_struct 上的函数指针向wait_address添加节点, 并设置节点的func
        // (如果是select或poll 则是 __pollwait, 如果是 epoll 则是 ep_ptable_queue_proc),
        p->_qproc(filp, wait_address, p);
    }
}


// wait_queue 头节点
typedef struct __wait_queue_head wait_queue_head_t;
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};

// wait_queue 节点
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE	0x01
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);


// 当文件的状态发生改变时, 文件会调用此函数,此函数通过调用wait_queue_t.func通知poll的调用者
// 其中key是文件当前的事件掩码
void __wake_up(wait_queue_head_t *q, unsigned int mode,
               int nr_exclusive, void *key)
{
    unsigned long flags;

    spin_lock_irqsave(&q->lock, flags);
    __wake_up_common(q, mode, nr_exclusive, 0, key);
    spin_unlock_irqrestore(&q->lock, flags);
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
                             int nr_exclusive, int wake_flags, void *key)
{
    wait_queue_t *curr, *next;
    // 遍历并调用func 唤醒, 通常func会唤醒调用poll的线程
    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        unsigned flags = curr->flags;

        if (curr->func(curr, mode, wake_flags, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) {
            break;
        }
    }
}

?poll 和 select

poll和select的实现基本上是一致的,只是传递参数有所不同,他们的基本流程如下:

1. 复制用户数据到内核空间

2. 估计超时时间

3. 遍历每个文件并调用f_op->poll 取得文件当前就绪状态, 如果前面遍历的文件都没有就绪,向文件插入wait_queue节点

4. 遍历完成后检查状态:

??????? a). 如果已经有就绪的文件转到5;

??????? b). 如果有信号产生,重启poll或select(转到 1或3);

??????? c). 否则挂起进程等待超时或唤醒,超时或被唤醒后再次遍历所有文件取得每个文件的就绪状态

5. 将所有文件的就绪状态复制到用户空间

6. 清理申请的资源

?

关键结构体?

下面是poll/select共用的结构体及其相关功能:

poll_wqueues 是?