[C] *nix pipe/buffer wazigheden

Pagina: 1
Acties:

  • froggie
  • Registratie: November 2001
  • Laatst online: 20-11-2024
Ik ben wat aan het prutsen met inter process communication. Ik heb het volgende eenvoudige proof of concept;

Het idee is eenvoudig. Ik heb een commandline app die 2 integers inleest via de commandline en daarna de uitkomst van de optelling teruggeeft.
Om dit commandline proggel heb ik een "wrapper" geschreven die het volgende zou moeten doen/doet:
er worden 2 pipes aangemaakt (1 voor input, 1 voor output). Daarna forkt het programma zich. De parent sluit vervolgens de kanten van de 2 pipes die hij niet gebruikt (de ene wordt naar geschreven en de andere wordt gelezen) en wacht daarna op keyboard input.
De child verbindt de kanten van de pipe die door de parent gesloten zijn met stdin/stdout en sluit daarna de kanten die de parent nog open heeft staan. Daarna wordt dmv de exec() call het proces vervangen voor de commandline app.
Doordat de 2 processen verbinden zijn dmv de pipes kan de parent naar de stdin van de app schrijven en van de stdout lezen.

Mijn probleem is het volgende: de eerste input en output gaat goed en werkt het zoals verwacht. Alles wat daarna komt is messed up. Om dit tegen te gaan flush ik dmv memset de character array voordat ik deze gebruik om naar toe te lezen of te schrijven. Maar ondanks dat ontstaat er ergens dirty input/output. Heeft iemand misschien een idee waar het mis gaat? Is het misschien nodig de pipe zelf ook te flushen voordat je deze gaat lezen/schrijven?

Voorbeeld output:
code:
1
2
3
4
5
6
$ ./add_wrapper
12 34
46
12 56
Input error
Input error


Ik heb wat gegoogled naar pipe communication en inter process communication maar dat leverde geen oplossing. M'n volgende stap is een boek kopen over dit onderwerp, maar omdat de boeken van Stevens nogal duur zijn (en ik maar een klein stukje van het IPC spectrum wil gebruiken) is dit een kostbare "oplossing".

De source van dit alles:

add.c
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
    char buff[32];
    int arg1;
    int arg2;

    while (fgets(buff, sizeof(buff), stdin) != NULL) {
    if (sscanf(buff, "%d%d", &arg1, &arg2) == 2) {
        printf("%d\n", arg1 + arg2);
    } else if (arg1 == 203) {
        break;
    } else {
        printf("Input error\n");
    }
    }

    return 0;
}

add_wrapper.c
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    int in_fd[2], out_fd[2]; /* I/O pipe filedescriptors */
    char buff[32]; /* I/O character buffer */

    /* Setup communication pipelines */
    if (pipe(in_fd) && pipe(out_fd)) {
    perror("Pipe error.\n");
    exit(1);
    }

    /* Attempt to fork and check for errors */
    if ((pid = fork()) == -1) {
    perror("Fork error.\n");
    exit(1);
    }

    if (pid) { /* We're the parent */
    /* Close the unused sides of the pipes */
    close(in_fd[0]);
    close(out_fd[1]);
    
    while (fgets(buff, sizeof(buff), stdin) != NULL) {
        write(in_fd[1], buff, sizeof(buff)); /* Write line to external process */
        memset(buff, 0, sizeof(buff)); /* Clear the buffer */

        read(out_fd[0], buff, sizeof(buff)); /* Read from the external process */
        printf("%s", buff); /* Echo buffer */
        memset(buff, 0, sizeof(buff)); /* Clear the buffer */
    }
    }
    else { /* We're the child */
    /* Connect pipes */
    dup2(in_fd[0], 0); /* Connect stdin to the in side of in_fd */
    close(in_fd[1]); /* Close out side of in_fd */
    dup2(out_fd[1], 1); /* Connect stdout to the out side of out_fd */
    close(out_fd[0]); /* Close in side of out_fd */
    
    /* Execute the external process */
    if(execl("add", "add", NULL) < 0) {
        perror("Execl error.\n");
        exit(1);
    }
    }

    return 0;
}

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 02-05 01:32
Je verwart sowieso fgets/fputs met read/write; de eerste werkt met zero-terminated strings en de andere met arrays van een vaste grootte. De fgets()-call leest een regel van maximaal 31 karakters, maar de write call daarna schrijft altijd 32 karakters, hoeveel karakters je ook ingelezen had. Dan kun je die buffers wel clearen, maar dan stuur je nog steeds allemaal loze 0-karakters door.

