• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            Fork me on GitHub
            隨筆 - 215  文章 - 13  trackbacks - 0
            <2017年6月>
            28293031123
            45678910
            11121314151617
            18192021222324
            2526272829301
            2345678


            專注即時通訊及網游服務端編程
            ------------------------------------
            Openresty 官方模塊
            Openresty 標準模塊(Opm)
            Openresty 三方模塊
            ------------------------------------
            本博收藏大部分文章為轉載,并在文章開頭給出了原文出處,如有再轉,敬請保留相關信息,這是大家對原創作者勞動成果的自覺尊重!!如為您帶來不便,請于本博下留言,謝謝配合。

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            相冊

            Awesome

            Blog

            Book

            GitHub

            Link

            搜索

            •  

            積分與排名

            • 積分 - 215445
            • 排名 - 118

            最新評論

            閱讀排行榜

            http://skoo.me/go/2014/04/21/go-net-core

            Go語言的出現,讓我見到了一門語言把網絡編程這件事情給做“正確”了,當然,除了Go語言以外,還有很多語言也把這件事情做”正確”了。我一直堅持著這樣的理念——要做"正確"的事情,而不是"高性能"的事情;很多時候,我們在做系統設計、技術選型的時候,都被“高性能”這三個字給綁架了,當然不是說性能不重要,你懂的。


            目前很多高性能的基礎網絡服務器都是采用的C語言開發的,比如:Nginx、Redis、memcached等,它們都是基于”事件驅動 + 事件回掉函數”的方式實現,也就是采用epoll等作為網絡收發數據包的核心驅動。不少人(包括我自己)都認為“事件驅動 + 事件回掉函數”的編程方法是“反人類”的;因為大多數人都更習慣線性的處理一件事情,做完第一件事情再做第二件事情,并不習慣在N件事情之間頻繁的切換干活。為了解決程序員在開發服務器時需要自己的大腦不斷的“上下文切換”的問題,Go語言引入了一種用戶態線程goroutine來取代編寫異步的事件回掉函數,從而重新回歸到多線程并發模型的線性、同步的編程方式上。
            用Go語言寫一個最簡單的echo服務器:
            package main
            import (
            "log"
            "net"
            )
            func main() {
            ln, err := net.Listen("tcp", ":8080")
            if err != nil {
                    log.Println(err)
                    return
            }
            for {
                    conn, err := ln.Accept()
                    if err != nil {
                        log.Println(err)
                        continue
                    }
                    go echoFunc(conn)
            }
            }
            func echoFunc(c net.Conn) {
            buf := make([]byte, 1024)
            for {
                    n, err := c.Read(buf)
                    if err != nil {
                        log.Println(err)
                        return
                    }
                    c.Write(buf[:n])
            }
            }
            main函數的過程就是首先創建一個監聽套接字,然后用一個for循環不斷的從監聽套接字上Accept新的連接,最后調用echoFunc函數在建立的連接上干活。關鍵代碼是:
            go echoFunc(conn)
            每收到一個新的連接,就創建一個“線程”去服務這個連接,因此所有的業務邏輯都可以同步、順序的編寫到echoFunc函數中,再也不用去關心網絡IO是否會阻塞的問題。不管業務多復雜,Go語言的并發服務器的編程模型都是長這個樣子。可以肯定的是,在linux上Go語言寫的網絡服務器也是采用的epoll作為最底層的數據收發驅動,Go語言網絡的底層實現中同樣存在“上下文切換”的工作,只是這個切換工作由runtime的調度器來做了,減少了程序員的負擔。
            弄明白網絡庫的底層實現,貌似只要弄清楚echo服務器中的Listen、Accept、Read、Write四個函數的底層實現關系就可以了。本文將采用自底向上的方式來介紹,也就是從最底層到上層的方式,這也是我閱讀源碼的方式。底層實現涉及到的核心源碼文件主要有:
            net/fd_unix.go 
            net/fd_poll_runtime.go
            runtime/netpoll.goc 
            runtime/netpoll_epoll.c 
            runtime/proc.c (調度器)
            netpoll_epoll.c文件是Linux平臺使用epoll作為網絡IO多路復用的實現代碼,這份代碼可以了解到epoll相關的操作(比如:添加fd到epoll、從epoll刪除fd等),只有4個函數,分別是runtime·netpollinit、runtime·netpollopen、runtime·netpollclose和runtime·netpoll。init函數就是創建epoll對象,open函數就是添加一個fd到epoll中,close函數就是從epoll刪除一個fd,netpoll函數就是從epoll wait得到所有發生事件的fd,并將每個fd對應的goroutine(用戶態線程)通過鏈表返回。用epoll寫過程序的人應該都能理解這份代碼,沒什么特別之處。
            void
            runtime·netpollinit(void)
            {
            epfd = runtime·epollcreate1(EPOLL_CLOEXEC);
            if(epfd >= 0)
            return;
            epfd = runtime·epollcreate(1024);
            if(epfd >= 0) {
            runtime·closeonexec(epfd);
            return;
            }
            runtime·printf("netpollinit: failed to create descriptor (%d)\n", -epfd);
            runtime·throw("netpollinit: failed to create descriptor");
            }
            runtime·netpollinit函數首先使用runtime·epollcreate1創建epoll實例,如果沒有創建成功,就換用runtime·epollcreate再創建一次。這兩個create函數分別等價于glibc的epoll_create1和epoll_create函數。只是因為Go語言并沒有直接使用glibc,而是自己封裝的系統調用,但功能是等價于glibc的。可以通過man手冊查看這兩個create的詳細信息。
            int32
            runtime·netpollopen(uintptr fd, PollDesc *pd)
            {
            EpollEvent ev;
            int32 res;
            ev.events = EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET;
            ev.data = (uint64)pd;
            res = runtime·epollctl(epfd, EPOLL_CTL_ADD, (int32)fd, &ev);
            return -res;
            }
            添加fd到epoll中的runtime·netpollopen函數可以看到每個fd一開始都關注了讀寫事件,并且采用的是邊緣觸發,除此之外還關注了一個不常見的新事件EPOLLRDHUP,這個事件是在較新的內核版本添加的,目的是解決對端socket關閉,epoll本身并不能直接感知到這個關閉動作的問題。注意任何一個fd在添加到epoll中的時候就關注了EPOLLOUT事件的話,就立馬產生一次寫事件,這次事件可能是多余浪費的。
            epoll操作的相關函數都會在事件驅動的抽象層中去調用,為什么需要這個抽象層呢?原因很簡單,因為Go語言需要跑在不同的平臺上,有Linux、Unix、Mac OS X和Windows等,所以需要靠事件驅動的抽象層來為網絡庫提供一致的接口,從而屏蔽事件驅動的具體平臺依賴實現。runtime/netpoll.goc源文件就是整個事件驅動抽象層的實現,抽象層的核心數據結構是:
            struct PollDesc
            {
            PollDesc* link; // in pollcache, protected by pollcache.Lock
            Lock; // protectes the following fields
            uintptr fd;
            bool closing;
            uintptr seq; // protects from stale timers and ready notifications
            G* rg; // G waiting for read or READY (binary semaphore)
            Timer rt; // read deadline timer (set if rt.fv != nil)
            int64 rd; // read deadline
            G* wg; // the same for writes
            Timer wt;
            int64 wd;
            };
            每個添加到epoll中的fd都對應了一個PollDesc結構實例,PollDesc維護了讀寫此fd的goroutine這一非常重要的信息。可以大膽的推測一下,網絡IO讀寫操作的實現應該是:當在一個fd上讀寫遇到EAGAIN錯誤的時候,就將當前goroutine存儲到這個fd對應的PollDesc中,同時將goroutine給park住,直到這個fd上再此發生了讀寫事件后,再將此goroutine給ready激活重新運行。事實上的實現大概也是這個樣子的。
            事件驅動抽象層主要干的事情就是將具體的事件驅動實現(比如: epoll)通過統一的接口封裝成Go接口供net庫使用,主要的接口也是:創建事件驅動實例、添加fd、刪除fd、等待事件以及設置DeadLine。runtime_pollServerInit負責創建事件驅動實例,runtime_pollOpen將分配一個PollDesc實例和fd綁定起來,然后將fd添加到epoll中,runtime_pollClose就是將fd從epoll中刪除,同時將刪除的fd綁定的PollDesc實例刪除,runtime_pollWait接口是至關重要的,這個接口一般是在非阻塞讀寫發生EAGAIN錯誤的時候調用,作用就是park當前讀寫的goroutine。
            runtime中的epoll事件驅動抽象層其實在進入net庫后,又被封裝了一次,這一次封裝從代碼上看主要是為了方便在純Go語言環境進行操作,net庫中的這次封裝實現在net/fd_poll_runtime.go文件中,主要是通過pollDesc對象來實現的:
            type pollDesc struct {
            runtimeCtx uintptr
            }
            注意:此處的pollDesc對象不是上文提到的runtime中的PollDesc,相反此處pollDesc對象的runtimeCtx成員才是指向的runtime的PollDesc實例。pollDesc對象主要就是將runtime的事件驅動抽象層給再封裝了一次,供網絡fd對象使用。
            var serverInit sync.Once
            func (pd *pollDesc) Init(fd *netFD) error {
            serverInit.Do(runtime_pollServerInit)
            ctx, errno := runtime_pollOpen(uintptr(fd.sysfd))
            if errno != 0 {
            return syscall.Errno(errno)
            }
            pd.runtimeCtx = ctx
            return nil
            }
            pollDesc對象最需要關注的就是其Init方法,這個方法通過一個sync.Once變量來調用了runtime_pollServerInit函數,也就是創建epoll實例的函數。意思就是runtime_pollServerInit函數在整個進程生命周期內只會被調用一次,也就是只會創建一次epoll實例。epoll實例被創建后,會調用runtime_pollOpen函數將fd添加到epoll中。
            網絡編程中的所有socket fd都是通過netFD對象實現的,netFD是對網絡IO操作的抽象,linux的實現在文件net/fd_unix.go中。netFD對象實現有自己的init方法,還有完成基本IO操作的Read和Write方法,當然除了這三個方法以外,還有很多非常有用的方法供用戶使用。
            // Network file descriptor.
            type netFD struct {
            // locking/lifetime of sysfd + serialize access to Read and Write methods
            fdmu fdMutex
            // immutable until Close
            sysfd       int
            family      int
            sotype      int
            isConnected bool
            net         string
            laddr       Addr
            raddr       Addr
            // wait server
            pd pollDesc
            }
            通過netFD對象的定義可以看到每個fd都關聯了一個pollDesc實例,通過上文我們知道pollDesc對象最終是對epoll的封裝。
            func (fd *netFD) init() error {
            if err := fd.pd.Init(fd); err != nil {
            return err
            }
            return nil
            }
            netFD對象的init函數僅僅是調用了pollDesc實例的Init函數,作用就是將fd添加到epoll中,如果這個fd是第一個網絡socket fd的話,這一次init還會擔任創建epoll實例的任務。要知道在Go進程里,只會有一個epoll實例來管理所有的網絡socket fd,這個epoll實例也就是在第一個網絡socket fd被創建的時候所創建。
            for {
            n, err = syscall.Read(int(fd.sysfd), p)
            if err != nil {
            n = 0
            if err == syscall.EAGAIN {
            if err = fd.pd.WaitRead(); err == nil {
            continue
            }
            }
            }
            err = chkReadErr(n, err, fd)
            break
            }
            上面代碼段是從netFD的Read方法中摘取,重點關注這個for循環中的syscall.Read調用的錯誤處理。當有錯誤發生的時候,會檢查這個錯誤是否是syscall.EAGAIN,如果是,則調用WaitRead將當前讀這個fd的goroutine給park住,直到這個fd上的讀事件再次發生為止。當這個socket上有新數據到來的時候,WaitRead調用返回,繼續for循環的執行。這樣的實現,就讓調用netFD的Read的地方變成了同步“阻塞”方式編程,不再是異步非阻塞的編程方式了。netFD的Write方法和Read的實現原理是一樣的,都是在碰到EAGAIN錯誤的時候將當前goroutine給park住直到socket再次可寫為止。
            本文只是將網絡庫的底層實現給大體上引導了一遍,知道底層代碼大概實現在什么地方,方便結合源碼深入理解。Go語言中的高并發、同步阻塞方式編程的關鍵其實是”goroutine和調度器”,針對網絡IO的時候,我們需要知道EAGAIN這個非常關鍵的調度點,掌握了這個調度點,即使沒有調度器,自己也可以在epoll的基礎上配合協程等用戶態線程實現網絡IO操作的調度,達到同步阻塞編程的目的。
            最后,為什么需要同步阻塞的方式編程?只有看多、寫多了異步非阻塞代碼的時候才能夠深切體會到這個問題。真正的高大上絕對不是——“別人不會,我會;別人寫不出來,我寫得出來。”

            http://ju.outofmemory.cn/entry/168649
            本文分析了Golang的socket文件描述符和goroutine阻塞調度的原理。代碼中大部分是Go代碼,小部分是匯編代碼。完整理解本文需要Go語言知識,并且用Golang寫過網絡程序。更重要的是,需要提前理解goroutine的調度原理。
            1. TCP的連接對象:
            連接對象:
            在net.go中有一個名為Conn的接口,提供了對于連接的讀寫和其他操作:
            type Conn interface {
                Read(b []byte) (n int, err error)
                Write(b []byte) (n int, err error)
                Close() error
                LocalAddr() Addr
                RemoteAddr() Addr
                SetReadDeadline(t time.Time) error
                SetWriteDeadline(t time.Time) error
            }
            這個接口就是對下面的結構體conn的抽象。conn結構體包含了對連接的讀寫和其他操作:
            type conn struct {
                fd *netFD
            }
            從連接讀取數據:
            // Read implements the Conn Read method.
            func (c *conn) Read(b []byte) (int, error) {
                if !c.ok() {
                    return 0, syscall.EINVAL
                }
                return c.fd.Read(b)
            }
            向連接寫入數據:
            // Write implements the Conn Write method.
            func (c *conn) Write(b []byte) (int, error) {
                if !c.ok() {
                    return 0, syscall.EINVAL
                }
                return c.fd.Write(b)
            }
            關閉連接:
            // Close closes the connection.
            func (c *conn) Close() error {
                if !c.ok() {
                    return syscall.EINVAL
                }
                return c.fd.Close()
            }
            設置讀寫超時:
            // SetDeadline implements the Conn SetDeadline method.
            func (c *conn) SetDeadline(t time.Time) error {
                if !c.ok() {
                    return syscall.EINVAL
                }
                return c.fd.setDeadline(t)
            }
            // SetReadDeadline implements the Conn SetReadDeadline method.
            func (c *conn) SetReadDeadline(t time.Time) error {
                if !c.ok() {
                    return syscall.EINVAL
                }
                return c.fd.setReadDeadline(t)
            }
            // SetWriteDeadline implements the Conn SetWriteDeadline method.
            func (c *conn) SetWriteDeadline(t time.Time) error {
                if !c.ok() {
                    return syscall.EINVAL
                }
                return c.fd.setWriteDeadline(t)
            }
            可以看到,對連接的所有操作,都體現在對*netFD的操作上。我們繼續跟蹤c.fd.Read()函數.
            2.文件描述符
            net/fd_unix.go:
            網絡連接的文件描述符:
            // Network file descriptor.
            type netFD struct {
                // locking/lifetime of sysfd + serialize access to Read and Write methods
                fdmu fdMutex
                // immutable until Close
                sysfd       int
                family      int
                sotype      int
                isConnected bool
                net         string
                laddr       Addr
                raddr       Addr
                // wait server
                pd pollDesc
            }
            文件描述符讀取數據:
            func (fd *netFD) Read(p []byte) (n int, err error) {
                if err := fd.readLock(); err != nil {
                    return 0, err
                }
                defer fd.readUnlock()
                if err := fd.pd.PrepareRead(); err != nil {
                    return 0, &OpError{"read", fd.net, fd.raddr, err}
                }
                // 調用system call,循環從fd.sysfd讀取數據
                for {
                    // 系統調用Read讀取數據
                    n, err = syscall.Read(int(fd.sysfd), p)
                    // 如果發生錯誤,則需要處理
                    // 并且只處理EAGAIN類型的錯誤,其他錯誤一律返回給調用者
                    if err != nil {
                        n = 0
                        // 對于非阻塞的網絡連接的文件描述符,如果錯誤是EAGAIN
                        // 說明Socket的緩沖區為空,未讀取到任何數據
                        // 則調用fd.pd.WaitRead,
                        if err == syscall.EAGAIN {
                            if err = fd.pd.WaitRead(); err == nil {
                                continue
                            }
                        }
                    }
                    err = chkReadErr(n, err, fd)
                    break
                }
                if err != nil && err != io.EOF {
                    err = &OpError{"read", fd.net, fd.raddr, err}
                }
                return
            }
            網絡輪詢器
            網絡輪詢器是Golang中針對每個socket文件描述符建立的輪詢機制。 此處的輪詢并不是一般意義上的輪詢,而是Golang的runtime在調度goroutine或者GC完成之后或者指定時間之內,調用epoll_wait獲取所有產生IO事件的socket文件描述符。當然在runtime輪詢之前,需要將socket文件描述符和當前goroutine的相關信息加入epoll維護的數據結構中,并掛起當前goroutine,當IO就緒后,通過epoll返回的文件描述符和其中附帶的goroutine的信息,重新恢復當前goroutine的執行。
            // Integrated network poller (platform-independent part).
            // 網絡輪詢器(平臺獨立部分)
            // A particular implementation (epoll/kqueue) must define the following functions:
            // 實際的實現(epoll/kqueue)必須定義以下函數:
            // func netpollinit()           // to initialize the poller,初始化輪詢器
            // func netpollopen(fd uintptr, pd *pollDesc) int32 // to arm edge-triggered notifications, 為fd和pd啟動邊緣觸發通知
            // and associate fd with pd.
            // 一個實現必須調用下面的函數,用來指示pd已經準備好
            // An implementation must call the following function to denote that the pd is ready.
            // func netpollready(gpp **g, pd *pollDesc, mode int32)
            // pollDesc contains 2 binary semaphores, rg and wg, to park reader and writer
            // goroutines respectively. The semaphore can be in the following states:
            // pollDesc包含了2個二進制的信號,分別負責讀寫goroutine的暫停.
            // 信號可能處于下面的狀態:
            // pdReady - IO就緒通知被掛起;
            //           一個goroutine將次狀態置為nil來消費一個通知。
            // pdReady - io readiness notification is pending;
            //           a goroutine consumes the notification by changing the state to nil.
            // pdWait - 一個goroutine準備暫停在信號上,但是還沒有完成暫停。
            // 這個goroutine通過把這個狀態改變為G指針去提交這個暫停動作。
            // 或者,替代性的,并行的其他通知將狀態改變為READY.
            // 或者,替代性的,并行的超時/關閉會將次狀態變為nil
            // pdWait - a goroutine prepares to park on the semaphore, but not yet parked;
            //          the goroutine commits to park by changing the state to G pointer,
            //          or, alternatively, concurrent io notification changes the state to READY,
            //          or, alternatively, concurrent timeout/close changes the state to nil.
            // G指針 - 阻塞在信號上的goroutine
            // IO通知或者超時/關閉會分別將此狀態置為READY或者nil.
            // G pointer - the goroutine is blocked on the semaphore;
            //             io notification or timeout/close changes the state to READY or nil respectively
            //             and unparks the goroutine.
            // nil - nothing of the above.
            const (
                pdReady uintptr = 1
                pdWait  uintptr = 2
            )
            網絡輪詢器的數據結構如下:
            // Network poller descriptor.
            // 網絡輪詢器描述符
            type pollDesc struct {
                link *pollDesc // in pollcache, protected by pollcache.lock
                // The lock protects pollOpen, pollSetDeadline, pollUnblock and deadlineimpl operations.
                // This fully covers seq, rt and wt variables. fd is constant throughout the PollDesc lifetime.
                // pollReset, pollWait, pollWaitCanceled and runtime·netpollready (IO readiness notification)
                // proceed w/o taking the lock. So closing, rg, rd, wg and wd are manipulated
                // in a lock-free way by all operations.
                // NOTE(dvyukov): the following code uses uintptr to store *g (rg/wg),
                // that will blow up when GC starts moving objects.
                //
                // lock鎖對象保護了pollOpen, pollSetDeadline, pollUnblock和deadlineimpl操作。
                // 而這些操作又完全包含了對seq, rt, tw變量。
                // fd在PollDesc整個生命過程中都是一個常量。
                // 處理pollReset, pollWait, pollWaitCanceled和runtime.netpollready(IO就緒通知)不需要用到鎖。
                // 所以closing, rg, rd, wg和wd的所有操作都是一個無鎖的操作。
                lock    mutex // protectes the following fields
                fd      uintptr
                closing bool
                seq     uintptr        // protects from stale timers and ready notifications
                rg      uintptr        // pdReady, pdWait, G waiting for read or nil
                rt      timer          // read deadline timer (set if rt.f != nil)
                rd      int64          // read deadline
                wg      uintptr        // pdReady, pdWait, G waiting for write or nil
                wt      timer          // write deadline timer
                wd      int64          // write deadline
                user    unsafe.Pointer // user settable cookie
            }
            將當前goroutine設置為阻塞在fd上:
            pd.WaitRead():
            func (pd *pollDesc) WaitRead() error {
                return pd.Wait('r')
            }
            func (pd *pollDesc) Wait(mode int) error {
                res := runtime_pollWait(pd.runtimeCtx, mode)
                return convertErr(res)
            }
            res是runtime_pollWait函數返回的結果,由conevertErr函數包裝后返回:
            func convertErr(res int) error {
                switch res {
                case 0:
                    return nil
                case 1:
                    return errClosing
                case 2:
                    return errTimeout
                }
                println("unreachable: ", res)
                panic("unreachable")
            }
            函數返回0,表示IO已經準備好,返回nil。
            返回1,說明連接已關閉,應該放回errClosing。
            返回2,說明對IO進行的操作發生超時,應該返回errTimeout。
            runtime_pollWait會調用runtime/thunk.s中的函數:
            TEXT net·runtime_pollWait(SB),NOSPLIT,$0-0
                JMP runtime·netpollWait(SB)
            這是一個包裝函數,沒有參數,直接跳轉到runtime/netpoll.go中的函數netpollWait:
            func netpollWait(pd *pollDesc, mode int) int {
                // 檢查pd的狀態是否異常
                err := netpollcheckerr(pd, int32(mode))
                if err != 0 {
                    return err
                }
                // As for now only Solaris uses level-triggered IO.
                if GOOS == "solaris" {
                    onM(func() {
                        netpollarm(pd, mode)
                    })
                }
                // 循環中檢查pd的狀態是不是已經被設置為pdReady
                // 即檢查IO是不是已經就緒
                for !netpollblock(pd, int32(mode), false) {
                    err = netpollcheckerr(pd, int32(mode))
                    if err != 0 {
                        return err
                    }
                    // Can happen if timeout has fired and unblocked us,
                    // but before we had a chance to run, timeout has been reset.
                    // Pretend it has not happened and retry.
                }
                return 0
            }
            netpollcheckerr函數檢查pd是否出現異常:
            // 檢查pd的異常
            func netpollcheckerr(pd *pollDesc, mode int32) int {
                // 是否已經關閉
                if pd.closing {
                    return 1 // errClosing
                }
                // 當讀寫狀態下,deadline小于0,表示pd已經過了超時時間
                if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
                    return 2 // errTimeout
                }
                // 正常情況返回0
                return 0
            }
            netpollblock():
            // returns true if IO is ready, or false if timedout or closed
            // waitio - wait only for completed IO, ignore errors
            // 這個函數被netpollWait循環調用
            // 返回true說明IO已經準備好,返回false說明IO操作已經超時或者已經關閉
            func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
                // 獲取pd的rg
                gpp := &pd.rg
                // 如果模式是w,則獲取pd的wg
                if mode == 'w' {
                    gpp = &pd.wg
                }
                // set the gpp semaphore to WAIT
                // 在循環中設置pd的gpp為pdWait
                // 因為casuintptr是自旋鎖,所以需要在循環中調用
                for {
                    // 如果在循環中發現IO已經準備好(pg的rg或者wg為pdReady狀態)
                    // 則設置rg/wg為0,返回true
                    old := *gpp
                    if old == pdReady {
                        *gpp = 0
                        return true
                    }
                    // 每次netpollblock執行完畢之后,gpp重置為0
                    // 非0表示重復wait
                    if old != 0 {
                        gothrow("netpollblock: double wait")
                    }
                    // CAS操作改變gpp為pdWait
                    if casuintptr(gpp, 0, pdWait) {
                        break
                    }
                }
                // need to recheck error states after setting gpp to WAIT
                // this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
                // do the opposite: store to closing/rd/wd, membarrier, load of rg/wg
                //
                // 當設置gpp為pdWait狀態后,重新檢查gpp的狀態
                // 這是必要的,因為runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl會做相反的操作
                // 如果狀態正常則掛起當前的goroutine
                //
                // 當netpollcheckerr檢查io出現超時或者錯誤,waitio為true可用于等待ioReady
                // 否則當waitio為false, 且io不出現錯誤或者超時才會掛起當前goroutine
                if waitio || netpollcheckerr(pd, mode) == 0 {
                    // 解鎖函數,設置gpp為pdWait,如果設置不成功
                    // 說明已經是發生其他事件,可以讓g繼續運行,而不是掛起當前g
                    f := netpollblockcommit
                    // 嘗試掛起當前g
                    gopark(**(**unsafe.Pointer)(unsafe.Pointer(&f)), unsafe.Pointer(gpp), "IO wait")
                }
                // be careful to not lose concurrent READY notification
                old := xchguintptr(gpp, 0)
                if old > pdWait {
                    gothrow("netpollblock: corrupted state")
                }
                return old == pdReady
            }
            runtime/proc.go: gopark():
            // Puts the current goroutine into a waiting state and calls unlockf.
            // If unlockf returns false, the goroutine is resumed.
            // 將當前goroutine置為waiting狀態,然后調用unlockf
            func gopark(unlockf unsafe.Pointer, lock unsafe.Pointer, reason string) {
                // 獲取當前M
                mp := acquirem()
                // 獲取當前G
                gp := mp.curg
                // 獲取G的狀態
                status := readgstatus(gp)
                // 如果不是_Grunning或者_Gscanrunning,則報錯
                if status != _Grunning && status != _Gscanrunning {
                    gothrow("gopark: bad g status")
                }
                // 設置lock和unlockf
                mp.waitlock = lock
                mp.waitunlockf = unlockf
                gp.waitreason = reason
                releasem(mp)
                // can't do anything that might move the G between Ms here.
                // 在m->g0這個棧上調用park_m,而不是當前g的棧
                mcall(park_m)
            }
            mcall函數是一段匯編,在m->g0的棧上調用park_m,而不是在當前goroutine的棧上。mcall的功能分兩部分,第一部分保存當前G的PC/SP到G的gobuf的pc/sp字段,第二部分調用park_m函數:
            // func mcall(fn func(*g))
            // Switch to m->g0's stack, call fn(g).
            // Fn must never return.  It should gogo(&g->sched)
            // to keep running g.
            TEXT runtime·mcall(SB), NOSPLIT, $0-8
                // 將需要執行的函數保存在DI
                MOVQ    fn+0(FP), DI
                // 將M的TLS存放在CX
                get_tls(CX)
                // 將G對象存放在AX
                MOVQ    g(CX), AX   // save state in g->sched
                // 將調用者的PC存放在BX
                MOVQ    0(SP), BX   // caller's PC
                // 將調用者的PC保存到g->sched.pc
                MOVQ    BX, (g_sched+gobuf_pc)(AX)
                // 第一個參數的地址,即棧頂的地址,保存到BX
                LEAQ    fn+0(FP), BX    // caller's SP
                // 保存SP的地址到g->sched.sp
                MOVQ    BX, (g_sched+gobuf_sp)(AX)
                // 將g對象保存到g->sched->g
                MOVQ    AX, (g_sched+gobuf_g)(AX)
                // switch to m->g0 & its stack, call fn
                // 將g對象指針保存到BX
                MOVQ    g(CX), BX
                // 將g->m保存到BX
                MOVQ    g_m(BX), BX
                // 將m->g0保存到SI
                MOVQ    m_g0(BX), SI
                CMPQ    SI, AX  // if g == m->g0 call badmcall
                JNE 3(PC)
                MOVQ    $runtime·badmcall(SB), AX
                JMP AX
                // 將m->g0保存到g
                MOVQ    SI, g(CX)   // g = m->g0
                // 將g->sched.sp恢復到SP寄存器
                // 即使用g0的棧
                MOVQ    (g_sched+gobuf_sp)(SI), SP  // sp = m->g0->sched.sp
                // AX進棧
                PUSHQ   AX
                MOVQ    DI, DX
                // 將fn的地址復制到DI
                MOVQ    0(DI), DI
                // 調用函數
                CALL    DI
                // AX出棧
                POPQ    AX
                MOVQ    $runtime·badmcall2(SB), AX
                JMP AX
                RET
            park_m函數的功能分為三部分,第一部分讓當前G和當前M脫離關系,第二部分是調用解鎖函數,這里是調用netpoll.go源文件中的netpollblockcommit函數:
            // runtime·park continuation on g0.
            void
            runtime·park_m(G *gp)
            {
                bool ok;
                // 設置當前g為Gwaiting狀態
                runtime·casgstatus(gp, Grunning, Gwaiting);
                // 讓當前g和m脫離關系
                dropg();
                if(g->m->waitunlockf) {
                    ok = g->m->waitunlockf(gp, g->m->waitlock);
                    g->m->waitunlockf = nil;
                    g->m->waitlock = nil;
                    // 返回0為false,非0為true
                    // 0說明g->m->waitlock發生了變化,即不是在gopark是設置的(pdWait)
                    // 說明了脫離了WAIT狀態,應該設置為Grunnable,并執行g
                    if(!ok) {
                        runtime·casgstatus(gp, Gwaiting, Grunnable);
                        execute(gp);  // Schedule it back, never returns.
                    }
                }
                // 這里是調度當前m繼續執行其他g
                // 而不是上面執行execute
                schedule();
            }
            netpollblockcommit函數,設置gpp為pdWait,設置成功返回1,否則返回0。1為true,0為false:
            func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
                return casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
            }
            到這里當前goroutine對socket文件描述符的等待IO繼續的行為已經完成。過程中首先盡早嘗試判斷IO是否已經就緒,如果未就緒則掛起當前goroutine,掛起之后再次判斷IO是否就緒,如果還未就緒則調度當前M運行其他G。如果是在調度goroutine之前IO已經就緒,則不會使當前goroutine進入調度隊列,會直接運行剛才掛起的G。否則當前goroutine會進入調度隊列。
            接下來是等待runtime將其喚醒。runtime在執行findrunnablequeue、starttheworld,sysmon函數時,都會調用netpoll_epoll.go中的netpoll函數,尋找到IO就緒的socket文件描述符,并找到這些socket文件描述符對應的輪詢器中附帶的信息,根據這些信息將之前等待這些socket文件描述符就緒的goroutine狀態修改為Grunnable。在以上函數中,執行完netpoll之后,會找到一個就緒的goroutine列表,接下來將就緒的goroutine加入到調度隊列中,等待調度運行。
            在netpoll_epoll.go中的netpoll函數中,epoll_wait函數返回N個發生事件的文件描述符對應的epollevent,接著對于每個event使用其data屬性,將event.data轉換為*pollDesc類型,再調用netpoll.go中的netpollready函數,將*pollDesc類型中的G數據類型去除,并附加到netpoll函數的調用者傳遞的G鏈表中:
            // 將ev.data轉換為*pollDesc類型
            pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
            // 調用netpollready將取出pd中保存的G,并添加到鏈表中
            netpollready((**g)(noescape(unsafe.Pointer(&gp))), pd, mode)
            所以runtime在執行findrunnablequeue、starttheworld,sysmon函數中會執行netpoll函數,并返回N個goroutine。這些goroutine期待的網絡事件已經發生,runtime會將這些goroutine放入到當前P的可運行隊列中,接下來調度它們并運行。
            posted on 2017-06-02 11:12 思月行云 閱讀(1760) 評論(0)  編輯 收藏 引用 所屬分類: Golang
            嫩草影院久久国产精品| 亚洲精品99久久久久中文字幕| 国产一区二区三精品久久久无广告| 精品伊人久久大线蕉色首页| 久久亚洲av无码精品浪潮| www亚洲欲色成人久久精品| 精品久久久久中文字幕日本| 波多野结衣AV无码久久一区| 久久精品国产亚洲av麻豆蜜芽| 久久99国产精品久久99小说 | 精品综合久久久久久88小说| 国产精品久久久天天影视| 久久99精品国产麻豆宅宅| 久久精品国产亚洲欧美| 成人精品一区二区久久| 欧美午夜A∨大片久久| 久久人人爽人人爽人人片AV东京热| 欧美黑人激情性久久| 久久天天躁狠狠躁夜夜躁2O2O | 久久人妻AV中文字幕| 97久久国产综合精品女不卡| 久久亚洲春色中文字幕久久久| 99久久精品毛片免费播放| 国产精品99久久久久久董美香 | 性做久久久久久久久| 久久婷婷五月综合97色直播| 无码国内精品久久人妻| 精品久久久久久久久中文字幕| 国产精品美女久久久免费| 亚洲精品97久久中文字幕无码 | 久久亚洲美女精品国产精品| 亚洲国产天堂久久综合网站| 国产精品99久久久久久宅男小说| 无码人妻久久一区二区三区| 国产精品女同一区二区久久| 99久久精品国产一区二区| 亚洲国产二区三区久久| 亚洲精品无码专区久久久| 久久国产精品无码网站| 久久久久久九九99精品| 日日狠狠久久偷偷色综合96蜜桃 |