TCP: Когда генерируется EPOLLHUP?

Также посмотрите этот вопрос, без ответа на данный момент.

Существует много путаницы по поводу EPOLLHUP даже в man и документы ядра. Люди, кажется, считают, что он возвращается при опросе дескриптора, локально закрытого для записи, т.е. shutdown(SHUT_WR) то есть тот же вызов, который вызывает EPOLLRDHUP на пэра. Но это неправда, в моих экспериментах я получаю EPOLLOUT, и нет EPOLLHUP, после shutdown(SHUT_WR) (да, нелогично получать доступ к записи, поскольку половина письма закрыта, но это не главное в этом вопросе).

Человек беден, потому что говорит EPOLLHUP происходит, когда произошел сбой в связанном файловом дескрипторе, не говоря, что означает "зависание" - что сделал одноранговый узел? какие пакеты были отправлены? Эта другая статья просто еще больше запутывает вещи и кажется мне совершенно неправильной.

Мои эксперименты показывают EPOLLHUP приходит, когда EOF (FIN-пакеты) обмениваются в обе стороны, то есть, когда обе стороны выдают shutdown(SHUT_WR), Это не имеет ничего общего с SHUT_RD, который я никогда не называю. Также не имеет ничего общего с close, Что касается пакетов, у меня есть подозрение, что EPOLLHUP устанавливается на подтверждение отправленного FIN хоста, т. е. инициатор завершения вызывает это событие на шаге 3 четырехстороннего рукопожатия при отключении, а партнер на шаге 4 (см. здесь). Если подтвердится, это здорово, потому что он заполняет пробел, который я искал, а именно, как опросить неблокирующие сокеты для окончательного подтверждения без LINGER. Это правильно?

(примечание: я использую ET, но я не думаю, что это актуально для этого)

Пример кода и вывод.

Код, находящийся в фреймворке, я извлек из него всю сущность, за исключением TcpSocket::createListener, TcpSocket::connect а также TcpSocket::accept, которые делают то, что вы ожидаете (не показано здесь).

void registerFd(int pollFd, int fd, const char* description)
{
    epoll_event ev = {
        EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET,
        const_cast<char*>(description) // union aggregate initialisation, initialises first member (void* ptr)
    };
    epoll_ctl(pollFd, EPOLL_CTL_ADD, fd, &ev);
}

struct EventPrinter
{
    friend std::ostream& operator<<(std::ostream& stream, const EventPrinter& obj)
    {
        return stream << "0x" << std::hex << obj.events_ << " = "
            << ((obj.events_& EPOLLIN) ? "EPOLLIN " : " ")
            << ((obj.events_& EPOLLOUT) ? "EPOLLOUT " : " ")
            << ((obj.events_& EPOLLERR) ? "EPOLLERR " : " ")
            << ((obj.events_& EPOLLRDHUP) ? "EPOLLRDHUP " : " ")
            << ((obj.events_& EPOLLHUP) ? "EPOLLHUP " : " ");
    }

    const uint32_t events_;
};

void processEvents(int pollFd)
{
    static int iterationCount = 0;
    ++iterationCount;

    std::array<epoll_event, 25> events;
    int eventCount;
    if (-1 ==
        (eventCount = epoll_wait(pollFd, events.data(), events.size(), 1)))
    {
        throw Exception("fatal: epoll_wait failed");
    }

    for (int i = 0; i < eventCount; ++i)
    {
        std::cout << "iteration #" << iterationCount << ": events on [" << static_cast<const char*>(events[i].data.ptr) << "]: [" << EventPrinter{events[i].events} << "]" << std::endl;
    }
}

TEST(EpollhupExample, SmokeTest)
{
    int pollFd_;
    if (-1 ==
        (pollFd_ = epoll_create1(0)))
    {
        throw Exception("fatal: could not create epoll socket");
    }

    const TcpSocket listener_ = TcpSocket::createListener(13500);
    if (!listener_.setFileStatusFlag(O_NONBLOCK, true))
        throw Exception("could not make listener socket non-blocking");
    registerFd(pollFd_, listener_.fd(), "listenerFD");

    const TcpSocket client = TcpSocket::connect("127.0.0.1", AF_INET, 13500);
    if (!client.valid()) throw;
    registerFd(pollFd_, client.fd(), "clientFD");





    //////////////////////////////////////////////
    /// start event processing ///////////////////
    //////////////////////////////////////////////

    processEvents(pollFd_); // iteration 1

    const TcpSocket conn = listener_.accept();
    if (!conn.valid()) throw;
    registerFd(pollFd_, conn.fd(), "serverFD");

    processEvents(pollFd_); // iteration 2

    conn.shutdown(SHUT_WR);

    processEvents(pollFd_); // iteration 3

    client.shutdown(SHUT_WR);

    processEvents(pollFd_); // iteration 4
}

