Primárním problémem je, že pokud signál přeruší malloc()
nebo nějakou podobnou funkci, může být vnitřní stav dočasně nekonzistentní, když se přesouvají bloky paměti mezi volným a použitým seznamem nebo jiné podobné operace. Pokud kód v obslužném programu signálu zavolá funkci, která potom vyvolá malloc()
, může to zcela zničit správu paměti.
Standard C má velmi konzervativní pohled na to, co můžete dělat v obslužném programu signálu:
ISO/IEC 9899:2011 §7.14.1.1
signal
funkce¶5 Pokud se signál objeví jinak než jako výsledek volání
abort
neboraise
funkce, chování není definováno, pokud obsluha signálu odkazuje na jakýkoli objekt se statickou délkou ukládání nebo ukládání vláken, který není atomický objekt bez zámku, jinak než přiřazením hodnoty objektu deklarovanému jakovolatile sig_atomic_t
, nebo obsluha signálu volá jakoukoli funkci ve standardní knihovně jinou nežabort
funkce_Exit
funkcequick_exit
nebosignal
funkce s prvním argumentem rovným číslu signálu odpovídajícímu signálu, který vyvolal vyvolání obslužné rutiny. Pokud navíc takové volání nasignal
výsledkem funkce jeSIG_ERR
návrat, hodnotaerrno
je neurčitý.Je-li nějaký signál generován obslužným programem asynchronního signálu, chování není definováno.
POSIX je mnohem velkorysejší, pokud jde o to, co můžete dělat v obslužném programu signálu.
Signal Concepts ve vydání POSIX 2008 říká:
Pokud je proces vícevláknový nebo pokud je proces jednovláknový a obsluha signálu je provedena jinak než v důsledku:
Proces volající
abort()
,raise()
,kill()
,pthread_kill()
nebosigqueue()
generovat signál, který není blokovánČekající signál je odblokován a doručen před voláním, které jej odblokovalo,
chování není definováno, pokud obsluha signálu odkazuje na jiný objekt než
errno
s délkou statického uložení jinou než přiřazením hodnoty objektu deklarovanému jakovolatile sig_atomic_t
nebo pokud obsluha signálu volá jakoukoli funkci definovanou v tomto standardu, jinou než jednu z funkcí uvedených v následující tabulce.Následující tabulka definuje sadu funkcí, které musí být asynchronně-signal-safe. Aplikace je tedy mohou bez omezení vyvolat z funkcí pro zachycení signálu:
_Exit() fexecve() posix_trace_event() sigprocmask() _exit() fork() pselect() sigqueue() … fcntl() pipe() sigpause() write() fdatasync() poll() sigpending()
Všechny funkce, které nejsou ve výše uvedené tabulce, jsou považovány za nebezpečné s ohledem na signály. V přítomnosti signálů se všechny funkce definované v tomto svazku POSIX.1-2008 musí chovat tak, jak je definováno, když jsou volány nebo přerušeny funkcí zachycení signálu, s jedinou výjimkou:když signál přeruší nebezpečnou funkci a signál- catching funkce volá nebezpečnou funkci, chování není definováno.
Operace, které získají hodnotu
errno
a operace, které přiřazují hodnotuerrno
musí být async-signal-safe.Když je do vlákna doručen signál a akce tohoto signálu specifikuje ukončení, zastavení nebo pokračování, celý proces bude ukončen, zastaven nebo pokračovat.
Nicméně printf()
rodina funkcí v tomto seznamu výrazně chybí a nemusí být bezpečně volána z obsluhy signálu.
POSIX 2016 update rozšiřuje seznam bezpečných funkcí tak, aby zahrnoval zejména velké množství funkcí z <string.h>
, což je zvláště cenný doplněk (nebo to bylo obzvláště frustrující nedopatření). Seznam je nyní:
_Exit() getppid() sendmsg() tcgetpgrp()
_exit() getsockname() sendto() tcsendbreak()
abort() getsockopt() setgid() tcsetattr()
accept() getuid() setpgid() tcsetpgrp()
access() htonl() setsid() time()
aio_error() htons() setsockopt() timer_getoverrun()
aio_return() kill() setuid() timer_gettime()
aio_suspend() link() shutdown() timer_settime()
alarm() linkat() sigaction() times()
bind() listen() sigaddset() umask()
cfgetispeed() longjmp() sigdelset() uname()
cfgetospeed() lseek() sigemptyset() unlink()
cfsetispeed() lstat() sigfillset() unlinkat()
cfsetospeed() memccpy() sigismember() utime()
chdir() memchr() siglongjmp() utimensat()
chmod() memcmp() signal() utimes()
chown() memcpy() sigpause() wait()
clock_gettime() memmove() sigpending() waitpid()
close() memset() sigprocmask() wcpcpy()
connect() mkdir() sigqueue() wcpncpy()
creat() mkdirat() sigset() wcscat()
dup() mkfifo() sigsuspend() wcschr()
dup2() mkfifoat() sleep() wcscmp()
execl() mknod() sockatmark() wcscpy()
execle() mknodat() socket() wcscspn()
execv() ntohl() socketpair() wcslen()
execve() ntohs() stat() wcsncat()
faccessat() open() stpcpy() wcsncmp()
fchdir() openat() stpncpy() wcsncpy()
fchmod() pause() strcat() wcsnlen()
fchmodat() pipe() strchr() wcspbrk()
fchown() poll() strcmp() wcsrchr()
fchownat() posix_trace_event() strcpy() wcsspn()
fcntl() pselect() strcspn() wcsstr()
fdatasync() pthread_kill() strlen() wcstok()
fexecve() pthread_self() strncat() wmemchr()
ffs() pthread_sigmask() strncmp() wmemcmp()
fork() raise() strncpy() wmemcpy()
fstat() read() strnlen() wmemmove()
fstatat() readlink() strpbrk() wmemset()
fsync() readlinkat() strrchr() write()
ftruncate() recv() strspn()
futimens() recvfrom() strstr()
getegid() recvmsg() strtok_r()
geteuid() rename() symlink()
getgid() renameat() symlinkat()
getgroups() rmdir() tcdrain()
getpeername() select() tcflow()
getpgrp() sem_post() tcflush()
getpid() send() tcgetattr()
Výsledkem je, že buď použijete write()
bez podpory formátování poskytované printf()
et al, nebo skončíte nastavením příznaku, který (pravidelně) testujete na vhodných místech v kódu. Tato technika je dovedně demonstrována v odpovědi Grijeshe Chauhana.
Standardní funkce C a bezpečnost signálu
chqrlie klade zajímavou otázku, na kterou nemám více než částečnou odpověď:
Jak pochází většina řetězcových funkcí z
<string.h>
nebo funkce třídy znaků z<ctype.h>
a mnoho dalších funkcí standardní knihovny C není ve výše uvedeném seznamu? Implementace by musela být záměrně zlá, aby vytvořilastrlen()
není bezpečné volat od obsluhy signálu.
Pro mnoho funkcí v <string.h>
, je těžké pochopit, proč nebyly prohlášeny za bezpečné pro asynchronní signál, a souhlasil bych s strlen()
je ukázkovým příkladem spolu s strchr()
, strstr()
, atd. Na druhou stranu další funkce jako strtok()
, strcoll()
a strxfrm()
jsou poměrně složité a pravděpodobně nebudou bezpečné pro asynchronní signál. Protože strtok()
zachovává stav mezi voláními a obsluha signálu nemohla snadno zjistit, zda některá část kódu používá strtok()
byl by zpackaný. strcoll()
a strxfrm()
funkce pracují s daty citlivými na národní prostředí a načítání národního prostředí zahrnuje všechny druhy nastavení stavu.
Funkce (makra) z <ctype.h>
všechny jsou citlivé na národní prostředí, a proto mohou narazit na stejné problémy jako strcoll()
a strxfrm()
.
Je pro mě těžké pochopit, proč matematické funkce z <math.h>
nejsou bezpečné pro asynchronní signál, pokud to není proto, že by mohly být ovlivněny SIGFPE (výjimka s pohyblivou řádovou čárkou), i když v těchto dnech vidím jednu z nich pouze pro celé číslo dělení nulou. Podobná nejistota vyplývá z <complex.h>
, <fenv.h>
a <tgmath.h>
.
Některé funkce v <stdlib.h>
mohla být vyňata — abs()
například. Jiné jsou konkrétně problematické:malloc()
a rodina jsou nejlepší příklady.
Podobné posouzení by bylo možné provést pro ostatní hlavičky ve standardu C (2011) používané v prostředí POSIX. (Standard C je tak restriktivní, že nemá zájem je analyzovat v čistém prostředí Standard C.) Ty označené jako „závislé na národním prostředí“ nejsou bezpečné, protože manipulace s místními nastaveními může vyžadovat alokaci paměti atd.
<assert.h>
— Pravděpodobně není bezpečné<complex.h>
— Možná bezpečné<ctype.h>
— Není bezpečné<errno.h>
— Bezpečné<fenv.h>
— Pravděpodobně není bezpečné<float.h>
— Žádné funkce<inttypes.h>
— Funkce citlivé na místní nastavení (nebezpečné)<iso646.h>
— Žádné funkce<limits.h>
— Žádné funkce<locale.h>
— Funkce citlivé na místní nastavení (nebezpečné)<math.h>
— Možná bezpečné<setjmp.h>
— Není bezpečné<signal.h>
— Povoleno<stdalign.h>
— Žádné funkce<stdarg.h>
— Žádné funkce<stdatomic.h>
— Možná bezpečné, pravděpodobně ne bezpečné<stdbool.h>
— Žádné funkce<stddef.h>
— Žádné funkce<stdint.h>
— Žádné funkce<stdio.h>
— Není bezpečné<stdlib.h>
— Ne všechny jsou bezpečné (některé jsou povoleny, jiné ne)<stdnoreturn.h>
— Žádné funkce<string.h>
— Ne všechny jsou bezpečné<tgmath.h>
— Možná bezpečné<threads.h>
— Pravděpodobně není bezpečné<time.h>
— Závisí na národním prostředí (aletime()
je výslovně povoleno)<uchar.h>
— Závisí na národním prostředí<wchar.h>
— Závisí na národním prostředí<wctype.h>
— Závisí na národním prostředí
Analýza hlaviček POSIX by byla … obtížnější v tom, že jich je mnoho a některé funkce mohou být bezpečné, ale mnohé nebudou… ale také jednodušší, protože POSIX říká, které funkce jsou bezpečné pro asynchronní signál (ne mnoho z nich). Všimněte si, že záhlaví jako <pthread.h>
má tři bezpečné funkce a mnoho nebezpečných funkcí.
Poznámka: Téměř veškeré hodnocení funkcí C a záhlaví v prostředí POSIX je polovzdělané hádání. Nemá smysl definitivní prohlášení od normalizačního orgánu.
Jak se vyhnout použití
printf
v obslužném programu signálu?
-
Vždy se tomu vyhněte, řekne:Jen nepoužívejte
printf()
v obslužných programech signálů. -
Alespoň na systémech vyhovujících POSIX můžete použít
write(STDOUT_FILENO, ...)
místoprintf()
. Formátování však nemusí být snadné:Tiskněte int z obslužného programu signálu pomocí funkcí zápisu nebo asynchronně zabezpečených funkcí
Můžete použít nějakou proměnnou příznaku, nastavit příznak uvnitř obslužného programu signálu a na základě tohoto příznaku zavolat printf()
funkce v main() nebo jiné části programu během normálního provozu.
Není bezpečné volat všechny funkce, například
printf
, ze zpracování signálu. Užitečnou technikou je použít obslužný program signálu k nastaveníflag
a poté zkontrolujteflag
z hlavního programu a v případě potřeby vytiskněte zprávu.
Všimněte si v příkladu níže, obsluha signálu ding() nastavila příznak alarm_fired
na 1 jako zachycený SIGALRM a v hlavní funkci alarm_fired
hodnota je zkoumána, aby podmíněně správně zavolala printf.
static int alarm_fired = 0;
void ding(int sig) // can be called asynchronously
{
alarm_fired = 1; // set flag
}
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror("fork failed");
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* if we get here we are the parent process */
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause();
if (alarm_fired) // check flag to call printf
printf("Ding!\n");
printf("done\n");
exit(0);
}
Reference:Beginning Linux Programming, 4th Edition, V této knize je přesně vysvětlen váš kód (co chcete), Kapitola 11:Procesy a signály, strana 484
Navíc musíte věnovat zvláštní pozornost psaní funkcí obslužných rutin, protože je lze volat asynchronně. To znamená, že obslužná rutina může být zavolána v libovolném bodě programu, nepředvídatelně. Pokud přijdou dva signály během velmi krátkého intervalu, může jeden psovod běžet v druhém. A považuje se za lepší praxi deklarovat volatile sigatomic_t
, tento typ je vždy přístupný atomicky, vyhnete se nejistotě ohledně přerušení přístupu k proměnné. (přečtěte si:Atomic Data Access and Signal Handling pro podrobné vysvětlení).
Přečtěte si Definování obslužných rutin signálů:abyste se naučili, jak napsat funkci obslužného rutiny signálu, kterou lze vytvořit pomocí signal()
nebo sigaction()
funkcí.
Seznam autorizovaných funkcí v manuálové stránce, volání této funkce uvnitř signal handleru je bezpečné.