Read-Write Pipe() Communication in R

Read-write pipe() communication in R

A long time ago I also used two-way pipes in Octave so, yes, this would be nice to have. But a perusal of help(pipe) does not suggest that this is support. You get read or write, but seemingly not both.

But maybe you can cheat. Open a pipe to write into an app which you can call with a stdout redirection to a file ... and then keep reading that file. Could be a mess due to non-flushed buffers though.

Using pipes to communicate between two programs

The first thing to do is ensure you are closing all unnecessary file descriptors in each process.

This means anything relating to the lexcmp child process should be closed in the lencmp child process, and vice versa. The parent needs to close the read ends of both "TO" pipes, and the write end of both "FROM" pipes.

Each of these closures should happen exactly once, where appropriate.

As is, in the parent, you are calling close(pfd[index][0]);, close(pfd[2][1]);, and close(pfd[3][1]); in a loop.

After calling dup2, you should immediately close the first argument (the original pipe end). As is, in the the children, you are attempting to close them after execvp is called, which leads into the next issue...

If execvp succeeds, it NEVER returns, as it will completely replace the process image. Anything expected to run after it is really operating in a failure state. So

if(execvp(myargs[0], myargs) == -1)
{
perror("exec");
return -2;
}

could be written as

execvp(myargs[0], myargs)
perror("exec");
return -2;

to the same effect.


Aside: the large if .. else if .. else structure of main is a bit hard to read, and not needed since the body of each if statement results in the child processes being replaced, or exiting on error.


The next issues have to do with deadlocking, which most typically occurs when two intercommunicating processes attempt blocking reads from one another at the same time.

Your child processes expect input in a very specific way: 2 lines at a time, creating a pair of strings. The two write calls, in the form of,

write(pfd[index][1], strX, strlen(strX))

do not write any newlines, thus the children wait forever, never to send any data back, and the parent will wait forever, never receiving any data.


Aside: mygets is severely flawed, in a few ways, including being unable to detect EOF or I/O failures (this function is a SIGSEGV in waiting). One of the more obnoxious failings is that the comment here

if (buf[strlen(buf) - 1] == 10) /* trim \r */

is just plain wrong. ASCII decimal 10 is '\n', the line feed, or newline character. '\r', or carriage return, would be decimal 13. This is why using character constants 'A' instead of integer constants 65 is highly encouraged.

The side effect here, generally speaking, is your strings are stripped of a trailing newline character.


The second deadlock occurs when you go to read the child process' response.

Firstly, this example

char rbuf[1];                    
while(read(pfd[N][0], &rbuf, 1) > 0)
{
write(STDOUT_FILENO, &rbuf, 1);
}

is malformed. Either remove the & operators, OR change char rbuf[1]; to char rbuf;. Fixing this, and the newline problem from above, will result in the parent process reading data back from the child.

The problem then becomes that a while (read(...) > 0) loop will continuously block execution of the calling process, waiting for more data to be available.

This means another deadlock when the child process has already moved on to trying to read another pair of lines from the parent process.

A simple solution is to attempt a single, reasonably large read in the parent, and rely on the behaviour of fflush(stdout); in the child to flush the pipe to the parent.

Here is a functional -ish example, with minimal changes made. This program still has some problems, such as: the parent process generally has no idea of the status of the child processes, and relying signal propagation (^C) from the terminal to end the process tree gracefully, since loopcmp does not handle EOF (should really discuss this with whoever wrote loopcmp.c / mygets).

Additionally, mygeti is flawed as well, as an invalid input cannot be distinguished from a valid input of 0. It also does not handle EOF, or prevent signed integer overflow.

Some more robust abstraction (functions and structures) around creating child processes would help a lot to clean this up further.

This should help you progress, though.

#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define LINELEN (80)

char *mygets(char *buf, int len);
int mygeti();

void close_pipe(int fd[2])
{
close(fd[0]);
close(fd[1]);
}

int main(void)
{
char *cmpstr[] = {"lexcmp", "lencmp"};
int veclen = sizeof(cmpstr)/sizeof(char *);
char str1[LINELEN + 1];
char str2[LINELEN + 1];
int index;
int pid[2];
int pfd[4][2];

/* pfd[0] is TO lexcmp
* pfd[1] is TO lencmp
* pfd[2] is FROM lexcmp
* pfd[3] is FROM lencmp
*/

for(int i = 0; i < 4; i++)
if(pipe(pfd[i]) < 0) {
perror("pipe");
return -2;
}

pid[0] = fork();

if (pid[0] == 0) {
/* child lexcmp */
close_pipe(pfd[1]);
close_pipe(pfd[3]);

close(pfd[0][1]);
close(pfd[2][0]);

dup2(pfd[0][0], STDIN_FILENO);
dup2(pfd[2][1], STDOUT_FILENO);
close(pfd[0][0]);
close(pfd[2][1]);

char *args[] = { "./loopcmp", "lexcmp", NULL };
execvp(*args, args);
perror("exec");
return -2; /* This only returns from the child */

}

pid[1] = fork();

if (pid[1] == 0) {
/* child lencmp */
close_pipe(pfd[0]);
close_pipe(pfd[2]);

close(pfd[1][1]);
close(pfd[3][0]);

dup2(pfd[1][0], STDIN_FILENO);
dup2(pfd[3][1], STDOUT_FILENO);

close(pfd[1][0]);
close(pfd[3][1]);

char *args[] = { "./loopcmp", "lencmp", NULL };
execvp(*args, args);
perror("exec");
return -2; /* This only returns from the child */
}

/* parent */

close(pfd[0][0]);
close(pfd[1][0]);
close(pfd[2][1]);
close(pfd[3][1]);

while (1) {
printf("Please enter first string: ");
if (mygets(str1, LINELEN) == NULL)
break;
printf("Please enter second string: ");
if (mygets(str2, LINELEN) == NULL)
break;

do {
printf("Please choose (");
for (int i=0 ; i < veclen ; i++)
printf(" [%d] %s", i, cmpstr[i]);
printf(" ): ");
index = mygeti();
} while ((index < 0) || (index >= veclen));

if (0 >= dprintf(pfd[index][1], "%s\n%s\n", str1, str2)) {
fprintf(stderr, "Failed to write to child %d\n", index);
perror("dprintf");
return -2;
}

char buf[64];
ssize_t bytes = read(pfd[index + 2][0], buf, sizeof buf - 1);

if (-1 == bytes) {
perror("read from child");
return -2;
}

buf[bytes] = 0;
printf("Result: %s", buf);
}
}

char *mygets(char *buf, int len)
{
char *retval;

retval = fgets(buf, len, stdin);
buf[len] = '\0';
if (buf[strlen(buf) - 1] == 10) /* trim \r */
buf[strlen(buf) - 1] = '\0';
else if (retval)
while (getchar() != '\n'); /* get to eol */

return retval;
}

int mygeti()
{
int ch;
int retval=0;

while(isspace(ch=getchar()));
while(isdigit(ch))
{
retval = retval * 10 + ch - '0';
ch = getchar();
}
while (ch != '\n')
ch = getchar();
return retval;
}

Note the use of dprintf. If not available for whatever reason, just make sure to write a single newline after each string.


Final aside: with the way fgets works, the + 1 to the string buffer sizes are rather meaningless (although they are indirectly required here due to mygets performing its own, poorly designed buf[len] = '\0'). fgets writes at most len - 1 non-null bytes, always leaving room for the null terminating byte, which it places.

Using two pipes to communicate between parent process and child process

There were several problems, and your diagnostic messages were not guaranteed to appear. Make sure you end your messages with newlines.

  1. You only created one pipe because you used && instead of ||.
  2. You closed the pipes 'for the parent' before you'd created the child (also noted by kaylum in a comment).

