GNU/Linux >> Znalost Linux >  >> Linux

Jak se vyhnout použití printf v obslužném programu signálu?

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 nebo raise 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 jako volatile sig_atomic_t , nebo obsluha signálu volá jakoukoli funkci ve standardní knihovně jinou než abort funkce _Exit funkce quick_exit nebo signal 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í na signal výsledkem funkce je SIG_ERR návrat, hodnota errno 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() nebo sigqueue() 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 jako volatile 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í hodnotu errno 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řila strlen() 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í (ale time() 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?

  1. Vždy se tomu vyhněte, řekne:Jen nepoužívejte printf() v obslužných programech signálů.

  2. Alespoň na systémech vyhovujících POSIX můžete použít write(STDOUT_FILENO, ...) místo printf() . 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é zkontrolujte flag 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é.


Linux
  1. Jak SSH na server pomocí jiného serveru?

  2. Jak trasovat Python skripty pomocí trace.py

  3. Jak napsat obsluhu signálu pro zachycení SIGSEGV?

  1. Použití barev s printf

  2. C++11:Jak vytvořit alias funkce?

  3. Jak zapsat celé číslo do binárního souboru pomocí Bash?

  1. IPC využívající signály na linuxu

  2. Jak se vyhnout varovné zprávě „jsou stejný soubor“ při použití cp v Linuxu?

  3. Bash - Jak tisknout víceřádkové řetězce (s '\n') pomocí printf