LINUX文件IO之epoll内核实现

引言

linux有select/poll/epoll三种常见的多路复用的IO编程模型。其中,epoll自内核2.6加入后,因其高性能、高并发等特性被众多服务端作为网络IO实现。今天看了下linux源码,将其实现过程分享给大伙。

1 epoll概述

如下图。整个epoll的数据模型主要分为三部分。首先,一切皆是文件的思想,同样我们的epoll对象也是一个抽象的文件,所以有一个file结构体;抽象的file的private_data指向一个eventpoll结构体,该结构体就是epoll对象本身,用户层面只是file对应所在进程的文件ID而已;然后eventpoll通过rbr红黑树和rdlist链表聚合epitem结构体,该结构是描述epoll对象关联的一个个文件的感兴趣事件,当然还包括事件通知的内核实体。
在这里插入图片描述

1.1 file结构体

关键属性

  • private_data:私有数据。指向eventpoll;
  • f_op:文件操作接口,c语言模拟c++虚函数表实现多态特性。当作为epoll时,指向eventpoll_fops变量。

1.2 eventpoll结构体

关键属性

  • poll_wait :poll等待队列。由于epoll也是文件,所以可以被其他epoll关联监听其上的事件,此时就会加入这个等待队列监听;
  • wq :正常等待队列。当我们调用epoll_wait函数时,无事件发生,就会加入到该队列等待;
  • rbr :存储关联文件的感兴趣事件的红黑树。真实指向epitem结构体,因其有struct rb_root类型的rbn字段便可组合成红黑树;
  • rdllist :存储已发生的IO事件的链表。真实指向epitem结构体,因其有struct list_head类型的rdllink字段便可组合成链表;
  • ovflist :也是存储已发生的IO事件的链表。由于当rdlist中有事件发生时唤醒一个task的时候,该task执行将事件拷贝到用户态的过程需要对epoll结构体加锁,所以此时有事件发生时,会临时加入到该无锁安全链表中。

1.3 epitem结构体

关键属性

  • rbn :红黑树节点。通过该字段epitem作为一个红黑树节点;
  • rdllink :链表节点。通过该字段epitem作为一个链表节点,链表节点与epitem的转换不做详细阐述,简单概括就是编译期推导字段与结构体首地址的偏移量来计算;
  • ep :所属eventpoll对象。
  • ffd :记录关联的文件。
  • event :记录相应的事件。若记录在epoll的rbr之中,则该值表示感兴趣的事件。若记录在epoll的rdlist之中,则该值表示发生的IO事件;
  • fllink :插入关联文件的链表项。该字段连接到监听文件ffd.file的f_ep_links字段,当有事件发生的时候就会回调事件通知。

2 运行时

2.1 epoll初始化

在内核启动初始化过程中,会初始化epoll。关键过程为创建两个slab对象缓存。其大致过程如下图:
在这里插入图片描述

2.2 创建epoll对象

当我们调用epoll_create1函数的时候,其内核流程如下:
在这里插入图片描述

  • 首先,用户态进程调用epoll_create1;
  • 然后,经过系统调用过程,最终执行到内核态SYSCALL_DEFINE1(epoll_create1, int, flags)函数;
  • 执行ep_alloc,最终通过slab分配一个eventpoll对象;
  • 通过get_unused_fd_flags返回一个没有使用的文件号;
  • 通过anon_inode_getfile创建一个struct file结构对象,并设置给eventpoll对象;
  • 将文件号和struct file结构对象加入当前打开文件表中;
  • 最后,将文件号通过内核态返回给用户态。

2.3 关联/修改/取消文件IO事件

当我们调用epoll_ctl函数的时候,其内核流程如下:
在这里插入图片描述

  • 首先,用户态进程调用epoll_ctl;
  • 然后,经过系统调用过程,最终执行到内核态SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,struct epoll_event __user *, event)函数;
  • 检查系列参数,当然也包括将event从用户态拷贝到内核态;
  • 调用ep_find查询存在的epitem,其实就是在红黑树中查询,其时间复杂度为O(㏒₂N);
  • 若op == EPOLL_CTL_ADD,则执行ep_insert函数。此时ep_find返回的必须为空指针;
  • 若op == EPOLL_CTL_DEL,则执行ep_remove函数;
  • 若op == EPOLL_CTL_MOD,则执行ep_modify函数。

2.3.1 关联文件IO事件

新加感兴趣的文件IO事件,其内核流程如下:
在这里插入图片描述

  • 进入ep_insert后,首先通过slab构建一个struct epitem对象epi,并初始化epi;
  • 初始化struct ep_pqueue结构体epq;
  • 调用函数list_add_tail_rcu将epi添加到监听文件的tfile->f_ep_links链表中;
  • 由于当前要插入的文件可能已经有事件发生,所以需要调用函数ep_item_poll(epi, &epq.pt)。特别是边缘触发模式,若不进行后续事件处理且后续没有事件触发,将永远得不到处理,其实非边缘不进行后续事件处理也是一样会有问题;
  • 通过ep_rbtree_insert函数将epi加入到eventpoll对象的红黑树中;
  • 若前面ep_rbtree_insert插入前,有事件发生,先将事件加入到eventpoll的rdllist中,然后唤醒eventpoll对象的等待队列wq上的task。此外,若当前epoll被另一个epoll监听,则唤醒等待队列poll_wait上的task。

注:修改/取消文件IO事件结合源码查看,就不单独介绍。。。

2.4 等待关联文件的IO事件

当我们调用epoll_wait函数的时候,其内核流程如下:
在这里插入图片描述

  • 首先,用户态进程调用epoll_wait;
  • 然后,经过系统调用过程,最终执行到内核态SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,int, maxevents, int, timeout)函数;
  • 通过fdget函数将epoll文件对象,转为struc fd结构,其内包含struct file结构体,并读出其中的private_data字段赋值给具备变量ep;
  • 然后进入ep_poll函数;
  • 在ep_poll函数中,首先是判断是否有事件发生,若是没有事件发生就会加入到eventpoll的wq中进行等待睡眠。被唤醒过后,当ep->rdllist不空、或超时、又或被信号中断,就会进行后续操作,否则就会继续睡眠等待。
  • 到这步上面上步判断有事件发生或被唤醒退出循环,然后调用ep_send_events函数。在ep_send_events中进而调用ep_scan_ready_list函数;
  • 在ep_scan_ready_list中,首先将ep->rdllist复制到txtlist并清空;
  • 然后调用函数指针sproc指向的函数,sproc指向函数ep_send_events_proc,该函数将txtlist的事件拷贝到用户空间,特别地,对于水平触发模式还需要重新拷贝到eventpoll的rdlist链表中;
  • 当正在进行上步的时候,可能底层IO已经又有了事件发生,由于已经被加锁,无法访问eventpoll结构的rdlist。底层IO会临时添加到eventpoll结构的ovflist链表,所以此时需要将之拷贝到rdlist;
  • 最后,若eventpoll结构rdlist有事件,就会唤醒eventpoll的等待队列wq上的其他task。此外,若当前epoll被另一个epoll监听,则还要唤醒其poll_wait上的等待队列。

3源码解读

上述的调用序列图足够详尽,可参照源码理解,我就不再把源码贴出来重复解读一番了!特别提醒,请注意里面的内核态锁同步机制
注:本文参考的linux内核源码版本为4.0。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值