OneRaynyDay OneRaynyDay - 9 months ago 50
C Question

c - spawned a bash subshell. Shell died but pipe not broken?


I'm trying to pipe contents from the main routine to a execvp'd bash shell. I'm encountering a problem where when I write "exit" into the subshell, it doesn't tell me that the pipe is really broken. It should be though - right? The process died and thus the pipe fd should also return an EOF or a SIGPIPE. It doesn't, however, and just keeps on reading/writing like normal.


The code is attached here:

* Includes:
* ioctl - useless(?)
* termios, tcsetattr, tcgetattr - are for setting the
* noncanonical, character-at-a-time terminal.
* fork, exec - creating the child process for part 2.
* pthread, pipe - creating the pipe process to communicate
* with the child shell.
* kill - to exit the process
* atexit - does some cleanups. Used in termios, tcsetattr,
* tcgetattr.
#include <sys/ioctl.h> // ioctl
#include <termios.h> // termios, tcsetattr, tcgetattr
#include <unistd.h> // fork, exec, pipe
#include <sys/wait.h> // waitpid
#include <pthread.h> // pthread
#include <signal.h> // kill
#include <stdlib.h> // atexit
#include <stdio.h> // fprintf and other utility functions
#include <getopt.h> // getopt
pid_t pid;

static const int BUFFER_SIZE = 16;
static const int STDIN_FD = 0;
static const int STDOUT_FD = 1;
static const int STDERR_FD = 2;

// these attributes are reverted to later
struct termios saved_attributes;
// to revert the saved attributes
reset_input_mode (void) {
tcsetattr (STDIN_FILENO, TCSANOW, &saved_attributes);

// to set the input mode to correct non-canonical mode.
set_input_mode (void) {
struct termios tattr;

/* Make sure stdin is a terminal. */
if (!isatty (STDIN_FILENO))
fprintf (stderr, "Not a terminal.\n");

/* Save the terminal attributes so we can restore them later. */
tcgetattr (STDIN_FILENO, &saved_attributes);
atexit (reset_input_mode);

/* Set the funny terminal modes. */
tcgetattr (STDIN_FILENO, &tattr);
tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
tattr.c_cc[VMIN] = 1;
tattr.c_cc[VTIME] = 0;
tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);

// pthread 1 will read from pipe_fd[0], which
// is really the child's pipe_fd[1](stdout).
// It then prints out the contents.
void* thread_read(void* arg){
int* pipe_fd = ((int *) arg);
int read_fd = pipe_fd[0];
int write_fd = pipe_fd[1];
char c;
int bytes_read = read(read_fd, &c, 1);
if(bytes_read > 0){
fprintf(stdout, "The read broke.");

// pthread 2 will write to child_pipe_fd[1], which
// is really the child's stdin.
// but in addition to writing to child_pipe_fd[1],
// we must also print to stdout what our
// argument was into the terminal. (so pthread 2
// does extra).
void* thread_write(void* arg){
int* pipe_args = ((int *) arg);
int child_read_fd = pipe_args[0];
int child_write_fd = pipe_args[1];
int parent_read_fd = pipe_args[2];
int parent_write_fd = pipe_args[3];
char c;
while(1) {
int bytes_read = read(STDIN_FD, &c, 1);
write(child_write_fd, &c, bytes_read);
if(c == 0x04){
// If an EOF has been detected, then
// we need to close the pipes.
kill(pid, SIGHUP);

int main(int argc, char* argv[]) {
* Getopt process here for --shell
int child_pipe_fd[2];
int parent_pipe_fd[2];

// We need to spawn a subshell.
pid = fork();
if(pid < 0){
perror("Forking was unsuccessful. Exiting");
else if(pid == 0){ // is the child.
// We dup the fd and close the pipe.

close(0); // close stdin. child's pipe should read.
dup(child_pipe_fd[0]); // pipe_fd[0] is the read. Make read the stdin.

close(1); // close stdout
dup(parent_pipe_fd[1]); // pipe_fd[1] is the write. Make write the stdout.

char* BASH[] = {"/bin/bash", NULL};
execvp(BASH[0], BASH);
else{ // is the parent
// We dup the fd and close the pipe.
// create 2 pthreads.
// pthread 1 will read from pipe_fd[0], which
// is really the child's pipe_fd[1](stdout).
// It then prints out the contents.
// pthread 2 will write to pipe_fd[1], which
// is really the child's pipe_fd[0](stdin)
// but in addition to writing to pipe_fd[1],
// we must also print to stdout what our
// argument was into the terminal. (so pthread 2
// does extra).
// We also need to take care of signal handling:
signal(SIGINT, sigint_handler);
/*signal(SIGPIPE, sigpipe_handler);*/
int write_args[] = {child_pipe_fd[0], child_pipe_fd[1],
parent_pipe_fd[0], parent_pipe_fd[1]};

pthread_t t[2];
pthread_create(t, NULL, thread_read, parent_pipe_fd);
pthread_create(t+1, NULL, thread_write, write_args);

pthread_join(t[0], NULL);
pthread_join(t[1], NULL);

int status;
if (waitpid(pid, &status, 0) == -1) {
perror("Waiting for child failed.");

printf("Subshell exited with the error code %d", status);

return 0;

The program basically pipes inputs from the terminal into the subshell and tries to execute them and return the outputs. To write to the pipe, I have a pthread that writes the stdin inputs into the subshell. To read to the pipe, I have a pthread that reads the pipe to the parent. To detect the broken pipe via the subshell dying(calling exit), I detect the EOF character from the read thread.

My attempts

I added a check for the 0x04 character(EOF), I checked for
read_bytes == 0
read_bytes < 0
. It seems that it never gets the memo unless I explicitly close the pipes on the writing end. It only meets the EOF character if I send the character ^D(which, in my code, handles via closing all pipes of the child & parent).

Any comments would be appreciated! Thank you.


Your parent process is holding copies of the child's file descriptors. Thus, even after the child has exited, those FDs are still open -- so the other ends of those pipelines remain open as well, preventing any SIGPIPE.

Modify your code as follows:

else {
  // pid >0; this is the parent
  close(child_pipe_fd[0]);  // ADD THIS LINE
  close(parent_pipe_fd[1]); // ADD THIS LINE