深入剖析poll,epoll机制(源码分析)

之前理解了一下select,poll,epoll机制,知道他们的区别,但对于是什么造成这种区别或是具体什么操作让epoll拥有更好的性质还是一知半解,于是决心研究下他们的源码

一.首先来看poll机制

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

 其中nfds是fds数组中pollfd对象的个数,timeout是poll函数超时的时间

poll机制用一个函数即可实现监控文件对象,它的文件对象是存储在pollfd结构中的

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

其中fd是被监听的文件描述符;events是告诉内核需要监听的事件;reevents是对文件描述符进行的操作事件。如果返回值>0则表示数组fds中有事件发生的描述符数量,如果所检测的描述符合集中没有任何事件发生,那它会一直阻塞,知道超时返回0;如果调用失败就返回-1,来简单实践下

int main(){
    int sockfd;
    struct pollfd pollfds;  //poll对象
    int timeout = 10000;
    
    pollfds.fd = sockfd;    //设置监控的sockfd
    pollfds.events = POLLIN|POLLPRI;    //设置监控的事件
    
    switch(poll(&pollfds,1,timeout)){   //开始监控
        case -1:                
            printf("poll error\n");
            break;
        case 0:
            printf("time out\n");
            break;
        default:                
            printf("event happen\n");
            printf("event value is 0x%x",pollfds.revents);
            break;
        }
}

都知道Poll对于select的优点就是它没有最大连接数的限制,原因就它是用链表来存储监听对象的,下面来看看它的具体实现机制

程序调用poll函数时会通过中断进入内核层,然后调用一系列内核函数,调用流程如下

1.Sys_poll函数:对timeout处理,将其换成10ms精度,然后调用do_sys_poll

2.do_sys_poll函数:主要是执行两个函数

poll_initwait(&table);

fdcount = do_poll(nfds, head,&table, timeout);

其中poll_initwait函数主要是初始化一个poll_wqueue变量即table,这个变量的用处后面讲

3.do_poll函数:处理超时,看循环里面是循环了poll监听的文件描述符链表,对于每个pollfd,如果do_poll成功count就加1

static int do_poll(unsigned int nfds,  struct poll_list *list,
                   struct poll_wqueues *wait, s64 *timeout)
{
    int count = 0;
    poll_table* pt = &wait->pt;

    if (!(*timeout))//处理没有超时的情况
        pt = NULL;

    for (;;) {//大循环,一直等待超时时间到或者有相应的事件触发唤醒进程
        struct poll_list *walk;
        long __timeout;

        set_current_state(TASK_INTERRUPTIBLE);//设置当前进程为可中断状态
        for (walk = list; walk != NULL; walk = walk->next) {//循环poll_fd列表
            struct pollfd * pfd, * pfd_end;

            pfd = walk->entries;
            pfd_end = pfd + walk->len;
            for (; pfd != pfd_end; pfd++) {

                if (do_pollfd(pfd, pt)) {//pwait = table->pt。调用驱动的poll函数获取mask值,另外将进程放入等待队列
                    count++;
                    pt = NULL;
                }
            }
        }

        pt = NULL;
        if (count || !*timeout || signal_pending(current))//如果超时时间到了或者没有poll_fd或者事件发生了,直接退出
            break;
        count = wait->error;
        if (count)
            break;

        if (*timeout < 0) {
            __timeout = MAX_SCHEDULE_TIMEOUT;
        } else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT-1)) {

            __timeout = MAX_SCHEDULE_TIMEOUT - 1;
            *timeout -= __timeout;
        } else {
            __timeout = *timeout;
            *timeout = 0;
        }

        __timeout = schedule_timeout(__timeout);//设置超时时间,进程休眠
        if (*timeout >= 0)
            *timeout += __timeout;
    }
    __set_current_state(TASK_RUNNING);//重新运行调用sys_poll的进程
    return count;
}

4.对于每个文件对象是调用do_pollfd(pfd, pt),在这里面就是将每个pollfd对象挂载到相应的文件的file_operation->poll(),当文件发生变化就会反过来通知pollfd

static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
    unsigned int mask;
    int fd;

    mask = 0;
    fd = pollfd->fd;//根据pollfd找到文件节点
    if (fd >= 0) {
        int fput_needed;
        struct file * file;

        file = fget_light(fd, &fput_needed);//根据文件节点fd找到文件的file结构
        mask = POLLNVAL;
        if (file != NULL) {
            mask = DEFAULT_POLLMASK;
            if (file->f_op && file->f_op->poll) //f_op:与文件相关联的操作函数
                mask = file->f_op->poll(file, pwait);//调用它的poll函数,并且返回mask,并将当前进程加到队列中
            /* Mask out unneeded events. */
            mask &= pollfd->events | POLLERR | POLLHUP;
            fput_light(file, fput_needed);
        }
    }
    pollfd->revents = mask;

    return mask;
}

理解下来整体流程是先会将监听的所有pollfd对象拷贝到内核中,在内核里循环遍历所有的fd,有事件就count++,最后返回,一直无事件并且超时了就返回0。在之前先将相应的对象挂载到其FILE* file的f_op->poll,这样当有事件发生就会回调将本进程唤醒,然后进程又来遍历全部的fd,然后保存有事件发生的对象,然后又拷贝到用户空间。可以看到会把fd合集拷贝两次,开销很大

 

