linux内核视角看epoll

一、概述

在传统的阻塞I/O模型中,一个Socket(套接字)通常只能处理一个TCP连接,即一对一的关系。每个TCP连接都需要分配一个独立的Socket来处理。
然而,使用多路复用技术,可以在单个线程中同时监视多个Socket的状态,以确定哪些Socket有可读或可写事件。通过在单个线程内等待和处理多个连接的事件,高效地处理大量的并发连接,减少资源消耗。

二、epoll

在linux上多路复用技术有select、poll、epoll。其中epoll的性能表现是最优异的,能支持的并发量也最大。linux中和epoll相关的函数有如下三个:

  1. epoll_create: 创建一个epoll对象。
  2. epoll_ctl: 向epoll对象添加要管理的连接。
  3. epoll_wait: 等待epoll对象管理的连接上的IO事件。

三、epoll内核对象的创建

用户进程调用epoll_create时,内核会创建一个eventpoll的内核对象,并把它加入到当前进程已打开的文件列表中。eventpoll内核对象的定义如下:,

struct eventpoll {
    //等待队列
    wait_queue_head_t wq;
    //就绪的描述符链表
    struct list_head rdllist;
    //每个eventpoll对象中都有一颗红黑树
    struct rb_root rbr;
}
  • wq:等待队列。用户进程陷入阻塞时,其进程描述符会保存在eventpoll对象的等待队列。
  • rbr:一颗红黑树。用来管理用户进程添加进来的所有的socket,为了支持对海量连接的高效查找、插入和删除。
  • rdllist:就绪的描述符链表。当有连接事件就绪的时候,内核会把就绪的连接放到rdllist,这样只需要判断rdllist链表就能找出就绪的连接,而不用去遍历整颗树

四、epoll添加socket

这一步是由用户进程调用epoll_ctl完成的。假设现在和客户端的多个连接的socket都创建好了,也创建好了eventpoll对象。epoll_ctl在注册每一个socket的时候,内核都会做如下三件事情:

  1. 初始化一个红黑树节点对象epitem。
  2. 在socket对象的等待队列中,注册事件到达回调函数ep_poll_callback
  3. 将epitem插入eventpoll的红黑树。
struct epitem {
    ......
    struct epoll_filefd ffd; //socket句柄
	struct eventpoll *ep; //所属的eventpoll
}

通过epoll_ctl添加两个socke以后,这些内核数据结构最终在进程中的关系如下:
linux内核视角看epoll_第1张图片

4.1 设置socket对象等待队列

epoll_ctl先完成epitem对象的初始化后,接着就会去设置socket的等待队列,把ep_poll_callback设置为socket数据就绪时的回调函数,注册到socket等待队列

// include/linux/wait.h
static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func){
    q->flags = 0;
    q->private = NULL;
    //将ep_poll_callback注册到socket等待队列wait_queue_t
    q->func = func;
}

前面介绍的阻塞式IO,private设置的是当前用户进程描述符current。而多路复用IO,所有的socket是交由epoll对象管理的,唤醒用户进程是由eventpoll对象完成的,所以这里的private就设置成了NULL。

五、等待接收数据

这一步是由epoll_wait来做的。当epoll_wait被调用时,观察eventpoll->rdllist就绪链表里有没有数据,如果有数据,就返回;如果没有数据,把当前用户进程添加到eventpoll的等待队列,然后把自己阻塞掉。所以说,epoll也是会阻塞的,当实在没什么事可做的时候,占着CPU也没什么意义。需要注意的是,阻塞式IO中,是把当前进程添加到socket的等待队列。

// fs/eventpoll.c
SYSCALL_DEFINE4(epoll_wait, int, epfd, ...){
    ......
    error = ep_poll(ep,events,maxevents,timeout);
}
static int ep_poll(struct eventpoll *ep, ...){
    ......
    // 1、判断就绪链表上有没有数据
    if(!ep_events_available(ep)){
        // 2、把当前用户进程current添加到eventpoll等待队列wq
        init_waitqueue_entry(&wait, current);
        __add_wait_queue_exclusive(&ep->wq, &wait);
        for(;;){
            // 3、设置用户进程状态,并调用schedule_hrtimeout_range让出CPU
            set_current_state(TASK_INTERRUPTIBLE);
            ......
        }
    }
}
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p){
    q->flags = 0;
    // 当前用户进程
    q->private = p;
    //设置epoll等待队列的回调函数
    q->func = default_wake_function;
}

设置了epoll等待队列的回调函数default_wake_function。这里需要注意的是,前面介绍的阻塞式IO中,内核唤醒用户进程的函数也是default_wake_function。

static inline void __add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait){
    //当前用户进程对应的等待添加到epoll的等待队列
    __add_wait_queue(q, wait);
}

前面说过,当网卡数据达到时,内核ksoftirqd线程会将数据放到socket对象的接收队列,接着调用socket的数据就绪回调函数sk_data_ready,这里不在过多介绍。

epoll_ctl已经在socket等待队列里注册了回调函数ep_poll_callback,sk_data_ready会调用它

// fs/eventpoll.c
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key){
    // 1、找到socket对应的epitem
    struct epitem *epi = ep_item_from_wait(wait);
    // 2、找到epitem对应的eventpoll
    struct eventpoll *ep = epi->ep;
    // 3、将epitem添加到eventpoll的就绪链表
    list_add_tail(&epi->rdllink, &ep->rdllist);
    // 4、查看eventpoll的等待队列上是否有等待
    if (waitqueue_active(&ep->wq))
        wake_up_lock(&ep->wq);
}

wake_up_lock()最终会调用epoll_wait传入的epoll等待队列回调函数default_wake_function来唤醒用户进程。

// kernel/sched/core.c
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags, void *key){
    return try_to_wake_up(curr->private, mode, wake_flags);
}

看一下epoll是如何把rdllist中就绪的事件返回给用户进程的:

// fs/eventpoll.c
static int ep_poll(...){
    ......
    __remove_wait_queue(&ep->wq, &wait);
    set_current_state(TASK_RUNNING);
    //给用户进程返回就绪事件
    ep_send_events(ep, events, maxevents);
}

六、Java NIO

如下是服务器端Java NIO的典型写法:

 while(true){
            selector.select();//这是一个阻塞方法,直到注册的channel有事件传过来,会放开阻塞。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            ......
}

eventpoll,顾名思义就是事件轮询。所谓的事件轮询就是通过不断调用系统调用epoll_wait来等待事件的发生。在事件轮询中,用户代码会反复进行系统调用epoll_wait函数,该函数将会一直阻塞等待,直到返回事件。

当有事件发生时,epoll_wait函数将返回一个正整数,表示收到的事件数量。用户代码可以通过遍历事件列表来逐个处理这些事件,例如读取或写入数据等。

七、总述

linux内核视角看epoll_第2张图片
实际上,只要活儿足够多,rdllist一直有数据,epoll_wait根本就不会让用户进程阻塞,用户进程会一直干活。这也是epoll性能高的关键所在。由一个内核eventpoll对象管理着所有的socket连接。IO多路复用,我的理解是复用了一个eventpoll对象。

你可能感兴趣的:(linux网络,linux,服务器,网络)