Linux select() vs ppoll() vs pselect()

В моем приложении существует io-thread, выделенный для

  • Обтекание данных, полученных от приложения в пользовательском протоколе
  • Отправка данных + пакет пользовательских протоколов через tcp/ip
  • Получение данных + пакет пользовательского протокола через tcp/ip
  • Развертывание пользовательского протокола и передача данных в приложение.

Приложение обрабатывает данные по другому потоку. Кроме того, требования диктуют, что размер неподтвержденного окна должен быть равен 1, то есть в любое время должно быть только одно ожидающее непризнанное сообщение. Это означает, что если io-thread отправил сообщение через сокет, он не отправит больше сообщений, пока не услышит ответ от получателя. Поток обработки приложений связывается с io-thread через трубу. Приложение должно быть изящно закрыто, если кто-то из CLI типов linux ctrl + C. Таким образом, учитывая эти требования, у меня есть следующие опции

  • Использовать PPoll() для дескрипторов сокетов и труб
  • Используйте Select()
  • Использовать PSelect()

У меня есть следующие вопросы

  • Решение между select() и poll(). В моем приложении работает менее 50 дескрипторов файлов. Можно ли предположить, что не будет никакой разницы, выбираю ли я выбор или опрос?

    • Решение между select() и pselect(). Я прочитал документацию по Linux, и в нем говорится о состоянии гонки между сигналами и select(). У меня нет опыта с сигналами, так может кто-нибудь объяснить более четко о состоянии гонки и выбрать()? Это связано с тем, что кто-то нажал ctrl + C на CLI и приложение не остановилось?

    • Решение pselect и ppoll()? Любые мысли по одному против другого

Ответ 1

Я бы предложил, начав сравнение с select() vs poll(). Linux также предоставляет как pselect(), так и ppoll(); и дополнительный аргумент const sigset_t * для pselect() и ppoll() (vs select() и poll()) оказывает одинаковое влияние на каждый "p-вариант". Если вы не используете сигналы, у вас нет гонок для защиты, поэтому базовый вопрос действительно об эффективности и простоте программирования.

Между тем уже есть ответ stackoverflow.com здесь: в чем разница между опросом и выбором.

Что касается гонки: как только вы начнете использовать сигналы (по какой-либо причине), вы узнаете, что в общем случае обработчик сигнала должен просто установить переменную типа volatile sig_atomic_t, чтобы указать, что сигнал обнаружен. Основная причина этого заключается в том, что многие вызовы библиотеки не являются re-entrant, и сигнал может быть доставлен, пока вы находитесь "в середине" "такой процедуры. Например, просто печать сообщения в структуру данных в стиле потока, такую ​​как stdout (C) или cout (С++), может привести к проблемам с повторным подключением.

Предположим, что у вас есть код, который использует переменную volatile sig_atomic_t flag, возможно, чтобы поймать SIGINT, что-то вроде этого (см. также http://pubs.opengroup.org/onlinepubs/007904975/functions/sigaction.html):

volatile sig_atomic_t got_interrupted = 0;
void caught_signal(int unused) {
    got_interrupted = 1;
}
...
    struct sigaction sa;
    sa.sa_handler = caught_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGINT, &sa, NULL) == -1) ... handle error ...
    ...

Теперь, в основной части вашего кода, вы можете "запустить до прерванного":

    while (!got_interrupted) {
         ... do some work ...
    }

Это нормально, пока вы не начнете выполнять вызовы, ожидающие ввода/вывода, например select или poll. Действие "wait" должно ждать этого ввода-вывода, но ему также нужно ждать прерывания SIGINT. Если вы просто пишете:

    while (!got_interrupted) {
        ... do some work ...
        result = select(...); /* or result = poll(...) */
    }

тогда возможно, что прерывание произойдет непосредственно перед тем, как вы вызовете select() или poll(), а не потом. В этом случае вы получили прерывание - и переменная got_interrupted устанавливается, но после этого вы начинаете ждать. Вы должны были проверить переменную got_interrupted, прежде чем вы начнете ждать, а не после.

Вы можете попробовать написать:

    while (!got_interrupted) {
        ... do some work ...
        if (!got_interrupted)
            result = select(...); /* or result = poll(...) */
    }

Это сокращает "окно гонки", потому что теперь вы обнаружите прерывание, если это произойдет, когда вы находитесь в коде "сделать некоторые работы"; но есть еще гонка, потому что прерывание может произойти сразу после проверки переменной, но прямо перед выбором или опросом.

Решение состоит в том, чтобы сделать последовательность "test, then wait" "atomic", используя свойства блокировки сигнала sigprocmask (или в потоковом коде POSIX, pthread_sigmask):

sigset_t mask, omask;
...
while (!got_interrupted) {
    ... do some work ...
    /* begin critical section, test got_interrupted atomically */
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    if (sigprocmask(SIG_BLOCK, &mask, &omask))
        ... handle error ...
    if (got_interrupted) {
        sigprocmask(SIG_SETMASK, &omask, NULL); /* restore old signal mask */
        break;
    }
    result = pselect(..., &omask); /* or ppoll() etc */
    sigprocmask(SIG_SETMASK, &omask, NULL);
    /* end critical section */
}

(приведенный выше код на самом деле не так велик, он структурирован для иллюстрации, а не эффективности - более эффективно выполнять манипуляции с массами сигнала немного по-другому, и по-разному поместить тесты с "прерванными" ).

До тех пор, пока вы на самом деле не начнете ловить SIGINT, вам нужно сравнить только select() и poll() (и если вы начнете нуждаться в большом количестве дескрипторов, некоторые из таких событий, как epoll(), более эффективный, чем один).

Ответ 2

Между (p) select и (p) опрос является довольно тонкой разницей:

Для выбора вы должны инициализировать и заполнять уродливые растровые изображения fd_set каждый раз, прежде чем вы выберете select, потому что select изменяет их на месте с "разрушительным" способом. (опрос различает членов .events и .revents в struct pollfd).

После выбора все растровые изображения часто проверяются (людьми/кодом) для событий, даже если большинство фсд даже не просматриваются.

В-третьих, растровое изображение может иметь дело только с fds, число которых меньше определенного предела (современные реализации: где-то между 1024..4096), что исключает его в программах, где можно достичь высокого уровня fds (несмотря на то, что такие программы скорее всего, уже используют epoll).

Ответ 3

Принятый ответ неверен в отношении разницы между select и pselect. Он хорошо описывает, как может возникнуть состояние гонки между sig-handler и select, но неверно в том, как он использует pselect для решения проблемы. Он не замечает основной вопрос о pselect, который заключается в том, что он ожидает, что файл-дескриптор или сигнал станут готовыми. pselect возвращает, когда любой из них готов. Выбрать ONLY ждет файловый дескриптор. Выберите игнорировать сигналы. См. Это сообщение в блоге для хорошего рабочего примера: https://www.linuxprogrammingblog.com/code-examples/using-pselect-to-avoid-a-signal-race

Ответ 4

Для полноты картины, представленной в принятом ответе, необходимо упомянуть следующий основной факт: select() и pselect() могут возвращать EINTR, как указано на их страницах руководства:

EINTR Сигнал был пойман; см. сигнал (7).

Это "пойман" означает, что сигнал должен быть распознан как "произошедший во время выполнения системного вызова":
1. Если во время выполнения выбора /pselect возникает сигнал без маски, то выбор /pselect завершит работу.
2. Если сигнал без маски появляется до вызова select/pselect, это не даст никакого эффекта, и select/pselect будет продолжать ожидание, возможно, навсегда.

Таким образом, если сигнал возникает во время выполнения select/pselect, мы в порядке - выполнение select/pselect будет прервано, и тогда мы можем проверить причину выхода и обнаружить, что это был EINTR, а затем мы можем выйти из цикла.
Реальная угроза, с которой мы сталкиваемся, - это возможность появления сигнала вне выполнения select/pselect, тогда мы можем навсегда зависнуть в системном вызове. Любая попытка обнаружить этот "посторонний" сигнал наивными средствами:

if (was_a_signal) {
   ...
  }

потерпит неудачу, поскольку независимо от того, насколько близко этот тест будет к вызову select/pselect, всегда существует вероятность того, что сигнал возникнет сразу после теста и перед вызовом select/pselect.
Затем, если единственное место, где можно поймать сигнал - это выполнение select/pselect, мы должны изобрести "винную воронку", чтобы все "брызги вина" (сигналы), даже за пределами "горлышка бутылки" ( выберите /pselect период выполнения) в конечном итоге придет к "бутылочному горлу".
Но как вы можете обмануть системный вызов и заставить его "думать", что сигнал произошел во время выполнения этого системного вызова, когда в действительности это произошло раньше?
Легко. Вот наша "винная воронка": вы просто блокируете сигнал интереса, и по этой причине он (если он вообще произошел) ждет за пределами процесса ", пока дверь к будьте открыты ", и вы" откроете дверь "(снимите маску с сигнала) только тогда, когда вы будете готовы" приветствовать гостя "(работает команда select/pselect). Тогда "поступивший" сигнал будет распознан как "только что произошедший" и прервет выполнение системного вызова.
Конечно, "открытие двери" является наиболее важной частью плана - это не может быть сделано обычными средствами (сначала снимите маску, а затем вызовите select/pselect), единственная возможность - выполнить оба действия (снять маску и выполнить систему). call) сразу (атомарно) - это то, на что способна pselect(), но select() не.