Linux select() vs ppoll() vs pselect()
В моем приложении существует io-thread, выделенный для
- Обтекание данных, полученных от приложения в пользовательском протоколе
- Отправка данных + пакет пользовательских протоколов через tcp/ip
- Получение данных + пакет пользовательского протокола через tcp/ip
- Развертывание пользовательского протокола и передача данных в приложение.
Приложение обрабатывает данные по другому потоку. Кроме того, требования диктуют, что размер неподтвержденного окна должен быть равен 1, то есть в любое время должно быть только одно ожидающее непризнанное сообщение. Это означает, что если io-thread отправил сообщение через сокет, он не отправит больше сообщений, пока не услышит ответ от получателя.
Поток обработки приложений связывается с io-thread через трубу. Приложение должно быть изящно закрыто, если кто-то из CLI типов linux ctrl + C.
Таким образом, учитывая эти требования, у меня есть следующие опции
- Использовать PPoll() для дескрипторов сокетов и труб
- Используйте Select()
- Использовать PSelect()
У меня есть следующие вопросы
Ответы
Ответ 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() не.