Operating Systems: Three Easy Pieces. Part 3: Process API (translation)

Introduction to Operating Systems

Hello, Habr! I would like to present to your attention a series of translation articles of some interesting literature in my opinion — OSTEP. This material dives deeply into the workings of Unix-like operating systems, specifically — working with processes, various schedulers, memory, and other similar components that make up a modern OS. You can view the original materials here here. Please note that the translation is done non-professionally (rather freely), but I hope I preserved the overall meaning.

Laboratory work on this subject can be found here:

Other parts:

You can also check out my channel on Telegram =)

Alarm! there's a lab for this lecture! check it out on GitHub

Process API

Let's consider an example of creating a process in a UNIX system. This occurs through two system calls fork() and exec().

The fork() call

Operating Systems: Three Easy Pieces. Part 3: Process API (translation)

Let's consider a program that makes a fork() call. The result of its execution will be as follows.

Operating Systems: Three Easy Pieces. Part 3: Process API (translation)

First, we enter the main() function and output a string to the screen. The string contains the process identifier which is originally called PID or process identifier. This identifier is used in UNIX to refer to a process. The following command will invoke fork(). At this moment, a nearly exact copy of the process is created. To the OS, it appears as if there are two copies of the same program running, which will subsequently exit the fork() function. The newly created child process (in relation to its parent process) will no longer execute from the main() function. It is important to remember that the child process is not an exact copy of the parent process, particularly as it has its own address space, its own registers, its own instruction pointer, and so forth. Therefore, the value returned to the caller of the fork() function will be different. Specifically, the parent process will receive the child's PID as a return value, while the child will receive a value of 0. Based on these return codes, processes can later be distinguished and each can be made to perform its own work. The execution of this program is not strictly defined. After splitting into two processes, the OS begins to monitor them and also schedule their work. In the case of execution on a single-core processor, one of the processes will continue to work, in this case, the parent, and then control will be passed to the child process. Upon resuming, the situation may turn out differently.

The wait() call

Operating Systems: Three Easy Pieces. Part 3: Process API (translation)

Let's consider the following program. In this program, due to the presence of the call wait() the parent process will always wait for the completion of the child process. In this case, we will get a strictly defined output of text to the screen.

Operating Systems: Three Easy Pieces. Part 3: Process API (translation)

The exec() call

Operating Systems: Three Easy Pieces. Part 3: Process API (translation)

Let's consider the call exec(). This system call is useful when we want to execute a completely different program. Here we will be invoking execvp() to launch the wc program, which is a word count program. What happens when exec() is called? The call takes the name of the executable file and some parameters as arguments. Then, the code and static data from this executable file are loaded, overwriting the current segment with code. Other memory areas, such as the stack and heap, are reinitialized. After that, the OS simply runs the program, passing it a set of arguments. Thus, we did not create a new process; we simply transformed the currently running program into another running program. After the exec() call, the child process has the impression that the original program was never actually launched.

This complexity in launching is completely normal for the Unix shell and allows this shell to execute code after the call fork(), but before the call exec(). An example of such code might be adjusting the shell environment to suit the needs of the program being launched, before its actual start.

Shell — is merely a user program. It shows you a prompt and waits for you to type something into it. In most cases, if you type the name of a program there, the shell will find its location, call the fork() method, and then to create a new process, call one of the exec() types and wait for it to complete using a wait() call. When the child process finishes, the shell will return from the wait() call and display the prompt again, waiting for the next command input.

The separation of fork() & exec() allows the shell to do the following, for example:
wc file > new_file.

In this example, the output of the wc program is redirected to a file. The way the shell achieves this is quite simple — when creating the child process before the call to exec(), the shell closes the standard output stream and opens the file new_file, thus, all output from the program that is later launched wc will be redirected to the file instead of the screen.

Unix pipes are implemented similarly, with the difference that they use the pipe() call. In this case, the output stream of the process will be connected to a pipe queue located in the kernel, which will also be connected to the input stream of another process.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster