使用Golang能夠輕鬆地爲每個TCP鏈接建立一個協程去服務而不用擔憂性能問題,這是由於Go內部使用goroutine結合IO多路複用實現了一個「異步」的IO模型,這使得開發者不用過多的關注底層,而只須要按照需求編寫上層業務邏輯。這種異步的IO是如何實現的呢?下面我會針對Linux系統進行分析。linux
在Unix/Linux系統下,一切皆文件,每條TCP鏈接對應了一個socket句柄,這個句柄也能夠看作是一個文件,在socket上收發數據,至關於對一個文件進行讀寫,因此一個socket句柄,一般也用表示文件描述符fd來表示。能夠進入/proc/PID/fd/查看進程佔用的fd。golang
系統內核會爲每一個socket句柄分配一個讀(接收)緩衝區和一個寫(發送)緩衝區,發送數據就是在這個fd對應的寫緩衝區上寫數據,而接收數據就是在讀緩衝區上讀數據,當程序調用write或者send時,並不表明數據發送出去,僅僅是把數據拷貝到了寫緩衝區,在時機恰當時候(積累到必定數量),會將數據發送到目的端。編程
Golang runtime仍是須要頻繁去檢查是否有fd就緒的,嚴格說並不算真正的異步,算是一種非阻塞IO複用。
借用教科書中幾張圖segmentfault
程序想在緩衝區讀數據時,緩衝區並不必定會有數據,這會形成陷入系統調用,只能等待數據能夠讀取,沒有數據讀取時則會阻塞住進程,這就是阻塞式IO。當須要爲多個客戶端提供服務時,可使用線程方式,每一個socket句柄使用一個線程來服務,這樣阻塞住的則是某個線程。雖然如此能夠解決進程阻塞,可是仍是會有至關一部分CPU資源浪費在了等待數據上,同時,使用線程來服務fd有些浪費資源,由於若是要處理的fd較多,則又是一筆資源開銷。數組
與之對應的是非阻塞IO,當程序想要讀取數據時,若是緩衝區不存在,則直接返回給用戶程序,可是須要用戶程序去頻繁檢查,直到有數據準備好。這一樣也會形成空耗CPU。網絡
而IO多路複用則不一樣,他會使用一個線程去管理多個fd,能夠將多個fd加入IO多路複用函數中,每次調用該函數,傳入要檢查的fd,若是有就緒的fd,直接返回就緒的fd,再啓動線程處理或者順序處理就緒的fd。這達到了一個線程管理多個fd任務,相對來講較爲高效。常見的IO多路複用函數有select,poll,epoll。select與poll的最大缺點是每次調用時都須要傳入全部要監聽的fd集合,內核再遍歷這個傳入的fd集合,當併發量大時候,用戶態與內核態之間的數據拷貝以及內核輪詢fd又要浪費一波系統資源(關於select與poll這裏不展開)。併發
接下來介紹一下epoll系統調用異步
epoll相比於select與poll相比要靈活且高效,他提供給用戶三個系統調用函數。Golang底層就是經過這三個系統調用結合goroutine完成的「異步」IO。socket
//用於建立並返回一個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);
大體工做原理如圖tcp
當用戶程序想要讀取fd數據時,系統調用直接通知到內核並返回處理其餘的事情,內核將數據準備好以後,通知用戶程序,用戶程序再處理這個fd上的事件。
咱們都知道,協程的資源佔有量很小,並且協程也擁有多種狀態如阻塞,就緒,運行等,可使用一個協程服務一個fd不用擔憂資源問題。將監聽fd的事件交由runtime來管理,實現協程調度與依賴fd的事件。當要協程讀取fd數據可是沒有數據時,park住該協程(改成Gwaiting),調度其餘協程執行。
在執行協程調度時候,去檢查fd是否就緒,若是就緒時,調度器再通知該park住的協程fd能夠處理了(改成Grunnable並加入執行隊列),該協程處理fd數據,這樣既減小了CPU的空耗,也實現了消息的通知,用戶層面上看實現了一個異步的IO模型。
Golang netpoll的大體思想就是這樣,接下來看一下具體代碼實現,本文基於go1.14。
接下來看下Golang netpoll對其的使用。
跟隨一個很簡單的demo探索一下。
func main() { fmt.Println("服務端進程id:",os.Getpid()) lister, err := net.Listen("tcp", "0.0.0.0:9009") if err != nil { fmt.Println("鏈接失敗", err) return } for { conn, err := lister.Accept() //等待創建鏈接 if err != nil { fmt.Println("創建鏈接失敗", err) continue } //開啓協程處理 go func() { defer conn.Close() for { buf := make([]byte, 128) n, err := conn.Read(buf) if err != nil{ fmt.Println("讀出錯",err) return } fmt.Println("讀取到的數據:",string(buf[:n])) } }() } }
net.Listen依次調用lc.Listen->sl.listenTCP->internetSocket->socket到fd.listenStream函數建立了一個監聽9009的tcp鏈接的socket接口,也就是建立了socket fd,
接下來爲了監聽該socket對象就須要把這個socket fd加入到eventpoll中了。
func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error { ...... //綁定該socket接口 if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil { return os.NewSyscallError("bind", err) } //監聽該socket if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil { return os.NewSyscallError("listen", err) } //初始化fd,也就是把socket放入epoll中,進入 if err = fd.init(); err != nil { return err } lsa, _ = syscall.Getsockname(fd.pfd.Sysfd) fd.setAddr(fd.addrFunc()(lsa), nil) return nil } func (fd *FD) Init(net string, pollable bool) error { ...... //將socket fd加到poll,進入 err := fd.pd.init(fd) ...... return err } //最終跳轉到該處,主要關注兩個函數runtime_pollServerInit,runtime_pollOpen, //這兩個函數都是runtime實現的,將epoll交由runtime來管理 func (pd *pollDesc) init(fd *FD) error { //sync.once方法,調用epoll_create建立eventpoll對象 serverInit.Do(runtime_pollServerInit) //將當前的fd加到epoll中,底層調用epollctl函數 ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd)) //若是出錯,處理相應的fd,刪除epoll中fd以及解除狀態等操做 if errno != 0 { if ctx != 0 { runtime_pollUnblock(ctx) runtime_pollClose(ctx) } return errnoErr(syscall.Errno(errno)) } pd.runtimeCtx = ctx return nil }
查看runtime_pollServerInit,是對epoll_create的封裝。
func poll_runtime_pollServerInit() { //初始化全局epoll對象 netpollinit() /全局標誌位設置爲1 atomic.Store(&netpollInited, 1) } func netpollinit() { //系統調用,建立一個eventpoll對象 epfd = epollcreate1(_EPOLL_CLOEXEC) if epfd >= 0 { return } ...... }
查看一下runtime_pollOpen方法,將當前監聽的socket fd加入eventpoll對象中。其實是對epoll_ctl的封裝。
func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) { //返回一個存儲在Go程序中的一個fd對應的結構體,算是用於記錄 //goroutine與fd之間的關係,後面會分析到 pd := pollcache.alloc() //加鎖,防止併發問題 lock(&pd.lock) if pd.wg != 0 && pd.wg != pdReady { throw("runtime: blocked write on free polldesc") } if pd.rg != 0 && pd.rg != pdReady { throw("runtime: blocked read on free polldesc") } pd.fd = fd pd.closing = false pd.everr = false pd.rseq++ pd.rg = 0 pd.rd = 0 pd.wseq++ pd.wg = 0 pd.wd = 0 unlock(&pd.lock) var errno int32 //epoll_ctl系統調用 errno = netpollopen(fd, pd) return pd, int(errno) } func netpollopen(fd uintptr, pd *pollDesc) int32 { var ev epollevent //註冊event事件,這裏使用了epoll的ET模式,相對於ET,ET須要每次產生事件時候就要處理事件, //不然容易丟失事件。 ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET //events記錄上pd的指針 *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd //系統調用將該fd加到eventpoll對象中,交由內核監聽 return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev) }
接下來返回到主函數。
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) { ...... //檢查fd狀態是否變化 if err := fd.pd.prepareRead(fd.isFile); err != nil { return -1, nil, "", err } for { //accept系統調用,若是有對監聽的socket的鏈接請求,則直接返回發起鏈接的socket文件描述符 //,不然返回EAGAIN錯誤,被下面捕獲到 s, rsa, errcall, err := accept(fd.Sysfd) if err == nil { return s, rsa, "", err } switch err { case syscall.EAGAIN: if fd.pd.pollable() { //進入waitRead方法,內部 if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } case syscall.ECONNABORTED: continue } return -1, nil, errcall, err } } func (pd *pollDesc) wait(mode int, isFile bool) error { if pd.runtimeCtx == 0 { return errors.New("waiting for unsupported file type") } //進入runtime_pollWait方法內部,該方法會跳轉到runtime包下,條件知足會park住goroutine res := runtime_pollWait(pd.runtimeCtx, mode) return convertErr(res, isFile) } func poll_runtime_pollWait(pd *pollDesc, mode int) int { ...... //進入netpollblock函數,該函數內部會阻塞住該goroutine for !netpollblock(pd, int32(mode), false) { err = netpollcheckerr(pd, int32(mode)) if err != 0 { return err } } return 0 } func netpollblock(pd *pollDesc, mode int32, waitio bool) bool { gpp := &pd.rg if mode == 'w' { gpp = &pd.wg } ...... if waitio || netpollcheckerr(pd, mode) == 0 { //gark住該g,此時傳參主要關注前兩個,一個netpollblockcommit函數,一個gpp爲當前pd的rg或者wg, //用於後面記錄fd對應的阻塞的goroutine gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5) } old := atomic.Xchguintptr(gpp, 0) if old > pdWait { throw("runtime: corrupted polldesc") } return old == pdReady } func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { ...... //主要關注兩個傳參,lock是gpp指針 mp.waitlock = lock //unlockf爲netpollblockcommit函數 mp.waitunlockf = unlockf ...... //切換到g0棧去執行park_m mcall(park_m) } func park_m(gp *g) { //獲取當前goroutine _g_ := getg() //修改狀態爲Gwaiting,表明當前的goroutine被park住了 casgstatus(gp, _Grunning, _Gwaiting) //解除m和g關聯 dropg() if fn := _g_.m.waitunlockf; fn != nil { //調用剛傳入的函數參數,也就是netpollblockcommit ok := fn(gp, _g_.m.waitlock) //調用完清除 _g_.m.waitunlockf = nil _g_.m.waitlock = nil if !ok { if trace.enabled { traceGoUnpark(gp, 2) } casgstatus(gp, _Gwaiting, _Grunnable) execute(gp, true) // Schedule it back, never returns. } } //調度新的g到m上來 schedule() } func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool { //把當前g的指針存爲gpp指針,gpp爲pd的rg或wg r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp))) if r { //將全局變量改成1,表明系統有netpoll的等待者 atomic.Xadd(&netpollWaiters, 1) } return r }
到此時,accept函數就被阻塞住了,系統會在這個監聽的socket fd事件(0.0.0.0:9009的這個fd)的狀態發生變化時候(也就是有新的客戶端請求鏈接的時候),將該park住的goroutine給ready。
//上面提到過的accept函數,根據序號順序分析 func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) { ...... for { //2.使用accept系統調用能獲取到新的鏈接,linux會爲新的鏈接分配一個新的fd, //這個函數會返回新的鏈接的socket fd對應的進程描述符 s, rsa, errcall, err := accept(fd.Sysfd) if err == nil { //3.返回新的進程描述符 return s, rsa, "", err } switch err { case syscall.EAGAIN: if fd.pd.pollable() { //1.剛纔阻塞到了這個goroutine,後來新的鏈接請求,該goroutine被喚醒 if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } ...... } ...... } } //返回上一層的函數 func (fd *netFD) accept() (netfd *netFD, err error) { //此時獲取到了新的fd d, rsa, errcall, err := fd.pfd.Accept() ...... //建立新的fd結構體 if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil { poll.CloseFunc(d) return nil, err } //init函數又會進入func (pd *pollDesc) init(fd *FD) error函數,並將新的socket鏈接經過epoll_ctl傳入 //epoll的監聽事件 if err = netfd.init(); err != nil { fd.Close() return nil, err } //系統調用,能夠得到客戶端的socket的ip信息等 lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd) netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa)) return netfd, nil }
go會在調度goroutine時候執行epoll_wait系統調用,檢查是否有狀態發生改變的fd,有的話就把他取出,喚醒對應的goroutine去處理。該部分對應了runtime中的netpoll方法。
源碼調用runtime中的schedule() -> findrunnable() -> netpoll()
func findrunnable() (gp *g, inheritTime bool) { _g_ := getg() //分別從本地隊列和全局隊列尋找可執行的g ...... //判斷是否知足條件,初始化netpoll對象,是否等待者,以及上次調用時間 if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 { //netpoll底層調用epoll_wait,傳參表明epoll_wait時候是阻塞等待或者非阻塞直接返回 //這裏是非阻塞模式,會當即返回內核eventpoll對象的rdlist列表 if list := netpoll(false); !list.empty() { gp := list.pop() //將可運行G的列表注入調度程序並清除glist injectglist(&list) //修改gp狀態 casgstatus(gp, _Gwaiting, _Grunnable) if trace.enabled { traceGoUnpark(gp, 0) } //返回可運行的g return gp, false } } ....... stopm() goto top } //對epoll_wait的進一步封裝 func netpoll(block bool) gList { if epfd == -1 { return gList{} } waitms := int32(-1) if !block { waitms = 0 } //聲明一個epollevent事件,在epoll_wait系統調用時候,會給該數組賦值並返回一個索引位, /以後能夠遍歷數組取出就緒的fd事件。 var events [128]epollevent retry: //陷入系統調用,取出內核eventpoll中的rdlist,返回就緒的事件 n := epollwait(epfd, &events[0], int32(len(events)), waitms) if n < 0 { if n != -_EINTR { println("runtime: epollwait on fd", epfd, "failed with", -n) throw("runtime: netpoll failed") } goto retry } var toRun gList //遍歷event事件數組 for i := int32(0); i < n; i++ { ev := &events[i] if ev.events == 0 { continue } var mode int32 //是否有就緒的讀寫事件,放入mode標誌位 if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 { mode += 'r' } if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 { mode += 'w' } if mode != 0 { //取出存入的pollDesc的指針 pd := *(**pollDesc)(unsafe.Pointer(&ev.data)) pd.everr = false if ev.events == _EPOLLERR { pd.everr = true } //取出pd中的rg或wg,後面放到運行隊列 netpollready(&toRun, pd, mode) } } if block && toRun.empty() { goto retry } return toRun } func netpollready(toRun *gList, pd *pollDesc, mode int32) { var rg, wg *g if mode == 'r' || mode == 'r'+'w' { rg = netpollunblock(pd, 'r', true) } if mode == 'w' || mode == 'r'+'w' { wg = netpollunblock(pd, 'w', true) } //將阻塞的goroutine加入gList返回 if rg != nil { toRun.push(rg) } if wg != nil { toRun.push(wg) } }
回到主函數,咱們使用go func形式使用一個協程去處理一個tcp鏈接,每一個協程裏面會有conn.Read,該函數在讀取時候若是緩衝區不可讀,該goroutine也會陪park住,等待socket fd可讀,調度器經過netpoll函數調度它。
func main() { ...... //開啓處理 go func() { defer conn.Close() for { buf := make([]byte, 128) //將緩衝區的數據讀出來放到buf中 n, err := conn.Read(buf) ...... } }() } } func (fd *FD) Read(p []byte) (int, error) { ...... for { //系統調用讀取緩衝區數據,這裏沒有可讀會直接返回,不會阻塞 n, err := syscall.Read(fd.Sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN && fd.pd.pollable() { //不可讀,進入waitRead方法,park住該goroutine, //並記錄goroutine到pd的rg中,等待喚醒 if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } } ...... } }
後面會等待緩衝區可讀寫,shchedule函數調用netpoll並進一步調用epoll_wait檢測到並喚醒該goroutine。能夠查看上面netpoll,這裏不作重複工做了。
Golang也提供了對於epoll item節點的刪除操做,具體封裝函數poll_runtime_pollClose
//當發生某些狀況,如鏈接斷開,fd銷燬等,會調用到此處 func poll_runtime_pollClose(pd *pollDesc) { ....... netpollclose(pd.fd) //釋放對應的pd pollcache.free(pd) } //調用epoll_ctl系統調用,刪除該fd在eventpoll上對應的epitem func netpollclose(fd uintptr) int32 { var ev epollevent return -epollctl(epfd, _EPOLL_CTL_DEL, int32(fd), &ev) }
抓了一部分系統調用分析一下上述程序與內核交互的大體過程。
$ strace -f ./server
部分系統調用函數以下。
#....省略內存管理部分以及線程管理部分 #執行到fmt.Println("服務端進程id:",os.Getpid()) [pid 30307] getpid() = 30307 [pid 30307] write(1, "346234215345212241347253257350277233347250213id357274232 30307n", 27服務端進程id:30307 ) = 27 ......因爲過多,省略關於socket的系統調用 [pid 30308] <... nanosleep resumed> NULL) = 0 #打開系統文件,該文件定義tcp最大鏈接數,會被設置成pollable,並加入epoll節點中 [pid 30307] openat(AT_FDCWD, "/proc/sys/net/core/somaxconn", O_RDONLY|O_CLOEXEC <unfinished ...> [pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> [pid 30307] <... openat resumed> ) = 4 #調用epoll_ctl,建立一個eventpoll [pid 30307] epoll_create1(EPOLL_CLOEXEC) = 5 #將fd加到epoll事件 [pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189320, u64=139635855949576}}) = 0 [pid 30307] fcntl(4, F_GETFL) = 0x8000 (flags O_RDONLY|O_LARGEFILE) [pid 30307] fcntl(4, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0 [pid 30308] <... nanosleep resumed> NULL) = 0 [pid 30307] read(4, <unfinished ...> #執行epoll_wait查看就緒事件 [pid 30308] epoll_pwait(5, <unfinished ...> [pid 30307] <... read resumed> "512n", 65536) = 4 [pid 30308] <... epoll_pwait resumed> [{EPOLLIN|EPOLLOUT, {u32=2174189320, u64=139635855949576}}], 128, 0, NULL, 139635812673280) = 1 [pid 30307] read(4, <unfinished ...> [pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> [pid 30307] <... read resumed> "", 65532) = 0 #將/proc/sys/net/core/somaxconn文件的fd從epoll中刪除 [pid 30307] epoll_ctl(5, EPOLL_CTL_DEL, 4, 0xc00005e8d4) = 0 #關掉打開的somaxconn描述符 [pid 30307] close(4) = 0 #設置監聽的socket描述符 [pid 30307] setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 [pid 30307] bind(3, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0 [pid 30307] listen(3, 512 <unfinished ...> [pid 30308] <... nanosleep resumed> NULL) = 0 [pid 30307] <... listen resumed> ) = 0 [pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> #將用於監聽的socket fd加入到epoll中 [pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189320, u64=139635855949576}}) = 0 [pid 30307] getsockname(3, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0 #執行accept4發現沒有鏈接,返回EAGAIN錯誤 [pid 30307] accept4(3, 0xc00005eb98, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable) #查看是否有就緒的fd,這次調用是非阻塞,當即返回 [pid 30307] epoll_pwait(5, [], 128, 0, NULL, 0) = 0 [pid 30308] <... nanosleep resumed> NULL) = 0 #查看是否有就緒的fd,這次會阻塞等待,直到有鏈接進來 [pid 30307] epoll_pwait(5, <unfinished ...> [pid 30308] futex(0x60dc70, FUTEX_WAIT_PRIVATE, 0, {tv_sec=60, tv_nsec=0} <unfinished ...> [pid 30307] <... epoll_pwait resumed> [{EPOLLIN, {u32=2174189320, u64=139635855949576}}], 128, -1, NULL, 0) = 1 [pid 30307] futex(0x60dc70, FUTEX_WAKE_PRIVATE, 1) = 1 [pid 30308] <... futex resumed> ) = 0 #新的鏈接,表明收到了一個客戶端鏈接,分配了一個fd是4 [pid 30307] accept4(3, <unfinished ...>, <... accept4 resumed> {sa_family=AF_INET6, sin6_port=htons(52082), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 4 #把4加入到epoll中管理 [pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189112, u64=139635855949368}}) = 0 [pid 30307] getsockname(4, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0 ...... #後來將client端關掉,此時tcp鏈接斷掉了,將epoll中的fd移除 [pid 30309] epoll_ctl(5, EPOLL_CTL_DEL, 4, 0xc00005fdd4 <unfinished ...> [pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> [pid 30309] <... epoll_ctl resumed> ) = 0 [pid 30309] close(4) = 0 [pid 30309] epoll_pwait(5, [], 128, 0, NULL, 824634114048) = 0 #阻塞等待 [pid 30309] epoll_pwait(5, <unfinished ...> ........