Назад к новостям для разработчиков

Signals in prod: dangers and pitfalls

В этой записи блога Крис Даун (Chris Down), инженер по ядру в Meta, рассказывает о подводных камнях использования сигналов Linux в производственных средах Linux и о том, почему разработчики должны избегать использования сигналов, когда это возможно.

Что такое сигналы Linux?

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

Сигналы — это один из базовых элементов Unix-подобных рабочих сред, которые существуют с момента их создания. Они служат фундаментом для многих основных компонентов операционной системы — дампа ядра, управления жизненным циклом процессов и т. д. И за пятьдесят с лишним лет, что мы их используем, они неплохо себя зарекомендовали. Поэтому, когда кто-то говорит, что использовать их для межпроцессного взаимодействия (IPC) потенциально опасно, можно подумать, что это просто заблуждения человека, отчаянно пытающегося изобрести колесо. Однако в этой статье мы хотим продемонстрировать случаи, когда сигналы всё же стали причиной производственных проблем, и предложить некоторые возможные способы их устранения и альтернативы.

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

Давайте рассмотрим недавний инцидент, произошедший в рабочей среде Meta, который ясно показывает подводные камни использования сигналов. Мы рассмотрим краткую историю некоторых сигналов и узнаем, как они помогли нам прийти к тому, что у нас есть сегодня. Затем мы сравним их с текущими потребностями и проблемами, которые мы наблюдаем в производстве.

Инцидент

Сначала немного отмотаем назад. Команда LogDevice почистила свою кодовую базу, удалив неиспользуемый код и функции. В числе прочего был удален тип журнала, который документировал определенные операции, выполняемые службой. Постепенно эта функция стала излишней, ее никто не использовал, поэтому ее удалили. Ознакомиться с изменением можно на GitHub. Хорошо.

Первое время после изменений всё было в порядке, все продолжало стабильно работать и обслуживать трафик в обычном режиме. Спустя несколько недель пришел отчет о том, что узлы обслуживания пропадают с катастрофической скоростью. Каким-то образом это было связано с новым выпуском, но что конкретно пошло не так, было непонятно. Что именно изменилось, почему всё перестало работать?

Ответственная команда решила искать проблему в уже упомянутых изменениях кода, связанных с удалением этих записей журнала. Но почему? Что не так с этим кодом? Если вы ещё не знаете ответ, предлагаем взглянуть на разницу между двумя файлами версий и попытаться понять, что не так. Это не так уж очевидно, но такую ошибку может совершить каждый.

На ринг выходит logrotate

logrotate — это стандартный инструмент для ротации журналов при использовании Linux. Он существует уже почти 30 лет и помогает управлять жизненным циклом версий путем ротации и вакуумизации.

logrotate не посылает никаких сигналов самостоятельно, поэтому вы не найдете какой-то особенной информации о них на главной странице logrotate или в его документации. Однако logrotate может принимать произвольные команды на выполнение до или после ротаций. В качестве базового примера из стандартной конфигурации logrotate в CentOS можно привести такую конфигурацию:

/var/log/cron
/var/log/maillog
/var/log/messages
/var/log/secure
/var/log/spooler
{
    sharedscripts
    postrotate
        /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
    endscript
}

Она немного непрочная, но мы будем считать, что она работает так, как задумано. Такая конфигурация подразумевает, что после того, как logrotate ротирует любой из перечисленных файлов, он должен отправить SIGHUP на pid, содержащийся в файле /var/run/syslogd.pid, который должен принадлежать запущенному экземпляру syslogd.

И это замечательно работает в случае стабильного общедоступного API, такого как syslog. А что делать, если реализация SIGHUP — внутренняя и может измениться в любой момент?

История зависаний

Одной из проблем является то, что семантическое значение сигналов зависит от интерпретации и программирования приложений разработчиками и пользователями, за исключением сигналов, которые невозможно поймать в пространстве пользователя, в результате чего они имеют только одно значение, например SIGKILL и SIGSTOP. В некоторых случаях различие в основном формальное: например, SIGTERM практически повсеместно означает "прекратить работу как можно скорее". В случае SIGHUP значение менее однозначно.

SIGHUP изобретен для последовательных соединений и первоначально использовался для указания того, что другой конец соединения оборвал его. Сейчас эта традиция, конечно, продолжается, и SIGHUP по-прежнему отправляется в качестве более современного эквивалента, когда псевдо- или виртуальный терминал закрыт (для этого существуют такие инструменты, как nohup, которые маскируют его).

В начале развития Unix возникла необходимость в реализации перезагрузки процесса daemon. Обычно для этого нужно по крайней мере повторное открытие конфигурации или файла журнала без перезапуска, и казалось, что сигналы — это не имеющий зависимостей способ сделать это. Никакого сигнала для этого, конечно, не существовало, но поскольку у процессов daemon нет управляющего терминала, причин получать SIGHUP не должно было быть. Поэтому этот сигнал казался удобным, и его можно было бы использовать без каких-либо очевидных побочных эффектов.

Однако в этом плане есть небольшая заминка. Состояние для сигналов по умолчанию не "игнорируется", оно зависит от сигнала. Например, программам не нужно настраивать SIGTERM вручную, чтобы завершать приложение. Пока в них нет никакого другого обработчика сигналов, ядро просто завершает их программу, никакой код в пространстве пользователя не требуется. Удобно!

Однако менее удобно то, что у SIGHUP тоже есть поведение по умолчанию — немедленное завершение программы. Это отлично подходит для первоначального случая зависания, когда эти приложения, скорее всего, больше не нужны, но не очень подходит при современном использовании.

Было бы здорово, если бы мы могли удалить все случаи, в которых программе отправляется SIGHUP. Вот только в объемной кодовой базе это довольно сложно. SIGHUP не похож на жестко контролируемый вызов IPC, для которого можно легко найти информацию в базе. Сигналы могут поступать откуда угодно, в любое время, и проверок их работы существует не так много (кроме самых простых — "являетесь ли вы этим пользователем или имеете CAP_KILL"). В итоге определить, откуда могут поступать сигналы, трудно, но при более явном IPC мы бы знали, что этот сигнал ничего для нас не значит и его следует игнорировать.

От зависания к катастрофе

Я полагаю, вы уже начали догадываться, что произошло. В один роковой день начался выпуск LogDevice, содержащий вышеупомянутое изменение кода. Сначала не происходило ничего особенного, но в полночь следующего дня всё таинственным образом начало сбоить. Причина заключается в следующей строке в конфигурации logrotate машины, которая отправляет необработанный (и поэтому фатальный) SIGHUP процессу daemon logdevice:

/var/log/logdevice/audit.log {
  daily
  # [...]
  postrotate
    pkill -HUP logdeviced
  endscript
}

Пропустить всего одну короткую строку в конфигурации logrotate невероятно легко, и это часто происходит при удалении большой функции. К сожалению, также трудно гарантировать, что всё полностью было удалено сразу. Даже в случаях, которые легче проверить, чем этот, при очистке кода часто что-нибудь оставляют по ошибке. Однако обычно это происходит без каких-либо разрушительных последствий, то есть остается просто мертвый или неработающий код.

С точки зрения концепции сам инцидент и его разрешение просты: не отправлять SIGHUP и распределять действия LogDevice по времени (то есть не запускать его ровно в полночь). Но здесь нужно обратить внимание не только на особенности этого инцидента. Этот инцидент, как никакой другой, должен послужить поводом для отказа от использования сигналов в производстве в любых случаях, кроме самых необходимых.

Опасности сигналов

Для чего стоит использовать сигналы

Во-первых, использование сигналов как механизма влияния на изменения в состоянии процесса операционной системы можно считать обоснованным. Сюда входят такие сигналы, как SIGKILL, для которых невозможно установить обработчик и которые выполняют то, что вы от них ожидаете. Кроме того, сигналы, влияющие на поведение ядра по умолчанию, например SIGABRT, SIGTERM, SIGINT, SIGSEGV, SIGQUIT и подобные им, которые обычно хорошо понимают как программисты, так и пользователи.

