关于c++:pollepoll实现分析一poll实现

53次阅读

共计 5594 个字符,预计需要花费 14 分钟才能阅读完成。

相干视频举荐
面试中正经“八股文”网络原理 tcp/udp,网络编程 epoll/reactor
epoll 原理分析 以及 reactor 模型利用
epoll 原理分析以及三握四挥的解决
LinuxC++ 后盾服务器开发架构师收费学习地址
1. 期待队列
在 Linux 内核中期待队列有很多用处,可用于中断解决、进程同步及定时。咱们在这里只说,过程常常必须期待某些事件的产生。期待队列实现了在事件上的条件期待: 心愿期待特定事件的过程把本人放进适合的期待队列,并放弃管制全。因而,期待队列示意一组睡眠的过程,当某一条件为真时,由内核唤醒它们。
期待队列由循环链表实现,由期待队列头(wait_queue_head_t)和期待队列项(wait_queue)组成,其元素(期待队列项)蕴含指向过程描述符的指针。每个期待队列都有一个期待队列头 (wait queue head), 期待队列头是一个类型为 wait_queue_head_t 的数据结构
定义期待队列头(相干内容能够在 linux/include/wait.h 中找到)
期待队列头构造体的定义:

struct wait_queue_head {
 spinlock_t  lock;          // 自旋锁变量,用于在对期待队列头 
 struct list_head task_list;  // 指向期待队列的 list_head
}; 

typedef struct __wait_queue_head wait_queue_head_t;
应用期待队列时首先须要定义一个 wait_queue_head,这能够通过 DECLARE_WAIT_QUEUE_HEAD 宏来实现,这是动态定义的办法。该宏会定义一个 wait_queue_head,并且初始化构造中的锁以及期待队列。

 Linux 中期待队列的实现思维如下图所示,当一个工作须要在某个 wait_queue_head 上睡眠时,将本人的过程管制块信息封装到 wait_queue 中,而后挂载到 wait_queue 的链表中,执行调度睡眠。当某些事件产生后,另一个工作(过程)会唤醒 wait_queue_head 上的某个或者所有工作,唤醒工作也就是将期待队列中的工作设置为可调度的状态,并且从队列中删除。

(2)期待队列中寄存的是在执行设施操作时不能取得资源而挂起的过程
定义期待对列:

struct wait_queue {unsigned int flags;  //prepare_to_wait()里有对 flags 的操作,查看以得出其含意
      #define WQ_FLAG_EXCLUSIVE        0x01 // 一个常数, 在 prepare_to_wait()用于批改 flags 的值
          void * private          // 通常指向当前任务管制块
          wait_queue_func_t func;    // 唤醒阻塞工作的函数,决定了唤醒的形式
 struct list_head task_list;    // 阻塞工作链表
};
typedef struct __wait_queue          wait_queue_t;

【文章福利】:小编整顿了一些集体感觉比拟好的学习书籍、视频材料共享在群文件外面,有须要的能够自行添加哦!(须要自取)

1.select/poll 毛病
select/poll 的毛病在于:

 1. 每次调用时要反复地从用户态读入参数。2. 每次调用时要反复地扫描文件描述符。3. 每次在调用开始时,要把以后过程放入各个文件描述符的期待队列。在调用完结后,又把过程从各个期待队列中删除。
  1. 内核实现
    2.1 次要数据结构:

(1) struct poll_table_entry {

    struct file  filp;
    wait_queue_t wait;// 外部有一个指针指向一个过程
    wait_queue_head_t   wait_address;// 期待队列头部(期待队列有多个 wait_queue_t 组成,通过双链表连贯)

};
(2) struct poll_table_page {

    struct poll_table_page   next;
    struct poll_table_entry   entry;
    struct poll_table_entry entries[0];

};
(3) struct poll_wqueues {

   poll_table pt;// 一个函数指针,通常指向__pollwait 或 null
   struct poll_table_page * table;
   int error;

};
(4) struct poll_list {

    struct poll_list *next;// 按内存页连贯,因为 kmalloc 有申请数据限度
    int len;// 用户空间传入 fd 的数量
    struct pollfd entries[0];// 寄存用户空间存入的数据

};
typedef void (poll_queue_proc)(struct file , wait_queue_head_t , struct poll_table_struct );
typedef struct poll_table struct {

 poll_queue_proc qproc;

} poll_table;

2.2 poll 零碎调用函数关系总图
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  1. 内核 2.6.9 poll 实现代码剖析
    [fs/select.c –>sys_poll]
    asmlinkage long sys_poll(struct pollfd __user * ufds, unsigned int nfds, long timeout)
    {
    struct poll_wqueues table;
    struct poll_list *head;
    struct poll_list *walk;
    ……
    poll_initwait(&table);
    ……
    while(i!=0) {
    struct poll_list *pp;
    pp = kmalloc(sizeof(struct poll_list)+ sizeof(struct pollfd)
    *(i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i), GFP_KERNEL));
    if (head == NULL)
    head = pp;
    else
    walk->next = pp;
    walk = pp;
    if (copy_from_user(pp->entries, ufds + nfds-i,
    sizeof(struct pollfd)*pp->len)) {
    err = -EFAULT;
    goto out_fds;
    }
    i -= pp->len;
    }
    / 这一大堆代码就是建设一个链表,每个链表的节点是一个 page 大小(通常是 4k),这链表节点由一个指向 struct poll_list 的指针掌控每个 poll_list 的 entrys 成员指向一个 struct pollfd。下面的循环就是把用户态的 struct pollfd 拷进这些 entries 里。通常用户程序的 poll 调用就监控几个 fd,所以下面这个链表通常也就只须要一个节点,即操作系统的一页。然而,当用户传入的 fd 很多时,因为 poll 零碎调用每次都要把所有 struct pollfd 拷进内核,所以参数传递和页调配此时就成了 poll 零碎调用的性能瓶颈。/
    fdcount = do_poll(nfds, head, &table, timeout);
    }
    其中 poll_initwait 较为要害,从字面上看,应该是初始化变量 table,留神此处 table 在整个执行 poll 的过程中是很要害的变量。而 struct poll_table 其实就只蕴含了一个函数指针。
    当初咱们来看看 poll_initwait 到底在做些什么
    void __pollwait(struct file filp, wait_queue_head_t wait_address, poll_table *p);
    void poll_initwait(struct poll_wqueues *pwq)
    {
    &(pwq->pt)->qproc = __pollwait; / 设置回调函数/
    ……
    }

