【问题标题】:Flush kernel's TCP buffer for `MSG_MORE`-flagged packets为带有“MSG_MORE”标志的数据包刷新内核的 TCP 缓冲区
【发布时间】:2010-03-30 19:11:45
【问题描述】:

send() 的man page 揭示了MSG_MORE 标志,该标志被断言为类似于TCP_CORK。我在send() 周围有一个包装函数:

int SocketConnection_Write(SocketConnection *this, void *buf, int len) {
    errno = 0;

    int sent = send(this->fd, buf, len, MSG_NOSIGNAL);

    if (errno == EPIPE || errno == ENOTCONN) {
        throw(exc, &SocketConnection_NotConnectedException);
    } else if (errno == ECONNRESET) {
        throw(exc, &SocketConnection_ConnectionResetException);
    } else if (sent != len) {
        throw(exc, &SocketConnection_LengthMismatchException);
    }

    return sent;
}

假设我想使用内核缓冲区,我可以使用TCP_CORK,在必要时启用,然后禁用它以刷新缓冲区。但另一方面,因此需要额外的系统调用。因此,MSG_MORE 的用法似乎更适合我。我只需将上面的 send() 行更改为:

int sent = send(this->fd, buf, len, MSG_NOSIGNAL | MSG_MORE);

根据lwm.net,如果数据包足够大,会自动刷新:

如果应用程序将该选项设置为 一个socket,内核不会发送出去 短数据包。相反,它将等待 直到出现足够的数据来填充 一个最大大小的数据包,然后发送它。 当 TCP_CORK 关闭时,任何 剩余的数据将在 电线。

但这部分只涉及TCP_CORK。现在,刷新MSG_MORE 数据包的正确方法是什么?

我只能想到两种可能:

  1. 使用空缓冲区调用 send() 且未设置 MSG_MORE
  2. 按照this 页面上的说明重新应用 TCP_CORK 选项

不幸的是,整个主题的文档记录很差,我在 Internet 上找不到太多内容。

我也想知道如何检查一切是否按预期工作?显然,通过strace 运行服务器不是一种选择。所以最简单的方法是使用netcat,然后查看它的strace 输出?或者内核会以不同的方式处理通过环回接口传输的流量吗?

【问题讨论】:

  • sendfile() 保留 MSG_MORE 标志。然后当 sendfile() 返回时刷新缓存。

标签: c linux tcp buffer


【解决方案1】:

我查看了内核源代码,这两个假设似乎都是正确的。以下代码摘自net/ipv4/tcp.c (2.6.33.1)。

static inline void tcp_push(struct sock *sk, int flags, int mss_now,
                int nonagle)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (tcp_send_head(sk)) {
        struct sk_buff *skb = tcp_write_queue_tail(sk);
        if (!(flags & MSG_MORE) || forced_push(tp))
            tcp_mark_push(tp, skb);
        tcp_mark_urg(tp, flags, skb);
        __tcp_push_pending_frames(sk, mss_now,
                      (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle);
    }
}

因此,如果标志没有设置,挂起的帧肯定会被刷新。但这是仅当缓冲区不为空时的情况:

static ssize_t do_tcp_sendpages(struct sock *sk, struct page **pages, int poffset,
             size_t psize, int flags)
{
(...)
    ssize_t copied;
(...)
    copied = 0;

    while (psize > 0) {
(...)
        if (forced_push(tp)) {
            tcp_mark_push(tp, skb);
            __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
        } else if (skb == tcp_send_head(sk))
            tcp_push_one(sk, mss_now);
        continue;

wait_for_sndbuf:
        set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
        if (copied)
            tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);

        if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
            goto do_error;

        mss_now = tcp_send_mss(sk, &size_goal, flags);
    }

out:
    if (copied)
        tcp_push(sk, flags, mss_now, tp->nonagle);
    return copied;

do_error:
    if (copied)
        goto out;
out_err:
    return sk_stream_error(sk, flags, err);
}

while 循环的主体永远不会被执行,因为psize 不大于 0。然后,在 out 部分,还有另一个机会,tcp_push() 被调用,但因为 copied 仍然有它的默认值值,它也会失败。

所以发送长度为 0 的数据包永远不会导致刷新。

下一个理论是重新申请TCP_CORK。我们先看一下代码:

static int do_tcp_setsockopt(struct sock *sk, int level,
        int optname, char __user *optval, unsigned int optlen)
{

(...)

    switch (optname) {
(...)

    case TCP_NODELAY:
        if (val) {
            /* TCP_NODELAY is weaker than TCP_CORK, so that
             * this option on corked socket is remembered, but
             * it is not activated until cork is cleared.
             *
             * However, when TCP_NODELAY is set we make
             * an explicit push, which overrides even TCP_CORK
             * for currently queued segments.
             */
            tp->nonagle |= TCP_NAGLE_OFF|TCP_NAGLE_PUSH;
            tcp_push_pending_frames(sk);
        } else {
            tp->nonagle &= ~TCP_NAGLE_OFF;
        }
        break;

    case TCP_CORK:
        /* When set indicates to always queue non-full frames.
         * Later the user clears this option and we transmit
         * any pending partial frames in the queue.  This is
         * meant to be used alongside sendfile() to get properly
         * filled frames when the user (for example) must write
         * out headers with a write() call first and then use
         * sendfile to send out the data parts.
         *
         * TCP_CORK can be set together with TCP_NODELAY and it is
         * stronger than TCP_NODELAY.
         */
        if (val) {
            tp->nonagle |= TCP_NAGLE_CORK;
        } else {
            tp->nonagle &= ~TCP_NAGLE_CORK;
            if (tp->nonagle&TCP_NAGLE_OFF)
                tp->nonagle |= TCP_NAGLE_PUSH;
            tcp_push_pending_frames(sk);
        }
        break;
(...)

如您所见,有两种刷新方式。您可以将 TCP_NODELAY 设置为 1 或将 TCP_CORK 设置为 0。幸运的是,两者都不会检查标志是否已设置。因此,我最初重新应用 TCP_CORK 标志的计划可以优化为禁用它,即使它当前没有设置。

我希望这对有类似问题的人有所帮助。

【讨论】:

  • 感谢您的研究。很有帮助。
【解决方案2】:

这是很多研究......我所能提供的就是这个经验性的帖子说明:

发送一堆设置了 MSG_MORE 的数据包,然后发送一个没有 MSG_MORE 的数据包,整个过程都消失了。它适用于这样的事情:

  for (i=0; i<mg_live.length; i++) {
        // [...]
        if ((n = pth_send(sock, query, len, MSG_MORE | MSG_NOSIGNAL)) < len) {
           printf("error writing to socket (sent %i bytes of %i)\n", n, len);
           exit(1);
        }
     }
  }

  pth_send(sock, "END\n", 4, MSG_NOSIGNAL);

也就是说,当您一次发送所有数据包并且有明确定义的结束时......并且您只使用一个套接字。

如果您在上述循环的中间尝试写入另一个套接字,您可能会发现 Linux 释放了之前持有的数据包。至少这似乎是我现在遇到的麻烦。但这对您来说可能是一个简单的解决方案。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-01-10
    • 2013-10-27
    • 1970-01-01
    • 1970-01-01
    • 2012-02-29
    • 1970-01-01
    • 2017-10-23
    相关资源
    最近更新 更多