Все эти сигналы объединяет то, что после их получения они переходят к конечному состоянию в самом ядре. То есть после получения сигнала SIGKILL или SIGTERM без обработчика в пространстве пользователя в этом пространстве больше не будет выполняться ни одна инструкция.

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

Опасное поведение по умолчанию

Вы можете заметить, что мы не упомянули некоторые другие сигналы, которые также завершают выполнение программы по умолчанию. Ниже перечислен список всех таких стандартных сигналов (исключая сигналы дампа ядра, такие как SIGABRT или SIGSEGV, поскольку их использование имеет смысл).

  • SIGALRM
  • SIGEMT
  • SIGHUP
  • SIGINT
  • SIGIO
  • SIGKILL
  • SIGLOST
  • SIGPIPE
  • SIGPOLL
  • SIGPROF
  • SIGPWR
  • SIGSTKFLT
  • SIGTERM
  • SIGUSR1
  • SIGUSR2
  • SIGVTALRM

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

  • SIGHUP: если использовать его только по назначению, по умолчанию он завершает выполнение. Опасность может возникнуть, если использовать в текущем смешанном значении для "повторного открытия" файла.
  • SIGPOLL и SIGPROF: эти сигналы следует обрабатывать внутренними стандартными функциями, а не вашей программой. Возможно, они безопасны, но их поведение завершения по умолчанию всё равно не кажется оптимальным.
  • SIGUSR1 и SIGUSR2: это сигналы, определяемые пользователем, которые вы можете использовать по своему усмотрению. Но поскольку они являются сигналами терминала по умолчанию, если вы реализуете USR1 для каких-то конкретных задач, которые позже станут ненужными, то не сможете просто безопасно удалить код. Вы должны принять решение, что нужно явно игнорировать сигнал. Это не так очевидно даже для опытного программиста.

Таким образом, почти треть сигналов терминала в лучшем случае сомнительны, а в худшем — явно опасны, поскольку потребности программы меняются. Хуже того, даже якобы "определяемые пользователем" сигналы — это катастрофа, которая может случиться, если кто-то забудет явно указать SIG_IGN. Даже безобидные SIGUSR1 и SIGPOLL могут стать причиной инцидентов.

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

Поток кода, параллелизм и миф о SA_RESTART

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

И я не говорю о "тривиальных" случаях, таких как безопасность в функции обработки сигналов, что можно решить, удалив sig_atomic_t или используя атомарный забор сигналов в C++. Нет, информацию о таких подводных камнях легко найти и запомнить после первого прохождения через сигнальный ад. Гораздо сложнее понять, в чем дело, при получении сигнала в совсем небольших частях сложной программы. Для этого нужно постоянно учитывать сигналы в каждой части жизненного цикла программы. Достаточно ли для работы EINTR одного SA_RESTART? Как изменить код, если программа завершается раньше времени? Теперь у меня есть параллельная программа, какие могут быть последствия? Либо вы можете установить sigprocmask или pthread_setmask в какой-либо части своего приложения и молиться, чтобы поток кода не менялся (что очень маловероятно при быстрой разработке). Выполнение signalfd или sigwaitinfo в отдельной цепочке, возможно, поможет, но и при их использовании есть определенные нюансы, из-за которых их сложно рекомендовать.

Хочется верить, что большинство опытных программистов уже знают, что написать потокобезопасный код очень сложно. И если вы (правильно) думали, что написать потокобезопасный код сложно, сигналы — ещё сложнее. Обработчики сигналов должны полагаться только на строго свободный от блокировок код с атомарными структурами данных, потому что основной поток выполнения приостановлен, и мы не знаем, какие блокировки в нем есть, и потому что основной поток выполнения может выполнять неатомарные операции. Они также должны быть полностью повторно воспроизводимыми, то есть иметь возможность вложения внутрь себя, так как обработчики сигналов могут перекрываться, если сигнал отправляется несколько раз (или даже если это один сигнал с SA_NODEFER). Это одна из причин, почему вы не можете использовать такие функции, как printf или malloc в обработчике сигналов — они полагаются на глобальные мьютексы для синхронизации. Если вы удерживали эту блокировку в момент получения сигнала, а затем вновь вызвали функцию, требующую этой блокировки, приложение окажется в замкнутом состоянии. Об этом очень сложно рассуждать. Поэтому в качестве обработки сигнала люди часто пишут что-то вроде этого:

