A signalkezelő nem egyszerűen egy másik callback: olyan ponton szakíthatja meg a programot, ahol egy könyvtári művelet belső állapota még átmeneti. Ezért a szabályos leállítás alapelve nem az, hogy „a handler mindent felszabadít”, hanem az, hogy a handler leállítási szándékot jelez, a normál vezérlési út pedig végrehajtja a leállítás protokollját.
A fork, exec, pipe és gyermekfolyamat részben
megismert erőforrás-életciklus itt is érvényes: minden erőforrásnak
legyen lezárásért felelős tulajdonosa. Az errno szabályai pedig különösen
fontossá válnak, mert a signalkezelő a megszakított kód hibajelzését is
elronthatja.
ISO C17: a <signal.h> korlátozott
signalmodellt biztosít, többek között a signal,
raise és sig_atomic_t eszközökkel. A POSIX
szolgáltatások signalkezeléséhez ennél részletesebb interfész
szükséges.
POSIX: egy signalhoz tartozó
disposition, vagyis kezelési beállítás lehet
alapértelmezett művelet, figyelmen kívül hagyás vagy saját handler. Ezt
célszerű sigaction segítségével beállítani, amely a handler
mellett maszkolási és működési szabályokat is megad.
A signal blokkolása nem figyelmen kívül hagyást jelent. A blokkolt signal függőben maradhat, majd a blokkolás megszüntetése után kézbesíthető. A hagyományos, nem valós idejű signalok nem megbízható eseményszámlálók: több azonos, függőben lévő jelzés összeolvadhat.
POSIX folyamat- és szálmodell:
Egyszálú programban a maszkot sigprocmask kezeli.
Többszálú programban erre pthread_sigmask használatos; ott
a sigprocmask viselkedésére nem építünk. A
SIGKILL és SIGSTOP nem fogható el, nem
ignorálható és nem blokkolható.
Az aszinkron signalbiztonság (async-signal-safety) azt jelenti, hogy egy művelet akkor is használható handlerből, amikor egy signal a normál végrehajtást tetszőleges ponton szakította meg. Ez erősebb követelmény, mint a szálbiztosság: egy belső mutex megvédhet több száltól, de holtpontot okozhat, ha a handler ugyanazon a szálon újra megszerezné.
Az alábbi részlet hibás tanítási példa, nem végrehajtási minta:
/* ISO C17 / POSIX: hibás, aszinkron handlerbe nem való műveletek. */
static void bad_handler(int signo)
{
printf("signal: %d\n", signo);
void *temporary = malloc(128);
free(temporary);
}POSIX: a printf, malloc és
free nem async-signal-safe. A megszakítás történhet éppen a
stdio-puffer vagy az allocator belső adatszerkezetének módosítása
közben. Az eredmény lehet holtpont vagy sérült állapot; nincs garantált
kimenet.
Ezzel szemben a write POSIX szerint async-signal-safe,
de ettől még blokkolhat. A signalbiztonság tehát nem időkorlát és nem
általános helyességi garancia.
A legegyszerűbb szabályos megoldás:
/* ISO C17; a teljes POSIX-példa ugyanezt használja. */
static volatile sig_atomic_t stop_requested = 0;
static void request_stop(int signo)
{
(void)signo;
stop_requested = 1;
}A sig_atomic_t olyan egész típus, amelynek hozzáférése
aszinkron megszakítás mellett is atomi egységként kezelhető. A
volatile biztosítja a jelző megfelelő megfigyelhetőségét
ebben a mintában; nem teszi általánosan szálbiztossá a
programot, és nem publikál más adatokat.
A szerződés szándékosan szűk:
++, összetett állapotmódosítás vagy megosztott
struktúra;Ez a handler nem módosítja az errno értékét. Ha egy
POSIX-handler olyan megengedett függvényt hív, amely módosíthatja,
belépéskor el kell mentenie, visszatérés előtt pedig
helyreállítania.
POSIX: a sigaction
SA_RESTART jelzője bizonyos blokkoló műveletek automatikus
újraindítását kéri a handler visszatérése után. Nem minden
rendszerhívásra érvényes; az egyes interfészek és a platform szabályait
külön kell vizsgálni.
Leállításnál az automatikus újraindítás hátrány is lehet: a handler
beállítja a jelzőt, de a program visszatér a blokkoló várakozásba. A
SA_RESTART elhagyása önmagában szintén nem garantálja, hogy
minden várakozás megszakad.
Az EINTR kezelése ezért alkalmazási policy:
-1 az eredmény és errno == EINTR,
megvizsgáljuk a leállítási állapotot.A „minden EINTR után feltétel nélkül újra” ciklus
könnyen leállíthatatlan szolgáltatást eredményez.
Ez a részlet hibás szinkronizációs minta:
/* POSIX-részlet: elveszett ébresztés lehetséges. */
while (!stop_requested) {
pause();
}Ha a signal az ellenőrzés után, de a pause előtt
érkezik, a jelző beáll, majd a program mégis elalszik.
POSIX: a pselect a várakozás idejére
atomian cseréli a hívó szál signalmaszkját. Így a jelző ellenőrzése
alatt blokkolhatjuk a leállító signalokat, és csak a várakozás
megkezdésével együtt engedélyezzük őket.
A következő program az ébresztési és tulajdonlási mintát mutatja, nem
teljes szolgáltatás. A tmpfile egy helyettesítő alkalmazási
erőforrás; a program leállításkor normál kódból ír bele és zárja le.
Profil: ISO C17 és POSIX interfészek, Linux hosted környezet. Választott fordítási célprofil: x86-64 System V ABI, LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A példa nem közöl ellenőrzött fordítási vagy futtatási eredményt.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
static volatile sig_atomic_t stop_requested = 0;
static void request_stop(int signo)
{
(void)signo;
stop_requested = 1;
}
int main(void)
{
sigset_t stop_set;
sigset_t old_mask;
sigset_t wait_mask;
sigemptyset(&stop_set);
sigaddset(&stop_set, SIGINT);
sigaddset(&stop_set, SIGTERM);
if (sigprocmask(SIG_BLOCK, &stop_set, &old_mask) == -1) {
perror("sigprocmask");
return EXIT_FAILURE;
}
struct sigaction action = {0};
action.sa_handler = request_stop;
action.sa_flags = 0;
sigemptyset(&action.sa_mask);
sigaddset(&action.sa_mask, SIGINT);
sigaddset(&action.sa_mask, SIGTERM);
if (sigaction(SIGINT, &action, NULL) == -1 ||
sigaction(SIGTERM, &action, NULL) == -1) {
perror("sigaction");
return EXIT_FAILURE;
}
wait_mask = old_mask;
sigdelset(&wait_mask, SIGINT);
sigdelset(&wait_mask, SIGTERM);
FILE *journal = tmpfile();
if (journal == NULL) {
perror("tmpfile");
return EXIT_FAILURE;
}
int failed = 0;
while (!stop_requested) {
int result = pselect(0, NULL, NULL, NULL, NULL, &wait_mask);
if (result == -1 && errno != EINTR) {
perror("pselect");
failed = 1;
break;
}
}
/* Normál vezérlési út: itt már használható stdio. */
if (fputs("shutdown\n", journal) == EOF) {
perror("fputs");
failed = 1;
}
if (fclose(journal) == EOF) {
perror("fclose");
failed = 1;
}
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}Fordítás a kiválasztott kiadásokkal:
gcc -std=c17 -O2 -Wall -Wextra -Wpedantic shutdown_wait.c -o shutdown_wait_gcc
clang -std=c17 -O2 -Wall -Wextra -Wpedantic shutdown_wait.c -o shutdown_wait_clangA parancsok a tanuló külön laborjához tartoznak. Ellenőrzési
feltétel: a futó programnak küldött SIGTERM után a
várakozás véget ér, és sikeres cleanup esetén a kilépési státusz
sikeres.
| Objektum vagy erőforrás | Tulajdonos és élettartam | Érvénytelenítés, hibakimenet |
|---|---|---|
stop_requested |
Statikus tárolási idejű objektum | A folyamat végéig él; a handler csak értéket rendel hozzá |
journal által elért stream |
A main a tulajdonos; a handler nem fér hozzá |
fclose után nem használható, hibás visszatérés esetén
sem |
&wait_mask kölcsönzött hozzáférés |
A tároló a main lokális objektuma |
A pselect hívás teljes ideje alatt érvényes; nincs
megtartott pointer |
| Signalbeállítások | A teljes program által kezelt folyamatállapot | A példa kilépésig megtartja őket; nem általános könyvtári inicializáló |
Bizonyításvázlat. A ciklusfeltétel vizsgálatakor a
két leállító signal blokkolt. Ha már függőben vannak, a
pselect ideiglenes maszkja kézbesíthetővé teszi őket. Ha
később érkeznek, megszakíthatják a várakozást. Visszatéréskor az
eredeti, blokkoló maszk helyreáll, majd a ciklus újra ellenőrzi a
jelzőt. Nincs külön, védelem nélküli rés az ellenőrzés és az elalvás
között.
A példa korlátja fontos: a cleanup alatt a leállító signalok
blokkoltak, és a fclose nem rendelkezik általános
időkorláttal. Ez helyes egyszeri leállítási minta, de még nem határidős
shutdown.
Linux-only: a signalfd segítségével
kiválasztott signalok file descriptoron olvasható eseményekké
alakíthatók. Ehhez az útvonalhoz nem szükséges aszinkron handler.
A használati szerződés:
SFD_CLOEXEC | SFD_NONBLOCK jelzőkkel.read segítségével
struct signalfd_siginfo rekordokat veszünk át.A puffernek legalább egy teljes rekordot kell befogadnia. Pozitív
olvasási eredménynél a teljes rekordokat dolgozzuk fel;
EAGAIN esetén pillanatnyilag nincs további esemény,
EINTR esetén a normál megszakítási policy érvényes. A
hagyományos signalok összeolvadását a signalfd nem szünteti
meg.
A descriptor tulajdonosa az eseményhurok; a regisztráció és a kiolvasás kölcsönzött hozzáférés. A tulajdonos lezárása érvényteleníti ezeket. Többszálú programban a maszkot célszerű a szálak létrehozása előtt beállítani: különben egy másik, nem blokkoló szálhoz kerülhet a signal. Másik szálnak címzett signalt a descriptor olvasója nem vehet át tetszőlegesen.
A descriptor eseményhurokba kötését a poll/epoll és explicit eseményállapotgép rész bontja ki.
A szabályos leállítás nem egyetlen close, hanem
állapotátmenetek sora:
| Állapot | Megengedett munka | Következő átmenet |
|---|---|---|
RUNNING |
Új munka fogadása és feldolgozása | Leállítási kérés → QUIESCING |
QUIESCING |
Új munka kizárása, befejezési határidő rögzítése | → DRAINING |
DRAINING |
Már elfogadott munka korlátozott befejezése | Kész → CLOSING; határidő → megszakítás |
CLOSING |
Erőforrások egyszeri lezárása | → kilépés |
A türelmi időt monoton órával kell értelmezni. Ha a határidő, az aktuális idő, akkor az eseményvárakozás legfeljebb
ideig tarthat. Ez csak akkor ad alkalmazási szintű korlátot, ha a köztes munkalépések sem blokkolhatnak korlátlanul.
Egy újabb, külön megfigyelt leállítási kérés gyorsíthatja a megszakítást, de az összeolvadás miatt nem szabad „pontosan két signalra” építeni. A határidő és a külső supervisor kényszerített leállítása külön védelmi réteg.
A handlerből végzett naplózást normál kódba kell áttenni. A
volatile jelzőt nem szabad általános konkurens
adatcsatornává bővíteni. A vak EINTR-újrapróbálás helyett
állapotfüggő döntés szükséges. Végül a „graceful” nem jelenthet végtelen
várakozást: külön kell tervezni az új munka kizárását, a folyamatban
lévő munka befejezését és a kényszerített megszakítást.
volatile?read után: külön kezelje a haladást, EOF-ot,
megszakítás miatti leállást, újrapróbálást és egyéb hibát.pause-t hív. Néha csak a második SIGTERM
után áll le. Adj eseménysorrendet és javítást.A naplózó tarthat belső lockot éppen akkor, amikor a signal
megszakítja. A handler ugyanazt a lockot kérve a megszakított művelet
befejezésére várhat, amely viszont csak a handler visszatérése után
folytatódhat. A volatile nem old fel lockot és nem teszi
újrabeléphetővé a könyvtárat.
Ellenőrzési feltétel: a handlerben ne legyen nem async-signal-safe hívás; a naplózás normál útvonalon történjen.
A következő C17/POSIX-részlet egy ciklusba illeszthető.
n a read eredménye, saved_errno
pedig a hiba után azonnal elmentett errno. A feldolgozó és
hibakezelő műveletek alkalmazásfüggők.
if (n > 0) {
/* Könyveld el és dolgozd fel az n bájtnyi haladást. */
} else if (n == 0) {
/* EOF. */
break;
} else if (saved_errno == EINTR) {
if (stop_requested) {
break;
}
continue;
} else {
/* Egyéb hiba kezelése. */
break;
}Ellenőrzési feltétel: pozitív eredménynél nincs újraküldés vagy újrafeldolgozás ugyanarra a tartományra. Ez a részlet az eredményt osztályozza; önmagában nem oldja meg a következő blokkoló hívás előtti ébresztési rést.
Az eseménysorrend: hamis jelző beolvasása → signal → handler
beállítja a jelzőt → visszatérés → pause. A program ekkor
újabb signalra vár.
A javítás blokkolt jelzőellenőrzés és atomikus maszkkal végzett
pselect, vagy Linuxon blokkolt signalok feldolgozása
signalfd-n keresztül.
Ellenőrzési feltétel: ne maradjon olyan végrehajtási rés, amelyben a leállítási eseményt már feldolgoztuk, de utána még korlátlanul elalhatunk.
A minimális handler csak jelez. A normál vezérlési út dönt az újrapróbálásról, vezeti a shutdown állapotgépét és zárja le az erőforrásokat. A helyességhez együtt szükséges az async-signal-safety, a maszkolás, az elveszett ébresztés kizárása és a határidős leállítási policy.