Processes
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.
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.
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:
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.
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
.
sudo(8)
- Execute a command as another usercredentials(7)
- Process identifiers
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.
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.
fork(2)
- Create a child processThe 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.
pstree(1)
- Display a tree of processesWithin 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.
read(2)
- Read from a file descriptorA process may terminate for a few reasons:
This is the usual (and preferred) route of process termination: The program finishes its job or identifies an error, and terminates itself. This may be done by either calling exit(), or returning from main().
A process may be told to terminate by another process through a signal, a primitive form of interprocess communication. This is abnormal process termination.
Signals are brutal business.
For the purposes of process termination,
SIGINT
is preferred because it
affords the program the oportunity to exit
cleanly.[3]
At the command line, it is created and sent
(to the foreground process) with CTRL+C.
Command-line tools for sending signals to arbitrary
processes are kill
,
which refers to processes by PID, and
killall
,
which refers to processes by program name.
Almost always, this happens when the process tries to read or write a memory location not assigned to it, a condition known in Linux as segmentation fault. The kernel steps in, halts program execution and immediately terminates the process. Again, this is abnormal program termination.
top(1)
- Display Linux processesThe 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:
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.
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.
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.
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.