IO多路复用使得一个线程就可就可以处理多个网络连接,无需要创建多个线程来处理多个socket连接,减少不必要的资源开销,但是Select还是Poll、Epoll模式都有着不同的区别;

  上篇在介绍Select模式是也介绍了Select模式存在的种种问题,如大量FD集从用户态拷贝到内核态、FD集合的遍历问题、通知机制、Select默认只支持1024个文件描述符的问题等;

Epoll介绍

  Epoll基本使用流程为:

  1、使用EpollCreate1函数创建Epoll

  2、使用EpollCtl函数在Epoll上注册需要监听的事件

  3、使用EpollWait函数等待事件就绪

在Go中函数定义

Epoll事件对象

type EpollEvent struct {

Events uint32

Fd int32

Pad int32

}

创建Epoll

func EpollCreate(size int) (fd int, err error)

注册监听

func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error)

等待就绪

func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)

1、EpollCreate

  创建epoll实例文件描述符,不使用时需关闭以便内核销毁实例释放资源; size参数为内核fd队列大小,内核2.6.8后已升级为动态队列该参数意义不大,但值需大于0;

  另有一个EpollCreate1函数, 参数flag:值为0时与EpollCreate一致。 还有一个取值EPOLL_CLOEXEC,设置文件描述符的标志,FD_CLOEXEC,指fork的子进程执行exec时关闭此fd;

2、EpollCtl

  注册监听事件(epfd,操作,监听的fd,需监听的事件),向内核注册、修改、删除文件描述符;

  epfd: 上一步创建epoll时所返回的文件描述符

  操作: 有这么三种EPOLL_CTL_ADD:新注册fd监听到epfd中, EPOLL_CTL_MOD:修改已注册的fd事件监听,EPOLL_CTL_DEL:从epfd中删除一个对fd监听事件;

  监听的fd: 需要监听的文件描述符本篇文章里就是创建Socket或建立连接返回的FD

  监听的事件: 也就是EpollEvent 对象,此对象中主要使用两个字段:FD与Events,表示监听的文件描述符、与监听的具体事件;

  Events事件类型的取值有:

  EPOLLIN:文件描述符可读;

  EPOLLOUT:文件描述符可写;

  EPOLLERR:发生错误;

  EPOLLOHUP:文件描述符被挂断;

  EPOLLET:将EPOLL设为边缘触发 (Edge Triggered) 模式;

  EPOLLPRI:文件描述符有紧急的数据可读;

  EPOLLONESHOT:一次监听,监听事件发生后,如还需要监听fd,需再次fd加入到EPOLL队列里;

3、EpollWait

  等待epfd上IO事件就绪,参数events:从内核获取的事件集合,msec:超时时间,-1 阻塞,返回值:就绪事件数目,-1为出错;

LT与ET触发模式

  Epoll默认为LT触发模式,Select与Poll只有该模式;

  LT触发(Level triggered 水平触发): epoll_wait检测描述符事件发生时将事件通知程序,程序可不立即处理事件。下次调用epoll_wait时,会再次响应程序通知此事件。

  只要缓冲区有数据调用EpollWait时都会立即返回事件就绪,直到缓冲区所有数据处理完;

  ET触发(Edge triggered 边缘触发): epoll_wait检测描述符事件发生时将事件通知程序,程序须立即处理该事件。如不处理,下次调用epoll_wait时不会再次响应程序通知此事件。

  不管缓存区是否有数据,只有新数据到来才触发,需一次性处理完所有数据,所以ET只支持非阻塞模式,否则当缓冲区没数据时Read会阻塞;

  LT支持Block与Non-Block Socket,ET只支持Non-Block Socket,ET比LT性能更好,其事件触发少效率高;

Golang中Epoll的使用

func epoll(fd int) {

var event syscall.EpollEvent

//创建epoll实例文件描述符,不使用时需关闭以便内核销毁实例释放资源; size参数为内核fd队列大小,内核2.6.8后已升级为动态队列该参数意义不大,但值需大于0

epfd, e := syscall.EpollCreate(1)

if e != nil {

log.Println("epoll_create: ", e)

os.Exit(1)

}

defer syscall.Close(epfd)

//设置事件模式

event.Events = syscall.EPOLLIN

event.Fd = int32(fd) //设置监听描述符

//注册监听事件(epfd,事件动作,监听的fd,需监听的事件)

if e = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, fd, &event); e != nil {

log.Println("epoll_ctl: ", e)

os.Exit(1)

}

epollWait(fd, epfd, event)

}

func epollWait(fd, epfd int, epollEvent syscall.EpollEvent) {

var events [10]syscall.EpollEvent

connect = &Connect{map[int]string{}}

for {

nevents, e := syscall.EpollWait(epfd, events[:], -1) //等待获取就绪事件

if e != nil {

log.Println("EpollWait: ", e)

}

for ev := 0; ev < nevents; ev++ {

event := events[ev].Events

efd := events[ev].Fd

//处理连接

if int(efd) == fd && event == syscall.EPOLLIN {

handConn(fd, epfd, &epollEvent)

} else if event == syscall.EPOLLIN { //可读

handMsg(epfd, int(efd))

}

//可写

if events[ev].Events == syscall.EPOLLOUT {

}

}

}

}

获得就绪事件后

  在通过EpollWait获得就绪事件后,通过对比文件描述符fd与事件类型可以进行对应逻辑处理,如是新连接或是读取数据;

  1、新连接: 调用syscall.Accept获取连接的文件描述符,并通过调用syscall.EpollCt函数监听此文件描述符的事件;

  2、读取数据: 调用syscall.Read获取缓冲区的数据,这里需注意是LT触发还是ET触发,如是ET触发需要在此次IO就绪事件中通过一次或多次调用syscall.Read函数读取完所有数据;

  这里介绍的Epoll模式则完全没有Select模式的所有缺点,比Select更灵活且没有文件描述符限制,将文件描述符事件放入到内核事件表中,通过回调而不是轮询来实现事件通知;并没有所监听的文件描述符数不受限制;

  对比Select与poll模式Epoll通过回调而不是轮询来检查就绪状态状态的FD使得性能有很大提升;

文章首发地址:https://mp.weixin.qq.com/s/mcOgZIv0B3bLyoTbvRw5YQ

参考资料:Epoll相关

查看原文