Writing a Unix Shell – Part I

What is a shell?

A lot has been written about this, so I will not go into too much detail about the definition. However, in one line –
A shell is an interface that allows you to interact with the kernel of an operating system.

How does a shell work?

A shell parses commands entered by the user and executes this. To be able to do this, the workflow of the shell will look like this:

  1. Startup the shell
  2. Wait for user input
  3. Parse user input
  4. Execute the command and return the result
  5. Go back to 2.

There is one important piece to all of this though: processes. The shell is the parent process. This is the main thread of our program which is waiting for user input. However, we cannot execute the command in the main thread itself, because of the following reasons:

  1. An erroneous command will cause the entire shell to stop working. We want to avoid this.
  2. Independent commands should have their own process blocks. This is known as isolation and falls under fault tolerance.

Fork

To be able to avoid this, we use the system call fork. I thought I understood fork until I wrote about four lines of code using it.

fork creates a copy of the current process. The copy is known as the child and each process in a system has a unique process id (pid) associated to it. Let’s take a look at the following piece of code:

fork.c

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

int
main() {
    pid_t child_pid = fork();
        
    // The child process
    if (child_pid == 0) {
        printf("### Child ###\nCurrent PID: %d and Child PID: %d\n",
               getpid(), child_pid);
    } else {
        printf("### Parent ###\nCurrent PID: %d and Child PID: %d\n",
               getpid(), child_pid);
    }
 
    return 0;
}

The fork system call returns twice, once for each process. This sounds counter intuitive at first. But let’s take a look at what goes underneath the hood.

  1. By invoking fork we are creating a new branch in our program. This is not the same as a traditional if-else branch. fork creates a copy of the current process and creates a new one out of it. The resulting system call returns the process id of the child process.
  2. Immediately after the fork call has succeeded, both the child and the parent process (the main thread of our code) are running simultaneously.

To give you a better idea of the program flow, take a look at this diagram:

The fork() creates a new child process, but at the same time, execution of the parent process is not halted. The child process begins and finishes its execution independent of the parent and vice versa.

A quick note before we proceed further, the getpid system call returns the current process id.

If you compile and execute the code, you’d get an output similar to the following:

### Parent ###
Current PID: 85247 and Child PID: 85248
### Child ###
Current PID: 85248 and Child PID: 0

In the block under ### Parent ###, the current process ID is 85247 and that of the child is 85248. Note that the pid of the child is greater than that of the parent, implying that the child was created after the parent. (Update: As someone correctly pointed out on Hacker News this isn’t guaranteed, although the more likely scenario. Reason being that, the OS could recycle older unused process ids.

In the block under ### Child ###, the current process ID is 85248, which is the same as the pid of the child in the previous block. However, the child pid here is 0.

Actual numbers would vary on each execution.

You’d be forgiven for thinking about how can child_pid assume two different values in the same execution flow, when we have clearly assigned a value to child_pid once on line 9. However, recall that invoking fork creates a new process, which is identical to the current one. As a result, in the parent process, child_pid is the actual value of the child process that just got created, and the child process itself has no child of its own, as a result of which the value of child_pid is 0.

Thus, the if-else block we have defined from lines 12 to 16 is required to control what code to execute in the child, vs the parent. When child_pid is 0, the code block will be executed under the child process, while the else block will be executed under the parent process instead. The order in which the blocks will be executed cannot be determined, and depends on the operating system’s scheduler.

Publicado por djalmabina

WordPress and Google Maps developer,Blogger, Article Writer,Freelance Writer. Please look my recent Blogs: https://geatland.wordpress.com/

%d blogueiros gostam disto: