La sortie du signal CGI est terminée avant la fin du programme

J’essaie d’obtenir une optimisation plus fiable pour un programme CGI écrit en C.

L’application effectue des écritures synchrones sur disque et ce que je voudrais, c’est que celles-ci soient effectuées une fois la sortie CGI terminée. Au départ, je pensais que c’était aussi simple que de fermer / rouvrir les stream stdin / stdout / stderr, et cela fonctionnait parfaitement sur un certain nombre de serveurs. en queue.

Malheureusement, je rencontre maintenant un problème sur quelques serveurs. Dès que le programme CGI ferme stdout, il reçoit un signal de terminaison d’Apache. Quelques secondes plus tard, la mise à mort est difficile. L’appel de setsid() semble n’avoir aucun effet sur ceci.

Existe-t-il un autre moyen de dire à Apache qu’il devrait envoyer la sortie au client sans mettre fin au programme CGI?

Il n’y a pas moyen de faire cela en utilisant mod_cgi et un seul processus. Il est lourd de ressources, mais une méthode généralement acceptée pour y parvenir s’appelle une double fourchette. Ca fait plutot comme ca:

 pid_t kid, grandkid; if ((kid = fork())) { waitpid(kid, null, 0); } else if ((grandkid = fork())) { exit(0); } else { // code here // do something long lasting exit(0); } 

Adapté de ce code PERL

Juste jeter des idées là-bas. La configuration actuelle ressemble à ceci

 +------------+ +------------+ | CGI |----------->| Apache | | Program |<-----------| Server | +------------+ +------------+ 

Que diriez-vous quelque chose comme ça

 +------------+ +------------+ +------------+ | Daemon |--->| CGI |--->| Apache | | Program |<---| Passthru |<---| Server | +------------+ +------------+ +------------+ 

Fondamentalement, déplacez toutes les fonctionnalités de votre programme actuel dans un démon lancé une fois au démarrage. Ensuite, créez un minuscule programme passthru pour Apache afin de le lancer via CGI. Le programme passthru se connecte au démon en utilisant IPC, soit en mémoire partagée, soit en sockets. Tout ce que le programme passthru reçoit sur stdin, il le transmet au démon. Tout ce que le programme passthru reçoit du démon, il le transmet à Apache sur stdout. De cette façon, Apache peut lancer / tuer le programme passthru comme bon lui semble, sans affecter ce que vous essayez de faire sur le backend.

Une option à considérer est:

  1. Le programme CGI génère une sortie vers Apache.
  2. Avant de fermer la sortie standard, il faille.
  3. Le processus parent ferme sa sortie standard et utilise l’une des fonctions de sortie d’urgence ( _exit() ou _Exit() ou un de ses proches) pour renflouer. Cela évite de vider les autres stream d’E / S.
  4. Le processus fils ferme le descripteur de fichier 1 avec close() – il évite d’utiliser fclose() car le parent écrit sur la sortie standard.
  5. Le processus enfant traite alors le rest du travail d’écriture.

Vous devrez peut-être isoler l’enfant de son parent en définissant son groupe de processus. Apache ne peut envoyer un signal au processus fils via le groupe de processus (car il ne connaît pas le PID du processus enfant). Ainsi, en le dissociant du groupe de processus original, votre processus enfant devient insensible aux messages envoyés par Apache.

Une autre option qui mérite d’être envisagée est que le parent fasse le travail, puis fourchette. Le processus parent ne vide pas la sortie standard; il utilise simplement la fonction de sortie d’urgence. Le processus enfant s’isole du parent, puis ferme la sortie standard avec fclose() , en vidant toute sortie en attente. Apache voit que le fichier est fermé et continue sur sa lancée. Le processus enfant effectue alors son nettoyage. Là encore, il est crucial de placer l’enfant dans son propre groupe de processus. L’avantage de ceci est que la sortie n’est pas fermée tant que l’enfant ne s’est pas isolé, donc Apache ne devrait pas pouvoir utiliser les coïncidences temporelles pour envoyer un signal à l’enfant (le petit-enfant d’Apache). Vous pourriez même simplement isoler le processus dans son propre groupe de processus avant de falsifier … ce qui élimine également la fenêtre de vulnérabilité pour l’enfant. Vous ne devriez pas faire cet isolement tant que vous n’êtes pas sur le sharepoint sortir et de sortir. sinon, vous contrecarrez les mécanismes de protection fournis par Apache.


Exemple de code

Voici une simulation de ce que vous pourriez faire, dans un fichier source playcgi.c compilé dans un programme playcgi :

 #include  #include  #include  #include  #include  #include  #include  #include  #include  static void err_exit(const char *fmt, ...); static void be_childish(void); static void be_parental(pid_t pid, int fd[2]); int main(void) { int fd[2]; if (pipe(fd) != 0) err_exit("pipe()"); pid_t pid = fork(); if (pid < 0) err_exit("fork()"); else if (pid == 0) { dup2(fd[1], STDOUT_FILENO); close(fd[0]); close(fd[1]); be_childish(); } else be_parental(pid, fd); return 0; } static void be_parental(pid_t pid, int fd[2]) { close(fd[1]); char buffer[1024]; int nbytes; while ((nbytes = read(fd[0], buffer, sizeof(buffer))) > 0) write(STDOUT_FILENO, buffer, nbytes); kill(-pid, SIGTERM); struct timespec nap = { .tv_sec = 0, .tv_nsec = 10 * 1000 * 1000 }; nanosleep(&nap, 0); int status; pid_t corpse = waitpid(pid, &status, WNOHANG); if (corpse <= 0) { kill(-pid, SIGKILL); corpse = waitpid(pid, &status, 0); } printf("PID %5d died 0x%.4X\n", corpse, status); } static void be_childish(void) { /* Simulate activity on pipe */ for (int i = 0; i < 10; i++) printf("Data block %d from child (%d)\n", i, (int)getpid()); fflush(stdout); /* Create new pipe to coordinate between child and grandchild */ int fd[2]; if (pipe(fd) != 0) err_exit("child's pipe()"); pid_t pid = fork(); if (pid < 0) err_exit("child's fork()"); if (pid > 0) { char buffer[4]; fprintf(stderr, "Child (%d) waiting\n", (int)getpid()); close(fd[1]); read(fd[0], buffer, sizeof(buffer)); close(fd[0]); fprintf(stderr, "Child (%d) exiting\n", (int)getpid()); close(STDOUT_FILENO); _exit(0); } else { /* Grandchild continues - with no standard output */ close(STDOUT_FILENO); pid_t sid = setsid(); fprintf(stderr, "Grandchild (%d) in session %d\n", (int)getpid(), (int)sid); /* Let child know grandchild has set its own session */ close(fd[0]); close(fd[1]); struct timespec nap = { .tv_sec = 2, .tv_nsec = 0 }; nanosleep(&nap, 0); for (int i = 0; i < 10; i++) { fprintf(stderr, "Data block %d from grandchild (%d)\n", i, (int)getpid()); } fprintf(stderr, "Grandchild (%d) exiting\n", (int)getpid()); } } static void err_vexit(const char *fmt, va_list args) { int errnum = errno; vfprintf(stderr, fmt, args); if (fmt[strlen(fmt)-1] != '\n') putc('\n', stderr); if (errno != 0) fprintf(stderr, "%d: %s\n", errnum, strerror(errnum)); exit(EXIT_FAILURE); } static void err_exit(const char *fmt, ...) { va_list args; va_start(args, fmt); err_vexit(fmt, args); va_end(args); } 

Les fonctions main() et be_parental() simulent ce que pourrait faire Apache. Il existe un canal qui devient la sortie standard du processus enfant CGI. Le parent lit à partir du tube, puis envoie un signal de terminaison à l'enfant et fait un snooz pendant 10 millisecondes, puis recherche un cadavre. (C'est probablement la partie la moins convaincante du code, mais ...) S'il n'en trouve pas, il envoie un signal SIGKILL et recueille le cadavre de l'enfant mort. Il décrit comment l'enfant est mort et revient (et dans cette simulation, il réussit).

