Очистить TCP-буфер ядра для пакетов с флагом `MSG_MORE`

справочная страница send() показывает флаг 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, пакеты будут сброшены автоматически, если они достаточно велики:

Если приложение устанавливает этот параметр для сокета, ядро ​​не будет отправлять короткие пакеты. Вместо этого он будет ждать, пока не появится достаточно данных для заполнения пакета максимального размера, а затем отправит его. Когда TCP_CORK отключен, все оставшиеся данные будут передаваться по сети.

Но этот раздел относится только к TCP_CORK. Теперь, как правильно сбрасывать пакеты MSG_MORE?

Я могу думать только о двух возможностях:

  1. Вызовите send() с пустым буфером и без установки MSG_MORE
  2. Повторно примените параметр TCP_CORK, как описано в эта страница

К сожалению, вся тема очень плохо документирована, и я не смог найти много в Интернете.

Мне также интересно, как проверить, что все работает как положено? Очевидно, запустить сервер через strace не вариант. Таким образом, самым простым способом было бы использовать netcat, а затем посмотреть на его вывод strace? Или ядро ​​будет по-другому обрабатывать трафик, передаваемый через петлевой интерфейс?


person user206268    schedule 30.03.2010    source источник
comment
sendfile() сохраняет флаг MSG_MORE. Затем кеш сбрасывается при возврате sendfile().   -  person user206268    schedule 31.03.2010


Ответы (2)


Я взглянул на исходный код ядра, и оба предположения кажутся верными. Следующий код является извлечением из 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 можно оптимизировать, чтобы просто отключить его, даже если он в настоящее время не установлен.

Я надеюсь, что это поможет кому-то с похожими проблемами.

person user206268    schedule 31.03.2010
comment
Спасибо за это исследование. Очень полезно. - person bvanderveen; 29.08.2010

Это много исследований ... все, что я могу предложить, это эта эмпирическая заметка:

Отправка пакета с установленным 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 освобождает ранее задержанные пакеты. По крайней мере, похоже, это проблема, которая у меня сейчас. Но это может быть простым решением для вас.

person Orwellophile    schedule 24.08.2011