Seed Metaphor
An operating system is a seed. The “kernel” of the seed is the core of the operating system, providing operating system services to applications programs, which is surrounded by the “shell” of the seed that is what users see from the outside.
Some people want to tie “kernel” (and, indeed, “shell”) down to be more specific than that. But in truth there’s a lot of variation across operating systems. Not the least these variations is what constitutes a “shell” (which can range from Solaris’ sh
through Netware’s Console Command Interpreter to OS/2’s Workplace Shell and Windows NT’s Explorer), but there’s also a lot of variance from one operating system to another in what is, and isn’t, a part of a “kernel” (which may or may not include disk I/O, for example).
There are other terminologies. In IBM mainframe terminology, the “kernel” in the seed/nut metaphor is called the control program. Other names include the supervisor, the monitor, the core, and the executive. Core is another fairly obvious biological metaphor. It is interesting that so too is another name. Harvey M. Deitel, in his Operating Systems, calls the control program the nucleus. The word “nucleus” comes from the Latin nucleus, and means the kernel of a nut (c.f. the Latin nux, which means “nut”). So even calling the control program “the nucleus” is in fact making this same operating-system-as-a-nut analogy. It’s an analogy that is used for more than just operating systems. If you are familiar with chemistry, for example, you’ll know that an atomic nucleus is surrounded by electrons in shells.
The kernel does all the interaction with the hardware, and manages resources like memory, the I/O devices, and networking.
That part of the system which permanently resides in main storage has historically been called the nucleus. The nucleus will usually consist of a minimal set of primitives and processes for the management of processes, resources, and input-output
OS distributions
An OS distribution has an OS kernel which interacts with utilities packaged with it. Compiling the software and making them work with one another is the job of the distribution. Some Linux distributions are intended for desktop computers, some for servers without a graphical interface, and others for special uses, such as home theater PCs, supercomputers, POS systems or embedded systems.
For example, the Linux kernel can be packaged with GNU shell utilities. The terminal and many of the commands we use on it are not part of the kernel, but of the Utilities.
Utilities might include:
A program (or a task or thread of execution) is very general thing - a set of ordered instructions for the CPU to carry out.
A process is an executing instance of a program. As an implementation detail, it is a (collection of) thread of execution and other context, such as address space and file descriptor table.
unix implementation details for process: Unix supports the notion of a current working directory for each process, maintained as part of the process state. This allows users to refer to files by their relative pathnames, which are interpreted relative to the current directory. Unix guarantees that every process has a unique identifier called the process ID. The process ID is always a non-negative integer. Similar to files, each process has one owner and group, and the owner and group permissions are used to determine which files and devices the process can open. Most processes also have a parent process that started them.
File descriptors are normally small non-negative integers that the kernel uses to identify the files being accessed by a particular process. Whenever it opens an existing file or creates a new file, the kernel returns a file descriptor that is used to read or write the file. (Sockets are based on a very similar mechanism i.e. socket descriptors).
A shell is a program (on Linux, typically written in C) that can be used for a wide variety of things, but specially good for OS-level resource manipulations in it:
curl
and wget
. Using these programs makes managing network resources look a lot like running applications and managing local files.An interactive shell (sh
, dash
, bash
, zsh
) interprets (compile source code into OS-readable binary on the fly) the commands you are typing in the terminal window, for example ls
, cat
and date
(these are not shells but executable binaries). You launch an interactive shell by logging into a terminal emulator. Each shell you get at login becomes the leader of its own new session and process group, and sets the controlling process group of the terminal to itself.
The shell language’s only data structure is the string. There are ways of decomposing strings into words.
All running applications and daemons are processes. The only thing running which is not a process is the kernel (including kernel threads). A (running) shell is a process but not all processes are shells. Sourcing a script will execute the commands in the current shell process, whereas executing a script will spawn a new shell process and execute the commands there (though it might be a child process).
Process Lifecycle
A process may start other processes (e.g. by making the fork()
system call, which uses clone(), and then the exec() call to replace the clone); these new processes will belong to the same process group as the parent unless other action is taken.
Each process may also have a “controlling terminal”, which starts off the same as its parent.
A zombie process is one which has died but still exists as a placeholder in the process table until its parent (or init if the zombie is itself an orphan) checks its exit status.
An orphan processes are processes that are still alive and running but whose parent has died
When the parent dies, it’s dead. With respect to its children, it doesn’t matter whether the parent stays on as a zombie: the children become orphans at the time the parent dies, and then they lose any connection with their parent, init adops them as its children.
The kernel, at least on Unix like OSes is launching one (or more) initial hand crafted processes, usually called init. These processes are the parents of a whole hierarchy of other processes.
The init process is normally started when the Kernel calls a certain filename – often found in /etc/rc
or /etc/inittab
– but this location can change based on OS. Normally this process sets the path, checks the file system, initializes serial ports, sets the clock, and more. Finally, the last thing the init process handles is starting up all the other background processes necessary for your operating system to run properly – and it runs them as daemons. Typically, all of these daemon scripts exist in /etc/init.d/; it’s conventional to end all of the daemon executables with the letter d (such as httpd, sshd, mysqld, etc.) – so you might think that this directory is named as such because of that, but it’s actually just a common unix convention to name directories that have multiple configuration files with a .d suffix.
Traditionally in Unix, the only way to create a process is to create a copy of the existing process and to go from there. This practice – known as process forking – involves duplicating the existing process to create a child process and making an exec system call to start another program. We get the phrase “process forking” because fork is an actual C method in the Unix standard library which handles creating new processes in this manner. The process that calls the fork command will be considered the parent process of the newly created child process. The child process is nearly identical to the parent process, with a few differences such as different process IDs and parent process IDs, no shared memory locks, no shared async I/O, and more.
As we mentioned, copying the entire memory of one process to another when fork is called is an expensive operation.
One optimisation is called copy on write. This means that similar to threads above, the memory is actually shared, rather than copied, between the two processes when fork is called. If the processes are only going to be reading the memory, then actually copying the data is unnecessary.
However, when a process writes to its memory, it needs to be a private copy that is not shared. As the name suggests, copy on write optimises this by only doing the actual copy of the memory at the point when it is written to.
Copy on write also has a big advantage for exec. Since exec will simply be overwriting all the memory with the new program, actually copying the memory would waste a lot of time. Copy on write saves us actually doing the copy.
Threads
A process may create another thread that shares its address space.
While fork copies all of the attributes of a process, imagine if everything was copied for the new process except for the memory. This means the parent and child share the same memory, which includes program code and data.
This hybrid child is called a thread. Threads have a number of advantages over where you might use fork.
Separate processes can not see each others memory. They can only communicate with each other via other system calls.
Threads however, share the same memory. So you have the advantage of multiple processes, without the expense of having to use system calls to communicate between them.
The problem that this raises is that threads can very easily step on each others toes. One thread might increment a variable, and another may decrease it without informing the first thread. These type of problems are called concurrency problems and they are many and varied.
To help with this, there are userspace libraries that help programmers work with threads properly. The most common one is called POSIX threads or, as it more commonly referred to pthreads
Switching processes is quite expensive, and one of the major expenses is keeping track of what memory each process is using. By sharing the memory this overhead is avoided and performance can be significantly increased.
There are many different ways to implement threads. On the one hand, a userspace implementation could implement threads within a process without the kernel having any idea about it. The threads all look like they are running in a single process to the kernel.
This is suboptimal mainly because the kernel is being withheld information about what is running in the system. It is the kernels job to make sure that the system resources are utilised in the best way possible, and if what the kernel thinks is a single process is actually running multiple threads it may make suboptimal decisions.
Memory Inside A Process
A process can be further divided into code and data sections.
Program code and data should be kept separately since they require different permissions from the operating system. The operating system needs to give program code permission to be read and executed, but generally not written to. On the other hand data (variables) require read and write permissions but should not be executable. Importantly, the separation of code and data facilitates sharing of code.
The Stack. The part of the data section of a process; intimately involved in the execution of any program fundamental to function calls. Each time a function is called it gets a new stack frame (which contains: the address to return to when complete, the input arguments to the function and space for local variables).
Stacks are ultimately managed by the compiler, as it is responsible for generating the program code. The stack is the memory set aside as scratch space for a thread of execution. When a function is called, a block is reserved on the top of the stack for local variables and some bookkeeping data. When that function returns, the block becomes unused and can be used the next time a function is called. The stack is always reserved in a LIFO (last in first out) order; the most recently reserved block is always the next block to be freed. This makes it really simple to keep track of the stack; freeing a block from the stack is nothing more than adjusting one pointer.
To the operating system the stack just looks like any other area of memory for the process.
The heap is an area of memory that is managed by the process for on the fly memory allocation. This is for variables whose memory requirements are not known at compile time.
The stack is the memory set aside as scratch space for a thread of execution. When a function is called, a block is reserved on the top of the stack for local variables and some bookkeeping data. When that function returns, the block becomes unused and can be used the next time a function is called. The stack is always reserved in a LIFO (last in first out) order; the most recently reserved block is always the next block to be freed. This makes it really simple to keep track of the stack; freeing a block from the stack is nothing more than adjusting one pointer.
The OS allocates the stack for each system-level thread when the thread is created. Typically the OS is called by the language runtime to allocate the heap for the application. The stack is attached to a thread, so when the thread exits the stack is reclaimed. The heap is typically allocated at process startup by the runtime, and is reclaimed when the process exits. The size of the stack is set when a thread is created. The size of the heap is set on application startup, but can grow as space is needed (the allocator requests more memory from the operating system). The stack is faster because the access pattern makes it trivial to allocate and deallocate memory from it (a pointer/integer is simply incremented or decremented), while the heap has much more complex bookkeeping involved in an allocation or deallocation. Also, each byte in the stack tends to be reused very frequently which means it tends to be mapped to the processor’s cache, making it very fast. Another performance hit for the heap is that the heap, being mostly a global resource, typically has to be multi-threading safe, i.e. each allocation and deallocation needs to be - typically - synchronized with “all” other heap accesses in the program.
Job
A shell provides an interface to handle processes. It does so with the help of an abstraction call jobs.
Any program you interactively start that doesn’t detach from the terminal (ie, not a daemon) is a job. To list all the jobs you are running, you can use jobs
.
Some programs are not designed to be run with continuous user input and disconnect from the terminal at the first opportunity. For example, a web server responds to web requests, rather than user input. Mail servers are another example of this type of application. These types of programs are known as daemons. they typically handle things such as network requests (ssh server), hardware activity, and other wait & watch type tasks (e.g. firewalls, docker daemon).
The shell has the concept of “foreground” jobs and “background” jobs. Foreground jobs are process groups with control of the terminal, and background jobs are process groups without control of the terminal. If you’re running an interactive program, you can press Ctrl+Z
to suspend it. Then you can start it back in the foreground (using fg
) or in the background (using bg
). While the program is suspended or running in the background, you can start another program - you would then have two jobs running. You can also start a program running in the background by appending an &
like this: program &
. That program would become a background job.
The shell creates a process group within the current session for each “job” it launches, and places each process it starts into the appropriate process group. For example, ls | head
is a pipeline of two processes, which the shell considers a single job, and will belong to a single, new process group.
Each terminal has a foreground process group. to which it sends signals generated by keyboard interrupts, notably SIGINT
(“interrupt”, Control+C
), SIGTSTP
(“terminal stop”, Control+Z
), and SIGQUIT
(“quit”, Control+\
). It also sends the SIGTTIN
and SIGTTOU
signals to any processes that attempt to read from or write to the terminal and that are not in the foreground process group. The shell, in turn, partitions the command pipelines that it creates into process groups, and controls what process group is the foreground process group of its controlling terminal, thus determining what processes (and thus what command pipelines) may perform I/O to and from the terminal at any given time.
When the shell forks a new child process for a command pipeline, both the parent shell process and the child process immediately make the child process into the leader of the process group for the command pipeline. In this way, it is ensured that the child is the leader of the process group before either the parent or the child relies on this being the case.
When bringing a job to the foreground, the shell sets it as the terminal’s foreground process group; when putting a job to the background, the shell sets the terminal’s foreground process group to another process group or itself.
Processes may read from and write to their controlling terminal if they are in the foreground process group. Otherwise they receive SIGTTIN
and SIGTTOU
signals on attempts to read from and write to the terminal respectively. By default these signals suspend the process, although most shells mask SIGTTOU
so that a background job can write to the terminal uninterrupted.
There is no key combination to send SIGSTOP. Control-S tells the terminal driver to suspend output, but does not send a signal to the process. Control-Q resumes output. Control+C
(control character intr) sends SIGINT
which will interrupt the application. Usually causing it to abort, but this is up to the application to decide. Control+Z
(control character susp) sends SIGTSTP
to a foreground application, effectively putting it in the background, suspended. This is useful if you need to break out of something like an editor to go and grab some data you needed. You can go back into the application by running fg (or %x where x is the job number as shown in jobs).
It’s worth adding that it’s also possible to run bg (instead of fg) to unsuspend an application that’s been Ctrl+Z’ed without putting it back into the foreground; effectively giving you control of both the shell that started the application and the application itself, as if you had used & when starting the application. This often comes in handy when you forgot to start it with &
How are daemons spawned?: init or orphansDaemons are spawned one of two ways: either the init process forks and creates them directly – like we mentioned above in the init process segment – or some other process will fork itself to create a child process, and then the parent process immediately exits. The first condition seems pretty straightforward – the init process forks to create a daemon – but how does that second condition work, and how does the init process end up becoming the parent of these daemons? When you fork a process to create a child process, and then immediately kill that parent process, the child process becomes an orphaned process – a running process with no parent (not to be confused with a zombie process, such as a child process that has been terminated but is waiting on the parent process to read its exit status). By default, if a child process gets orphaned, the init process will automatically adopt the process and become its parent. This is a key concept to understand, because this is normally how daemons that you start after boot up relate to the init process.
How are background jobs different from daemons? Daemon differ from simple background processes that are spawned in the terminal because these background process are typically bound to that terminal session, and when that terminal session ends it will send the SIGHUP
message to all background processes – which normally terminates them. Because daemons are normally children of the init process, it’s more difficult to terminate them. If I start a program in the background using &
(for example ./script &
), what makes this process’ execution different than if I ran normally a program that turns itself into a daemon? Running a program in the background, it no longer is directly controlled by the terminal (you can’t simply ^C
it), but it can still write to the terminal and interfere with your work. Typically a daemon will separate itself from the terminal (in addition to forking) and its output/error would be redirected to files. Does this simply mean that if I logout the background process will stop and the daemon will keep running? The background process could be protected with nohup but unless its output were redirected (no hup does it, no?), closing the terminal would prevent it from writing, producing an error that likely would stop it. Besides the problem of keeping track of the program’s output (and error messages), there’s the problem of restarting it if it happens to die. A service script fits into the way the other services on the system are designed, providing a more/less standard way of controlling the daemon.
Language Runtime
Most programming languages have some form of runtime system that provides an environment in which programs run. This environment may address a number of issues including the management of process memory, how the program accesses variables, mechanisms for passing parameters between procedures, interfacing with the operating system, and otherwise. The compiler makes assumptions depending on the specific runtime system to generate correct code. Typically the runtime system will have some responsibility for setting up and managing the stack and heap, and may include features such as garbage collection, threads or other dynamic features built into the language.
an implementation of the desktop metaphor made of a bundle of programs running on top of a computer operating system that share a common graphical user interface (GUI), sometimes described as a graphical shell. Desktop GUIs help the user to easily access and edit files, while they usually do not provide access to all of the features found in the underlying operating system. Instead, the traditional command-line interface (CLI) is still used when full control over the operating system is required. typically consists of icons, windows, toolbars, folders, wallpapers and desktop widgets (see Elements of graphical user interfaces and WIMP). GUI might also provide drag and drop functionality. aims to be an intuitive way for the user to interact with the computer using concepts which are similar to those used when interacting with the physical world, such as buttons and windows. ↩