Golangs的程序为程序(-programmer)提供了一个阻塞I/O的接口。在后台,运行时自然地使用某种非阻塞I/O来防止操作系统挂起操作系统线程,以便运行时可以在执行I/O时在操作系统线程上运行另一个例程。
运行时何时考虑已执行的I/O,以便它可以重新调度程序?
为了说明这一点,假设我有一个net.TCPConn
,我调用Write
,我什么时候可以期望该例程被重新调度?
conn, err := net.Dial("tcp", serverAddr)
conn.Write(buffer)
timestamp = time.Now()
那就是我什么时候可以期望时间戳被取走?
- 当缓冲区被复制到golang运行时?
- 当缓冲区被复制到运行时和操作系统的内核空间?
- 当缓冲区被复制到运行时,内核空间和额外的NIC的发送缓冲区?
- 缓冲区何时通过网络/从网卡发送?
- 当缓冲区被接收端TCP栈确认时?
您可以在文件https://github.com/golang/go/blob/master/src/net/fd_unix.go (Write函数)中查看。
基本上,它取决于socket缓冲区是否有足够的空间。
如果套接字缓冲区中有足够的空间容纳写操作的大小,则数据将立即写入套接字缓冲区。我猜这符合你的第二个答案。此外,内核可能实际发送数据包(或将其添加到NIC队列中),但它独立于Go运行时。
如果套接字缓冲区中没有足够的空间来容纳整个写操作,则只有部分数据会立即写入套接字缓冲区。然后,调用将阻塞(通过运行时轮询引擎),直到内核在套接字缓冲区中腾出一些空间(通过发送一些数据包)。一旦有空间可用,并且所有数据都已复制,调用将解除阻塞。
您应该考虑当net包通过系统调用在套接字缓冲区中写入整个缓冲区时所采用的时间戳。
下面的文章描述了netpolller是如何工作的:
因此,我们得出结论,只要底层系统调用完成对整个缓冲区的写操作,就可以重新调度该例程。在Linux的情况下,似乎是当消息被复制到内核空间发送缓冲区时:阻塞套接字:确切地说,什么时候"send()";返回吗?。这反过来又是我的第二个原始选项"当缓冲区被复制到运行时和操作系统的内核空间";无论何时,当一个线程试图读取或写入连接时,网络代码将执行该操作,直到它接收到这样一个错误,然后调用netpolller,告诉它在准备好再次执行I/O时通知这个线程。然后,该线程被调度到它所运行的线程之外,另一个线程在它的位置上运行。
当netpoller从OS接收到它可以对文件描述符执行I/O的通知时,它将查看其内部数据结构,查看是否有任何在该文件上被阻塞的程序,并通知它们是否有。然后,该例程可以重试导致其阻塞的I/O操作并成功执行。"