출처: https://xiaoxubeii.github.io/articles/linux-io-models-and-go-network-model-2/

Go netpoller의 구현은 간단하므로 Go 프로그램에서는 기동 시에 M(여기에서는 The Go scheduler가 관여하고 있다)을 만들고 감시 기능을 실행한다.

runtime/proc.go:110

func main() {
   ...

   
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
       systemstack(
func() {
           newm(sysmon,
nil) // 모니터링 기능
       })
   }

   
// Lock the main goroutine onto this, the main OS thread,
   
// during initialization. Most programs won't care, but a few
   
// do require certain calls to be made by the main thread.
   
// Those can arrange for main.main to run in the main thread
   
// by calling runtime.LockOSThread during initialization
   
// to preserve the lock.
   lockOSThread()
   ...
}

netpoll는 sysmon 에서 폴링되고, 기초가 되는 fd를 listen 하고, fd 준비가 완료되면, 폴러는 블럭된 G를 웨이크업 한다(플랫폼에 따라서 인터페이스 구현이 서로 다르다. 여기에서는 Linux epoll 에 대해서만 이야기한다).

runtime/proc.go:4315

func sysmon() {
   ...
   
for {
     
if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
           atomic.Cas64(&sched.lastpoll,
uint64(lastpoll), uint64(now))
           gp := netpoll(
false) // non-blocking - returns list of goroutines
           
if gp != nil {
               
// Need to decrement number of idle locked M's
               
// (pretending that one more is running) before injectglist.
               
// Otherwise it can lead to the following situation:
               
// injectglist grabs all P's but before it starts M's to run the P's,
               
// another M returns from syscall, finishes running its G,
               
// observes that there is no work to do and no other running M's
               
// and reports deadlock.
               incidlelocked(-1)
               injectglist(gp)
               incidlelocked(1)
           }
       }
       ...
   }
}

Linux 플랫폼에서는 netpoll는 준비가 된 fd를 가지고 있는 epollwait를 호출한다.

runtime/netpoll_epoll.go:61

func netpoll(block bool) *g {
   ...
retry:
   n := epollwait(epfd, &events[0],
int32(len(events)), waitms)
   ...
}

Go epoll 작성과 등록

Go epoll는 어떻게 fd를 만들고, listen 하고 있을까?

  1. 커널 내에 컨텍스트를 만들기 위해 epoll_create를 호출한다.
  2. epoll_ctl 을 호출해서 fd를 추가 또는 삭제한다.
  3. 이벤트가 등록될 때까지 epoll_wait를 호출한다.

Go epoll 중의 실제 콜 스텝은 같다. 단순한 TCP 접속을 예로 하면 클라이언트 코드는 아래와 같다.

func main() {
 conn, _ := net.Dial(
"tcp", "127.0.0.1:8081")
 
for {
   reader := bufio.NewReader(os.Stdin)
   fmt.Print(
"Text to send: ")
   text, _ := reader.ReadString(
'\n')
   fmt.Fprintf(conn, text +
"\n")
   message, _ := bufio.NewReader(conn).ReadString(
'\n')
   fmt.Print(
"Message from server: "+message)
 }
}

net.Dial은 최종적으로 net/dial.go의 DialContext를 호출한다.

net/dial.go:341

func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
   ...
   
var c Conn
   
if len(fallbacks) > 0 {
       c, err = dialParallel(ctx, dp, primaries, fallbacks)
   }
else {
       c, err = dialSerial(ctx, dp, primaries)
   }
   ...
}

net/dial.
go:489

func dialSerial(ctx context.Context, dp *dialParam, ras addrList) (Conn, error) {
   ...
   c, err := dialSingle(dialCtx, dp, ra)
   ...
}

net/dial.
go:532

func dialSingle(ctx context.Context, dp *dialParam, ra Addr) (c Conn, err error) {
   ...
   
case *TCPAddr:
       la, _ := la.(*TCPAddr)
       c, err = dialTCP(ctx, dp.network, la, ra)
   ...
}

net/tcpsock_posix.
go:61

func doDialTCP(ctx context.Context, net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
   fd, err := internetSocket(ctx, net, laddr, raddr, syscall.SOCK_STREAM, 0,
"dial")
   ...
}

net/ipsock_posix.
go:136

func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string) (fd *netFD, err error) {
   ...
   
return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr)
}

net/sock_posix.
go:18

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr) (fd *netFD, err error) {
   ...
   
if fd, err = newFD(s, family, sotype, net); err != nil {
       poll.CloseFunc(s)
       
return nil, err
   }
   
   
if err := fd.dial(ctx, laddr, raddr); err != nil {
       fd.Close()
       
return nil, err
   }
   ...
}

이상의 일련의 호출 후, fd.dial 까지 코드를 실행하고, 마지막으로 pollDesc.init를 호출하여 epoll 초기화와 등록을 한다.

internal/poll/fd_poll_runtime.go:35

func (pd *pollDesc) init(fd *FD) error {
   serverInit.Do(runtime_pollServerInit)
   ctx, errno := runtime_pollOpen(
uintptr(fd.Sysfd))
   
if errno != 0 {
       
if ctx != 0 {
           runtime_pollUnblock(ctx)
           runtime_pollClose(ctx)
       }
       
return syscall.Errno(errno)
   }
   pd.runtimeCtx = ctx
   
return nil
}

go:linkname을 사용하여 링크된 runtime_pollServerInit 와 runtime_pollOpen 함수가 실제로 호출되는 것에 주의하자.

runtime/netpoll.go:85

//go:linkname poll_runtime_pollServerInit internal/poll.runtime_pollServerInit
func poll_runtime_pollServerInit() {
   netpollinit()
   atomic.Store(&netpollInited, 1)
}

//go:linkname poll_runtime_pollOpen internal/poll.runtime_pollOpen
func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
   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.seq++
   pd.rg = 0
   pd.rd = 0
   pd.wg = 0
   pd.wd = 0
   unlock(&pd.lock)

   
var errno int32
   errno = netpollopen(fd, pd)
   
return pd, int(errno)
}

처음의 poll_runtime_pollServerInit 를 보자. Linux 플랫폼에서는 epollcreate 또는 epollcreate1 을 호출하는 netpollinit 에 의해 epoll을 초기화 한다.

runtime/netpoll_epoll.go:25

func netpollinit() {
   epfd = epollcreate1(_EPOLL_CLOEXEC)
   
if epfd >= 0 {
       
return
   }
   epfd = epollcreate(1024)
   
if epfd >= 0 {
       closeonexec(epfd)
       
return
   }
   
println("runtime: epollcreate failed with", -epfd)
   throw(
"runtime: netpollinit failed")
}

poll_runtime_pollOpen이 netpollopen을 경유하여 epollctl 등록 리스닝 fd를 호출하고 있는 사이에 poll_runtime_pollOpen은 epollctl 등록 리스닝 fd를 호출한다.

runtime/netpoll_epoll.go:43

func netpollopen(fd uintptr, pd *pollDesc) int32 {
   
var ev epollevent
   ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
   *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
   
return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}

그리고 처음에 이야기한  fd 준비를 위해 epollwait에 호출해서 전체 프로세스는 종료한다. 이 프로세스 전체는 실제로는 Linux epoll의 콜 스텝에 정확하게 대응한다.