Как стать автором
Обновить

Практика работы с сигналами

Время на прочтение6 мин
Количество просмотров110K
Хочу запечатлеть небольшой опыт работы с сигналами в Linux. Ниже будут представлены примеры использования наиболее значимых конструкций в этой области. Постараюсь разложить все по отдельным полочкам, чтобы всегда было легко глянуть и вспомнить, что и как использовать.

Важные факты о сигналах:
  • Сигналы в Linux играют роль некоего средства межпроцессного взаимодействия (а так же и межпоточного)
  • Каждый процесс имеет маску сигналов (сигналов, получение которых он игнорирует)
  • Каждая нить (thread), так же как и процесс, имеет свою маску сигналов
  • При получении сигнала(если он не блокируется) процесс/нить прерывается, управление передается в функцию обработчик сигнала, и если эта функция не приводит к завершению процесса/нити, то управление передается в точку на которой процесс/нить была прервана
  • Можно установить свою функцию обработчик сигнала, но только для процесса. Данный обработчик будет вызываться и для каждой нити порожденной из этого процесса

Я не буду углубляться в теорию сигналов, что откуда зачем и куда. Меня в первую очередь интересует сам механизм работы с ними. Поэтому в качестве используемых сигналов будут выступать SIGUSR1 и SIGUSR2, это два единственных сигнала отданных в полное распоряжение пользователя. А так же я постараюсь уделить больше внимание именно межпоточному взаимодействию сигналов.
Итак, поехали.

Функция обработчик сигналов


Данная функция вызывается, когда процесс (или нить) получает неблокируемый сигнал. Дефолтный обработчик завершает наш процесс (нить). Но мы можем сами определить обработчики для интересующих нас сигналов. Следует очень осторожно относится к написанию обработчика сигналов, это не просто функция, выполняющаяся по коллбеку, происходит прерывание текущего потока выполнения без какой либо подготовительной работы, таким образом глобальные объекты могут находится в неконсистентном состоянии. Автор не берется приводить свод правил, так как сам их не знает, и призывает последовать совету Kobolog (надеюсь он не против, что я ссылаюсь на него) и изучить хотя бы вот этот материал FAQ.
void hdl(int sig)
{
	exit = true;
}

Установить новый обработчик сигнала можно двумя функциями
sighandler_t signal(int signum, sighandler_t handler);

Которая принимает номер сигнала, указатель на функцию обработчик (или же SIG_IGN (игнорировать сигнал) или SIG_DFL (дефолтный обработчик)), и возвращает старый обработчик. Сигналы SIGKILL и SIGSTOP не могут быть «перехвачены» или проигнорированы. Использование этой функции крайне не приветствуется, потому что:
  • функция не блокирует получение других сигналов пока выполняется текущий обработчик, он будет прерван и начнет выполняться новый обработчик
  • после первого получения сигнала (для которого мы установили свой обработчик), его обработчик будет сброшен на SIG_DFL

Этих недостатков лишена функция
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

Которая также принимает номер сигнала (кроме SIGKILL и SIGSTOP). Второй аргумент это новое описание для сигнала, через третий возвращается старое значение. Структура struct sigaction имеет следующие интересующие нас поля
  • sa_handler — аналогичен sighandler_t в функции signal
  • sa_mask — маска сигналов который будут блокированы пока выполняется наш обработчик. + по дефолту блокируется и сам полученный сигнал
  • sa_flags — позволяет задать дополнительные действия при обработке сигнала о которых лучше почитать тут

Использование данной функции выглядит совсем просто
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = hdl;
sigset_t   set; 
sigemptyset(&set);                                                             
sigaddset(&set, SIGUSR1); 
sigaddset(&set, SIGUSR2);
act.sa_mask = set;
sigaction(SIGUSR1, &act, 0);
sigaction(SIGUSR2, &act, 0);

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

Блокирование сигналов


Для того, чтобы заблокировать некоторый сигналы для процесса, необходимо добавить их в маску сигналов данного процесса. Для этого используется функция
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

Мы можем к уже существующей маске сигналов добавить новые сигналы (SIG_BLOCK), можем из этой маски убрать часть сигналов (SIG_UNBLOCK), а так же установить полностью нашу маску сигналов (SIG_SETMASK).
Для работы с маской сигналов внутри нити используется функция
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset);

которая позволяет сделать все тоже, но уже для каждой нити в отдельности.
Невозможно заблокировать сигналы SIGKILL или SIGSTOP при помощи этих функций. Попытки это сделать будут игнорироваться.

sigwait


Данная функция позволяет приостановить выполнении процесса (или нити) до получения нужного сигнала (или одного из маски сигналов). Особенностью этой функции является то, что при получении сигнала не будет вызвана функции обработчик сигнала. См. пример 2.

Посыл сигнала


Для того, чтобы послать сигнал процессу можно использовать две функции
int kill(pid_t pid, int sig);
int raise(int sig);

