Как можно избежать гонок при переопределении обработчиков сигналов по умолчанию go? - PullRequest
0 голосов
/ 28 апреля 2020

tl; dr , если сигнал может быть обработан средой выполнения go в любое время, как мы можем безопасно использовать signal.Ignore, чтобы игнорировать SIGINT так, чтобы не было гонки между временем по умолчанию обработчик сигнала установлен, и когда наша инструкция внутри main() запускает

go документы для pkg / signal заявляет это о поведении сигналов по умолчанию

Сигнал SIGHUP, SIGINT или SIGTERM вызывает выход из программы.

Итак, запишите двоичный файл golang, который вращает процессор, нажмите Ctrl + c, чтобы отправьте SIGINT, и программа закроется.

Теперь, скажем, вы хотите переписать это поведение. Один из способов сделать это - проигнорировать сигнал с помощью signal.Ignore(syscall.SIGINT)

Но теперь рассмотрим следующий

package main

import (
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    fmt.Println("Looping, SIGINT will have default behavior")
    for start := time.Now(); time.Now().Before(start.Add(time.Second * 5)); {

    }
    signal.Ignore(syscall.SIGINT)
    fmt.Println("OK")

    for start := time.Now(); time.Now().Before(start.Add(time.Second * 5)); {

    }
}

Здесь у нас есть простой двоичный файл golang, который зацикливается на 5 секунд. Это занято oop, поэтому мы знаем, что не участвуем в совместной многозадачности, уступая время в потоке ОС. Затем он регистрируется для игнорирования SIGINT.

Если вы попробуете это, вы заметите, что если вы введете Ctrl + c в течение первых 5 секунд, программа закроется. Кажется, это имеет смысл - мы получаем стандартное поведение обработки сигналов golang во время выполнения, потому что мы еще не переопределили его с помощью нашего вызова signal.Ignore.

Теперь, возможно, мы могли бы решить эту проблему, переместив signal.Ignore - это первое, что нужно сделать в main (), однако, эта программа доказывает, что среда выполнения go не дает никаких гарантий того, что обработчик сигналов по умолчанию не запустится до завершения выполнения вашего синхронного кода в main ().

Даже если вы переместите его, мы, похоже, находимся в гонке между

  1. Точкой, в которой среда выполнения go регистрирует свои обработчики сигналов по умолчанию, и
  2. Самая ранняя точка, в которой наш код может выполняться (первая (или вторая, если вам нужен канал) инструкция в основном)

Я не могу найти документацию по этому вопросу. Какие гарантии дает время выполнения go, чтобы я чувствовал себя абсолютно уверенным, что сигнал не может прийти между этапами 1 и 2?

1 Ответ

1 голос
/ 29 апреля 2020

TL; DR: поймать рано, например, прямо в верхней части main.


Как отмечалось в комментариях, это не очень хороший способ описать это как проблема, так как она довольно общая c для всех языков программирования: если вы не установили обработчик сигнала для syscall.SIGINT, вы будете убиты по умолчанию на SIGINT, а когда вы это сделаете, вы не будете. Это правда, но любая программа может быть убита во время ее запуска, прежде чем она сможет начать перехватывать сигналы. Это правда, независимо от языка программирования. Он влияет на программы C и C ++ так же, как и на программы Go.

В общем, все, что вам нужно сделать, чтобы надежно поймать или отбросить SIGINT сигналы и как можно свободнее от гонки должен использовать signal.Notify(ch, os.Interrupt) очень рано, в верхней части вашего main, где ch - это канал, который вы создаете для этого. 1 Затем вы можете написать Ваш собственный код без расы, чтобы справиться с этим через goroutines и каналы:

  • Сделайте так, чтобы ваша собственная программа прочитала канал, чтобы увидеть, поступит ли / когда сигнал.
  • Прочитайте его другой канал или некоторая область общей памяти, использующая ваше собственное управление мьютексом, чтобы увидеть, как обрабатывать сигнал, если и когда он доставлен.
  • Когда сигнал доставляется, если вы должны выйти, позвоните os.Exit ( или - возможно, лучше - используйте os.Reset на syscall.SIGINT, а затем предоставьте себе syscall.SIGINT для генерации правильного состояния выхода на уровне ОС, «убитого сигналом 1» 2 ). Если сигнал следует игнорировать, просто отбросьте уведомление о канале.

В некоторой степени связано с этим: было довольно недавнее исправление для обработки syscall.SIGPIPE. В частности, вызов signal.Ignore(syscall.SIGPIPE) должен игнорировать SIGPIPE, но этого не произошло. Кажется, это исправлено в Go 1.14.


1 Как отмечается в документации к пакету, преднамеренный перехват сигнала будет обходить любой "предварительно игнорируемый" "состояние от nohup или trap "" 1 2 15 в оболочке. Если вы хотите sh проверить это, используйте функцию signal.Ignored.

2 Если signal_unix.go экспортировал функцию dieFromSignal, вы можете использовать ее напрямую , но это не так. Было бы неплохо иметь обертку OS-agnosti c, чтобы хотя бы попытаться совершить самоубийство такого рода. Можно даже использовать sigprocmask на уровне операционной системы, чтобы самоубийство было как можно свободнее от гонки.

...