阻塞式套接字:在什么情况下,“send()”函数会返回?

55

当 BSD 套接字的 send() 函数返回给调用者时,它到底是在什么时候返回的呢?

非阻塞模式下,它应该立即返回,对吗?

对于 阻塞模式手册中说:

当消息不适合套接字的发送缓冲区时,send() 通常会阻塞,除非将套接字置于非阻塞 I/O 模式。

问题:

  1. 这是否意味着如果内核发送缓冲区有空间,那么 send() 调用将总是立即返回?
  2. send() 在 TCP 和 UDP 上的行为和性能是否相同?如果不同,原因是什么?

1
不同的观点,请查看答案 - Rick
5个回答

46

这是否意味着,如果内核发送缓冲区中有空间,send()调用将总是立即返回?

是的。只要“立即”指的是在你提供的内存被复制到内核缓冲区后就会立即返回。然而,在某些边缘情况下,可能并不那么立即。例如,如果您传递的指针触发需要从存储在内存映射文件或交换空间中的缓冲区中拉取数据的页故障,则会为调用返回添加显著延迟。

对于TCP和UDP,send()调用的行为和性能是否相同?如果不是,为什么?

不完全相同。可能的性能差异取决于操作系统实现TCP/IP协议栈的方式。理论上,UDP套接字可能会稍微更便宜,因为操作系统需要处理的工作较少。

编辑:另一方面,由于您可以使用TCP每次系统调用发送更多的数据,因此每个字节的成本通常可以更低。这可以通过最近Linux内核中的sendmmsg()来缓解。

至于行为,它几乎相同。

对于阻塞套接字,TCP和UDP都将阻塞,直到内核缓冲区中有空间。但是,区别在于UDP套接字将等待,直到您的整个缓冲区可以存储在内核缓冲区中,而TCP套接字可能只会将单个字节复制到内核缓冲区中(通常是多个字节)。

如果您尝试发送大于64kiB的数据包,则UDP套接字很可能会始终失败,并显示EMSGSIZE。这是因为作为数据报套接字的UDP保证将您的整个缓冲区作为单个IP数据包(或IP数据包片段列车)发送,否则不发送。

非阻塞套接字与阻塞版本完全相同,唯一的区别是当内核缓冲区中没有足够空间时,调用不会被阻塞,而是以 EAGAIN (或 EWOULDBLOCK) 失败。当出现这种情况时,需要将套接字重新放回 epoll/kqueue/select(或者使用的任何其他机制)中等待它再次变得可写。

像 POSIX 的常规操作一样,要记住您的调用可能会因为信号中断而失败,并返回EINTR。在这种情况下,最好重新调用 send()


TCP套接字只复制了您缓冲区的一部分,这是如何工作的?它只返回一个小于您消息长度的写入值吗?如果它返回EAGAIN,则无法知道写入了多少字节... - gct
1
EAGAIN不是返回值,而是“errno”的值。在TCP套接字上调用send()函数时,如果一切正常,它会告诉你发送了多少字节;如果出现错误,则返回-1。在出现错误的情况下,你需要查看“errno”以获取详细信息。EAGAIN表示无法再发送更多数据,因为其发送缓冲区已满(或超过高水位标记)。 - Arvid
1
巨型帧是比标准尺寸更大的以太网帧 - 无论是否使用巨型帧,UDP本身始终限制为64KiB。 - Remember Monica
1
[...] 区别在于 UDP 套接字将等待,直到整个缓冲区可以存储在内核缓冲区中。但是,在 BSD 系统中并非如此。如果发送缓冲区已满,则数据包会被无情地丢弃。真是太遗憾了!我几个月前就注意到了这一点。当以最大速度编写时,在 BSD 上编写正确的 UDP 应用程序很困难。 - Kr0e
1
有趣。你有没有收到任何错误指示?例如 EAGAIN? - Arvid
在BSD和macOS/iOS上,如果您尝试通过UDP套接字发送更多数据,而这些数据不再适合套接字缓冲区,则send()会立即失败,并显示错误ENOBUFS。无论套接字是阻塞还是非阻塞,都会发生这种情况。因此,在这些系统上,UDP套接字永远不会在发送时阻塞,也永远不会返回EAGAIN。这是合法的行为,因为UDP本身就不保证数据传递。如果发送缓冲区已满,则会出现积压,沿途总会有一些积压会被丢弃,最好尽早发现以便应用程序能够及时做出反应。 - Mecki

9
如果内核缓冲区有空间,那么send()会尽可能多地将字节复制到缓冲区中,并立即退出,返回实际复制的字节数(可能少于请求的字节数)。如果内核缓冲区没有空间,则send()会阻塞,直到有空间可用或超时发生(如果已配置)。

如果字节可以传递给客户端会发生什么?发起者将如何收到通知? - Guillaume Paris
4
一旦内核接受了字节到缓冲区,它就不在你的掌控范围内了。你只能接受这些字节将在后台传输并继续进行。除非接收者发送回复,否则没有通知表明它们实际上已经被传输或是否/何时已经被接收。如果缓冲区空间用尽,send()将会阻塞(如果处于阻塞模式),直到有空间可用,或者立即失败并返回EWOULDBLOCKEAGAIN(如果处于非阻塞模式)。如有需要,您可以使用select()poll()等方法在调用send()之前检测是否有空间可用。 - Remy Lebeau

1
send()方法将在内核接受数据后立即返回。
阻塞套接字情况下:如果内核缓冲区没有足够的空间来接收send()调用提供的数据,则send()方法会阻塞。
非阻塞套接字情况下:send()方法不会阻塞,但可能会失败并返回-1或部分复制的字节数(取决于可用的缓冲区空间)。它设置errno为EWOULDBLOCK或EAGAIN。这意味着在send()时,缓冲区无法接受所有字节,您应该使用select()调用再次尝试发送数据。或者您可以使用循环和sleep()来调用send(),但是您必须注意实际发送的字节数以及剩余待发送的字节数。

1
这是否意味着如果内核发送缓冲区有空间,send()调用将始终立即返回?
应该是这样吧?数据“已发送”的时间点可以定义得不同。我认为这是一个操作系统接受您的数据并将其放入堆栈以供传递的时刻。否则很难定义它。是数据传输到网络卡缓冲区的时刻?还是在数据推出网络卡缓冲区之后的时刻?
您需要确定这一点是否存在问题,还是只是好奇呢?

1
嗯,我正在尝试确定函数调用是否会阻塞任何类型的I / O,如果是的话,会阻塞多长时间。这不是一个存在的问题 :-) - David Citron

0

你的假设是正确的。如果内核发送缓冲区有空间,内核将把数据复制到发送缓冲区中,send()函数将返回。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接