Golang是怎么利用epoll的

Golang是怎么利用epoll的

飞雪无情 1月21日

LinuGo

名不见经传的Gopher,公众号主要分享一些关于Golang,Linux,Kubernetes的经验与学习踩坑总结。

图片

    使用Golang可以轻松地为每一个TCP连接创建一个协程去服务而不用担心性能问题,这是因为Go内部使用goroutine结合IO多路复用实现了一个“异步”的IO模型,这使得开发者不用过多的关注底层,而只需要按照需求编写上层业务逻辑。这种异步的IO是如何实现的呢?下面我会针对Linux系统进行分析。

    在Unix/Linux系统下,一切皆文件,每条TCP连接对应了一个socket句柄,这个句柄也可以看做是一个文件,在socket上收发数据,相当于对一个文件进行读写,所以一个socket句柄,通常也用表示文件描述符fd来表示。可以进入/proc/PID/fd/查看进程占用的fd。

    系统内核会为每个socket句柄分配一个读(接收)缓冲区和一个写(发送)缓冲区,发送数据就是在这个fd对应的写缓冲区上写数据,而接收数据就是在读缓冲区上读数据,当程序调用write或者send时,并不代表数据发送出去,仅仅是把数据拷贝到了写缓冲区,在时机恰当时候(积累到一定数量),会将数据发送到目的端。

Golang runtime还是需要频繁去检查是否有fd就绪的,严格说并不算真正的异步,算是一种非阻塞IO复用。

 

IO模型

    借用教科书中几张图

阻塞式IO

    程序想在缓冲区读数据时,缓冲区并不一定会有数据,这会造成陷入系统调用,只能等待数据可以读取,没有数据读取时则会阻塞住进程,这就是阻塞式IO。当需要为多个客户端提供服务时,可以使用线程方式,每个socket句柄使用一个线程来服务,这样阻塞住的则是某个线程。虽然如此可以解决进程阻塞,但是还是会有相当一部分CPU资源浪费在了等待数据上,同时,使用线程来服务fd有些浪费资源,因为如果要处理的fd较多,则又是一笔资源开销。

图片

 

非阻塞式IO

    与之对应的是非阻塞IO,当程序想要读取数据时,如果缓冲区不存在,则直接返回给用户程序,但是需要用户程序去频繁检查,直到有数据准备好。这同样也会造成空耗CPU。

图片

 

IO多路复用

    而IO多路复用则不同,他会使用一个线程去管理多个fd,可以将多个fd加入IO多路复用函数中,每次调用该函数,传入要检查的fd,如果有就绪的fd,直接返回就绪的fd,再启动线程处理或者顺序处理就绪的fd。这达到了一个线程管理多个fd任务,相对来说较为高效。常见的IO多路复用函数有select,poll,epoll。select与poll的最大缺点是每次调用时都需要传入所有要监听的fd集合,内核再遍历这个传入的fd集合,当并发量大时候,用户态与内核态之间的数据拷贝以及内核轮询fd又要浪费一波系统资源(关于select与poll这里不展开)。

图片

 

epoll介绍

    接下来介绍一下epoll系统调用

    epoll相比于select与poll相比要灵活且高效,他提供给用户三个系统调用函数。Golang底层就是通过这三个系统调用结合goroutine完成的“异步”IO。

//用于创建并返回一个epfd句柄,后续关于fd的添加删除等操作都依据这个句柄。
int epoll_create(int size);
//用于向epfd添加,删除,修改要监听的fd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
//传入创建返回的epfd句柄,以及超时时间,返回就绪的fd句柄。
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

 

  • 调用epoll_create会在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,可简单理解为fd集合。

  • 调用epoll_ctl函数用于将fd封装成epitem加入这个eventpoll对象,并给这个epitem加了一个回调函数注册到内核,会在这个fd状态改变时候触发,使得该epitem加入eventpoll的就绪列表rdlist。

  • 当相应数据到来,触发中断响应程序,将数据拷贝到fd的socket缓冲区,fd缓冲区状态发生变化,回调函数将fd对应的epitem加入rdlist就绪队列中。

  • 调用epoll_wait时无需遍历,只是返回了这个就绪的rdlist队列,如果rdlist队列为空,则阻塞等待或等待超时时间的到来。

大致工作原理如图

图片

 

异步IO

    当用户程序想要读取fd数据时,系统调用直接通知到内核并返回处理其他的事情,内核将数据准备好之后,通知用户程序,用户程序再处理这个fd上的事件。

图片

 

Golang异步IO实现思路

    我们都知道,协程的资源占有量很小,而且协程也拥有多种状态如阻塞,就绪,运行等,可以使用一个协程服务一个fd不用担心资源问题。将监听fd的事件交由runtime来管理,实现协程调度与依赖fd的事件。当要协程读取fd数据但是没有数据时,park住该协程(改为Gwaiting),调度其他协程执行。

    在执行协程调度时候,去检查fd是否就绪,如果就绪时,调度器再通知该park住的协程fd可以处理了(改为Grunnable并加入执行队列),该协程处理fd数据,这样既减少了CPU的空耗,也实现了消息的通知,用户

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值