Pull to refresh

Comments 61

Доколе ж на Хабре будут писать «итак» раз дель но. Это ж полный швах, почти как жи-ши.
простите, плохо меня учили в школе.
Не бывает плохо учили, бывает плохо учился.
первое правило работы с сигналами в с++: никогда не используйте в обработчиках нефатальных сигналов ни для чего другого кроме как выставить значение PoD переменной.
Первое правило работы с сигналами, на самом деле, такое: из обработчика сигналов безопасно можно менять только значения переменных типа volatile sig_atomic_t и больше ничего. Вот в этом FAQ очень хорошо написано что можно делать из обработчика сигналов, а что нет.
Безопасно можно менять и без volatile, если место где происходит проверка этой переменной вызывается только время от времени. Конечно для совсем неразбирающихся в том до какого вида компилятор може соптимайзить код лучше внушить про volatile :)
WAT? Что значит время от времени? Вы сейчас хотите сказать, что компилятор будет «время от времени» делать load на переменную просто так? Без volatile flow analysis покажет, что переменная никогда не записывается кроме инициализации, и никакое «время от времени» само по себе не сработает.
О боже :) вы представляете вообще себе не код аля в этой статье а большую программу? :)
например:

init_some_variables(); // тут мы чтото делаем, в том числе инициализируем многострадальную не volatile переменную в классе A, заодно и создаем экземпляр класса.

check_incoming_connections(); // чтонить вроде poll, accept, добавления сокетов в контейнер, создание обработчика событий на сокете и тп

do_some_job(); // тут мы, например, делаем запрос в sql базу и чтонить там лопатим

what_about_our_signal_variable(); // тут мы проверяем ту самую переменную, которая как написано выше является полем класса созданного в куче.

и все это в одном потоке исполнения :)

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

P.S. да и таки изменять переменную все равно безопасно даже учитывая то что компилятор может ее сделать регистровой и незаметить «изменение» в сигнале :) ничего же страшного не произойдет :) просто тот участок кода что следит за переменной не узнает о изменении :)))

Я дам вам совет: остановиться сейчас и не выставлять себя ещё большим дураком, окей?

Стандарт C++, ISO-IEC 14882, 3rd Edition, пункт 1.9.6:
When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects which are neither
— of type volatile std::sig_atomic_t nor
— lock-free atomic objects (29.4)
are unspecified during the execution of the signal handler, and the value of any object not in either of these two categories that is modified by the handler becomes undefined

Ваш отличный план опустить volatile приводит к undefined behavior, что означает, что весь ваш код, который создаёт сокеты, ходить в mysql и так далее, можно спокойно выбросить, потому что он — говно.
то есть вы не можете объяснить каким макаром компилятор сделает такой код просто ссылаетесь на стандарт? :) Хороший подход, правильный :) можете вообще выбросить весь свой код, если вы даже не можете себе представить во что его компилирует компилятор :)
Мм, вы это серьёзно, да? Вам термин «неопределённое поведение» о чем-нибудь говорит?
мне то говорит :) но я в отличии от вас ЗНАЮ во что компилируется этот код :) такчто для меня на всех существующих компиляторах именно МОЙ пример вполне себе имеет определенное поведение. Это не f(++i,i++) где реально поведение может отличаться от компилятора к компилятору. И не while( signal_flag ){}; где signal_flag будет лежать в регистре практически с любой опцией оптимизации. Вы прежде чем читать undefined behavior подумали также бы в чем этот undefined состоит… Но видимо поколение нынче такое, вглубь нишагу…
А я вас узнал — вы прошивки для Фобос-Грунтов пишете, да?
И про фобос-грунт мимо, хоть бы почитали в чем там причину откопали: использование статичтеской памяти очень чувствительной к радиации да еще и оба дублирующих модуля стояли рядом. Такчто там прошивка не причем. Всетаки это мода текущего поколения, не зная ничего глубоко пытаться рассуждать о том что они «знают».
Окей.

1) Вы объявляете глобальную не-volatile переменную для работы с сигналами. Вы хотите проверять её только из what_about_our_signal_variable();

