Processes

"Understanding is the key to success with Linux."
Linux System Administrator's Guide

A process is an instance of an executing program. This essay will chisel out the fundamentals of processes, as they exist on Unix-like platforms.

Actors

Note:

There is a close relationship between a program and a process— Namely, a program is a file containing the instructions that the process carries out.

In Unix-like operating systems, processes are the actors of the system: Left to its own devices, the kernel does nothing. The system depends entirely upon processes to direct the kernel, and through the kernel, processes manipulate the machine.

The kernel is responsible for:

  1. Creating new processes
  2. Allocating hardware resources to processes
  3. Maintaining information about each

Per the Unix philosophy, each process has a narrowly defined role, which it is expected to do well. It might copy a file, display a window, provide audio services, or display the time. Many small programs are used together to accomplish a given task, thereby easing the maintenance and compounding the utility of each.

A given user may have more than one process acting on their behalf at a time, and there are usually hundreds of processes on the system at a time. Much like with files, the kernel maintains a large volume of information about each process on the machine.

Interacting with Files: Process Permissions

Every process is associated with some user, and we say that it is executing with that user's permissions. A process can freely manipulate files owned by its associated user, and files created by a process inherit its associated user.

Processes acting as user root are special: They can change any file on the system, without qualification. That is, they by-default transcend the file ownership scheme; we often use superuser to describe this user.

In practice, root is the administrative user. A typical use of superuser privileges is to mount a physical device to prepare it for system-wide use. Superuser privileges are typically acquired through sudo.

See Also:

Process Creation

and System Startup

At the end of its boot sequence, the kernel launches a single program, /sbin/init,[1] and in so doing creates the system's first process. While init schemes have evolved over the years, its purpose has not changed: It is a process with superuser permissions that acts as a manager of the system and trustee among processes.

Aside:

You can check your init program with

$ ps --pid 1[2]

init's first responsibility is to bring up userspace. It does this by launching a sequence of programs, most of which act in the background, some of which the user interacts with directly. After this, it manages available services and logs events for administrative purposes. Its final responsibility is to shut the system down cleanly.

After the the kernel launches /sbin/init, process creation is dictated by processes themselves, and is carried out through the fork() system call.

fork() instructs the kernel to duplicate the current process, and the two resulting processes are very nearly identical. Of note, they both continue to execute the same program at the same spot, have the same open files, and have duplicate memory spaces. The newly created process is referred to as the child process, the original is called the parent, and we say that the child process inherits its parent's attributes.

Parent and child do differ in a few ways. First, upon birth, each process is given its own (unique) identification number by the kernel. Second, they each receive a different return value from the fork() system call. This allows them to act differently within the same program:

        pid_t rv = fork();      // One process executes this line
        if(rv == 0) {           // Two processes execute this line
            // I am child

        } else if (rv > 0) {
            // I am parent

        }
    

Their executions thereby diverge, and each is free to act independently.

See Also:

Executing a Different Program

The exec() family of functions replace the program currently being executed. A simple invocation is:

        char *args[] = {"ls", "-l", NULL};
        execv("/usr/bin/ls", args); 
            // Execution of *this* program stops here,
            // and the process continues executing at /usr/bin/ls
    

exec() simply loads a new program into the calling process's memory. During this operation, the old program and its memory (its "state") are discarded.

exec() usually follows fork():

        pid_t rv = fork();
        if( rv == 0 ) {
            // I am child
            char *args[] = {"ls", "-l", NULL};
            execv("/usr/bin/ls", args); 

        } else if ( rv > 0 ) {
            // Execution of parent process continues within 
            // this program
        }
    

This sequence results in one process launching another program, and is traditionally referred to as "fork and exec." Note that there is no need to adjust other process attributes, and they are preserved across exec(). In particular, the PID of the calling process is left unchanged.

See Also:

File Descriptors

Within a process, each open file is associated with a small nonegative integer. This number is called a file descriptor; it is returned by the kernel in response to a request to open a file. The process uses this number to refer to the file whenever it wishes to act upon it.

In the following line, a process is instructing the kernel to open the file rubber_ducky for reading. The process is storing the kernel's response in the (integer) variable fd:

        int fd = open("rubber_ducky", O_RDONLY);
    

