0、引言
在學習完了Socket程式設計的基礎知識、Linux系統提供的I/O多路複用的實現以及Golang的GMP排程模型之後,我們進而學習Golang的網路模型——netpoll。本文將從為什麼需要使用netpoll模型,以及netpoll的具體流程實現兩個主要角度來展開學習。當前使用的Go的版本為1.22.4,Linux系統。
1、為什麼要使用netpoll模型?
首先,什麼是多路複用?
多路,指的是存在著多個需要服務的物件;複用,指的是重複利用一個單元來為上述的多個目標提供服務。
我們知道,Linux系統為使用者提供了三個核心實現的IO多路複用技術的系統呼叫,用發展時間來排序分別為:select->poll->epoll
。其中,epoll
在當今使用的最為廣泛,對比與select
呼叫,它有以下的優勢:
fd
數量靈活:可監聽的fd
數量上限靈活,使用方可以在呼叫epoll_create
操作時自行指定。- 更少的核心複製次數:在核心中,使用紅黑樹的結構來儲存需要監聽的
fd
,相比與呼叫select
每次需要將所有的fd
複製進核心,監聽到事件後再全部複製回使用者態,epoll
只需要將需要監聽的fd
新增到事件表後,即可多次監聽。 - 返回結果明確:
epoll
執行將就緒事件新增到就緒事件列表中,當使用者呼叫epoll_wait
操作時,核心只返回就緒事件,而select
返回的是所有的事件,需要使用者再進行一次遍歷,找到就緒事件再處理。
需要注意的是,在不同的條件環境下,epoll的優勢可能反而作用不明顯。epoll只適用在監聽fd基數較大且活躍度不高的場景,如此epoll事件表的空間複用和epoll_wait操作的精準才能體現出其優勢;而當處在fd基數較小且活躍度高的場景下,select反而更加簡單有效,構造epoll的紅黑樹結構的消耗會成為其累贅。
考慮到場景的多樣性,我們會選擇使用epoll
去完成核心事件監聽的操作,那麼如何將golang
和epoll
結合起來呢?
在 Go 語言的併發模型中,GMP 框架實現了一種高效的協程排程機制,它遮蔽了作業系統執行緒的細節,使用者可以透過輕量級的 Goroutine 來實現細粒度的併發操作。然而,底層的 IO 多路複用機制(如 Linux 的 epoll)排程的單位仍然是執行緒(M)。為了將 IO 排程從執行緒層面提升到協程層面,充分發揮 Goroutine 的高併發優勢,netpoll 應運而生。
接下來我們就來學習netpoll
框架的實現。
2、netpoll實現原理
2.1、核心結構
1、pollDesc
為了將IO排程從執行緒提升到協程層面,netpoll
框架有個重要的核心結構pollDesc
,它有兩個,一個為表層,含有指標指向了裡層的pollDesc
。本文中講到的pollDesc
都為裡層pollDesc
。
表層pollDesc
定位在internel/poll/fd_poll_runtime.go
檔案中:
type pollDesc struct {
runtimeCtx uintptr
}
使用一個runtimeCtx
指標指向其底層實現例項。
裡層的位於runtime/netpoll.go
中。
//網路poller描述符
type pollDesc struct {
//next指標,指向在pollCache連結串列結構中,以下個pollDesc例項。
link *pollDesc
//指向fd
fd uintptr
//讀事件狀態標識器,狀態有四種:
//1、pdReady:表示讀操作已就緒,等待處理
//2、pdWait:表示g將要被阻塞等待讀操作就緒,此時還未阻塞
//3、g:讀操作的g已經被阻塞,rg指向阻塞的g例項
//4、pdNil:空
rg atomic.Uintptr
wg atomic.Uintptr
//...
}
pollDesc
的核心欄位是讀/寫標識器rg/wg
,它用於標識fd的io事件狀態,並且持有被阻塞的g例項。當後續需要喚醒這個g處理讀寫事件的時候,可以透過pollDesc
追溯得到g的例項進行操作。有了pollDesc
這個資料結構,Golang就能將對處理socket的排程單位從執行緒Thread
轉換成協程G
。
2、pollCache
pollCache
緩衝池採用了單向連結串列的方式儲存多個pollDesc
例項。
type pollCache struct {
lock mutex
first *pollDesc
}
其包含了兩個核心方法,分別是alloc()
和free()
//從pollCache中分配得到一個pollDesc例項
func (c *pollCache) alloc() *pollDesc {
lock(&c.lock)
//如果連結串列為空,則進行初始化
if c.first == nil {
//pdSize = 248
const pdSize = unsafe.Sizeof(pollDesc{})
//4096 / 248 = 16
n := pollBlockSize / pdSize
if n == 0 {
n = 1
}
//分配指定大小的記憶體空間
mem := persistentalloc(n*pdSize, 0, &memstats.other_sys)
//完成指定數量的pollDesc建立
for i := uintptr(0); i < n; i++ {
pd := (*pollDesc)(add(mem, i*pdSize))
pd.link = c.first
c.first = pd
}
}
pd := c.first
c.first = pd.link
lockInit(&pd.lock, lockRankPollDesc)
unlock(&c.lock)
return pd
}
//free用於將一個pollDesc放回pollCache
func (c *pollCache) free(pd *pollDesc) {
//...
lock(&c.lock)
pd.link = c.first
c.first = pd
unlock(&c.lock)
}
2.2、netpoll框架宏觀流程
在宏觀的角度下,netpoll框架主要涉及了以下的幾個流程:
poll_init
:底層呼叫epoll_create
指令,在核心態中開闢epoll事件表。poll_open
:先構造一個pollDesc例項,然後透過epoll_ctl(ADD)
指令,向核心中新增要監聽的socket,並將這一個fd繫結在pollDesc中。pollDesc含有狀態標識器rg/wg
,用於標識事件狀態以及儲存阻塞的g。poll_wait
:當g依賴的事件未就緒時,呼叫gopark
方法,將g置為阻塞態存放在pollDesc中。net_poll
:GMP排程器會輪詢netpoll流程,通常會用非阻塞的方式發起epoll_wait
指令,取出就緒的pollDesc,提前出其內部陷入阻塞態的g然後將其重新新增到GMP的排程佇列中。(以及在sysmon流程和gc流程都會觸發netpoll)
3、流程原始碼實現
3.1、流程入口
我們參考以下的簡易TCP伺服器實現框架,走進netpoll框架的具體原始碼實現。
// 啟動 tcp server 程式碼示例
func main() {
//建立TCP埠監聽器,涉及以下事件:
//1:建立socket fd,呼叫bind和accept系統介面函式
//2:呼叫epoll_create,建立eventpool
//3:呼叫epoll_ctl(ADD),將socket fd註冊到epoll事件表
l, _ := net.Listen("tcp", ":8080")
// eventloop reactor 模型
for {
//等待TCP連線到達,涉及以下事件:
//1:迴圈+非阻塞呼叫accept
//2:若未就緒,則呼叫gopark進行阻塞
//3:等待netpoller輪詢喚醒
//4:獲取到conn fd後註冊到eventpool
//5:返回conn
conn, _ := l.Accept()
// goroutine per conn
go serve(conn)
}
}
// 處理一筆到來的 tcp 連線
func serve(conn net.Conn) {
//關閉conn,從eventpool中移除fd
defer conn.Close()
var buf []byte
//讀取conn中的資料,涉及以下事件:
//1:迴圈+非阻塞呼叫recv(read)
//2:若未就緒,透過gopark阻塞,等待netpoll輪詢喚醒
_, _ = conn.Read(buf)
//向conn中寫入資料,涉及以下事件:
//1:迴圈+非阻塞呼叫writev (write)
//2:若未就緒,透過gopark阻塞,等待netpoll輪詢喚醒
_, _ = conn.Write(buf)
}
3.2、Socket建立
以net.Listen
方法為入口,進行建立socket fd
,呼叫的方法棧如下:
方法 | 檔案 |
---|---|
net.Listen() | net/dial.go |
net.ListenConfig.Listen() | net/dial.go |
net.sysListener.listenTCP() | net/tcpsock_posix.go |
net.internetSocket() | net/ipsock_posix.go |
net.socket() | net/sock_posix.go |
核心的呼叫在net.socket()
方法內,原始碼核心流程如下:
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) (fd *netFD, err error) {
//進行socket系統呼叫,建立一個socket
s, err := sysSocket(family, sotype, proto)
//繫結socket fd
fd, err = newFD(s, family, sotype, net);
//...
//進行了以下事件:
//1、透過syscall bind指令繫結socket的監聽地址
//2、透過syscall listen指令發起對socket的監聽
//3、完成epollEvent表的建立(全域性執行一次)
//4、將socket fd註冊到epoll事件表中,監聽讀寫就緒事件
err := fd.listenStream(ctx, laddr, listenerBacklog(), ctrlCtxFn);
}
首先先執行了sysSocket
系統呼叫,建立一個socket
,它是一個整數值,用於標識作業系統中開啟的檔案或網路套接字;接著呼叫newFD
方法包裝成netFD
物件,以便實現更高效的非同步 IO 和 Goroutine 排程。
3.3、poll_init
緊接3.2中的net.socket
方法,在內部還呼叫了net.netFD.listenStream()
,poll_init
的呼叫棧如下:
方法 | 檔案 |
---|---|
net.netFD.listenStream() | net/sock_posix.go |
net.netFD.init() | net/fd_unix.go |
poll.FD.init() | internal/poll/fd_unix.go |
poll.pollDesc.init() | internal/poll/fd_poll_runtime.go |
runtime.poll_runtime_pollServerInit() | runtime/netpoll.go |
runtime.netpollinit() | runtime/netpoll_epoll.go |
net.netFD.listenStream()
核心步驟如下:
func (fd *netFD) listenStream(ctx context.Context, laddr sockaddr, backlog int, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) error {
//....
//透過Bind系統呼叫繫結監聽地址
if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
return os.NewSyscallError("bind", err)
}
//透過Listen系統呼叫對socket進行監聽
if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
return os.NewSyscallError("listen", err)
}
//fd.init()進行了以下操作:
//1、完成eventPool的建立
//2、將socket fd註冊到epoll事件表中
if err = fd.init(); err != nil {
return err
}
//...
return nil
}
- 使用
Bind
系統呼叫繫結需要監聽的地址 - 使用
Listen
系統呼叫監聽socket - 呼叫
fd.init
完成eventpool
的建立以及fd的註冊
net.netFD.init()
方法在內部轉而呼叫poll.FD.init()
func (fd *netFD) init() error {
return fd.pfd.Init(fd.net, true)
}
func (fd *FD) Init(net string, pollable bool) error {
fd.SysFile.init()
// We don't actually care about the various network types.
if net == "file" {
fd.isFile = true
}
if !pollable {
fd.isBlocking = 1
return nil
}
err := fd.pd.init(fd)
if err != nil {
// If we could not initialize the runtime poller,
// assume we are using blocking mode.
fd.isBlocking = 1
}
return err
}
然後又轉入到poll.pollDesc.init()
的呼叫中。
func (pd *pollDesc) init(fd *FD) error {
//透過sysOnce結構,完成epoll事件表的唯一一次建立
serverInit.Do(runtime_pollServerInit)
//完成init後,進行poll_open
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
//...
//繫結裡層的pollDesc例項
pd.runtimeCtx = ctx
return nil
}
這裡的poll.pollDesc
是表層pollDesc
,表層pd的init是poll_init
和poll_open
流程的入口:
- 執行
serverInit.Do(runtime_pollServerInit)
,其中serverInit
是名為sysOnce
的特殊結構,它會保證執行的方法在全域性只會被執行一次,然後執行runtime_pollServerInit
,完成poll_init
操作 - 完成
poll_init
後,呼叫runtime_pollOpen(uintptr(fd.Sysfd))
將fd加入到eventpool
中,完成poll_open
操作 - 繫結裡層的
pollDesc
例項
我們先來關注serverInit.Do(runtime_pollServerInit)
中,執行的runtime_pollServerInit
方法,它定位在runtime/netpoll.go
下:
//go:linkname poll_runtime_pollServerInit internal/poll.runtime_pollServerInit
func poll_runtime_pollServerInit() {
netpollGenericInit()
}
func netpollGenericInit() {
if netpollInited.Load() == 0 {
lockInit(&netpollInitLock, lockRankNetpollInit)
lock(&netpollInitLock)
if netpollInited.Load() == 0 {
//進入netpollinit呼叫
netpollinit()
netpollInited.Store(1)
}
unlock(&netpollInitLock)
}
}
func netpollinit() {
var errno uintptr
//進行epollcreate系統呼叫,建立epoll事件表
epfd, errno = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
//...
//建立pipe管道,接收訊號,如程式終止:
//r:訊號接收端,會註冊對應的read事件到epoll事件表中
//w:訊號傳送端,有訊號到達的時候,會往w傳送訊號,並對r產生讀就緒事件
r, w, errpipe := nonblockingPipe()
//...
//在epollEvent中註冊監聽r的讀就緒事件
ev := syscall.EpollEvent{
Events: syscall.EPOLLIN,
}
*(**uintptr)(unsafe.Pointer(&ev.Data)) = &netpollBreakRd
errno = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, r, &ev)
//...
//使用全域性變數快取pipe的讀寫端
netpollBreakRd = uintptr(r)
netpollBreakWr = uintptr(w)
}
在netpollinit()
方法內部,進行了以下操作:
-
執行
epoll_create
指令建立了epoll事件表,並返回epoll檔案描述符epfd
。 -
建立了兩個pipe管道,當向w端寫入訊號的時候,r端會發生讀就緒事件。
-
註冊監聽r的讀就緒事件。
-
快取管道。
在這裡,我們建立了兩個管道r
以及w
,並且在eventpool
中註冊了r的讀就緒事件的監聽,當我們向w管道寫入資料的時候,r管道就會產生讀就緒事件,從而打破阻塞的epoll_wait操作,進而執行其他的操作。
3.3、poll_open
方法 | 檔案 |
---|---|
net.netFD.listenStream() | net/sock_posix.go |
net.netFD.init() | net/fd_unix.go |
poll.FD.init() | internal/poll/fd_unix.go |
poll.pollDesc.init() | internal/poll/fd_poll_runtime.go |
runtime.poll_runtime_pollOpen() | runtime/netpoll.go |
runtime.netpollopen | runtime/netpoll_epoll.go |
在poll.pollDesc.init()
方法中,完成了poll_init
流程後,就會進入到poll_open
流程,執行runtime.poll_runtime_pollOpen()
。
//go:linkname poll_runtime_pollOpen internal/poll.runtime_pollOpen
func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
//獲取一個pollDesc例項
pd := pollcache.alloc()
lock(&pd.lock)
wg := pd.wg.Load()
if wg != pdNil && wg != pdReady {
throw("runtime: blocked write on free polldesc")
}
rg := pd.rg.Load()
if rg != pdNil && rg != pdReady {
throw("runtime: blocked read on free polldesc")
}
//繫結socket fd到pollDesc中
pd.fd = fd
//...
//初始化讀寫狀態標識器為無狀態
pd.rg.Store(pdNil)
pd.wg.Store(pdNil)
//...
unlock(&pd.lock)
//將fd新增進epoll事件表中
errno := netpollopen(fd, pd)
//...
//返回pollDesc例項
return pd, 0
}
func netpollopen(fd uintptr, pd *pollDesc) uintptr {
var ev syscall.EpollEvent
//透過epollctl操作,在EpollEvent中註冊針對fd的監聽事件
//操作型別宏指令:EPOLL_CTL_ADD——新增fd並註冊監聽事件
//事件型別:epollevent.events:
//1、EPOLLIN:監聽讀就緒事件
//2、EPOLLOUT:監聽寫就緒事件
//3、EPOLLRDHUP:監聽中斷事件
//4、EPOLLET:使用邊緣觸發模式
ev.Events = syscall.EPOLLIN | syscall.EPOLLOUT | syscall.EPOLLRDHUP | syscall.EPOLLET
tp := taggedPointerPack(unsafe.Pointer(pd), pd.fdseq.Load())
*(*taggedPointer)(unsafe.Pointer(&ev.Data)) = tp
return syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, int32(fd), &ev)
}
不僅在net.Listen()
流程中會觸發poll open
,在net.Listener.Accept
流程中也會,當我們獲取到了連線之後,也需要為這個連線封裝成一個pollDesc
例項,然後執行poll_open
流程將其註冊到epoll事件表中。
func (fd *netFD) accept()(netfd *netFD, err error){
// 透過 syscall accept 接收到來的 conn fd
d, rsa, errcall, err := fd.pfd.Accept()
// ...
// 封裝到來的 conn fd
netfd, err = newFD(d, fd.family, fd.sotype, fd.net)
// 將 conn fd 註冊到 epoll 事件表中
err = netfd.init()
// ...
return netfd,nil
}
3.4、poll_close
當連線conn需要關閉的時候,最終會進入到poll_close
流程,執行epoll_ctl(DELETE)
刪除對應的fd。
方法 | 檔案 |
---|---|
net.conn.Close | net/net.go |
net.netFD.Close | net/fd_posix.go |
poll.FD.Close | internal/poll/fd_unix.go |
poll.FD.decref | internal/poll/fd_mutex.go |
poll.FD.destroy | internal/poll/fd_unix.go |
poll.pollDesc.close | internal/poll/fd_poll_runtime.go |
poll.runtime_pollClose | internal/poll/fd_poll_runtime.go |
runtime.poll_runtime_pollClose | runtime/netpoll.go |
runtime.netpollclose | runtime/netpoll_epoll.go |
syscall.EpollCtl | runtime/netpoll_epoll.go |
//go:linkname poll_runtime_pollClose internal/poll.runtime_pollClose
func poll_runtime_pollClose(pd *pollDesc) {
if !pd.closing {
throw("runtime: close polldesc w/o unblock")
}
wg := pd.wg.Load()
if wg != pdNil && wg != pdReady {
throw("runtime: blocked write on closing polldesc")
}
rg := pd.rg.Load()
if rg != pdNil && rg != pdReady {
throw("runtime: blocked read on closing polldesc")
}
netpollclose(pd.fd)
pollcache.free(pd)
}
func netpollclose(fd uintptr) uintptr {
var ev syscall.EpollEvent
return syscall.EpollCtl(epfd, syscall.EPOLL_CTL_DEL, int32(fd), &ev)
}
3.5、poll_wait
poll_wait
流程最終會執行gopark
將g陷入到使用者態阻塞。
方法 | 檔案 |
---|---|
poll.pollDesc.wait | internal/poll/fd_poll_runtime.go |
poll.runtime_pollWait | internal/poll/fd_poll_runtime.go |
runtime.poll_runtime_pollWait | runtime/netpoll.go |
runtime.netpollblock | runtime/netpoll.go |
runtime.gopark | runtime/proc.go |
runtime.netpollblockcommit | runtime/netpoll.go |
在表層pollDesc
中,會透過其內部的裡層pollDesc
指標,呼叫到runtime
下的netpollblock
方法。
/*
針對某個 pollDesc 例項,監聽指定的mode 就緒事件
- 返回true——已就緒 返回false——因超時或者關閉導致中斷
- 其他情況下,會透過 gopark 操作將當前g 阻塞在該方法中
*/
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
//針對mode事件,獲取相應的狀態
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}
for {
//關心的io事件就緒,直接返回
if gpp.CompareAndSwap(pdReady, pdNil) {
return true
}
//關心的io事件未就緒,則置為等待狀態,G將要被阻塞
if gpp.CompareAndSwap(pdNil, pdWait) {
break
}
//...
}
//...
//將G置為阻塞態
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceBlockNet, 5)
//當前g從阻塞態被喚醒,重置標識器
old := gpp.Swap(pdNil)
if old > pdWait {
throw("runtime: corrupted polldesc")
}
//判斷是否是因為所關心的事件觸發而喚醒
return old == pdReady
}
在gopark方法中,會閉包呼叫netpollblockcommit
方法,其中會根據g關心的事件型別,將其例項儲存到pollDesc的rg或wg容器
中。
// 將 gpp 狀態標識器的值由 pdWait 修改為當前 g
func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
if r {
//增加等待輪詢器的例程計數。
//排程器使用它來決定是否阻塞
//如果沒有其他事情可做,則等待輪詢器。
netpollAdjustWaiters(1)
}
return r
}
接著我們來關注何時會觸發poll_wait
流程。
首先是在listener.Accept
流程中,如果當前尚未有連線到達,則執行poll wait
將當前g阻塞掛載在該socket fd對應pollDesc的rg
中。
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
//...
for {
//以非阻塞模式發起一次accept,嘗試接收conn
s, rsa, errcall, err := accept(fd.Sysfd)
if err == nil {
return s, rsa, "", err
}
switch err {
//忽略中斷類錯誤
case syscall.EINTR:
continue
//尚未有到達的conn
case syscall.EAGAIN:
//進入poll_wait流程,監聽fd的讀就緒事件,當有conn到達表現為fd可讀。
if fd.pd.pollable() {
//假如讀操作未就緒,當前g會被阻塞在方法內部,直到因為超時或者就緒被netpoll ready喚醒。
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
//...
}
}
// 指定 mode 為 r 標識等待的是讀就緒事件,然後走入更底層的 poll_wait 流程
func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}
其次分別是在conn.Read
/conn.Write
流程中,假若conn fd下讀操作未就緒(無資料到達)/寫操作未就緒(緩衝區空間不足),則會執行poll wait將g阻塞並掛載在對應的pollDesc中的rg/wg
中。
func (fd *FD) Read(p []byte) (int, error) {
//...
for {
//非阻塞模式進行一次read呼叫
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
//進入poll_wait流程,並標識關心讀就緒事件
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, err
}
}
func (fd *FD)Write(p []byte)(int,error){
// ...
for{
// ...
// 以非阻塞模式執行一次syscall write操作
n, err := ignoringEINTRIO(syscall.Write, fd.Sysfd, p[nn:max])
if n >0{
nn += n
}
// 緩衝區內容都已寫完,直接退出
if nn ==len(p){
return nn, err
}
// 走入 poll_wait 流程,並標識關心的是該 fd 的寫就緒事件
if err == syscall.EAGAIN && fd.pd.pollable(){
// 倘若寫操作未就緒,當前g 會 park 阻塞在該方法內部,直到因超時或者事件就緒而被 netpoll ready 喚醒
if err = fd.pd.waitWrite(fd.isFile); err ==nil{
continue
}
}
// ...
}
3.6、net_poll
netpoll
流程至關重要,它會在底層呼叫系統的epoll_wait
操作,找到觸發事件的fd,然後再逆向找到繫結fd的pollDesc
例項,返回內部阻塞的g叫給上游處理喚醒。其呼叫棧如下:
方法 | 檔案 |
---|---|
runtime.netpoll | runtime/netpoll_epoll.go |
runtime.netpollready | runtime/netpoll.go |
runtime.netpollunblock | runtime/netpoll.go |
netpoll
具體的原始碼如下:
//netpoll用於輪詢檢查是否有就緒的io事件
//若發現了就緒的io事件,檢查是否有pollDesc中的g關心其事件
//若找到了關心其io事件就緒的g,新增到list返回給上游處理
func netpoll(delay int64) (gList, int32) {
if epfd == -1 {
return gList{}, 0
}
var waitms int32
//根據傳入的delay引數,決定呼叫epoll_wait的模式:
//delay < 0:設為阻塞模式(在 gmp 排程流程中,如果某個 p 遲遲獲取不到可執行的 g 時,會透過該模式,使得 thread 陷入阻塞態,但該情況全域性最多僅有一例)
//delay = 0:設為非阻塞模式(通常情況下為此模式,包括 gmp 常規排程流程、gc 以及全域性監控執行緒 sysmon 都是以此模式觸發的 netpoll 流程)
//delay > 0:設為超時模式(在 gmp 排程流程中,如果某個 p 遲遲獲取不到可執行的 g 時,並且透過 timer 啟動了定時任務時,會令 thread 以超時模式執行 epoll_wait 操作)
if delay < 0 {
waitms = -1
} else if delay == 0 {
waitms = 0
} else if delay < 1e6 {
waitms = 1
} else if delay < 1e15 {
waitms = int32(delay / 1e6)
} else {
waitms = 1e9
}
//最多接收128個io就緒事件
var events [128]syscall.EpollEvent
retry:
//以指定模式呼叫epoll_wait
n, errno := syscall.EpollWait(epfd, events[:], int32(len(events)), waitms)
//...
//儲存關心io事件就緒的G例項
var toRun gList
delta := int32(0)
//遍歷返回的就緒事件
for i := int32(0); i < n; i++ {
ev := events[i]
if ev.Events == 0 {
continue
}
//pipe接收端的訊號處理,檢查是否需要退出netpoll
if *(**uintptr)(unsafe.Pointer(&ev.Data)) == &netpollBreakRd {
if ev.Events != syscall.EPOLLIN {
println("runtime: netpoll: break fd ready for", ev.Events)
throw("runtime: netpoll: break fd ready for something unexpected")
}
//...
continue
}
var mode int32
//記錄io就緒事件的型別
if ev.Events&(syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {
mode += 'r'
}
if ev.Events&(syscall.EPOLLOUT|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {
mode += 'w'
}
// 根據 epollevent.data 獲取到監聽了該事件的 pollDesc 例項
if mode != 0 {
tp := *(*taggedPointer)(unsafe.Pointer(&ev.Data))
pd := (*pollDesc)(tp.pointer())
//...
//檢查是否為G所關心的事件
delta += netpollready(&toRun, pd, mode)
}
}
return toRun, delta
}
func netpollready(toRun *gList, pd *pollDesc, mode int32) int32 {
delta := int32(0)
var rg, wg *g
if mode == 'r' || mode == 'r'+'w' {
//就緒事件包含讀就緒,嘗試喚醒pd內部的rg
rg = netpollunblock(pd, 'r', true, &delta)
}
if mode == 'w' || mode == 'r'+'w' {
//就緒事件包含讀就緒,嘗試喚醒pd內部的wg
wg = netpollunblock(pd, 'w', true, &delta)
}
//存在G例項,則加入list中
if rg != nil {
toRun.push(rg)
}
if wg != nil {
toRun.push(wg)
}
return delta
}
func netpollunblock(pd *pollDesc, mode int32, ioready bool, delta *int32) *g {
//獲取儲存的g例項
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}
for {
old := gpp.Load()
//...
new := pdNil
if ioready {
new = pdReady
}
//將gpp的值從g置換成pdReady
if gpp.CompareAndSwap(old, new) {
if old == pdWait {
old = pdNil
} else if old != pdNil {
*delta -= 1
}
//返回需要喚醒的g例項
return (*g)(unsafe.Pointer(old))
}
}
}
那麼,我們也同樣需要關注在哪個環節進入了net_poll
流程。
首先,是在GMP排程器中的findRunnable
方法中被呼叫,用於找到可執行的G例項。具體的實現在之前的GMP排程文章中有講解,這裡只關心涉及到net_poll
方面的原始碼。
findRunnable
方法定位在runtime/proc.go
中
func findRunnable()(gp *g, inheritTime, tryWakeP bool){
// ..
/*
同時滿足下述三個條件,發起一次【非阻塞模式】的 netpoll 流程:
- epoll事件表初始化過
- 有 g 在等待io 就緒事件
- 沒有空閒 p 在以【阻塞或超時】模式發起 netpoll 流程
*/
if netpollinited()&& atomic.Load(&netpollWaiters)>0&& atomic.Load64(&sched.lastpoll)!=0{
// 以非阻塞模式發起一輪 netpoll,如果有 g 需要喚醒,一一喚醒之,並返回首個 g 給上層進行排程
if list := netpoll(0);!list.empty(){// non-blocking
// 獲取就緒 g 佇列中的首個 g
gp := list.pop()
// 將就緒 g 佇列中其餘 g 一一置為就緒態,並新增到全域性佇列
injectglist(&list)
// 把首個g 也置為就緒態
casgstatus(gp,_Gwaiting,_Grunnable)
// ...
//返回 g 給當前 p進行排程
return gp,false,false
}
}
// ...
/*
同時滿足下述三個條件,發起一次【阻塞或超時模式】的 netpoll 流程:
- epoll事件表初始化過
- 有 g 在等待io 就緒事件
- 沒有空閒 p 在以【阻塞或超時】模式發起 netpoll 流程
*/
if netpollinited()&&(atomic.Load(&netpollWaiters)>0|| pollUntil !=0)&& atomic.Xchg64(&sched.lastpoll,0)!=0{
// 預設為阻塞模式
delay :=int64(-1)
// 存在定時時間,則設為超時模式
if pollUntil !=0{
delay = pollUntil - now
// ...
}
// 以【阻塞或超時模式】發起一輪 netpoll
list := netpoll(delay)// block until new work is available
}
// ...
}
其次,是位於同檔案下的sysmon
方法中,它會被一個全域性監控者G執行,每隔10ms發一次非阻塞的net_poll流程。
// The main goroutine.
func main(){
// ...
// 新建一個 m,直接執行 sysmon 函式
systemstack(func(){
newm(sysmon,nil,-1)
})
// ...
}
// 全域性唯一監控執行緒的執行函式
func sysmon(){
// ...
for{
// ...
/*
同時滿足下述三個條件,發起一次【非阻塞模式】的 netpoll 流程:
- epoll事件表初始化過
- 沒有空閒 p 在以【阻塞或超時】模式發起 netpoll 流程
- 距離上一次發起 netpoll 流程的時間間隔已超過 10 ms
*/
lastpoll :=int64(atomic.Load64(&sched.lastpoll))
if netpollinited()&& lastpoll !=0&& lastpoll+10*1000*1000< now {
// 以非阻塞模式發起 netpoll
list := netpoll(0)// non-blocking - returns list of goroutines
// 獲取到的 g 置為就緒態並新增到全域性佇列中
if!list.empty(){
// ...
injectglist(&list)
// ...
}
}
// ...
}
}
最後,還會發生在GC流程中。
func pollWork() bool{
// ...
// 若全域性佇列或 p 的本地佇列非空,則提前返回
/*
同時滿足下述三個條件,發起一次【非阻塞模式】的 netpoll 流程:
- epoll事件表初始化過
- 有 g 在等待io 就緒事件
- 沒有空閒 p 在以【阻塞或超時】模式發起 netpoll 流程
*/
if netpollinited()&& atomic.Load(&netpollWaiters)>0&& sched.lastpoll !=0{
// 所有取得 g 更新為就緒態並新增到全域性佇列
if list := netpoll(0);!list.empty(){
injectglist(&list)
return true
}
}
// ...
}
4、參考博文
感謝觀看,本篇博文參考了小徐先生的文章,非常推薦大家去觀看並且進入到原始碼中學習,連結如下:
萬字解析 golang netpoll 底層原理