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

в 19:25, , рубрики: c++, linux, signals, метки: , ,

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

Важные факты о сигналах:

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

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

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

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

void hdl(int sig)
{
	std::string signal;
	if(sig == SIGUSR1)
		signal = "SIGUSR1";
	else if(sig == SIGUSR2)
		signal = "SIGUSR2";
	else
		signal = "Something else";
		
	std::cout << "The signal handler for thread: '" << pthread_self() << "' receive: " << signal << std::endl;
}

Установить новый обработчик сигнала можно двумя функциями

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 abortedn";
		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 abortedn";
		pthread_sigmask(SIG_SETMASK, &orig, 0);
		pthread_exit((void *)0);
	}
		
	// Здесь нас не могут прервать сигналом SIGINT
	std::cout << "Start thread to blocking readn";
	
	// ...
	// создаем, настраиваем сокет, подготавливаем структуру для ppoll
    
    ppoll((struct pollfd*) &clients, 1, NULL, &orig);
    
    if(stop)
    {
    	// получили сигнал о завершении работы
    	std::cout << "Thread was abortedn";
    	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. Посоветуйте куда залить сорцы, что то мне не очень нравиться на дроббоксе их держать, а вдруг удалю.

Автор: Cupper

Поделиться

* - обязательные к заполнению поля