浅谈 go netpoll

1.先谈epoll

https://zhuanlan.zhihu.com/p/64746509

1.1 epoll定义

是I/O多路复用的一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪,就是这个文件描述符进行读写操作之前),能够通知程序进行相应的读写操作。

Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!

Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!

1.2 epoll为什么高效

这里的高校是相对于select/poll。

select/poll是需要将所有用户态的fd拷贝到内核态,每次调用select都需要将线程加入到所有监视socket的等待队列,数量巨大时这个效率比较慢。当其中监控的socket有数据到达的时候,还需要将线程从这些监控的socket队列中移除。并且返回之后,还需要将内核空间的数据(包括fd)拷贝到用户空间,然后还需要将所有的fd遍历一遍,对isset的fd进行处理。

但是epoll呢,epoll_create时创建内核高速cache区:就是建立连续的物理内存页,然后在之上建立slab层,简单的说就是物理上分配好你想要的大小的内存对象,每次使用时都是使用空闲的已分配好的对象、内核cache中建立个红黑树来存储通过epoll_ctl添加进来的fd,这些fd其实已经在内核态了,当你再次调用epoll_wait时,不需要再拷贝进内核态、内核cache中再建立就绪链表存储所有就绪的fd。epoll_ctl 时将fd添加到红黑树中(若存在则不添加)并向内核注册回调函数,当fd有数据到达的时候,会触发这个回调,然后将这个fd添加到就绪列链表中。 epoll_wait时返回就绪链表里面的数据就可以了,所以这里只需要将就绪链表的数据从内核太拷贝到用户态。

2.再谈netpoll

https://www.sohu.com/a/152233995_99930294

https://blog.csdn.net/secondtonone1/article/details/106190115

https://blog.csdn.net/secondtonone1/article/details/106234465

https://cloud.tencent.com/developer/article/1458351

https://studygolang.com/articles/21029

https://zhuanlan.zhihu.com/p/108509080

https://blog.csdn.net/u013029603/article/details/102992521

https://www.cnblogs.com/charlieroro/p/11490664.html

2.1 netpoll定义

go网络库对epoll的封装

2.2netpoll的实现

这里go的版本是go1.13.3。

我们知道epoll机制提供的接口有三个函数:创建epoll句柄int epfd = epoll_create(intsize)、将被监听的文件描述符加入创建的epoll句柄或者从epoll句柄删除或者修改被监听的文件描述符int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)、等待监听的文件描述符有事件的发生int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout)。go网络库对epoll进行封装,其实就是怎么调用这个三个系统函数,实现用户态对多个socket的监听。这里讲的主要是针对单例epoll。

2.2.1 go是怎么实现创建epoll实例并监听fd列表的呢?

入口函数在 net/http server.go  func ListenAndServe(addr string, handler Handler) error {}

从这个函数看下去,我们会发现三个非常关键的数据结构。其中一个是netFD

另外一个是FD

还有一个就是pollDesc(I/O poller)

截图中有对每一个数据结构进行描述,其中的包含关系就是netFD-->FD-->pollDesc。这三个数据结构都有一个init方法,但都是netFD的i nit调用的是FD的init,FD的init调用的是pollDesc的init,所以我们来看pollDesc的init做了什么。

可以看到pollDesc的init有调用runtime_pollServerInit和runtime_pollOpen,并且runtime_pollServerInit只调用了一次。接下来来看这两个函数的实现。

通过//go:linkname 可以看到runtime_pollServerInit的实现是在runtime netpoll.go下的poll_runtime_pollServerInit。

poll_runtime_pollServerInit调用了netpollinit。

netpollinit实现了系统调用epoll_create,在内核态创建了一个epoll实例,这里单个进程只能创建一个epoll实例。

接通过//go:linkname 可以看到runtime_pollOpen的实现是在runtime netpoll.go下的poll_runtime_pollOpen。

这里先分配得到一个pd *polldesc。

这里主要看rg和wg。rg,wg默认是0,rg表述读goroutine为pdReady表示读就绪,可以将协程恢复,为pdWait表示读阻塞,协程将要被挂起。wg也是如此。

然后再调用了netpollopen。

netpollopen实现了系统调用epoll_ctl。netpollopen把这个fd添加到了epoll表里(用红黑树存储所有添加进来的fd),同样pd作为event的data传入内核表,从而实现内核态和用户态数据的关联。

2.2.2那go在用户态是怎么实现将goroutine挂起的呢?

看go的网络库源码,我们可以看到有两种情况需要将goroutine挂起。第一种情况是accept一个新的连接的时候,如果新的连接没有数据,需要将该goroutine挂起等待新数据的到来。还有一种情况就是,对于每一个tcp连接,都会开一个goroutine去处理这个连接的数据,但是当从该连接接收不到数据的时候,我们得把goroutine挂起。这两个地方其实底层的实现都是一样的,所以我这里讲一下第一种情况go是怎么实现将goroutine挂起的呢,这里我们需要从网络库中的TCPListener数据结构的Accept方法讲起。最终的实现是在FD数据结构Accept方法。

accept接受一个新的连接,如果没有错误直接返回。当有错误EAGAIN说明当前没有连接到来,就调用waitRead等待连接。我们来看*pollDesc的waitRead。

这一层一层的调用看下里,最后调用的是gopark,此函数汇编实现,具体功能是将当前的goroutine修改为等待状态,然后执行其他的goroutine。等待IO就绪后,epoll将此goroutine修改为就绪状态。等待调度器调度。

2.2.3那go在用户态是怎么实现将goroutine唤醒的呢?

当go程序启动的时候会创建一个M去跑我们的系统监测任务,它没有和任何的P(逻辑处理器)进行绑定,而是通过自身改变睡眠时间和时间间隔来一直循环下去(代码位于runtime/proc.go)。网络轮循器就在这个M中去实现了。在runtime/proc.go中findrunnable会判断是否初始化epoll,如果初始化了则调用netpoll,从而获取glist,然后traceGoUnpark激活挂起的协程

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值