很显著,poll_initwait 的次要动作就是把 table 变量的成员 poll_table 对应的回调函数置为__pollwait。这个__pollwait 不仅是 poll 零碎调用须要,select 零碎调用也一样是用这个__pollwait,说白了,这是个操作系统的异步操作的“御用”回调函数。当然了,epoll 没有用这个,它另外新增了一个回调函数,以达到其高效运行的目标,这是后话,暂且不表。
最初一句 do_poll,咱们跟进去:

static int do_poll(unsigned int nfds, struct poll_list list,struct poll_wqueues wait,
long timeout)
{
int count = 0;
poll_table* pt = &wait->pt;
for (;;) {
struct poll_list *walk;
set_current_state(TASK_INTERRUPTIBLE);
walk = list;
while(walk != NULL) {
do_pollfd(walk->len, walk->entries, &pt, &count);
walk = walk->next;
}
pt = NULL;
if (count || !timeout || signal_pending(current))
break;
count = wait->error;
if (count)
break;
timeout = schedule_timeout(timeout); /* 让 current 挂起,别的过程跑,timeout 到了
当前再回来运行 current*/
}
__set_current_state(TASK_RUNNING);
return count;
}

留神 set_current_state 和 signal_pending,它们两句保障了当用户程序在调用 poll 后挂起时,发信号能够让程序迅速推出 poll 调用,而通常的零碎调用是不会被信号打断的。纵览 do_poll 函数,次要是在循环内期待,直到 count 大于 0 才跳出循环,而 count 次要是靠 do_pollfd 函数解决。留神标红的 while 循环,当用户传入的 fd 很多时(比方 1000 个),对 do_pollfd 就会调用很屡次,poll 效率瓶颈的另一起因就在这里。
do_pollfd 就是针对每个传进来的 fd,调用它们各自对应的 poll 函数,简化一下调用过程,如下:
[fs/select.c–>sys_poll()–>do_poll()]
static void do_pollfd(unsigned int num, struct pollfd fdpage, poll_table pwait, int count)
{
……
struct file* file = fget(fd);
file->f_op->poll(file, &(table->pt));
……
}

如果 fd 对应的是某个 socket,do_pollfd 调用的就是网络设备驱动实现的 poll;如果 fd 对应的是某个 ext3 文件系统上的一个关上文件,那 do_pollfd 调用的就是 ext3 文件系统驱动实现的 poll。一句话,这个 file->f_op->poll 是设施驱动程序实现的,那设施驱动程序的 poll 实现通常又是什么样子呢?其实,设施驱动程序的规范实现是:调用 poll_wait,即以设施本人的期待队列为参数(通常设施都有本人的期待队列,不然一个不反对异步操作的设施会让人很郁闷)调用 struct poll_table 的回调函数。
作为驱动程序的代表,咱们看看 socket 在应用 tcp 时的代码:

[net/ipv4/tcp.c–>tcp_poll]
unsigned int tcp_poll(struct file file, struct socket sock, poll_table *wait)
{
……
poll_wait(file, sk->sk_sleep, wait);
tcp_poll 的外围实现就是 poll_wait,而 poll_wait 就是调用 struct poll_table 对应的回调函数,那 poll 零碎调用对应的回调函数就是__poll_wait,所以这里简直就能够把 tcp_poll 了解为一个语句:
__poll_wait(file, sk->sk_sleep, wait);
由此也能够看出,每个 socket 本人都带有一个期待队列 sk_sleep,所以下面咱们所说的“设施的期待队列”, 其实不止一个。
这时候咱们再看看__poll_wait 的实现:
[fs/select.c–>__poll_wait()]
void __pollwait(struct file filp, wait_queue_head_t wait_address, poll_table *_p)
{
……
}

__poll_wait 的作用就是创立了上图所示的数据结构(一次__poll_wait 即一次设施 poll 调用只创立一个 poll_table_entry),并通过 struct poll_table_entry 的 wait 成员,把 current 挂在了设施的期待队列上,此处的期待队列是 wait_address,对应 tcp_poll 里的 sk->sk_sleep。
当初咱们能够回顾一下 poll 零碎调用的原理了:先注册回调函数__poll_wait,再初始化 table 变量(类型为 struct poll_wqueues),接着拷贝用户传入的 struct pollfd(其实次要是 fd)(瓶颈 1),而后轮流调用所有 fd 对应的 poll(把 current 挂到各个 fd 对应的设施期待队列上)(瓶颈 2)。在设施收到一条音讯(网络设备)或填写完文件数据(磁盘设施)后,会唤醒设施期待队列上的过程,这时 current 便被唤醒了。current 醒来后来到 sys_poll 的操作绝对简略,这里就不逐行剖析了。

正文完
 0