Shell Simulator

Child Process

In parallel programming, especially when we are focusing system running multiple processes at the same time, the child process becomes an important part of it.

Illustration of parent & child process (reference: cmu 15213 slide)

A child process is created using pid = fork() . When pid==3 , it means that we are still in the parent function (or, we can say at the parent branch) and the process ID of the parent process is 3. However, when pid=0 , we know that currently, we are in the child branch. So, now we can use this code in the same function to determine if we are running code for a child process or the parent process.

if ((pid = fork()) == 0){
  // Child
  child_process_function();
} else {
  // Parent
  parent_process_function();
}

Make Child Process to Execute Some Function

Now, we want to build a shell. Instead of letting my parent process deal with all the system functions like "usr/bin/sleep" or "/bin/ls", I would rather let it run the main logic. For example, the most important job it should do is to parse the input argument and determine what is the BUILDIN function and what is not, instead of doing all the functions that exist in the library! We want the child process to do it, and since the function has been well written and provided in the library, we don't have to write "usr/bin/sleep" or "/bin/ls" again. So now we are introducing execve().

if ((pid = fork()) == 0){
  // Child
  execve("bin/ls", argv, environ); 
  /* argv is the argument for function bin/ls */
}

Note: What is the environ? environ is a global variable in c. According to https://man7.org/linux/man-pages/man7/environ.7.html, it contains name of the log-in user, the path to the user log-in shell, and etc.

But, the child process does not reap itself, if no one reaps it, it becomes a zombie process until the parent process ends and is reaped by kernel code. You don't want your computer world to be full of zombies 😼.

Signals

Great! Then how to reap a child when I create one?

pid_t waitpid(pid_t pid, int *status_ptr, int options);
wait()

That's convenient. But what if I want to know why the state change? What is the cause of death? Answer: Status! Does that seem familiar? waitpid(-1, &status, NULL).

Option:

  • WCONTINUED
    Reports the status of any continued child processes as well as terminated ones. The WIFCONTINUED macro lets a process distinguish between a continued process and a terminated one.
  • WNOHANG
    Demands status information immediately. If status information is immediately available on an appropriate child process, waitpid() returns this information. Otherwise, waitpid() returns immediately with an error code, indicating that the information was not available. In other words, WNOHANG checks child processes without causing the caller to be suspended.
  • WUNTRACED
    Reports on stopped child processes as well as terminated ones. The WIFSTOPPED macro lets a process distinguish between a stopped process and a terminated one.

Now I know the cause of death. Can I do something according to the different causes? For example, if I want to let the child remain as a zombie if it is just stopped so that I can revive(SIGCONT) it later? And, I would like to delete the child from my list when it is dead(SIGINT or finished and exit). Use below to check!

  • WIFEXITED(*status_ptr)
    This macro evaluates to a nonzero (true) value if the child process ended normally (that is, if it returned from main(), or else called the exit() or uexit() function).
  • WEXITSTATUS(*status_ptr)
    When WIFEXITED() is nonzero, WEXITSTATUS() evaluates to the low-order 8 bits of the status argument that the child passed to the exit() or uexit() function, or the value the child process returned from main().
  • WIFSIGNALED(*status_ptr)
    This macro evaluates to a nonzero (true) value if the child process ended because of a signal that was not caught.
  • WTERMSIG(*status_ptr)
    When WIFSIGNALED() is nonzero, WTERMSIG() evaluates to the number of the signal that ended the child process.
  • WIFSTOPPED(*status_ptr)
    This macro evaluates to a nonzero (true) value if the child process is currently stopped. You should only use this macro after a waitpid() with the WUNTRACED option.
  • WSTOPSIG(*status_ptr)
    When WIFSTOPPED() is nonzero, WSTOPSIG() evaluates to the number of the signal that stopped the child.
  • WIFCONTINUED(*status_ptr)
    Special Behavior for XPG4.2:
     This macro evaluates to a nonzero (true) value if the child process has continued from a job control stop. You should only use this macro after a waitpid() with the WCONTINUED option.

As such, we can use below code inside the sigchild_handler to ask it to behave different way according to different status that cased sigchild to be signal.

        if (WIFSTOPPED(status)) {
            // Job is stopped by sigstp. 
            sio_printf("Job [%d] (%d) stopped by signal %d\n",
                       job_from_pid(pid), pid, WSTOPSIG(status));
        } else if (WIFSIGNALED(status)) {
            // job is terminated by sigint.
            sio_printf("Job [%d] (%d) terminated by signal %d\n",
                       job_from_pid(pid), pid, WTERMSIG(status));
        } else if (WIFEXITED(status)) {
            // Child is finished and exit normally
            // Delete the child from the job list
            delete_job(job_from_pid(pid)); 
        }

But forking and reaping are expensive! And so we are going to use Thread which will be another post content ;)

Signal Mask

As mentioned above, the child would send the signal to the parent when it exit. The default action of the parent is to ignore it. When the user uses Ctrl+c, the process would get a SIGINT signal and the process would be terminated. What if we don't want the process to terminate right now since a child of it is running in the background? What if we are reading a file and we want the process to terminate after finish reading?

We can block the signal until we finish what we want to do! When we finish, we can just unblock using sigprocmask to set the mask back to what it was. One thing that needs to be noticed is that when the signal, let's say SIGINT, is blocked, and during it was blocked, a user triggers SIGINT twice, so SIGINT is sent two times. After we unblock SIGINT, only one SIGINT will be received.

use sigprocmask to set which signal to block, and unblock it.

sigemptyset(&mask);
sigaddset(&mask, SIGINT);

/* Block SIGINT and save previous blocked set */
sigprocmask(SIG_BLOCK, &mask, &prev_mask);

...

/* Restore previous blocked set, unblocking SIGINT */
sigprocmask(SIG_SETMASK, &prev_mask, NULL);

note: It is not possible to block SIGKILL or SIGSTOP.

What if we want to wait until a signal is changed?

while (!pid)
    pause();

This is incorrect because if the signal is received between pid check and pause, then we are not able to see the signal change. sleep() also does not work because this takes too much time. So the correct way is:

while (!pid)
  sigsuspend(&prev_all);

sigsuspend(mask) temporarily replaces the signal mask of the calling thread with the mask given by mask and then suspends the thread until delivery of a signal whose action is to invoke a signal handler or to terminate a process. [source]

Input/Output Redirection

if (infile != NULL) {
    int fd_in = open(infile, O_RDONLY, 0);
    if (fd_in < 0) {
        sio_eprintf("%s: %s\n", infile, strerror(errno));
        exit(1);
    }
    dup2(fd_in, STDIN_FILENO);
    close(fd_in);
}
// if has output file
if (outfile != NULL) {
    int fd_out =
        open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd_out < 0) {
        sio_eprintf("%s: %s\n", outfile, strerror(errno));
        exit(1);
    }
    dup2(fd_out, STDOUT_FILENO);
    close(fd_out);
}

open(infile, O_RDONLY, 0) here meaning create a file descripter pointing to the the infile in read-only mode, since we are only need to read the file to get the content in it as input.

For dup2(old_fd, new_fd) we are making the new_fd points to the open file table entry that old_fd is pointing to.

(reference: cmu 15213 slide)

So we are making STDIN_FIENO to point to the open file table entry that fd_in is pointing to. To be more specific, the open file table for infile.

Same reason apply to the output section. By using dup2(fd_out, STDOUT_FILENO); we are making the standard out file points to fd_out which contains the output file we desired. Going back to fd_out = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0666) , the fd_out fild descriptor is pointing to open table entry that

  • O_WRONLY: open for writing only
  • O_CREAT: If pathname does not exist, create it as a regular file.
  • O_TRUNC: all the contents of the file will be deleted, but not the file itself. So you start fresh and write the content you want. condition: need to be a WRONLY file.

Simulator

In summary, we can utilize child process with execv function to let it executed background job, e.g. those add '&' at the end of the command. While for foreground job, we should use sigsuspend to hang the code until it finish executing. For input/output redirection, we use file descriptor to manipulate where the flow should go.