Выход:

    Info| TCP connection established to [127.0.0.1:13500]
iteration #1: events on [listenerFD]: [1 = EPOLLIN     ]
iteration #1: events on [clientFD]: [4 =  EPOLLOUT    ]
    Info| TCP connection accepted from [127.0.0.1:35160]

iteration #2: events on [serverFD]: [4 =  EPOLLOUT    ]
    // calling serverFD.shutdown(SHUT_WR) here

iteration #3: events on [clientFD]: [2005 = EPOLLIN EPOLLOUT  EPOLLRDHUP  ]           // EPOLLRDHUP arrives, nice.
iteration #3: events on [serverFD]: [4 =  EPOLLOUT    ]                               // serverFD (on which I called SHUT_WR) just reported as writable, not cool... but not the main point of the question
    // calling clientFD.shutdown(SHUT_WR) here

iteration #4: events on [serverFD]: [2015 = EPOLLIN EPOLLOUT  EPOLLRDHUP EPOLLHUP ]   // EPOLLRDHUP arrives, nice. EPOLLHUP too!
iteration #4: events on [clientFD]: [2015 = EPOLLIN EPOLLOUT  EPOLLRDHUP EPOLLHUP ]   // EPOLLHUP on the other side as well. Why? What does EPOLLHUP mean actually?

Нет лучшего способа перефразировать вопрос, кроме того, что означает EPOLLHUP? Я доказал, что документация плохая, а информация в других местах (например, здесь и здесь) неверна или бесполезна.

Примечание. Чтобы считать ответ "Q", я хочу получить подтверждение того, что EPOLLHUP поднят на последних FIN-ACK обоих направлений.

1 ответ

Решение

Для подобных вопросов используйте источник! Среди других интересных комментариев есть этот текст:

EPOLLHUP НЕИЗМЕРЕННОЕ событие (...). Это означает, что после того, как мы получили EOF, poll всегда возвращается немедленно, делая невозможным poll() на write() в состоянии CLOSE_WAIT, Одно решение очевидно --- установить EPOLLHUP если и только если shutdown был сделан в обоих направлениях.

И тогда единственный код, который устанавливает EPOLLHUP:

if (sk->sk_shutdown == SHUTDOWN_MASK || state == TCP_CLOSE)
    mask |= EPOLLHUP;

бытие SHUTDOWN_MASK равно RCV_SHUTDOWN |SEND_SHUTDOWN,

TL; DR; Вы правы, этот флаг отправляется только тогда, когда завершение было как для чтения, так и для записи (я считаю, что одноранговое отключение, завершающее запись, равно моему закрытию чтения). Или когда соединение закрыто, конечно.

ОБНОВЛЕНИЕ: Из чтения исходного кода более подробно, это мои выводы.

Около shutdown:

  1. дела shutdown(SHUT_WR) отправляет FIN и помечает розетку SEND_SHUTDOWN,
  2. дела shutdown(SHUT_RD) ничего не отправляет и помечает сокет RCV_SHUTDOWN,
  3. Получение FIN помечает розетку RCV_SHUTDOWN,

И о epoll:

  1. Если розетка помечена SEND_SHUTDOWN а также RCV_SHUTDOWN, poll вернусь EPOLLHUP,
  2. Если розетка помечена RCV_SHUTDOWN, poll вернусь EPOLLRDHUP,

Итак HUP события могут быть прочитаны как:

  1. EPOLLRDHUP: ты получила FIN или вы позвонили shutdown(SHUT_RD), В любом случае ваша половина сокета чтения зависла, то есть вы больше не будете читать данные.
  2. EPOLLHUP: у вас обе половинки подвешены. Считывающая половина сокета аналогична предыдущему пункту. Для отправляющей половины сокета вы сделали что-то вроде shutdown(SHUT_WR),

Чтобы завершить корректное отключение, я бы сделал:

  1. Делать shutdown(SHUT_WR) отправить FIN и отметьте конец отправки данных.
  2. Подождите, пока пэр сделает то же самое, опросив, пока не получите EPOLLRDHUP,
  3. Теперь вы можете закрыть розетку с изяществом.

PS: о вашем комментарии:

нелогично получать доступ к записи, так как половина записи закрыта

Это на самом деле ожидается, если вы понимаете вывод epoll не так, как готов, но как не будет блокировать. То есть, если вы получите EPOLLOUT у вас есть гарантия, что звонит write() не будет блокировать. И, конечно же, после shutdown(SHUT_WR), write() вернется немедленно.

Другие вопросы по тегам