Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Signály v Linuxu

28.11.2022

Jedním ze způsobů komunikace mezi procesy jsou v Linuxu signály. I když primárně jsou posílány z jiné aplikace nebo z jádra, můžeme je také využít interně v rámci jedné aplikace. Signál má jeden parametr a tím je jeho číslo.

Signály můžeme v aplikaci zachycovat a nějak na ně reagovat nebo je ignorovat. Některé speciální "předdefinované" signály však aplikace ingorovat nemůže. Je to především signál s číslem 9 SIGKILL který je určený k vynucenému "tvrdému ukončení" procesu. Druhým takovým signálem je SIGSTOP s číslem 19, sloužící k zastavení běhu procesu s možností jeho pozdějšího obnovení signálem SIGCONT (číslo 18).

Pro nás budou dále zajímavější signály, které můžeme v aplikaci zachytit a ignorovat jejich výchozí efekt. Takovým je například signál SIGINT s číslem 2, který občas posíláme procesu běžícímu v terminálu stisknutím kombinace CTRL+c. Tento signál za normálních okolností přeruší proces. Podobně můžeme zachytit a ignorovat signál SIGTERM (číslo 15). Obojí si dále ukážeme.

Dále existuje několik signálů, které pokud nejsou zachyceny, nemají na běh procesu žádný efekt. Jednak jsou to signály SIGUSR1 (číslo 10) a SIGUSR2 (číslo 12) a dále tzv. "real-time signály" v rozsahu hodnot od SIGRTMIN (34) DO SIGRTMAX (64). Tyto signály můžeme libovolně využívat pro vlastní komunikaci mezi procesy nebo třeba vlákny v rámci naší aplikace.

Přehled všech signálů si můžeme vypsat do terminálu příkazem kill -l. Dostaneme následující výpis:

 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

Jako uživatel můžeme požadovanému procesu poslat signál z terminálu příkazem pkill napřklad takto:

$ pkill -SIGRTMIN+1 nazev_procesu
$ pkill -SIGTERM nazev_procesu

Nebo pokud běží více instancí nějakého procesu, můžeme jedním příkazem poslat požadovaný signál všem běžícím instancím příkazem kilall

$ killall -SIGRTMIN+1 nazev_procesu

Jako příklad konkrétního využití se můžeme podívat na následující řádky ve výchozím konfiguračním souboru správce oken i3.

set $refresh_i3status killall -SIGUSR1 i3status
bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +10% && $refresh_i3status
bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -10% && $refresh_i3status
bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle && $refresh_i3status
bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute @DEFAULT_SOURCE@ toggle && $refresh_i3status

Nejprve je (pro opakované použití) nadefinován vlastní příkaz refresh_i3status, který pošle signál -SIGUSR1 procesu i3status, což je výchozí stavový panel v i3, který na tento signál reaguje aktualizací svého výstupu, tj. např. zobrazením aktuální nastavené hodnoty hlasitosti. Tento příkaz je pak použit při stisku jedné ze speciálních kláves ovládajících hlasitost které bývají na multimediálních klávesnicích a notebooku.

Práce se signály v programu v jazyce C

Jak tedy v aplikaci napsané v jazyce C zachycovat signály? Nejprve si musíme napsat vlastní funkci, kterou nastavíme jako handler příchozího signálu, jednoduše řečeno tato funkce bude vyvolána při zachycení jednoho nebo více signálů, kterým ji nastavíme. Můžeme tedy mít společnou funkci pro více různých signálů. O který signál se v tom kterém případě jedná rozlišíme jejím parametrem. Tím že takto zachytíme signál způsobíme současně ignorování jeho výchozího účinku, samozřejmě s výjimkou výše zmíněných signálů které nelze ignorovat a dokonce ani detkovat. Tato funkce má následující prototyp:

static void signal_handler(int sig)
{
}

Dále si napíšeme vlastní pomocnou funkci, jejímž parametrem bude číslo signálu, který chceme zachytit výše uvedenou funkcí:

static void set_signal_handler(int sig_cislo)
{
	struct sigaction sa;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sa.sa_handler = signal_handler;
	sigaction(sig_cislo, &sa, NULL);
}

Nyní si již můžeme napsat jednoduchý ukázkový program, který bude zachycovat a ignorovat výše uvedené signály SIGTERM a SIGINT a vyzkoušet efekt.

/* gcc signaly.c -Wall -O2  -otest-signaly */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

static void signal_handler(int sig_cislo)
{
	if (SIGINT == sig_cislo)
	{
		printf("\nsignal SIGINT handled: %d\n", sig_cislo);
	}
	else if (SIGTERM == sig_cislo)
	{
		printf("\nsignal SIGTERM handled: %d\n", sig_cislo);
	}
}

static void set_signal_handler(int sig_cislo)
{
	struct sigaction sa;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sa.sa_handler = signal_handler;
	sigaction(sig_cislo, &sa, NULL);
}

int main(int argc, char** argv)
{
	set_signal_handler(SIGINT);
	set_signal_handler(SIGTERM);
	puts("q pro ukonceni");
	char ch;
	while ('q' != (ch = getchar())) {}
	return EXIT_SUCCESS;
}

Zdrojový kód přeložíme pomocí gcc příkazem (uvedeným v komentáři kódu):

$ gcc signaly.c -Wall -O2  -otest-signaly

Když nyní program spustíme, můžeme hned vyzkoušet efekt ignorace signálu SIGINT stisknutím kombinace CTRL+c v terminálu za běhu programu. Výsledkem bude následující výstup:

q pro ukonceni
^C
signal SIGINT handled: 2
q

Dále můžeme zkusit poslat naší spuštěné aplikaci signály z jiného terminálu takto:

$ pkill -SIGINT test-signaly
$ pkill -SIGTERM test-signaly

výsledkem bude

q pro ukonceni

signal SIGINT handled: 2

signal SIGTERM handled: 15
q

Posílání signálů z programu v jazyce C

Signály samozřejmě můžeme posílat z vlastní aplikace pomocí funkce int kill(pid_t pid, int sig);, která má dva parametry: identifikátor procesu a číslo signálu. Pokud chceme poslat signál v rámci procesu, zjistíme jeho identifikátor pomocí funkce pid_t getpid(void);

Rozšíříme si tedy funkci main která bude nyní vypadat takto:

int main(int argc, char** argv)
{
	pid_t mypid = getpid();
	set_signal_handler(SIGINT);
	set_signal_handler(SIGTERM);
	puts("q pro ukonceni");
	char ch;
	while ('q' != (ch = getchar()))
	{
		if ('i' == ch)
		{
			kill(mypid, SIGINT);
		}
		else if ('t' == ch)
		{
			kill(mypid, SIGTERM);
		}
	}
	return EXIT_SUCCESS;
}

A po sestavení a spuštění vyzkoušíme a dostaneme následující výstup:

q pro ukonceni
i

signal SIGINT handled: 2
t

signal SIGTERM handled: 15
i

signal SIGINT handled: 2
q