Page 1 of 1

FIXED: Fork() and redirecting STDOUT/STDERR to a pipe

Posted: Tue Nov 11, 2008 4:24 am
by Brendan
Hi,

Ok, I'm stumped.

I'm writing a multi-threaded utility in C (GCC on Linux with POSIX). The idea is to fork a new process to run an external command, and redirect the new process' STDOUT and STDERR to a pipe; where the original/main process does other work then reads anything from the pipe and buffers it (and then displays it even later).

It all works perfectly, except...

When a child process is running; if the original/main process sends something to it's STDOUT, then it'll be sent where it should (to the shell/display, like you'd expect) but it'll also be received from the child process' pipe as if the child process sent it. Nothing changes the original/main process' STDOUT (it's only the child process that's messed with), and I'm having trouble figuring out how the original/main process' STDOUT (which is mostly a normal file descriptor) can point to 2 different streams at the same time.

Note: A previous version of the utility just used the "popen()" function, which is almost exactly what I need, except the "popen()" function only redirects STDOUT and this causes problems because the child sends some text to STDERR (which was displayed at the wrong time because it wasn't redirected/buffered, and makes a mess of the main process' output).

My code does the following:
  • "pipe()" to create the pipe
  • "fork()" to create the child process
  • [child] "close()" to close the input side of it's copy of the pipe
  • [child]"dup2()" to replace STDOUT and STDERR with the output side of it's copy of the pipe
  • [child]"system()" to execute the shell command
  • [child] "close()" to close the output side of it's copy of the pipe
  • [child]"exit()"
  • [main] "close()" to close the output side of it's copy of the pipe
  • [main] "fgetc()" in a loop to receive all the data from the pipe (from the child), until EOF (received data placed in a buffer)
  • [main] "waitpid()" to make sure the child process has done "exit()"
  • [main] "close()" to close the input side of it's copy of the pipe
  • [main] Eventually, send the child's output from the buffer to STDOUT
Am I doing something wrong, or did I forget something, or is POSIX threads messing with something, or...?

If I can't find a way to fix this, then I'll need to protect it all with a mutex or something, so that the original/main process can't do "printf()" while any child process is running (and even then, I'm not too sure if the output from one child processes interferes with the output from other child processes). Of course the utility spawns many threads and forks many processes because everything is I/O bound and I'm trying to keep a quad core busy - slapping a huge mutex around the problem is the last thing I want to do...


Thanks,

Brendan

Re: Fork() and redirecting STDOUT/STDERR to a pipe

Posted: Tue Nov 11, 2008 5:38 am
by Brendan
Hi,

Um, guess what I just found out...

I've got lots of threads reading from streams and writing to streams, all going (hopefully) flat out on 4 cores, while at the same time one (or more) of the threads calls "fork()"; and some idiot decided that the "fork()" function should clone buffered output, including buffered STDOUT output and any buffered output to normal files. :(

I'm guessing this might be what causes my STDOUT problem (original/main process has buffered output that's cloned, and then sent by the original/main process and the child process). I tried doing a "fflush(stdout);" before the "fork()" but that didn't seem to make much difference (I assumed it'd help, even though I know there's a race condition).

Hmph. Now I'm supposed to flush every file used by any thread and make all threads block until "fork()" has forked everything?

I'm sure I could fix the problem if I had a time machine and a baseball bat...

Maybe I should be using "vfork()" and "exec()"?


Thanks,

Brendan

Re: Fork() and redirecting STDOUT/STDERR to a pipe

Posted: Tue Nov 11, 2008 5:59 am
by DeletedAccount
Hi,
I acutally does not have the experience to correct you but a few questions , system() i guess forks 1 more process ?? doesnt it ? and using system() is not a great way to do anything .I guess if its a syncronization problem then you might have to use sem_post() and sem_wait() to get round the syncronization issues .Its a long while since I have worked with nixes.vfork() shuns portablity i think its not the right way to go either .

Sorry if none of my comments make sense.

Regards
Sandeep

Re: Fork() and redirecting STDOUT/STDERR to a pipe

Posted: Tue Nov 11, 2008 6:36 am
by Brendan
Hi,
SandeepMathew wrote:I acutally does not have the experience to correct you but a few questions , system() i guess forks 1 more process ?? doesnt it ? and using system() is not a great way to anything .
You're right - "system()" does fork another process, so what I'm doing is forking a process that forks a process, which *is* silly.

But, the "system()" function blocks (IMHO it's like "fork(); exec(); waitpid()" all combined) so my process (or just one of my threads?) can't do any other work while the child process is running; and there's also no way to redirect STDOUT and STDERR if I use "system()" alone.

The "popen()" function is closer to what I want, but I can't redirect STDERR for that.

Then there's the "exec()" functions - I don't know a portable way to start a shell (which shell?), and don't want to implement code to use the "PATH" environment variable to find the shell executable. Maybe I could skip the shell and "exec()" the executable directly (instead of getting the shell to process command line arguments and find the executable), but I'd still need to use the "PATH" environment variable to find the executable.
SandeepMathew wrote:I guess if its a syncronization problem then you might have to use sem_post() and sem_wait() to get round the syncronization issues .Its a long while since I have worked with nixes.vfork() shuns portablity i think its not the right way to go either .
If the OS/kernel doesn't support "vfork()", then the "vfork()" library function uses "fork()" instead. This means that attempting to use "vfork()" won't really fix my problem on all OSs (but might help on Linux).

Note: I've got plenty of "pthread_mutex_lock()" and "pthread_cond_wait()" stuff in this thing already - synchronization primitives aren't really a problem, but doing a "fflush()" on all files and blocking all threads while I "fork()" would be a serious problem for scalability. Imagine 30 people living in a house, where everyone has to stop anything they're doing for a minute when anybody goes to the toilet). ;)
SandeepMathew wrote:Sorry if none of my comments make sense.
All your comments make perfect sense... :)

It does make me wonder if I can find the source code for the GLIBC library's "popen()" function call...


Thanks,

Brendan

Re: Fork() and redirecting STDOUT/STDERR to a pipe

Posted: Tue Nov 11, 2008 7:02 am
by JamesM
Hi,

A couple of points, which may or may not be related to the cause of your problem.

As mentioned by SandeepMathew, system() does fork, I believe. I personally would prefer calling execl("/usr/bin/bash", {"bash", "-c", my_shell_command}, env); from my child process. (Or, better, instead of /usr/bin/bash use "getenv("SHELL");").

Also, check whether the process that you're running opens "/dev/tty", because then it would be able to write directly to your terminal.

But, the most likely thing that's incorrect is that you're not rerouting STDERR. [edit] Woops, wait, yes you are! - /ignore.[/edit]

Cheers,

James

Re: Fork() and redirecting STDOUT/STDERR to a pipe

Posted: Tue Nov 11, 2008 9:04 am
by Brendan
Hi,

Huge thanks to JamesM and SandeepMathew!

I got rid of my "system()" call and replaced it with "execl(shellCommand, "sh", "-c", processData->commandString, NULL);", made a few other adjustments to the return status handling, and replaced all "exit()" calls in the child process with "_exit()" calls; and now it works perfectly! :)

I think I even understand what was going wrong - "exec()" kills everything from the parent process, but with the way I was using "fork()" then "system()" only the child's child ended up calling "exec()", which left one process that didn't get rid of anything from the parent process (including duplicated buffered I/O).
JamesM wrote:(Or, better, instead of /usr/bin/bash use "getenv("SHELL");").
I didn't even consider environment variables... :oops:


Thanks again!

Brendan :D