С первой все понятно. Вторая нужна для того, чтобы послать сигнал самому себе, и по сути равносильна kill(getpid(), signal). Функция getpid() возвращает PID текущего процесса.
Для того, чтобы послать сигнал отдельной нити, используется функция
int pthread_kill(pthread_t thread, int sig);


Пример использования сигналов


Все, что я описал выше, не дает ответа на вопрос «Зачем мне использовать сигналы». Теперь я хотел бы привести реальный пример использования сигналов и где без них попросту не обойтись.
Представьте, что вы хотите читать или писать какие-то данные в какое то устройство, но это может привести к блокированию. Ну например, чтение в случае работы с сокетами. Или может быть запись в пайп. Вы можете вынести это в отдельный поток, чтобы не блокировать основную работу. Но что делать когда вам нужно завершить приложение? Как корректно прервать блокирующую операцию IO? Можно было бы задавать таймаут, но это не очень хорошее решение. Для этого есть более удобные средства: функции pselect и ppoll. Разница между ними исключительно в юзабельности, поведение у них одинаковое. В первую очередь эти функции нужны для мультиплексирования работы с IO (select/poll). Префикс 'p' в начале функции указывает на то, что данная функция может быть корректно прервана сигналом.

Итак, сформулируем требование:
Необходимо разработать приложение, открывающее сокет (для простоты UDP) и выполняющее в потоке операцию чтения. Данное приложение должно корректно без задержек завершаться по требованию пользователя.
Функция треда выглядит вот так
void* blocking_read(void* arg)
{
	if(stop)
	{
		// не успели стартовать, а нас уже прикрыли ?
		std::cout << "Thread was aborted\n";
		pthread_exit((void *)0); 
	}
		
	// Блокируем сигнал SIGINT
	sigset_t set, orig; 
	sigemptyset(&set);
	sigaddset(&set, SIGINT);
	sigemptyset(&orig);
	pthread_sigmask(SIG_BLOCK, &set, &orig);
	
	if(stop)
	{
		// пока мы устанавливали блокировку сигнала он уже произошол
		// возвращаем все как было и выходим
		std::cout << "Thread was aborted\n";
		pthread_sigmask(SIG_SETMASK, &orig, 0);
		pthread_exit((void *)0);
	}
		
	// Здесь нас не могут прервать сигналом SIGINT
	std::cout << "Start thread to blocking read\n";
	
	// ...
	// создаем, настраиваем сокет, подготавливаем структуру для ppoll
    
    ppoll((struct pollfd*) &clients, 1, NULL, &orig);
    
    if(stop)
    {
    	// получили сигнал о завершении работы
    	std::cout << "Thread was aborted\n";
    	close(sockfd);
    	pthread_sigmask(SIG_SETMASK, &orig, 0);
    	// сдесь сигнал SIGINT все еще заблокирован
    }
    
    // Мы либо считали данные, либо произошла какаято ошибка. Но мы не получали 
    // сигнала о завершении работы и продолжаем работать "по плану"
    
 	close(sockfd);   
	pthread_exit((void *)0);  
}

stop это глобальный булев флаг который устанавливается в true нашим обработчиком, что сообщает потоку о необходимости завершиться.
Логика работы такая:
  • проверяем, что пока стартовал тред его еще не пожелали завершить
  • блокируем завершающий сигнал
  • проверяем, что пока блокировали, нас не пожелали завершить
  • вызываем ppoll передавая в качестве последнего параметра маску сигналов по которой ждется сигнал
  • после выхода из ppoll проверяем что вышли не из за сигнала о завершении

Вот так выглядит главная функция
int main()
{
...
	struct sigaction act;
	memset(&act, 0, sizeof(act));
	act.sa_handler = hdl;

	sigemptyset(&act.sa_mask);                                                             
	sigaddset(&act.sa_mask, SIGINT); 
	sigaction(SIGINT, &act, 0);
	
...
	pthread_kill(th1, SIGINT);
...
}

Устанавливаем наш обработчик для SIGINT, и когда нужно завершить дочерний поток шлем ему этот сигнал.
Полный листинг см. пример 3.

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

PS. Исходные коды разместил на сервисе PasteBin (ссылку не даю, а то еще за рекламу посчитают).
PPS. Прошу простить за обилие ошибок. Язык, слабая моя сторона. Спасибо, всем кто помог их исправить.



Данная статья не претендует на полное (и глубокое) описание работы с сигналами и нацелена в первую очередь на тех, кто до этого момента не сталкивались с понятием «сигнал». Для более глубоко понимания работы сигналов автор призывает обратиться в более компетентные источники и ознакомиться с конструктивной критикой в комментариях.
Теги:
Хабы:
Всего голосов 73: ↑62 и ↓11+51
Комментарии61

Публикации

Истории

Работа

Программист C++
109 вакансий
QT разработчик
4 вакансии

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань