Перехват пользовательских сигналов в Go
Перевод статьи "Handling User Defined Signals in Go"
Сигналы представляют собой ограниченный способ коммуникации между процессами. Сигналы используют для прерывания выполнения программы и срабатывания определенного обработчика. К примеру, когда вы нажимаете комбинацию клавиш CTRL+C в терминале для остановки запущенной программы, то таким образом вы посылаете сигнал SIGINT. Обычно, при получении сигнала SIGINT процесс убивается, но некоторые процессы могут перехватывать сигнал и игнорировать его или выполнять некоторую подготовительную работу перед выходом. Еще один довольно известный сигнал SIGKILL намного "брутальнее". Как только процесс получает этот сигнал, то сразу же прекращает свою работу и нет никаких способов игнорировать этот сигнал. Существует очень много различных сигналов, их описание можно найти на wikipedia.
Посылать сигналы процессу можно с помощью команды kill <signal> <PID>
. Тут PID
- это идентификатор процесса. К примеру, чтобы завершить определенный процесс, вы можете послать сигнал SIGKILL
воспользовавшись командой kill -KILL <PID>
. Для удобства можно использовать команду pkill
и вместо идентификатора указывать имя процесса.
В этом посте мы будем говорить о специальных пользовательских сигналах USR1
и USR2
. Оба эти сигнала предоставляются всецело в ваше распоряжение и вы сами можете определить их обработчики. Я покажу несколько популярных способов их использования. Все примеры написанны на языке Go, но основные идеи не зависят от выбранного языка и могут быть реализованны на чем угодно.
Перехватывание сигналов
В Go вы можете перехватывать сигналы с использованием функции signal.Notify(chan os.Signal, syscall.Signal)
. Эта функция принимает канал и сообщает вам, когда произошел перехвать сигнала. Когда программа получает сигнал, то в канал посылается значение типа os.Signal
. Примерно это выглядит так:
func handle(c chan os.Signal) {
for {
<-c // This line will block until a signal is received
// DO SOMETHING
}
}
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGUSR1)
go handle(c)
}
Теперь мы знаем как перехватывать сигналы и определять наши собственные обработчики.
Переключатель для режима логирования.
Давайте представим, что у вас есть веб-приложение, которое, конечно же, пишет логи. Сделать лог очень полным и многословным считается не очень хорошей идеей, потому что файлы логов разрастутся моментально. Именно поэтому придумали различные уровни логирования (INFO, DEBUG, WARN, и т.д.). Вы запускаете свое приложение с указанием уровня, но если вы захотите отдебажить какую ни будь проблему, то вам нужно будет перезагрузить ваше приложение с указанием другого уровня логирования.
Для простоты представим, что у вас есть только для два уровня логирования(бесшумный и многословный). Код ниже переключает уровни логирования как только процес получает сигнал USR1
.
package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// Определяем, куда мы будем выводить сообщения
var writeTo io.Writer = ioutil.Discard
var mutex sync.Mutex
func toggleOutput(c chan os.Signal) {
for {
// На этой строчке мы заблокируемся пока не получим сигнал
<-c
// Как только мы получили сигнал, необходимо сменить наш writer.
// Если раньше мы писали в stdout, то меняем на ioutil.Discard
// и, соответствено, наоборот.
mutex.Lock()
if writeTo == os.Stdout {
writeTo = ioutil.Discard
} else {
writeTo = os.Stdout
}
mutex.Unlock()
}
}
func main() {
fmt.Printf("Process PID : %v\n", os.Getpid())
c := make(chan os.Signal, 1)
// В канал `c` попадет сообщение как только получим сигнал SIGUSR1.
signal.Notify(c, syscall.SIGUSR1)
go toggleOutput(c)
// Бесконечный цикл, в котором мы просто увеличиваем счетчик.
counter := 1
for {
mutex.Lock()
fmt.Fprintln(writeTo, counter)
mutex.Unlock()
counter++
time.Sleep(time.Second)
}
}
Напомню, что для отправки сигнала нужно выполнить команду:
$ ps -C signals
PID TTY TIME CMD
17659 pts/6 00:00:00 signals
$ kill -s USR1 17659
В этом примере бинарник с программой называется signals
.
И как это выглядит в действии.
Чтение конфигурационных файлов.
Еще один удобный вариант использования сигналов, это перегрузка конфигурационных файлов без перезапуска приложения. Представим, что у вас есть приложение, которое читает конфигурацию из YAML файла и просто выводит значения конфигурации на экран. Немного модифицировав это приложение, мы можем сменить старую конфигурацию на новую просто отправив необходимый сигнал.
package main
import (
"fmt"
"io/ioutil"
"os"
"os/signal"
"sync"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
// Структура для нашего конфига. Есть только одно поле, его и будем выводить.
type config struct {
Name string
}
// Обертка над конфигом, которая защищает его через mutex
type context struct {
config
sync.Mutex
}
// Глобальная переменная контекста
var ctx *context
func (c *context) Config() config {
c.Lock()
cnf := c.config
c.Unlock()
return cnf
}
// Функция читает конфигурационный файл "config.yml" и возвращает
// указатель на структуру конфигурации.
func readConfig() config {
content, err := ioutil.ReadFile("config.yml")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var tmpCnf config
err = yaml.Unmarshal(content, &tmpCnf)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return tmpCnf
}
// Это самая интересная часть
func swapConfig(c chan os.Signal) {
for {
// На этой строчке мы заблокируемся пока не получим сигнал
<-c
// Как только получаем сигнал, читаем конфигурационный
// файл и переключаем старый конфиг.
ctx.Lock()
ctx.config = readConfig()
ctx.Unlock()
}
}
func main() {
fmt.Printf("Process PID : %v\n", os.Getpid())
ctx = &context{config: readConfig()}
c := make(chan os.Signal, 1)
// В канал `c` попадет сообщение как только получим сигнал SIGUSR1.
signal.Notify(c, syscall.SIGUSR1)
go swapConfig(c)
for {
fmt.Println(ctx.Config().Name)
time.Sleep(time.Second)
}
}
И как все это выглядит в жизни.
Заключение
В этом посте я продемонстрировал два примера использования пользовательских сигналов, но вы можете использовать их по своему усмотрению. Если у вас появятся идеи или вопросы, то милости просим в комментарии.