Linux epoll内核源码剖析

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42462202/article/details/95377075

IO多路复用接口Linux内核源码剖析,源码之前,了无秘密
Linux poll内核源码剖析
Linux select内核源码剖析
Linux epoll内核源码剖析

Linux epoll内核源码剖析


前面介绍了select/poll,此文章将讲解epoll,epoll是select/poll的增强版,epoll更加高效,当然也更加复杂

epoll高效的一个重要的原因是在获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就可以

本文将先讲解如何使用epoll,然后再深入Linux内核源码分析epoll机制

epoll应用程序编写

epoll机制提供了三个系统调用epoll_createepoll_ctlepoll_wait

下面是这三个函数的原型

  • epoll_create

    int epoll_create(int size);
    

    此函数返回一个epoll的文件描述符

  • epoll_ctl

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    

    此函数是添加、删除、修改事件

    epfd:epoll的文件描述符

    op:表示选项,EPOLL_CTL_ADD(添加事件)、EPOLL_CTL_MOD(修改事件)、EPOLL_CTL_DEL(删除事件)

    fd:要操作的文件描述符

    event:事件信息

  • epoll_wait

    int epoll_wait(int epfd, struct epoll_event *events,
                   int maxevents, int timeout);
    

    此函数是等待条件满足,放回值为准备就绪的事件数

    epfd:epoll的文件描述符

    events:返回的事件信息

    maxevents:要等待的最大事件数

    timeout:超时时间

    • epoll_event

      struct epoll_event
      {
        uint32_t events;		//epoll事件
        epoll_data_t data; 	//联合体,一般表示文件描述符
      };
      

demo

下面这段代码,监听标准输入,当标准输入可读时,就打印读取到的信息

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>

#define MAX_EVENTS 10

int main(int argc, char* argv[])
{
    struct epoll_event ev, events[MAX_EVENTS];
    int nfds, epollfd, len;
    char buf[1024];
    int n;

    epollfd = epoll_create(10);
    if (epollfd == -1)
    {
        perror("epoll_create");
        exit(-1);
    }

    ev.events = EPOLLIN;
    ev.data.fd = 0;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev) == -1)
    {
        perror("epoll_ctl: listen_sock");
        exit(-1);
    }

    while(1)
    {
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) 
        {
            perror("epoll_pwait");
            exit(-1);
        }

        for (n = 0; n < nfds; ++n)
        {
            if(events[n].events & EPOLLIN)
            {
                len = read(events[n].data.fd, buf, 1024);
                buf[len] = '\0';
                printf("fd:%d; read buf:%s\n", events[n].data.fd, buf);
            }
        }
    }

    return 0;
}

epoll机制内核源码剖析

由上面的应用程序可知,epoll机制有三个系统调用,分别为epoll_createepoll_ctlepoll_wait,下面将详细地来分析这三个系统调用

epoll_create

epoll_create对应地系统调用如下

SYSCALL_DEFINE1(epoll_create, int, size)

展开后变成

long sys_epoll_create(int size)

接下来看看epoll_create做了什么事情

SYSCALL_DEFINE1(epoll_create, int, size)
{
    if (size <= 0)
        return -EINVAL;
    
    return sys_epoll_create1(0);
}

由上面的程序可以看出,指定size并没有什么作用,只是判断是否小于等于0而已

下面看一看sys_epoll_create1

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    struct eventpoll *ep = NULL;
    
    ep_alloc(&ep); //分配内存
    
    /* 定义一个epoll的文件描述符 */
    error = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
                     O_RDWR | (flags & O_CLOEXEC));
    
    /* 返回文件描述符 */
    return error;
}

eventpoll起到管理epoll事件的作用,这个结果贯穿整个epoll机制,下面来看看这个结构体

struct eventpoll {
    /* 等待队列头,被sys_epoll_wait使用 */
	wait_queue_head_t wq;
	
    /* 保准准备就绪的文件描述符的一个链表 */
    struct list_head rdllist;
    
    /* 红黑树节点,epoll使用红黑树存储事件信息 */
    struct rb_root rbr;
    
   	...
};

epoll_create的作用是申请一个eventpoll结构体,申请一个epoll文件描述符,然后放回到用户空间

epoll_ctl

epoll_ctl用于添加,删除,修改事件

