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

相干视频举荐
面试中正经“八股文”网络原理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的操作绝对简略,这里就不逐行剖析了。

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据