static volatile sig_atomic_t received_sighup; 

static void sighup(int sig __attribute__((unused))) { received_sighup = 1; }

static int configure_signal_handlers(void) {
  return sigaction(
    SIGHUP,
    &(const struct sigaction){.sa_handler = sighup, .sa_flags = SA_RESTART},
    NULL);
}

int main(int argc, char *argv[]) {
  if (configure_signal_handlers()) {
       /* failed to set handlers */
  }

  /* usual program flow */

  if (received_sighup) {
    /* reload */
    received_sighup = 0;
  }

  /* usual program flow */
}

Проблема в том, что, хотя функция signalfd и другие попытки асинхронной обработки сигналов могут выглядеть простыми и надежными, они игнорируют тот факт, что момент прерывания так же важен, как и действия после получения сигнала. Например, предположим, что ваш пользовательский код выполняет ввод-вывод или изменяет метаданные объектов, поступающих из ядра (например, дескрипторы inode или FD). В этом случае в момент прерывания вы, вероятно, находитесь в стеке пространства ядра. Например, вот как может выглядеть поток, когда он пытается закрыть дескриптор файла:

# cat /proc/2965230/stack
 [<0>] schedule+0x43/0xd0
 [<0>] io_schedule+0x12/0x40
 [<0>] wait_on_page_bit+0x139/0x230
 [<0>] filemap_write_and_wait+0x5a/0x90
 [<0>] filp_close+0x32/0x70
 [<0>] __x64_sys_close+0x1e/0x50
 [<0>] do_syscall_64+0x4e/0x140
 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9

Здесь __x64_sys_close — x86_64-разрядный вариант системного вызова close, который закрывает дескриптор файла. На этом этапе выполнения мы ждем обновления резервного хранилища (это wait_on_page_bit). Поскольку операции ввода-вывода обычно выполняются на несколько порядков медленнее, чем другие, schedule — это способ указать планировщику ЦП ядра, что мы собираемся выполнить операцию с высокой задержкой (например, дисковый или сетевой ввод-вывод) и что он должен рассмотреть возможность поиска другого процесса для планирования вместо текущего. Это хорошо, так как позволяет нам сигнализировать ядру, что можно пойти вперед и выбрать процесс, который действительно будет использовать процессор, а не тратить время на тот, который не может продолжить работу, пока не закончит ждать ответа, что может занять некоторое время.

Представьте, что мы отправляем сигнал процессу, который запустили. Сигнал, который мы отправили, имеет обработчик в пользовательском пространстве в принимающем потоке, поэтому мы возобновим выполнение в пользовательском пространстве. Один из вариантов, которым это может закончиться: ядро попытается выйти из schedule, ещё больше раскрутить стек и, в конце концов, вернуть в пространство пользователя значение errno ESYSRESTART или EINTR, чтобы указать, что процесс прерван. Но как далеко мы продвинулись в его закрытии? Каково состояние дескриптора файла сейчас?

Теперь, когда мы вернулись в пространство пользователя, запустим обработчик сигнала. При выходе обработчика сигнала мы передадим ошибку в функцию-обертку libc close в пространстве пользователя, а затем в приложение, которое теоретически может что-то сделать в возникшей ситуации. Мы говорим "теоретически", потому что во многих из этих ситуаций с сигналами трудно понять, что делать, и многие службы в производстве не очень хорошо справляются с крайними случаями. Это может быть нормально в некоторых приложениях, где целостность данных не так важна. Однако в производственных приложениях, где важны согласованность и целостность данных, это создает значительную проблему: ядро не предоставляет никакого детального способа понять, как далеко оно зашло, что выполнило, а что нет, и что мы должны делать в этой ситуации. Хуже того, если функция close возвращается с EINTR, состояние дескриптора файла теперь не определено:

