Golang網路模型netpoll原始碼解析

MelonTe發表於2024-11-27

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去完成核心事件監聽的操作,那麼如何將golangepoll結合起來呢?

在 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_initpoll_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 底層原理

相關文章