La fonction be_childish() est le processus enfant CGI. Il écrit une sortie sur sa sortie standard et vide la sortie standard. Il crée ensuite un canal afin que l'enfant et le petit-enfant puissent synchroniser leur activité. L'enfant fourche. L'enfant survivant signale qu'il attend le petit-enfant, ferme la fin de l'écriture du canal, lit l'extrémité de lecture du canal (pour les données qui n'arriveront jamais, la lecture retournera 0 indiquant EOF). Il ferme la fin de lecture du tube, signale (en cas d'erreur standard) qu'il va quitter, ferme sa sortie standard et quitte.

Pendant ce temps, le petit-fils ferme la sortie standard et devient ensuite un leader de session avec setsid() . Il rend compte de son nouveau statut, ferme les deux extrémités du canal, libérant ainsi son parent (le processus enfant d'origine) afin qu'il puisse sortir. Il faut ensuite un sumil de 2 secondes - beaucoup de temps pour le parent et le grand-parent pour sortir - puis écrit des informations sur l'erreur standard, un message "sortant" et se ferme.

Sortie de l'échantillon

 $ ./playcgi Data block 0 from child (60867) Data block 1 from child (60867) Data block 2 from child (60867) Data block 3 from child (60867) Data block 4 from child (60867) Data block 5 from child (60867) Data block 6 from child (60867) Data block 7 from child (60867) Data block 8 from child (60867) Data block 9 from child (60867) Child (60867) waiting Grandchild (60868) in session 60868 Child (60867) exiting PID 60867 died 0x0000 $ Data block 0 from grandchild (60868) Data block 1 from grandchild (60868) Data block 2 from grandchild (60868) Data block 3 from grandchild (60868) Data block 4 from grandchild (60868) Data block 5 from grandchild (60868) Data block 6 from grandchild (60868) Data block 7 from grandchild (60868) Data block 8 from grandchild (60868) Data block 9 from grandchild (60868) Grandchild (60868) exiting 

Vous pouvez appuyer sur Entrée pour entrer une ligne de commande vide et obtenir une autre invite de commande.

Il y a une pause perceptible entre le message 'PID 60867 deceased 0x0000' (et l'invite qui apparaît) et la sortie du message 'Data block 0 from petitchild (60868)'. Cela montre que l’enfant continue malgré le parent mourant, etc. kill(-pid, SIGKILL) ), etc. Mais je crois que le petit-fils survivra pour faire son écriture.

Vous pouvez également essayer de capturer le signal TERM et l’ignorer jusqu’à ce que vous ayez terminé le traitement.

Vous pourriez vérifier FastCGI . Il vous permet de restr fidèle au modèle de programmation CGI mais dissocie la durée de vie de la requête de la durée de vie du processus. Vous pourriez alors faire quelque chose comme ceci:

 while (FCGI_Accept() >= 0) { // handle normal request FCGI_Finish(); // do synchronous I/O outside of request lifetime }