Parent-Child Processes

Diagram of the operations taking place

In this post, we’ll follow what happens when in the shell, the user presses the keys l,s, ,-,l consecutively.

In the beginning there are the Kernel and the Shell. The shell is an application that manages the user-system interactions. As all applications, it runs in its own process environment, and it can communicate with the kernel. It is a program that provides an interface to invoke or launch commands. In our case, the command is ls -l.

Let’s follow what happens after the user presses enter.

1 — The shell was actually waiting for a command. When the user types in the command, the shell makes a system call read to the kernel. System calls are the way for the shell to communicate with the kernel. This one means that at this point, the shell requires the kernel to drop what it was doing to read the input from the user. It will stop reading when the user presses return. We are at step (1) in the graphic above.

As a note, when the shell requires the kernel to read a command, it needs some space to store it. In C, we use malloc. At the start of the program, malloc is granted by the kernel a certain amount of free memory to manage. So malloc usually works as a function, and as such stays inside the process environment. However, it may run out. In those cases, it makes a system call to get more memory. This is showed in (1bis).

2 — After the shell receives the command and stores it, it will parse it around the spaces. So in our example it has ls and -l, stored in an array {"ls", "-l"} of strings. This is step (2). As the shell will need to store the line in a new way, it will use malloc again, free as well, and so there might be some system calls involved.

3 — The shell will analyze the tokens in turn. It will look for operators such as < or && that have a special meaning to redirect or control the flow of a command. There are none here. The shell will then look for any special character to expand on, for example ", $, *, or comments #. They all require a special handling. There are none here. This is step (3), it all takes place in the shell environment.

4 — The shell will focus on the first term of the array, ls . This token is considered to be a command name.

First things first, this name could be an alias for another command. It is possible in a shell, using the alias command or writing in the correct files to make aliases for commands. For example, when compiling with all those flags gcc4.8.4 -Wall -Werror -Wextra -pedantic, it gets tiring to write all that all the time. So we can set up an alias like that alias gccw=gcc4.... And later on we can write gccw instead of the full text and the shell reverses the process. This requires the shell to store all those commands in a dedicated structure. This is the first part of step (4), look if ls is an alias. We will assume it is not and move on.

Commands here can be of 2 kinds: built-in commands or normal programs. The shell starts by looking for the name ls in the structure that holds its built-in functions. It will loop through it, ready to launch the corresponding function in the shell environment, but chances are it will find nothing. This was the second part of step (4), which took place entirely in the current environment.

5 — The shell will now look for the command in the PATH variable. The PATH is variable part of the environment granted to the shell when it starts. It is an array of variables, one being the PATH. The PATH holds in a colon separated format all the directories the shell should look into if it is searching for an executable. That requires a system call, either with access or stat to loop through those directories to find a file called ls . It should find ls in /bin directory.

6 — The shell will launch the ls program from a child process. First, it will fork the current process, and that is a system call. That means the kernel will create an almost exact duplicate of the current process. However, we are not interested in running the same thing twice, so we will make a second system call execve to replace the current main with ls . I works like this: execve("/bin/ls", {"ls", "-l"}, envp); In that step, we combine all we learned from the previous steps, and add the current environment to ask the kernel to run ls . This is step (6) in the graphic above.

7 — There is another system call the shell will make. As the child process is running, we want the shell to stop, and wait for the child process to finish before moving on. So, we will use wait to require the shell, the parent process here to wait for the child process to change state. In our case, to terminate.

8 — After the child process is done, the shell will clean up ( free) , prompt again and wait for another command. In elaborated shells, the shell will read the PS1 environment variable to get the format of the prompt.

What happened when the kernel executed ls ? ls is a program to list directory contents. By default, if no directory is given, it will list the current working directory. -l is an option to control the format of the listing. It means “use a long listing format”.

Here is an example in my computer, it shows the difference between using a flag or not.

~/medium$ ls
greeting.c libgreeting.a libnumber.a number.c onelib
greeting.h libgreetnumber main.c number.o ordergn
greeting.o libgreetnumber.a nol ofiles
~/medium$ ls -l
total 88
-rw-rw-r-- 1 anne anne 143 Nov 6 21:29 greeting.c
-rw-rw-r-- 1 anne anne 22 Nov 6 21:26 greeting.h
-rw-rw-r-- 1 anne anne 1624 Nov 6 21:40 greeting.o
-rw-rw-r-- 1 anne anne 1770 Nov 6 21:45 libgreeting.a
-rw-rw-r-- 1 anne anne 3084 Nov 6 21:58 libgreetnumber
-rw-rw-r-- 1 anne anne 3084 Nov 6 21:59 libgreetnumber.a
-rw-rw-r-- 1 anne anne 1388 Nov 6 21:44 libnumber.a
-rw-rw-r-- 1 anne anne 151 Nov 6 21:31 main.c
-rwxrwxr-x 1 anne anne 8744 Nov 6 22:00 nol
-rw-rw-r-- 1 anne anne 46 Nov 6 21:23 number.c
-rw-rw-r-- 1 anne anne 1240 Nov 6 21:40 number.o
-rwxrwxr-x 1 anne anne 8744 Nov 6 22:01 ofiles
-rwxrwxr-x 1 anne anne 8744 Nov 6 22:00 onelib
-rwxrwxr-x 1 anne anne 8744 Nov 6 21:53 ordergn
~/medium$