J’essaie d’implémenter un système de communication de type client-serveur à l’aide de la fonction de sondage de C.
exec
pour exécuter some_binary
J’ai essayé d’implémenter cette poll
aide de l’ poll
, mais des problèmes se sont posés, car le processus enfant stocke la sortie dans la mémoire tampon, ce qui a entraîné l’expiration du délai d’appel. Voici mon code:
int main() { char *buffer = (char *) malloc(1000); int n; pid_t pid; /* pid of child process */ int rpipe[2]; /* pipe used to read from child process */ int wpipe[2]; /* pipe used to write to child process */ pipe(rpipe); pipe(wpipe); pid = fork(); if (pid == (pid_t) 0) { /* child */ dup2(wpipe[0], STDIN_FILENO); dup2(rpipe[1], STDOUT_FILENO); close(wpipe[0]); close(rpipe[0]); close(wpipe[1]); close(rpipe[1]); if (execl("./server", "./server", (char *) NULL) == -1) { fprintf(stderr, "exec failed\n"); return EXIT_FAILURE; } return EXIT_SUCCESS; } else { /* parent */ /* close the other ends */ close(wpipe[0]); close(rpipe[1]); /* poll to check if write is good to go This poll succeeds, write goes through */ struct pollfd pfds[1]; pfds[0].fd = wpipe[1]; pfds[0].events = POLLIN | POLLOUT; int pres = poll(pfds, (nfds_t) 1, 1000); if (pres > 0) { if (pfds[0].revents & POLLOUT) { printf("Writing data...\n"); write(wpipe[1], "hello\n", 6); } } /* poll to check if there's something to read. This poll times out because the child buffers its stdout stream. */ pfds[0].fd = rpipe[0]; pfds[0].events = POLLIN | POLLOUT; pres = poll(pfds, (nfds_t) 1, 1000); if (pres > 0) { if (pfds[0].revents & POLLIN) { printf("Reading data...\n"); int n = read(rpipe[0], buffer, 1000); buffer[n] = '\0'; printf("child says:\n%s\n", buffer); } } kill(pid, SIGTERM); return EXIT_SUCCESS; } }
Le code du serveur est simplement:
int main() { char *buffer = (char *) malloc(1000); while (scanf("%s", buffer) != EOF) { printf("I received %s\n", buffer); } return 0; }
Comment empêcher le délai d’appels des scrutateurs en raison de la mise en mémoire tampon?
MODIFIER:
Je voudrais que le programme fonctionne même lorsque le binary exec
est externe, c’est-à-dire que je n’ai aucun contrôle sur le code – comme une commande unix, par exemple cat
ou ls
.
Comme je l’ai répondu dans une réponse à une question précédente de votre part, vous devez mettre en place une boucle d’événements ; comme son nom l’indique, il est en boucle , vous devriez donc coder dans le processus parent:
while (1) { // simplistic event loop! int status=0; if (waitpid(pid, &status, WNOHANG) == pid) { // clean up, child process has ended handle_process_end(status); break; }; struct pollpfd pfd[2]; memset (&pfd, 0, sizeof(pfd)); // probably useless but dont harm pfd[0].fd = rpipe[0]; pfd[0].events = POLL_IN; pfd[1].fd = wpipe[1]; pfd[0].event = POLL_OUT; #define DELAY 5000 /* 5 seconds */ if (poll(pfd, 2, DELAY)>0) { if (pfd[0].revents & POLL_IN) { /* read something from rpipe[0]; detect end of file; you probably need to do some buffering, because you may eg read some partial line chunk written by the child, and you could only handle full lines. */ }; if (pfd[1].revents & POLL_OUT) { /* write something on wpipe[1] */ }; } fflush(NULL); } /* end while(1) */
vous ne pouvez pas prédire dans quel ordre les tubes sont lisibles ou inscriptibles, et cela peut se produire plusieurs fois. Bien sûr, la mise en mémoire tampon (dans le processus parent) est impliquée, je vous laisse les détails … Vous n’avez aucune influence sur la mise en mémoire tampon dans le processus enfant (certains programmes détectent que leur sortie est ou non un terminal avec est gras ).
Ce que vous donne une boucle d’interrogation d’événement comme ci-dessus, c’est d’éviter la situation de blocage où le processus enfant est bloqué car son canal stdout est plein, alors que le parent est bloqué en écriture (sur le canal stdin de l’enfant) car le canal est plein: avec un événement boucle, vous lisez dès que certaines données sont interrogées lisibles sur le canal d’entrée (c’est-à-dire la sortie standard du processus enfant) et vous écrivez certaines données dès que le canal de sortie est interrogé en écriture (c’est-à-dire qu’il n’est pas plein). Vous ne pouvez pas prédire à l’avance dans quel ordre ces événements “la sortie de l’enfant est lisible par le parent” et “l’entrée de l’enfant est en écriture pour le parent” se produisent.
Je recommande de lire Advanced Linux Programming (Programmation avancée de Linux) qui contient plusieurs chapitres expliquant ces problèmes!
BTW ma boucle d’événement simpliste est un peu fausse: si le processus enfant s’est terminé et que certaines données restnt dans son canal stdout, sa lecture n’est pas terminée. Vous pouvez déplacer le test waitpid
après le poll
De même, ne vous attendez pas à ce qu’une seule write
(du processus enfant) dans un canal déclenche une seule read
dans le processus parent. En d’autres termes, il n’y a pas de notion de longueur de message. Cependant, POSIX est au courant de PIPE_MAX
…. Voir sa documentation en écriture . Votre tampon passé en read
et en write
doit probablement PIPE_MAX
taille PIPE_MAX
.
Je répète: vous devez appeler poll
intérieur de votre boucle d’ événement et très probablement, poll
sera appelé plusieurs fois (car votre boucle sera répétée plusieurs fois!) Et générera des résultats de lecture lisibles ou inscriptibles dans un résultat imprévisible (et non reproductible). ordre! Une première exécution de votre programme peut indiquer ” rpipe[0]
lisible”, vous en read
324 octets, vous répétez la boucle d’événements, un poll
indique ” wpipe[1]
écriture”, vous pouvez y write
10 octets, vous répétez la boucle d’événement, poll
indique ” rpipe[0]
lisible”, vous en read
110 octets, vous répétez la boucle d’événement, poll
indique à nouveau ” rpipe[0]
lisible”, vous en read
4096 octets, etc., etc. .. Une deuxième exécution du même programme dans le même environnement donnerait des événements différents, tels que: poll
indique que ” wpipe[1]
accessible en écriture”, vous write
1000 octets, vous répétez la boucle, un poll
indique que ” rpipe[0]
lisible, etc ….
NB: votre problème n’est pas la mise en mémoire tampon dans le programme enfant (“client”), ce qui, nous le supposons, ne peut pas être modifié. Ce qui compte, ce ne sont donc pas les données mises en tampon qui y sont contenues, mais la véritable entrée et sortie (c’est la seule chose que votre processus parent peut observer; la mise en mémoire tampon interne de l’enfant est sans importance pour le parent), c’est-à-dire les données que votre programme enfant a pu vraiment lire (2) et écrire (2) . Et si vous passez par un pipe (7) , ces données deviendront poll (2) dans le processus parent (et votre processus parent pourra en read
ou en write
une partie après POLL_IN
ou POLL_OUT
dans le champ revents
mis à jour après le poll
). Au fait, si vous avez codé l’enfant, n’oubliez pas d’appeler fflush
aux endroits appropriés à l’intérieur.
Il semble y avoir deux problèmes dans votre code. “stdout” est mis en tampon par défaut, le serveur doit donc le vider explicitement:
printf("I received %s\n", buffer); fflush(stdout);
Et le programme principal ne doit pas s’inscrire à POLLOUT
lorsqu’il tente de lire (mais vous voudrez peut-être vous inscrire à POLLERR
):
pfds[0].fd = rpipe[0]; pfds[0].events = POLLIN | POLLERR;
Avec ces modifications, vous obtenez le résultat attendu:
$ ./main Écrire des données ... Lecture des données ... l'enfant dit: J'ai reçu bonjour
En règle générale, vous devez également vérifier la valeur de retour de poll()
et répéter l’appel si nécessaire (par exemple, dans le cas d’un appel système interrompu ou d’un délai d’attente).