If this call failed (which it would if ./rubber_ducky did not exist, for example), then fd would be assigned -1, an invalid file descriptor. We can therefore check with:

        if (fd < 0) {
                perror("open");     // Print error message
                exit(1);            // Terminate process 
        }
    

and then continue as though open() had succeeded.

In the following line, the kernel is directed to read at-most buff_size bytes from fd, and place the data into the memory denoted by buff:

        int num_bytes_read = read(fd, buff, buff_size);
    

If all goes well, the system call will return with a nonnegative value, the number of bytes read; if the kernel encountered an error, it will return with a negative value. In either case, this value will be assigned to num_bytes_read, which may be checked similarly to open().

write() is handled analogously, as is closing a file. Every system call has an entry in Section 2 of the man pages.

See Also:

Process Termination

A process may terminate for a few reasons:

See Also:

Process Attributes

The kernel stores a number of attributes for each process it is hosting; we will not exhaust them here. The process's PID is one such piece of information; it cannot be changed.

Each process has a current working directory. Relative filenames are resolved with respect to this path, and a process may change it via the chdir() system call.

Environment variables are name=value strings that are associated with each process. The kernel does not interpret these variables directly, but instead simply allows processes to set them, and retrieve them later. They are preserved across both fork() and exec().

Environment variables allow a single process to communicate with all of its descendants. A good example of their use is LANG, which stipulates the current locale, including preferred language. We may view the shell's environment variables with the printenv command:

$ printenv
SHELL=/bin/bash
COLORTERM=truecolor
XDG_CONFIG_DIRS=/etc/xdg
DESKTOP_SESSION=xfce
EDITOR=nvim
LANG=en_US.UTF-8
[...]

See Also:

Ya But What Is It?

The short answer is, a section of memory distinct from the rest of the machine, and some restricted access to the CPU, both granted by the kernel. The long answer is the primary abstraction presented to programmers, and the environment within which all programs execute. The process is one of the most important ideas of computer science, and we can only scratch the surface of what it is here.

Memory

Modern operating systems provide each process with its own memory in the form of a distinct address space. On all modern computers, memory is byte-addressable, so that each address refers to exactly one byte. Addresses themselves are simply numbers, and an address space is a collection of memory addresses.

Virtual address space and physical address space relationship

For each machine, there exists a unique physical address space. An address in this space refers to an actual byte of memory (RAM); only the kernel has access to this address space. The kernel, working in conjunction with hardware, imposes a layer of indirection between each process's memory references and the physical address space. This layer is known as virtual memory, and consists of a collection of virtual addresses mapped to physical ones.

Virtual memory requires support from both the kernel and CPU: The kernel to assign address translations (mappings from virtual addresses to physical addresses), and the CPU to carry them out. Each time a process references an address, the CPU translates the virtual address to a physical address, so that for all intents and purposes, virtual addresses behave exactly as physical ones.

Since each process operates within its own virtual address space, each process is presented with the illusion that it has the entire physical address space to itself. By excluding physical addresses from translation, the rest of the machine is implicitly protected: Other sections of memory simply do not exist within the context of the currently running process, and therefore cannot be read from, nor written to.

CPU

Modern CPU's support two distinct modes of operation. Kernel mode, sometimes called "supervisor mode," is the unrestricted mode. Here, all CPU instructions are available, and address translation is not performed, so that code running in this mode has unmediated access to the machine.

User mode is a proper subset of kernel mode: A program running in user mode does not have access to all instructions, and its memory references are redirected through the virtual memory scheme mentioned above. Executing programs in User Mode is known as limited direct execution.[4] It is limited in the sense that each process has access to only a restricted subset of the CPU's instructions, and it is direct in the sense that the program runs directly on hardware.

Virtual memory and limited direct execution are intended to make safe and efficient use of available hardware. Together, they form a sandboxed environment within which a program may safely be executed, separate from the rest of the machine.

Suggested Reading

Dr. Brian Fraser:

Fork and Exec Linux Programming

References

  1. init - Wikipedia (3 March 2021). Retrieved March 30, 2021, from https://en.wikipedia.org/wiki/Init
  2. Comparison of init systems - Gentoo wiki. Retrieved February 26, 2024, from https://wiki.gentoo.org/wiki/Comparison_of_init_systems
  3. Kerrisk, Michael. The Linux Programming Interface. San Francisco, CA, No Starch Press, 2010.
  4. Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. (2018). Operating systems: Three easy pieces. Arpaci-Dusseau Books.