“If close() is interrupted by a signal [...] the state of [the file descriptor] is unspecified.”

Вот и попробуйте порассуждать о том, как безопасно и надежно обработать это в вашем приложении. В целом, обработка EINTR сложна даже для хорошо управляемых системных вызовов. Существует множество тонкостей, из-за которых использовать SA_RESTART недостаточно. Не все системные вызовы можно перезапустить, а ожидать, что все разработчики вашего приложения понимают глубокие нюансы получения сигнала для каждого отдельного системного вызова в каждом отдельном месте вызова, — значит ожидать сбоев. Из сигнала man 7 signal:

“The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR [...]”

Также использование sigprocmask и ожидание, что поток кода останется статичным, приводит к проблемам, поскольку разработчики обычно не тратят много времени на размышления о границах обработки сигналов или о том, как создать или сохранить код, корректный для сигналов. То же самое касается обработки сигналов в выделенном потоке с помощью sigwaitinfo, что может легко привести к тому, что GDB и подобные инструменты не смогут отладить процесс. Даже едва заметные ошибки в коде или обработке ошибок могут привести к ошибкам, сбоям, повреждениям, которые трудно отладить, тупикам и многим другим проблемам, из-за которых вам придется обратиться к любимому инструменту управления инцидентами.

Высокая сложность многопотоковых сред

Если вы думали, что параллелизм, повторное воспроизведение и атомарность — это слишком, то добавление многопоточности ещё больше усложняет ситуацию. Это особенно важно, если учесть, что многие сложные приложения запускают отдельные потоки неявно, например как часть jemalloc, GLib и др. Некоторые из этих библиотек даже сами устанавливают обработчики сигналов, открывая ещё один портал в ад.

Сигнал man 7 signal ответит на это так:

“A signal may be generated (and thus pending) for a process as a whole (e.g., when sent using kill(2)) or for a specific thread [...] If more than one of the threads has the signal unblocked, then the kernel chooses an arbitrary thread to which to deliver the signal.”

Если кратко, "в большинстве случаев ядро отправляет сигнал любому потоку, у которого он не заблокирован с помощью sigprocmask". SIGSEGV, SIGILL и им подобные напоминают ловушки и имеют сигнал, явно направленный на нарушающий поток. Однако большинство сигналов невозможно явно отправить одному потоку в группе, даже с помощью tgkill или pthread_kill.

Это означает, что вы не можете тривиально изменить общие характеристики обработки сигналов, как только у вас появится набор потоков. Если служба должна периодически блокировать сигналы с помощью sigprocmask в главном потоке, вам нужно как-то сообщить другим потокам о том, как они должны это делать. В противном случае сигнал бесповоротно поглотит другой поток. Вы, конечно, можете блокировать сигналы в дочерних потоках, чтобы избежать этого, но если они должны делать свою собственную обработку сигналов, даже для таких примитивных вещей, как waitpid, это в итоге усложнит ситуацию.

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

Отсутствие определения и передачи сообщения об успехе или сбое

Сигналы распространяются в ядре асинхронно. Системный вызов kill возвращается, как только происходит запись ожидающего сигнала для task_struct процесса или потока. Таким образом, даже если сигнал не блокируется, гарантии своевременной доставки не существует.

Даже если доставка сигнала происходит вовремя, возможность сообщить отправителю сигнала о статусе его запроса на действие отсутствует. Таким образом, никакие значимые действия нельзя реализовывать с помощью сигналов, поскольку они только работают по принципу "выстрелил и забыл" без реального механизма для сообщения об успехе или сбое доставки и последующих действий. Как мы увидели выше, даже безобидные на первый взгляд сигналы могут быть опасными, если они не настроены в пространстве пользователя.