对应的系统调用如下

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
		struct epoll_event __user *, event)

展开后得到

long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user * event)

下面来详细分析

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
		struct epoll_event __user *, event)
{
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;
    
    copy_from_user(&epds, event, sizeof(struct epoll_event)); //拷贝事件
    
    switch (op) {
    	case EPOLL_CTL_ADD: //添加事件
            ep_insert(ep, &epds, tfile, fd);
            break;
         case EPOLL_CTL_DEL: //删除事件
            ep_remove(ep, epi);
            break;
    	case EPOLL_CTL_MOD: //修改事件
            ep_modify(ep, epi, &epds);
            break;
    }
    
}

在这里主要分析ep_insert添加事件,在分析之前,先将一下epitem,epoll在内核实现的时候,事件是以epitem为单位,保存到红黑树的,下面看一看epitem结构体

struct epitem {
    struct rb_node rbn; //红黑树
    struct list_head rdllink; //就绪链表
    struct eventpoll *ep; //用于指向eventpoll
    struct epoll_event event; //event事件
};

好,接下来分析ep_insert

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
		     struct file *tfile, int fd)
{
    struct epitem *epi;
    struct ep_pqueue epq; //ep_pqueue结构体见下方
    
    epi = kmem_cache_alloc(epi_cache, GFP_KERNEL); //分配一个epoll项
    epi->ep = ep; //指向所属的eventpoll
    epi->event = *event; //赋值事件
    
    /* 初始化poll_table;pt->qproc = ep_ptable_queue_proc; */
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    
    /* 调用驱动程序的poll函数,具体下面分析 */
    tfile->f_op->poll(tfile, &epq.pt);
    
    /* 添加到红黑树中 */
    ep_rbtree_insert(ep, epi);
}
  • ep_pqueue

    struct ep_pqueue {
    	poll_table pt; //用于调用文件描述符的驱动程序的poll使用
    	struct epitem *epi; //epoll项
    };
    

下面分析上面的tfile->f_op->poll(tfile, &epq.pt)

一般驱动程序的poll实现如下

static unsigned int poll(struct file *fp, poll_table * wait)
{
	unsigned int mask = 0;

    /* 调用poll_wait */
	poll_wait(fp, &wq, wait); //wq为自己定义的一个等待队列头

	/* 如果条件满足,返回相应的掩码 */
	if(condition)
		mask |= POLLIN; 

	return mask;
}

看一看poll_wait什么内容

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && wait_address)
		p->qproc(filp, wait_address, p);
}

调用了p->qproc(filp, wait_address, p)函数,还记得上面程序将其初始化init_poll_funcptr(&epq.pt, ep_ptable_queue_proc)

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
				 poll_table *pt)
{
    struct epitem *epi = ep_item_from_epqueue(pt); //找到对应的epoll项
    struct eppoll_entry *pwq; //具体定义看下面
    
    pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)//分配内存
        
    /* 初始化等待队列的唤醒函数,当驱动程序唤醒等待队列时,会调用此函数(ep_poll_callback) */
    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    pwq->base = epi; //设置好epoll项
    add_wait_queue(whead, &pwq->wait); //将等待队列元素添加到驱动的等待队列中
}
  • eppoll_entry

    struct eppoll_entry {
    	struct epitem *base; //指向epoll项
        wait_queue_t wait; //等待队列元素
        wait_queue_head_t *whead; //等待队列头
    };
    

回到ep_insert函数,我们可知道tfile->f_op->poll(tfile, &epq.pt)做了什么事情

  • 1、添加等待队列元素到驱动的等待队列中
  • 2、初始化驱动唤醒等待队列时调用的函数,init_waitqueue_func_entry(&pwq->wait, ep_poll_callback)

epoll_wait

epoll_wait对应的系统调用如下

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
		int, maxevents, int, timeout)

展开后变成

long sys_epoll_wait(int epfd, struct epoll_event __user * events, int maxevents, int timeout)

