从Go编程看IO多路复用Epoll

IO多路复用使得一个线程就可就可以处理多个网络连接,无需要创建多个线程来处理多个socket连接,减少不必要的资源开销,但是Select还是Poll、Epoll模式都有着不同的区别;
上篇在介绍Select模式是也介绍了Select模式存在的种种问题,如大量FD集从用户态拷贝到内核态、FD集合的遍历问题、通知机制、Select默认只支持1024个文件描述符的问题等;

Epoll介绍

Epoll基本使用流程为:
1、使用EpollCreate1函数创建Epoll
2、使用EpollCtl函数在Epoll上注册需要监听的事件
3、使用EpollWait函数等待事件就绪

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)

创建epoll实例文件描述符,不使用时需关闭以便内核销毁实例释放资源; size参数为内核fd队列大小,内核2.6.8后已升级为动态队列该参数意义不大,但值需大于0;
另有一个EpollCreate1函数, 参数flag:值为0时与EpollCreate一致。 还有一个取值EPOLL_CLOEXEC,设置文件描述符的标志,FD_CLOEXEC,指fork的子进程执行exec时关闭此fd;

注册监听事件(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队列里;

等待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
        //&#x5904;&#x7406;&#x8FDE;&#x63A5;
        if int(efd) == fd && event == syscall.EPOLLIN {
            handConn(fd, epfd, &epollEvent)
        } else if event == syscall.EPOLLIN { //&#x53EF;&#x8BFB;
            handMsg(epfd, int(efd))
        }
        //&#x53EF;&#x5199;
        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使得性能有很大提升;

Original: https://www.cnblogs.com/softlin/p/16153133.html
Author: AiFly
Title: 从Go编程看IO多路复用Epoll

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/621956/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球