There are multiple other cleanups in the code below. The code (still) does not ensure that the write-to-pipe operations succeed (they were failing before). It does ensure that the strings read from the pipes are not longer than the buffers in which they are placed; it doesn't ensure there's enough space to append the extra information in the child. The code shown waits for any child processes to complete before exiting. The child executes the wait() call but it immediately fails (and the child doesn't print anything) and it exits. The parent waits for the child to complete and reports on it doing so before exiting.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
if (argc == 1)
{
fprintf(stderr, "Usage: %s <string>\n", argv[0]);
return EXIT_FAILURE;
}

// create two pipes:
// - parent_fds used by parent to write to child
// - child_fds used by child to write to parent
int parent_fds[2], child_fds[2];

// read:[0] - write:[1]
if (pipe(parent_fds) != 0 || pipe(child_fds) != 0) /* || not && */
{
fprintf(stderr, "pipes failed!\n");
return EXIT_FAILURE;
}

// fork() child process
int child = fork();

if (child < 0)
{
fprintf(stderr, "fork failed!");
return EXIT_FAILURE;
}
else if (child == 0)
{
printf("%d: I reached the child :)\n", (int)getpid());
// close unwanted pipe ends by child
close(child_fds[0]);
close(parent_fds[1]);

// read from parent pipe
char fromParent[100];
int n = read(parent_fds[0], fromParent, sizeof(fromParent) - 1);
fromParent[n] = '\0';
printf("%d: Child: read from parent pipe '%s'\n", (int)getpid(), fromParent);
close(parent_fds[0]);

// Append to what was read in
strcat(fromParent, " (added this.)");

write(child_fds[1], fromParent, strlen(fromParent));
close(child_fds[1]);
printf("%d: Child: writing to pipe - '%s'\n", (int)getpid(), fromParent);
}
else
{
// close unwanted pipe ends by parent
close(parent_fds[0]);
close(child_fds[1]);

// write from terminal to parent pipe FOR child to read
printf("%d: Parent: writing to pipe '%s'\n", (int)getpid(), argv[1]);
write(parent_fds[1], argv[1], strlen(argv[1]));
close(parent_fds[1]);
// read from child pipe
char fromChild[100];
int n = read(child_fds[0], fromChild, sizeof(fromChild) - 1);
fromChild[n] = '\0';
close(child_fds[0]);
printf("%d: Parent: read from pipe - '%s'\n", (int)getpid(), fromChild);
}

int corpse;
int status;
while ((corpse = wait(&status)) > 0)
printf("%d: child PID %d exited with status 0x%.4X\n", (int)getpid(), corpse, status);

return EXIT_SUCCESS;
}

Sample output (source pipe43.c, program pipe43):

$ pipe43 'Nobody expects the Spanish Inquisition!'
84543: Parent: writing to pipe 'Nobody expects the Spanish Inquisition!'
84544: I reached the child :)
84544: Child: read from parent pipe 'Nobody expects the Spanish Inquisition!'
84544: Child: writing to pipe - 'Nobody expects the Spanish Inquisition! (added this.)'
84543: Parent: read from pipe - 'Nobody expects the Spanish Inquisition! (added this.)'
84543: child PID 84544 exited with status 0x0000
$

2 way pipe communication. hang

The C I/O streams are normally buffered, that means that when you do e.g. fprintf then what you print to the stream isn't actually written to the file, it's written into an in-memory buffer. When the buffer is full then the data in it is actually written to the file. The fflush function flushes the buffer, i.e. it takes what's in the buffer and writes it immediately to the file.

The problem here is that when you do fdopen then the file stream is created with full buffering (unlike the line buffering of e.g. stdout) so the buffer really has to be filled for it to be actually written. By explicitly force it to be written, the other end of the pipe can read it.



Related Topics



Leave a reply



Submit