2) Эта функция инлайнится в call-site, потому что больше она ниоткуда не вызывается.

3) При заходе в call-site функцию переменная кладется в регистр.

4) Происходит сигнал, регистры и прочие вещи сохраняются, ваш хендлер инкрементирует переменную в памяти.

5) Старое значение в регистре восстанавливается, инкремент вы не заметили, но в памяти при этом будет новое значение.

6) Ваша программа начинает вести себя непредсказуемо, потому что вы ввели в неё неопределённое поведение; стандарт в этом случае развязывает руки компилятору, оставляя вас без единого формального способа доказать корректность вашего кода.
5) ничего страшного не произошло :) мы просто не узнали в тот самый момент времени :) но мы узнаем об изменени на следующей итерации :)) Думаете мне важно узнать о сигнале точь в точь когда он пришел? :) нет мне важно узнать о том что он вообще был :) для этого и используется флаг.

6) с чего вы это взяли? :) Читайте пункт 5 :)
Никто не гарантирует, что переменная будет перечитана, она же лежит в регистре и _там_ её никто не менял, так что вы можете никогда не узнать о том, что она была изменена из хендлера.
опять двадцать пять? :) с чего она лежит в регистре то? :) мы из участка проверки давным давно ушли и делаем все из изначального списка по кругу :)
Она лежит в регистре, потому что вы не покидали call-site-а с заинлайнеными одноразовыми функциями, мало понимаете в C++, почти ничего не знаете о компиляторах и любите почесать языком впустую =) Удачи!
обоже :) я покинул call-site c «заинлаенными функциями» читайте внимательно :) такчто мало понимаете с++ и вообще ничего не знаете как исполняется код (соответственно и знание компиляторов на томже уровне). удачи в ваших «познаниях»
Это очень хорошее и полезное правило.
Но оно, конечно, может быть уточнено.
1) В обработчике сигнала можно выполнять reenterable-операции. К сожалению, к ним практически можно отнести лишь простое присваивание и функции, написанные реентерабельным образом.
2) Если время приема сигнала аккуратно отслеживать (например, принимать сигналы только в определенные моменты или интервалы времени), то можно безопасно совершать большее число действий.
3) Из обработчика можно производить возврат в другой контекст (отличный от контекста, сохраненного при получении сигнала).
В обработчике сигнала можно выполнять reenterable-операции. К сожалению, к ним практически можно отнести лишь простое присваивание и функции, написанные реентерабельным образом.
А ещё можно вызывать syscall-ы, т. к. их выполнение не может быть прервано сигналом.
скрывать не буду, мое знание работы обработчика сигналов находится на уровне юзера. Но, что может случиться такого плохого если в обработчике для например SIGUSR1 будет нетривиальная логика?
«Нетривиальная логика» — понятие растяжимое. Но если вы например будете выделять память в обработчике сигнала как-то иначе кроме как с помощью mmap, вас ждут большие неприятности. А что если тред как раз выполнял malloc() в момент прихода сигнала? У вас и куча в неконсистентном состоянии, и мьютекс залочен.
Кстати, операции с std::string как раз во многих случаях вызывают malloc. Но только точно ли malloc нереентерабельна? Мне кажется, что она с большой вероятностью реентерабельна, иначе бы тысячи программ сыпались.
Нет, malloc не реентерабельна. А также масса других функций.
Вы путаете реентерабельность с thread safety. malloc — thread safe, но не signal-safe.
Но мы можем сами определить обработчики для интересующих нас сигналов. К примеру вот такой
void hdl(int sig)
{
    std::string signal;
    if(sig == SIGUSR1)
        signal = "SIGUSR1";

ZOMG! Вы это серьезно?
нет, это чистой воды провокация и надругательство над Linux way
Есть ненулевая вероятность, что вашу статью будет читать человек, не знакомый с темой сигналов. Вы создаете ложное впечатления, что в обработчике сигналов можно использовать std::string и std::cout. Делать этого нельзя, но абстрактный Linux way тут совершенно не при чем. Проблема в реентерабельности — доставка сигнала прерывает выполнение потока в произвольный момент времени, поэтому в момент вызова функции-обработчика глобальные объекты, такие как аллокатор динамической памяти (хип) или cout находятся в потенциально неконсистентном состоянии.
UFO just landed and posted this here
Неуклюжий способ, так как в сигналах куча ограничений (многое делать нельзя), и нельзя делать стек обработчиков (так как установка нового отменяет старый). Неудобная система.
UFO just landed and posted this here
Самое главное ограничение — это то, что для выполнения обработчика сигнала система «одалживает» поток приложения, прерывая выполнение в случайной точке. Как следствие — куча ограничений на то, что можно делать в обработчике сигнала. Например Windows в аналогичной ситуации (обработчик Ctrl-C в консольном приложении), просто создает новый поток и выполняет обработчик в нем. Вы только не забывайте о том, что сигналы были изобретены в 1970-х, и никаких потоков еще не было.

Претензии на счет отсутствия стека обработчиков несколько странны — вы не обосновали потребности в такой фиче. При необходимости, можно реализовать обертку вокруг механизма сигналов с поддержкой стека, индивидуальными обработчиками для каждого потока и так далее.

Современные системы кстати позволяют вобще не пользоваться обработчиками сигналов. Нотификации о сигналах можно получать через механизмы eventfd и kqueue.
Есть мнение, что статья хорошо вылезет в яндексе-гугле; однако ж, в ней есть грубые ошибки.

1.SIGKILL нельзя проигнорировать, но можно перехватить и обработать (rtfm sigtap)
2. Любой обработчик сигналов должен быть реентерабельным, это особая магия; про volatile rtfm выше.
oops :) букавка потерялась;(
Не-не-не, не сбивайте с толку. man SystemTap.
# probe signal.send = _signal.send.*
# {
# 	sig=$sig
# 	sig_name = _signal_name($sig)
# 	sig_pid = task_pid(task)
# 	pid_name = task_execname(task)
# [...]

probe signal.send {
  if (sig_name == "SIGKILL")
    printf("%s was sent to %s (pid:%d) by %s uid:%d\n",
           sig_name, pi
>2. Любой обработчик сигналов должен быть реентерабельным

Не всегда. Если обработчик установлен с помощью sigaction без флага SA_NODEFER, то реентерабельность можно не обеспечивать, так как сигнал будет заблокирован во время работы обработчика.
Ценное замечание, но с помощью функции sigaction нельзя повесить обработчик на SIGKILL. Если я ошибаюсь приведите пример. У меня не получилось.
Кстати в статье не рассмотрены несколько важных тем, без которых претендовать на полное освещение вопроса не приходится.

— сигналы реального времени;
— использование сигналов в системных API для асинхронной нотификации (aio, нотификации готовности файловых дескрипторов (fcntl(F_SETSIG)));
— альтернативные способы получения нотификаций о приходе сигналов (signalfd, kqueue).
>PS. Посоветуйте куда залить сорцы, что то мне не очень нравиться на дроббоксе их держать, а вдруг удалю.

github же
Вот такой у меня вопрос к хабрсообществу. Данную статью, да и данную тему меня заставило изучить одна насущная проблема. И именно последний пример который я привел.

Представьте ситуацию: есть поток он блокируется на операции чтения. И мне нужно его уметь прервать в любой момент. В WIN это делается с помощью WaitForMultipleObject + Event. Аналог под Linux это только ppoll (pselect). Но приходиться работать с сигналами на уровне треда, что не очень то хорошо. Да и само использование сигналов обычно не приветствуется. Но как тогда сделать задуманное? В особенности если прервать поток нужно не в момент завершение работы, а просто как прервать таску. И таких потоков много, и каждый их них независим, а обработчик сигналов один.
Я было думал написать механизм «обработчик сигнала для треда», но в свете новых фактов я уже сомневаюсь, что это возможно, а если и возможно, то уж точно не так как я себе это представлял.
Кто мешает в качестве флага выхода завести витовый вектор по биту на тред, и проверять в треде соответствующий бит?
битовый вектор, конечно
1. вектор = динамическая память = mmaloc как я понял нельзя использовать в обработчике сигналов.
2. привязываем трет к нужному биту на этапе проектирования? а если не получается?
Слово «вектор» я использовал в классическом значении — массив, без привязки к конкретной реализации.
В простейшем случае, когда нитей немного, это может быть простой инт. Если тредов больше, чем разрядность целевой машины, это будет массив.
На класс памяти никаких ограничений на самом деле нет. Конечно, логично разместить его статически, но даже если пришла фантазия разместить его в куче — да пожалуйста, главное сделать это при старте до настройки обработки сигналов.
FYI: флаги можно держать в TLS. Pthread_get(_set)specific безопасны для использования в обработчиках сигналов (к сожалению, только de facto, но не de jure.)
Зачем усложнять простые вещи. Флаги по определению thread safe. Устанавливаются только в обработчике прерываний, треды его только читают. Обычного статического инта, массива вполне достаточно.
ИМХО это вы усложняете. Как именно предполагается сопоставлять pthread_t (а больше в обработчике сигнала ничего не доступно) и индекс элемента в массиве?
На самом деле в обработчике доступен только номер сигнала.
Общая схема предполагается такой: при старте нити она получает порядковый номер. Далее управляющая нить решает, что какую-то, скажем третью, нить нужно остановить. Тогда она в стоп-векторе третий бит ставит в один и вызывает сигнал. В обработчике ничего не происходит. Все нити в состоянии чтения выходят из функции с состоянием EINTR. Нити проверяют наличие флага в векторе, третья завершается, остальные перезапускают функцию чтения.
а если порядок нитей не определен?

А разве когда пошлется сигнал (наверно SIGHUP) прервутся блокирующие операции по всех нитях? Или только в той которой адресовался сигнал?

Предложенный вами способ применим, но имеет явные ограничения на архитектуру.
есть поток он блокируется на операции чтения

Я почему-то думал, что в этом случае EINTR должен приходить, нужно только убедиться, что системный вызов не будет перезапущен. К примеру, вот: pastebin.com/KtQ3migv

Если послать SIGHUP, то приложение недемеленно прервется с сообщением «read: interrupted system call», главное сбросить SA_RESTART из флагов при установке обработчика и корректно обрабатывать ситуацию с EAGAIN в коде обработчика.
спасибо, долго же я это искал :) Правда я думал как такое сделать без сигналов, ну а потом постепенно пришел к ppoll.
Представьте ситуацию: есть поток он блокируется на операции чтения. И мне нужно его уметь прервать в любой момент. В WIN это делается с помощью WaitForMultipleObject + Event. Аналог под Linux это только ppoll (pselect). Но приходиться работать с сигналами на уровне треда, что не очень то хорошо.

Справедливости ради, надо сказать что есть еще epoll (более современный и эффективный аналог). Можно тупо добавив pipe в список наблюдаемых файловых дескрипторов, и сигналить методом «записать один байт в пайп». В современных линуксах есть eventfd.
Годная статья, однако я не поняял что тут нового относительно учебников?



Лично я использую сигналы, для корректного завершения программы по нажатию ctrl-c/z. В особенности это актуально при работе во многопоточном режиме, когда идёт запись на диск и получения данных от сторонних устройств (или сокетов, что не суть важно).

Делаю процедуры, для корректного завершения, в которых идёт освобождение памяти, закрытие открытых файлов, и т.п. На которую ссылаюсь при инициализации

atexit(обрабочик_завершения);

А обработчик сигнала выглядит например так:

void sig_stop_all (int signo)
{
exit(EXIT_SUCCESS);
}
А зачем освобождать память и закрывать файлы при завершении? Я на это всегда забиваю.
На всякий пожарный. Например, если был запущен процесс таким образом:

scriptf = popen(«gnuplot -persist»,«w»);

то он продолжит работать и после завершения программы. Бывает, что сие недопустимо.
простите за grammar-nazism, но thread в хорошей переводной литературе принято переводить как «поток», а в случае, где это допускает двоякое толкование (т.е где рядом еще есть «поток данных») — «поток команд». Что, впрочем, заметно и по комментариям.
Sign up to leave a comment.

Articles