Le système d’exploitation (POSIX) termine-t-il une modification d’un fichier mappé en mémoire si le processus est SIGKILLed?

Un article similaire raconte si les modifications apscopes à un fichier mappé en mémoire sont vidées sur disque après un SIGKILL, mais que se passe-t-il si le processus est modifié, par exemple écriture / suppression, dans la mémoire tampon disque?

Le fichier sous-jacent est-il mis à jour et corrompu? L’opération d’écriture / suppression est-elle terminée avant de tuer le processus? Y at-il des garanties pour cela?

Disons que vous avez quelque chose comme

 volatile unsigned char *map; /* memory-mapped file */ size_t i; for (i = 0; i < 1000; i++) map[i] = slow_calculation(i); 

et pour une raison quelconque, le processus est tué lorsque i = 502 .

Dans un tel cas, le contenu du fichier reflètera en effet le contenu du mappage à ce stade.

Non, il n'y a aucun moyen d'éviter cela (en ce qui concerne le signal KILL), car KILL est bloquable et inaccessible.

Vous pouvez minimiser la fenêtre en utilisant un tampon temporaire en tant que tampon "transactionnel", en calculant les nouvelles valeurs dans ce tampon, puis simplement copier les valeurs. Ce n'est pas une garantie, mais cela signifie qu'il existe une probabilité beaucoup plus élevée que le contenu du fichier soit intact même si le processus est tué. (En outre, cela signifie que si vous utilisez, par exemple, des mutex pour synchroniser l'access au mappage, il vous suffit de conserver le mutex pour la durée minimale requirejse.)

Tuer un processus via le signal KILL est une terminaison très anormale et, à mon avis, des fichiers mappés en mémoire sont brouillés à cause de cela. Ce n'est pas quelque chose qui devrait être fait pendant le fonctionnement normal du tout; le signal TERM est utilisé pour cela.

Ce dont vous devez vous soucier, c'est que votre processus réponde rapidement à un signal TERM. TERM est capturable et bloquable, et constitue essentiellement un moyen pour un processus de supervision externe (ou l'utilisateur auquel appartient le processus, ou le superutilisateur) de demander que le processus se termine correctement dès que possible. Cependant, le processus ne devrait pas se dérouler, car il est courant d'envoyer un signal KILL au processus s'il ne sort pas dans les secondes qui suivent la réception d'un signal TERM.

Dans mes propres démons, je m'efforce de leur permettre de répondre à un TERM dans une seconde environ, à moins que le système ne soit soumis à une lourde charge. Il s’agit, bien sûr, d’une mesure très subjective car la vitesse des différents systèmes varie, mais il n’ya pas de règles ssortingctes ici.

Une façon de gérer cela est d'installer un gestionnaire de signal TERM qui, en fonctionnement normal, met immédiatement fin au processus. Pour les sections critiques, la sortie est rescope:

 static volatile int in_critical = 0; static volatile int need_to_exit = 0; static void handle_exit_signal(int signum) { __atomic_store_n(&need_to_exit, 1, __ATOMIC_SEQ_CST); if (!__atomic_load_n(&in_critical, __ATOMIC_SEQ_CST)) exit(126); } static int install_exit(int signum) { struct sigaction act; memset(&act, 0, sizeof act); sigemptyset(&act.sa_mask); act.sa_handler = handle_exit_signal; act.sa_flags = SA_RESTART; if (sigaction(signum, &act, NULL) == -1) return errno; return 0; } 

Pour entrer et sortir des sections critiques (par exemple, lorsque vous détenez un mutex dans la zone de mémoire partagée):

 static inline void critical_begin(void) { __atomic_add_fetch(&in_critical, 1, __ATOMIC_SEQ_CST); } static inline void critical_end(void) { if (!__atomic_sub_fetch(&in_critical, 1, __ATOMIC_SEQ_CST)) if (__atomic_load_n(&need_to_exit, __ATOMIC_SEQ_CST)) exit(126); } 

Donc, si un signal TERM est reçu pendant que vous êtes dans une section critique (et le critical_begin() et critical_end() font imbriquer), l'appel final à critical_end() quitte le processus.

Notez que j'ai utilisé les composants atomiques GCC pour gérer les drapeaux de manière atomique, sans courses de données, même si le gestionnaire de signaux est exécuté dans un thread différent. J'ai trouvé cela la solution la plus propre pour Linux , même si elle devrait fonctionner sur d'autres systèmes d'exploitation aussi. (D'autres compilateurs C que vous pouvez utiliser sous Linux, comme Clang et Intel CC, les prennent également en charge.)

Donc, en pseudocode, faire le calcul lent de 1000 éléments comme indiqué au début, serait alors

 volatile unsigned char *map; unsigned char cache[1000]; size_t i; /* Nothing critical yet, we're just calculating new values... */ for (i = 0; i < 1000; i++) cache[i] = slow_calculation(i); /* Update shared memory map. */ critical_begin(); /* pthread_mutex_lock() */ memcpy(map, cache, 1000); /* pthread_mutex_unlock() */ critical_end(); 

Si un signal TERM est délivré avant le critical_begin() , le processus est terminé à cet endroit. Si un signal TERM est délivré après cela, mais avant le critical_end() , l'appel à critical_end() mettra fin au processus.

Ceci est juste un modèle qui peut résoudre le problème sous-jacent; il y en a d'autres. Celui avec un seul volatile sig_atomic_t done = 0; Le gestionnaire de signaux défini sur non nul, et les boucles de traitement principales vérifient régulièrement, est encore plus courant.

Comme indiqué par R .. dans un commentaire, le pointeur utilisé pour faire référence à la carte mémoire devrait être un pointeur sur volatile (ie, volatile some_type *map ) pour empêcher le compilateur de réorganiser les magasins dans la carte mémoire.