二.epoll机制

  epoll是对select和poll的改进,它提供了三个函数

int epoll_create(int size);    //创建一个 epoll 的句柄

 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);    //可增删改监听对象
/*等待事件的产生*/
int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );

 关于其详细的使用在另一篇博客里有总结epoll机制详解 ,这里主要是分析它的源码

1.先来看其中一些关键的结构体

首先是epoll对象结构体

struct eventpoll {
    spinlock_t lock;
    struct mutex mtx;
    wait_queue_head_t wq;

    wait_queue_head_t poll_wait;

    struct list_head rdllist;   //所有ready的文件描述符的epitem存储在这个链表里

    struct rb_root rbr; //所有监听的文件对象,是红黑树存储

    struct epitem *ovflist; //这是一个单链表链接着所有的struct epitem

    struct user_struct *user;   //这里保存了一些用户变量, 比如fd监听数量的最大值等等
};

里面用到红黑树来保存所有的监听对象,每个节点是一个epitem结构体,对应一个文件描述符

struct epitem {
    //每个epitem对应一个fd,fd以epitem的形式存储在红黑树中
    struct rb_node rbn; //红黑树节点
    struct list_head rdllink;   //链表节点, 所有活动的epitem都会被链到eventpoll的rdllist中
    
    struct epitem *next;    //链表的下一节点
    struct epoll_filefd ffd;    // epitem对应的fd和struct file
    int nwait;  //其文件对象上活跃的事件个数
    
    struct list_head pwqlist;   //文件对象上等待链表表头
   
    struct eventpoll *ep;   //记录他属于那个epoll对象
    struct list_head fllink;
    struct epoll_event event;   //感兴趣的事件
};

其中pwalist是与文件对象相关联的一个变量看到后面就能理解其含义,然后是其中的epoll_filefd结构体,里面就是保存了文件对象及其描述符

struct epoll_filefd {
    struct file *file;
    int fd;
};

然后是epoll_entry结构体,其主要是记录一个文件对象的等待队列

struct eppoll_entry {
    struct list_head llink; //与epitem相联系,一个eppoll_entry对应一个epitem
    struct epitem *base;
    wait_queue_t wait;  //一个文件对象的等待队列
    wait_queue_head_t *whead;   //等待队列对头
};

然后是在poll队列上挂载的对象,每个epitem对象会挂载到文件对象的等待队列里

struct ep_pqueue {
    poll_table pt;
    struct epitem *epi;
};

主要的结构体就这些,现在看到或许还有些茫然,没关系,再往下分析就会明了

2.相关函数

(1)epoll_create

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

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
int error;
struct eventpoll *ep = NULL;//主描述符
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;

error = ep_alloc(&ep);  //构造一个 struct eventpoll
if (error < 0)
return error;

//让epollfd对应一个文件描述符,分配fd,当对这个文件进行读写操作时f_op里的函数会进行操作,即驱动程序,就是能通过fd找到epollfd
error = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
                         O_RDWR | (flags & O_CLOEXEC)); //让epollfd对应一个文件描述符,分配fd
if (error < 0)
ep_free(ep);
return error;
}

 epoll_create函数主要就是创建一个eventpoll对象管理整个epoll,可以看到它也对应了一个文件描述符

(2)epoll_ctl

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
    int error;
    struct file *file, *tfile;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;
    error = -EFAULT;
    if (ep_op_has_event(op) &&
    copy_from_user(&epds, event, sizeof(struct epoll_event)))
    goto error_return;

    error = -EBADF;
    file = fget(epfd);
    if (!file)
    goto error_return;
    tfile = fget(fd);
    if (!tfile)
    goto error_fput;

    error = -EPERM;

    if (!tfile->f_op || !tfile->f_op->poll) //将该进程加入文件等待序列
    goto error_tgt_fput;

    error = -EINVAL;

    if (file == tfile || !is_file_epoll(file))
        goto error_tgt_fput;

    ep = file->private_data;

    mutex_lock(&ep->mtx);
    epi = ep_find(ep, tfile, fd);   //首先查找该fd是不是已经存在了.
    error = -EINVAL;
    switch (op) {   //各个操作类型对应红黑树中相应的操作
        case EPOLL_CTL_ADD:
            if (!epi) {
            epds.events |= POLLERR | POLLHUP;   //添加
            error = ep_insert(ep, &epds, tfile, fd);
            } else
            error = -EEXIST;
            break;

        case EPOLL_CTL_DEL: //删除
            if (epi)
            error = ep_remove(ep, epi);
            else
            error = -ENOENT;
            break;
        case EPOLL_CTL_MOD:
            if (epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &epds);
            } else
            error = -ENOENT;
            break;
            }
        mutex_unlock(&ep->mtx);
    error_tgt_fput:
    fput(tfile);
    error_fput:
    fput(file);
    error_return:
    return error;
}

根据用户设置的操作类型来调用相应的函数,然后就是随影红黑树里的增加,删除,修改操作