Dat kun je verhelpen door ofwel de file descriptors te wrappen, of te zorgen dat je write call evenveel karakters stuurt als de string lang is, door sizeof(buff) te vervangen door strlen(buff). Bij het inlezen maak je trouwens een soortgelijke fout: read() leest een aantal karakters (in principe weer 32, behalve als het proces beëindigt is) maar het is niet gegarandeerd dat er dan een geldige string (mét afsluitend 0-karakter) in de buffer staat, waardoor printf() waarschijnlijk rommel print (vooral als er 32 karakters gelezen zijn, want dan zijn al de 0-karakters die je er met memset in hebt gezet overschreven).

Merk op dat als je dit op de goede manier doet je nooit een buffer hoeft leeg te maken. Maar.... volgens mij lost dat je probleem nog niet op, ik zal even debuggen. Het scheelt wel in de foute uitvoer, denk ik.

  • Onno
  • Registratie: Juni 1999
  • Niet online
Waar je precieze probleem ligt moet je zelf maar uitzoeken (gdb al geprobeerd?), maar ik zie zo wel een hele lijst slordigheidjes:
code:
1
    if (pipe(in_fd) && pipe(out_fd)) {

&& moet || zijn.
code:
1
    while (fgets(buff, sizeof(buff), stdin) != NULL) {

Als je input nou langer dan 31 tekens is mis je die.
code:
1
        write(in_fd[1], buff, sizeof(buff)); /* Write line to external process */

Je schrijft hier je hele buffer (of je nou 1 teken gelezen hebt of 32), dat is niet wat je "add" programma verwacht. Verder schrijf je geen \n, iets wat "add" juist wel verwacht.
code:
1
        memset(buff, 0, sizeof(buff)); /* Clear the buffer */

Overbodig.
code:
1
2
        read(out_fd[0], buff, sizeof(buff)); /* Read from the external process */
        printf("%s", buff); /* Echo buffer */

De gelezen data is niet gegarandeerd 0-terminated, dus die kun je niet zomaar printf'en.
code:
1
        memset(buff, 0, sizeof(buff)); /* Clear the buffer */

Overbodig.
code:
1
2
3
4
5
    /* Connect pipes */
    dup2(in_fd[0], 0); /* Connect stdin to the in side of in_fd */
    close(in_fd[1]); /* Close out side of in_fd */
    dup2(out_fd[1], 1); /* Connect stdout to the out side of out_fd */
    close(out_fd[0]); /* Close in side of out_fd */

Waarom sluit je de andere kant, maar niet de dubbelen aan deze kant? in_fd[0] en out_fd[1] mogen ook dicht.
code:
1
2
    /* Execute the external process */
    if(execl("add", "add", NULL) < 0) {

Hier ga je er vanuit dat . in je PATH staat? Gevaarlijke aanname..

  • Onno
  • Registratie: Juni 1999
  • Niet online
Soultaker schreef op maandag 04 juli 2005 @ 21:36:
Bij het inlezen maak je trouwens een soortgelijke fout: read() leest een aantal karakters (in principe weer 32, behalve als het proces beëindigd is)
Dat klopt niet helemaal. read() leest daar maximaal 32 tekens in, maar zodra er uberhaupt data beschikbaar is (of dat nou 1 teken is of 32) keert ie terug, hij blijft niet wachten tot er 32 tekens zijn.

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 02-05 01:32
Onno schreef op maandag 04 juli 2005 @ 21:42:
Waar je precieze probleem ligt moet je zelf maar uitzoeken (gdb al geprobeerd?), maar ik zie zo wel een hele lijst slordigheidjes:
code:
1
    if (pipe(in_fd) && pipe(out_fd)) {

&& moet || zijn.
Die had ik ook net gevonden; en da's wel een erg belangrijke want daardoor bestond een van de twee pipes helemaal niet! De reden dat het leek te werken was dat het child process de oude descriptors van het parent process gebruikte als invoer/uitvoer.
Je schrijft hier je hele buffer (of je nou 1 teken gelezen hebt of 32), dat is niet wat je "add" programma verwacht. Verder schrijf je geen \n, iets wat "add" juist wel verwacht.
Jawel, fgets() laat het newline-character uit de invoer in de string staan. Die wordt dus wel doorgegeven.
Onno schreef op maandag 04 juli 2005 @ 21:48:
Dat klopt niet helemaal. read() leest daar maximaal 32 tekens in, maar zodra er uberhaupt data beschikbaar is (of dat nou 1 teken is of 32) keert ie terug, hij blijft niet wachten tot er 32 tekens zijn.
Ah, dat klopt wel, maar dat maakt voor het punt niet zoveel uit.

Als we toch aan het zeuren zijn mag de code trouwens ook wel fatsoenlijk geïndenteerd worden. ;)

[ Voor 29% gewijzigd door Soultaker op 04-07-2005 21:55 ]


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 02-05 01:32
De laatste fout zit trouwens in je 'add' implementatie: je moet na elke regel de uitvoer flushen met fflush(stdout). Normaal gesproken hoeft dat niet omdat uitvoer naar de terminal line buffered is (na elke regel wordt automatisch geflusht) maar een pipe is dat niet, en je hebt dat niet expliciet aangezet (met fcntl).

  • Onno
  • Registratie: Juni 1999
  • Niet online
Soultaker schreef op maandag 04 juli 2005 @ 21:53:
Jawel, fgets() laat het newline-character uit de invoer in de string staan. Die wordt dus wel doorgegeven.
Oh, dan was het zeker gets() die dat niet deed.. hoe dan ook, akelige functies om te gebruiken. :)

  • froggie
  • Registratie: November 2001
  • Laatst online: 20-11-2024
code:
1
if (pipe(in_fd) && pipe(out_fd)) {

"Kwakkeloos" code overnemen van iemand anders is nooit goed blijkt hier maar weer. Het moet zelfs
code:
1
if (pipe(in_fd) < 0 || pipe(out_fd) < 0) {
zijn.
Waarom sluit je de andere kant, maar niet de dubbelen aan deze kant? in_fd[0] en out_fd[1] mogen ook dicht.
Ik had niet begrepen dat je alle pointers kunt sluiten (ik denk nog steeds dat een fd iets magisch is, maar het is natuurlijk alleen maar een getal 8)7). Maar door dup2 wordt stdin natuurlijk het eindpunt van je pipe (en stdout het begin).
code:
1
2
    /* Execute the external process */
    if(execl("add", "add", NULL) < 0) {

Hier ga je er vanuit dat . in je PATH staat? Gevaarlijke aanname..
Eigenlijk ging ik er vanuit dat 'add' in dezelfde dir staat. Net echt netjes, dat weet ik, maar daar is het een proof of concept voor.
Als we toch aan het zeuren zijn mag de code trouwens ook wel fatsoenlijk geïndenteerd worden. ;)
M'n code is netjes geïndenteerd. Dit forum maakt er gewoon een rommeltje van, heus! :+

Bedankt iedereen voor de input. Dit is c&c waar ik wat aan heb. Scripttalen en programmeer colleges maken je slordig, dat blijkt maar weer. Ik ga morgen nog eens serieus over de code om de slordigheden met read/write/fgets te verbeteren.

[ Voor 15% gewijzigd door froggie op 04-07-2005 23:17 ]

Pagina: 1