Каждый, кто пользуется Linux достаточно долго, несомненно, сталкивался с ситуацией, когда он хочет завершить какой-то процесс, но процесс не реагирует даже на якобы всегда фатальные сигналы типа SIGKILL. Проблема в том, что, по ошибочному мнению, цель kill(1) не в том, чтобы завершать процессы, а в том, чтобы поставить в очередь ядру запрос (без указания, когда он будет обработан) о том, что кто-то попросил выполнить какое-то действие.

Задача системного вызова kill — пометить сигнал как ожидающий в метаданных задачи ядра, что он и делает, даже если задача SIGKILL не завершается. Конкретно в случае SIGKILL ядро гарантирует, что ни одна команда пользовательского режима больше не будет выполнена, но нам всё равно может потребоваться выполнить команды в режиме ядра для завершения действий, которые в противном случае могут привести к повреждению данных, или для освобождения ресурсов. Поэтому мы всё равно добьемся успеха, даже если состояние будет D (беспрерывный сон). Вызов kill сам по себе не приводит к сбою, если только вы не предоставили неверный сигнал, у вас нет разрешения на отправку этого сигнала или pid, на который вы просили отправить сигнал, не существует, и потому не может быть полезным для надежного распространения нетерминальных состояний в приложениях.

Заключение

  • Сигналы подходят для состояния терминала, обрабатываемого исключительно в ядре без обработчика в пространстве пользователя. Если вы хотите использовать сигналы, которые бы немедленно завершили программу, поручите их обработку ядру. Это также означает, что ядро может завершить свою задачу раньше, быстрее освобождая ресурсы программы, в то время как IPC-запрос пользовательского пространства должен ожидать, пока часть пользовательского пространства вновь начнет выполняться.
  • Способ избежать неприятностей при работе с сигналами — не работать с ними вообще. Однако для приложений, обрабатывающих состояния, которые должны что-то делать с такими случаями, как SIGTERM, лучше использовать высокоуровневый API, например folly::AsyncSignalHandler, где многие недочеты уже более интуитивно понятны.

  • Избегайте передачи запросов приложений с помощью сигналов. Используйте самоуправляемые уведомления (например, inotify) или RPC в пространстве пользователя с выделенной частью жизненного цикла приложения для их обработки, а не полагайтесь на прерывание работы приложения.
  • По возможности ограничьте область применения сигналов подразделом программы или потоками с помощью sigprocmask, уменьшая объем кода, который необходимо регулярно проверять на корректность сигналов. Имейте в виду: если потоки кода или стратегии потоков изменятся, маска может не дать того эффекта, на который вы рассчитывали.
  • При запуске процесса daemon маскируйте сигналы терминала, которые понимаются неодинаково и могут в какой-то момент использоваться вашей программой, чтобы избежать возврата к поведению ядра по умолчанию. Я предлагаю следующее:
signal(SIGHUP, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGUSR1, SIG_IGN);
signal(SIGUSR2, SIG_IGN);

Поведение сигнала чрезвычайно сложно объяснить даже в хорошо написанных программах, и его использование представляет ненужный риск в приложениях, где доступны другие альтернативы. В общем, не используйте сигналы для связи с пользовательским пространством своей программы. Вместо этого нужно либо сделать так, чтобы программа сама прозрачно обрабатывала события (например, с помощью inotify), либо использовать коммуникацию в пространстве пользователя, которая может сообщать об ошибках издателю, является перечислимой и демонстрируемой во время компиляции, например Thrift, gRPC или аналогичные.

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

Внесем ясность: существуют обоснованные варианты использования сигналов. Сигналы подходят для базовой коммуникации с ядром о желаемом состоянии процесса, когда отсутствует компонент пользовательского пространства, например о том, что процесс необходимо удалить. Однако если предполагается, что сигналы будут отлавливаться в пространстве пользователя, написать корректный с точки зрения сигналов код с первого раза сложно.

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

Чтобы узнать больше о Meta Open Source, перейдите на наш сайт, посвященный открытому исходному коду, подпишитесь на наш YouTube-канал или следите за нами в Twitter, на Facebook и в LinkedIn.