来着重看看插入函数

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
                     struct file *tfile, int fd)
{
    int error, revents, pwake = 0;
    unsigned long flags;
    struct epitem *epi;
    struct ep_pqueue epq;
    // 查看是否达到当前用户的最大监听数 
    if (unlikely(atomic_read(&ep->user->epoll_watches) >=
                 max_user_watches))
        return -ENOSPC;

    if (!(epi = kmem_***_alloc(epi_***, GFP_KERNEL)))
        return -ENOMEM;

    INIT_LIST_HEAD(&epi->rdllink);
    INIT_LIST_HEAD(&epi->fllink);
    INIT_LIST_HEAD(&epi->pwqlist);
    epi->ep = ep;
    //将要监听的fd加入到创建的epitem中
    ep_set_ffd(&epi->ffd, tfile, fd);
    epi->event = *event;
    epi->nwait = 0;
    epi->next = EP_UNACTIVE_PTR;
    epq.epi = epi;
    /* 初始化一个poll_table
     * 其实就是指定调用poll_wait时的回调函数,和我们关心哪些events,
     * ep_ptable_queue_proc()是相应的回调函数 */
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    
    // 将其挂载到文件对象等待队列上
    revents = tfile->f_op->poll(tfile, &epq.pt);
   
    error = -ENOMEM;
    if (epi->nwait < 0)
        goto error_unregister;
    //这个就是每个文件会将所有监听自己的epitem链起来 
    spin_lock(&tfile->f_lock);
    list_add_tail(&epi->fllink, &tfile->f_ep_links);
    spin_unlock(&tfile->f_lock);
    
    //将epitem插入到对应的eventpoll中去 
    ep_rbtree_insert(ep, epi);
    spin_lock_irqsave(&ep->lock, flags);
    // 到达这里后, 如果我们监听的fd已经有事件发生, 那就要处理一下 
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
        // 将当前的epitem加入到ready list中去 
        list_add_tail(&epi->rdllink, &ep->rdllist);
        // Notify waiting tasks that events are available 
        // 唤醒eventpoll中的等待进程即epoll_wait
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
    spin_unlock_irqrestore(&ep->lock, flags);
    atomic_inc(&ep->user->epoll_watches);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);
    return 0;
    error_unregister:
    ep_unregister_pollwait(ep, epi);
   
    spin_lock_irqsave(&ep->lock, flags);
    if (ep_is_linked(&epi->rdllink))
        list_del_init(&epi->rdllink);
    spin_unlock_irqrestore(&ep->lock, flags);
    kmem_***_free(epi_***, epi);
    return error;
}

插入函数主要就是创建一个epoll_entry,设置其灰调函数,将其挂载到设备等待队列,当设备就绪就会唤醒等待队列上的所有等待进程,回调函数就会被调用,然后就会通过回调函数将fd放到eventpoll对象的rdlist上,然后eventpoll就是唤醒epoll_wait,然后epoll_wait就会手机rdlist中的epoll_event将其拷贝到用户空间。

下面ep_ptable_queue_proc就是指定回调函数,ep_poll_callback是回调函数

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);
    struct eppoll_entry *pwq;
    if (epi->nwait >= 0 && (pwq = kmem_***_alloc(pwq_***, GFP_KERNEL))) {
        /* 初始化等待队列, 指定ep_poll_callback为唤醒时的回调函数,
         * 当我们监听的fd发生状态改变时, 也就是队列头被唤醒时,
         * 指定的回调函数将会被调用. */
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        pwq->whead = whead;
        pwq->base = epi;
        // 将刚分配的等待队列成员加入到头中, 头是由fd持有的 
        add_wait_queue(whead, &pwq->wait);
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        epi->nwait = -1;
    }
}
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    int pwake = 0;
    unsigned long flags;
    struct epitem *epi = ep_item_from_wait(wait);//从等待队列获取epitem.需要知道哪个进程挂载到这个设备
    struct eventpoll *ep = epi->ep;//获取
    spin_lock_irqsave(&ep->lock, flags);
    if (!(epi->event.events & ~EP_PRIVATE_BITS))
        goto out_unlock;
    if (key && !((unsigned long) key & epi->event.events))
    if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
        if (epi->next == EP_UNACTIVE_PTR) {
            epi->next = ep->ovflist;
            ep->ovflist = epi;
        }
        goto out_unlock;
    }
    // 将当前的epitem放入ready list 
    if (!ep_is_linked(&epi->rdllink))
        list_add_tail(&epi->rdllink, &ep->rdllist);
    // 唤醒epoll_wait... 
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);
    if (waitqueue_active(&ep->poll_wait))
        pwake++;
    out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);
    return 1;
}

回调函数就是把收到event的epitem插入ep->rdllist中

到这其实整个epoll的底层机制就比较明了了,它相对于select,poll就是一种空间换时间的思想,它会在内核区域建立一个红黑树,每次添加一个监听对象就将其拷贝到内核包装为eptiem对象,然后挂载到其设备等待队列上设置回调函数,接着就添加进红黑树进行管理,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入到rdlist,接着唤醒epoll_wait进程进行后续处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值