下面来详细分析

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
		int, maxevents, int, timeout)
{
	ep_poll(ep, events, maxevents, timeout);
}
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
		   int maxevents, long timeout)
{
	init_waitqueue_entry(&wait, current); //初始化等待队列元素
    __add_wait_queue_exclusive(&ep->wq, &wait); //将等待队列元素添加到event_poll的等待队列中
    
    for (;;) {
    	set_current_state(TASK_INTERRUPTIBLE); //设置当前进程状态
        
        /* 如果就绪链表中有元素或者超时则退出 */
        if (!list_empty(&ep->rdllist) || timed_out)
            break;
    
        /* 任务调度,睡眠 */
    	schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS);
    }
    
    /* 设置进程状态 */
    set_current_state(TASK_RUNNING);

    /* 返回信息到应用层 */
	ep_send_events(ep, events, maxevents));

    /* 返回就绪的事件数 */
	return res;
}

当ep_poll调用schedule_hrtimeout_range的时候,会睡眠等待,直到驱动程序调用wake_up唤醒等待队列时,会再次醒过来,而wake_up的实现如下

...
wait_queue_t *curr;
curr->func(curr, mode, wake_flags, key)
...

会调用等待队列中的等待元素的func函数,epoll添加到驱动程序的等待队列的等待元素的func已经被初始化init_waitqueue_func_entry(&pwq->wait, ep_poll_callback)

下面来分析ep_poll_callback函数,来看看驱动程序是怎么唤醒进程的

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    struct epitem *epi = ep_item_from_wait(wait); //得到对应的epoll项
    struct eventpoll *ep = epi->ep; //得到eventpoll
    
    list_add_tail(&epi->rdllink, &ep->rdllist); //将该epoll项添加到eventpoll的就绪链表中
    
    wake_up_locked(&ep->wq); //唤醒eventpoll睡眠的进程
}

唤醒之后,继续回到ep_poll函数运行,此时会判断if (!list_empty(&ep->rdllist) || timed_out)就绪链表不为空,则退出循环,然后调用ep_send_events(ep, events, maxevents)来获取就绪链表中的epoll项的状态,接下来分析ep_send_events(ep, events, maxevents))

static int ep_send_events(struct eventpoll *ep,
			  struct epoll_event __user *events, int maxevents)
{
	struct ep_send_events_data esed;

	esed.maxevents = maxevents;
	esed.events = events;

	return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}
static int ep_scan_ready_list(struct eventpoll *ep,
			      int (*sproc)(struct eventpoll *,
					   struct list_head *, void *),
			      void *priv)
{
    LIST_HEAD(txlist); //定义一个链表头
    
    list_splice_init(&ep->rdllist, &txlist); //将就绪链表中的元素交换到txlist中
    
    error = (*sproc)(ep, &txlist, priv); //调用回调函数,此回调函数被初始化为ep_send_events_proc
    
    return error; //返回就绪的事件数
}

下面来分析ep_send_events_proc函数

static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
			       void *priv)
{
    struct ep_send_events_data *esed = priv; //包含事件和事件数
    
    unsigned int revents;
    struct epitem *epi;
    struct epoll_event __user *uevent;

    /* 遍历就绪链表 */
    for(...)
    {
        epi = list_first_entry(head, struct epitem, rdllink);
		list_del_init(&epi->rdllink);
        
        /* 调用驱动程序的poll获取状态 */
        revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) & epi->event.events;
    
        /* 将事件状态拷贝到应用层 */
    	__put_user(revents, &uevent->events);
        __put_user(epi->event.data, &uevent->data);
        
        eventcnt++;
        uevent++;
    }
    
    return eventcnt; //返回事件数
}

到这里可以知道ep_poll函数做了什么事情

  • 1、定义一个等待队列元素,添加到eventpoll的等待队列中,等待唤醒
  • 2、当IO准备就绪时,驱动程序会调用回调函数,将就绪的epoll项添加到就绪链表中,并唤醒eventpoll的等待队列
  • 3、继续运行ep_poll函数,发现就绪链表中有元素,则跳出循环
  • 4、调用ep_send_events函数,调用就绪的epoll项对应驱动程序的poll函数,得到状态,然后再返回到应用层

至此,epoll也就分析完了

总结

epoll是select/poll的增强版,select/epoll是采用轮询的方式,而epoll是通过回调,然后将就绪的IO添加到就绪链表,然后只查询这些就绪的IO状态,从而大大减少不必要的操作,所以在IO数量较多时,epoll的性能优于select/poll

展开阅读全文

没有更多推荐了,返回首页