0% found this document useful (0 votes)
47 views7 pages

Processes and Threads

The document discusses processes and threads in operating systems. It covers the lifecycle of a process from creation to termination. Key concepts discussed include process states, process control blocks (PCB), context switching, and the fork and exec system calls. File descriptors and I/O are also briefly covered.

Uploaded by

Shubham Jadhav
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
47 views7 pages

Processes and Threads

The document discusses processes and threads in operating systems. It covers the lifecycle of a process from creation to termination. Key concepts discussed include process states, process control blocks (PCB), context switching, and the fork and exec system calls. File descriptors and I/O are also briefly covered.

Uploaded by

Shubham Jadhav
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

Lecture Notes for CS347: Operating Systems, Spring 2016

Mythili Vutukuru, Department of Computer Science and Engineering, IIT Bombay

2. Processes and Threads


2.1 Background
The study of operating systems requires knowledge of other areas of computer science, especially
computer architecture and compilers. We will briefly review some of the essential concepts.

• Recall that a process is the basic unit of execution for the OS. To run a process, the OS allo-
cates memory for the process, and loads the address of the instruction to execute in the CPU’s
program counter register, at which point the CPU starts executing the instruction.

• Every process is allocated a virtual memory address space starting from 0 to the maximum
value (that depends on the architecture of the system). For example, the virtual address space
of a process ranges from 0 to 4GB on 32-bit machines. Virtual addresses requested by the CPU
are converted to actual physical addresses by the OS and the memory management unit (MMU)
at runtime.

• A part of the virtual memory address space of every proces is reserved for the use of the kernel.
That is, the kernel resides in the virtual address space of every process, though only one physical
copy of it exists in memory.

• The memory image of a process has several segments, notably: the code segment, data segment,
the heap (for dynamic memory allocation using malloc etc.), and the stack. The stack area
of a process is used to store temporary state during function calls. The stack is composed of
several stack frames, with each function invocation pushing a new stack frame onto the stack.
The stack frame consists of the parameters to the function, the return address of the function
call, and a pointer to the previous stack frame, among other things. The stack frame of a
function is popped once the function returns.

• When a process is in kernel mode, it does not use the regular (user-mode) stack of the process.
Instead, a separate kernel stack is used.

• Several CPU registers are referenced in the discussion of operating system concepts. While the
names and number of registers differ based on the CPU architecture, here are some names of
x86 registers that we will find useful. (Note: this list of registers is by no means exhaustive.)

1. EAX, EBX, ECX, EDX, ESI, and EDI are general purpose registers used to store variables
during computations.
2. The general purpose registers EBP and ESP store the base and top of the current stack
frame.
3. The program counter is also referred to as EIP (instruction pointer).

1
4. The segment registers CS, DS, ES, FS, GS, and SS store pointers to various segments
(e.g., code segment, data segment, stack segment) of the process memory.
5. The control registers like CR0 hold control information like pointers to page tables (which
translate virtual addresses to physical addresses).

• Of these registers, EAX, ECX, and EDX are called caller-save registers, and the rest (EBX,
ESI, EDI, ESP, EBP, EIP) are callee-save registers. The callee-save registers must be preserved
across function calls and context switches. Suppose an executing process pushes a new stack
frame onto a stack during a function call, or the CPU moves away to another process. When the
function call returns, or the CPU is switched back to the process, the callee-save registers must
have the same values as before the event. The ESP register should be pointing to the old stack
as before, and EIP should contain the return address from where the process resumes execution.
The EAX register is used to pass arguments and return values. The caller save registers may
have changed, and no assumptions would be made of them. This convention is followed by
compilers and operating systems.

2.2 Life cycle of a process


• A process can create a child process using the fork system call. After the fork, the memory
image of the child process is a complete copy of the memory image of the parent (the child
and parent memory images may diverge subsequently). The fork system call returns in both
the parent and child processes, with different return values. In the parent, it returns the pid of
the new child process. In the child, it returns 0. Both processes then resume execution from
the instruction right after fork. A typical use case of fork is to create a child and run the exec
system call in the child. This system call loads a new executable into the memory image of
the process calling it. (This is the reason why most operating systems use the lazy mode of
copying, called “copy-on-write” during fork.)

• A process can terminate itself using the exit system call. Alternately, a process can be ter-
minated by the OS under several circumstances (e.g., when the process misbehaves). Every
process that terminates must be reaped by its parent using the wait system call. When the
parent calls wait, the return value of the child’s termination is returned to the parent. The state
of a dead process is cleared only after it is reaped by its parent.

• After a child process termiantes, it exists as a zombie until the parent process calls wait and
reaps it. When the parent process itself terminates before the children, the orphan children are
adopted by the init process. (Some operating systems may also terminate all children upon
termination of parent.)

• The wait system call has several variants provided by the C library. When a parent calls the
wait function of the C library, the OS will return the exit value of any of its dead children.
Alternately, the parent can used waitpid to wait for a specific child by providing the pid of
the child as an argument to the system call. The wait system call is generally blocking, though
it can be invoked in a non-blocking fashion by providing suitable options during invocation.

2
• The command shell provides a good case study on the lifecycle of processes. Upon receiving a
user’s input, the shell forks a new child, and executes the command in the child. For foreground
commands, the shell waits for the child process(es) to complete before asking for the next input.
For background processes, the shell reaps terminated children at a later time, and asks the user
for the next input without waiting for the background command to complete.

• In summary, every process exists in one of the following states (there may be slight differences
across operating systems):

1. New. A process that is in the process of being created, and has never executed so far.
2. Terminated. A process that has exited and is waiting to be reaped by its parent.
3. Running. A process that is currently executing on a CPU.
4. Blocked / waiting. A process that is blocked on some event, and cannot execute.
5. Ready. A process that is ready to execute and is waiting to be scheduled.

2.3 Process Control Block


• All state pertaining to a process is stored in a kernel data structure, generally referred to as the
process control block (PCB). Note that the exact name of the data structure may differ from
OS to OS. For example, it is called task struct in Linux. Some important information
stored in the PCB includes:

1. The process identifier. Every executing process typically has a unique identifier in every
OS. For example, this identifier is referred to as pid in Linux.
2. The identifier and pointers to its parent and child processes.
3. The current state of the process (running / blocked etc.).
4. The process context, consisting of the values in its program counter and other CPU reg-
isters. This information is not updated in the PCB every time the registers change, but is
only stored in the PCB when the process context has to be saved, e.g., when moving from
user mode to kernel mode, or during a context switch.
5. Information related to scheduling the process (e.g., its priority for scheduling).
6. Information related to the memory management of the process (pointers to page tables,
user memory, the kernel stack, and so on).
7. Information about list of open files, files descriptors, and other information pertaining to
I/O activity.
File descriptors serve as an index into the array of open files or network connections of
a process. By default, file descriptor number 0 points to standard input, 1 to standard
output, and 2 to standard error. Other files / sockets opened by the process will get higher
numbered file descriptors in order. A process gets a file descriptor from a kernel when
it opens a file (analogously, it gets a socket file descriptor when it opens a socket). All

3
subsequent operations on the file, such as the read or write system calls, will reference
this file descriptor.
(Note: The exec system call replaces the memory image of the process, but keeps its table
of file descriptors intact. So, both child and parent will share the same file descriptors, and
this is the reason why the output from a command that is implemented in a child process
will appear in the standard output of the shell. I/O redirection is implemented in the shell
by clever manipulations of the file descriptor table. For example, to implement output
redirection from a command, the shell forks a child process to implement the command,
opens the file to write the output to, and “copies” the file descriptor information of the
opened file into the file descriptor entry of the child’s standard output. So, while the child
process assumes it is writing to the standard output, it is infact writing to a file.)

• The kernel usually maintains the list of PCBs as a doubly linked list over all the processes in the
system. In addition, the PCBs point to other PCBs (e.g., to parent, to children). The memory for
the PCB can be allocated on the kernel stack of the process itself, or from the kernel memory
pool.

2.4 Inter-process communication (IPC)


• Processes often need to communicate with each other in order to accomplish useful tasks. The
OS provides several mechanisms to enable processes to communicate with each other.

• Shared memory is the simplest way in which two processes can communicate with each other.
By default, two separate processes have two separate memory images, and do not share any
memory. (Note: a forked child has the same memory image as the parent at creation, but any
changes made to the child’s memory will not be reflected in the parent.) Processes wishing to
share memory can request the kernel for a shared memory segment. Once they get access to
the shared memory segment, it can be used as regular memory, with the property that changes
made by one process will be visible to the other and vice versa.
A significant issue with using shared memory is the problem of synchronization and race condi-
tions. For example, if several processes share a memory segment, it is possible for two processes
to make a concurrent update, resulting in a wrong value. Therefore, appropriate synchronization
mechanisms like locking should be used when using shared memory.

• Signals are another light-weight way for processes and kernel to communicate with each other,
and are mainly used to notify processes of events. For example, when a user hits Ctrl+C, the
OS sends a a signal called SIGINT to the running process (as part of handling the interrupt
generated by the keyboard). When a process receives a signal, the normal execution of the
process is halted, and a separate piece of code called the signal handler is executed by the
process. A process template comes with default signal handlers for all signals, so that every
program will not need to have code to handle signals. For example, for the Ctrl+C signal,
the process will terminate by default. However, a program can have its own in-built signal
handling function, that can be passed to the kernel with the signal system call. The OS

4
will then invoke this new function when the process receives a signal. Signals are not just for
communication between the OS and processes. One process can use signals to comminicate
with another process also. The kill system call can be used by a process to send a signal to
another process whose pid is specified as an argument to the system call.

• Sockets are a mechanism to communicate between two processes on the same machine, and
even between processes on different machines. Network sockets are a standard way for two
application layer processes running on different machines (e.g., a web client and a web server)
to exchange data with each other. Similarly, two processes on the same machine can use Unix
domain sockets to send and receive messages between each other. The usage of Unix domain
sockets is very similar to the usage of network sockets. That said, sockets are more widely used
for communicating across hosts than across processes on the same host.
Sockets present an interesting case study on blocking vs. non-blocking system calls. Some
socket system calls (e.g., accept, read) are blocking. For example, when a process reads
from a socket, the system call blocks until data appears on the socket. As a result, while the
process is blocked, it cannot handle data coming on any other socket. This limits the number
of concurrent communications a process can sustain. There are several techniques to fix this
problem. A process could fork off a new child process for every connection it has, so that a
child process can be dedicated to reading and writing on one connection only. Alternately, a
socket can be set to be non-blocking, and the process can periodically poll the socket to see if
data has arrived. Finally, system calls such as select can be used to get notifications from
the kernel on when a socket is ready for a read.

• Pipes. A pipe is a half-duplex connection between two file descriptors — data written into one
file descriptor can be read out through the other. A pair of file descriptors can be bound this
way using the pipe system call. The two ends of the pipe are referred to as a read end and a
write end. Reading from and writing to a pipe can be blocking or non-blocking, depending on
how the pipe is configured. The data written to a pipe is stored in temporary memory by the
OS, and is made available to the process that reads from it.
Pipes are anonymous, i.e., there is no way to refer to them outside the process. The typical use
case of pipes is for a parent process to create a pipe, and hand over endpoints to one or more
children. Alternately, named pipes or FIFOs (First-In-First-Out) enable a process to create a
pipe with a specified name, so that the endpoints of the pipe can be accessed outside the process
as well.

• Message passing is another IPC mechanism provided by many operating systems. A process
can create a message queue (much like a mailbox), and another process can send a message
to this queue via the OS. A message queue is maintained as a linked list inside the kernel.
Every message has a type, content, and other optional features like priority. System calls are
available to create a message queue, and post and retrieve messages from the queue. As in
the previous cases, blocking and non-blocking versions of the system calls are available. For
example, when the message queue is full, the writing process can choose to block or return
with an error message. Similarly, when the message queue is empty, the system call to read a
message can block or return empty.

5
2.5 Threads
• Multiprogramming is the concept of having multiple executing entities (e.g., processes) to ef-
fectively utilize system resources. For example, if a system has multiple processes, then the
CPU can switch to another process when one process makes a blocking system call. Multipro-
gramming is also essential to effectively utilize multiple computing cores that are common in
modern systems.

• An application (say, a web server or a file server) that wishes to achieve efficiency via mul-
tiprogramming can fork multiple processes and distribute its work amongst them. However,
having a lot of processes is not efficient because every process consumes memory, and sharing
information across processes is not easy. Threads are a type of light-weight processes that are
widely used in such situations.
Every process has a single thread (of execution) by default, but can create several new threads
once it starts. Threads of a process share memory corresponding to the code and data of the
process, which makes it easy to share data between them. This is also the reason why creating
threads consumes fewer resources than creating processes. Each thread represents a separate
unit of execution, and one thread of a process can execute while the other blocks. Because
threads can be executing on different parts of the process code, each thread has its own program
counter and CPU register values. Threads can optionally have some thread-specific data.

• POSIX provides an API for creating and managing threads. Linux implements these POSIX
threads or pthreads. When a process creates a thread using pthread create, it provides
a pointer to a function that the thread must run. A thread completes execution when the function
exits. The parent process can choose to wait for all threads it created to complete (this is called
a join operation), or it can detatch the threads and not wait for their completion.

• Sharing data across threads is easy, as they share all data in the program. However, race con-
ditions can occur when two threads concurrently update a shared variable. So care must be
taken in designing multi-threaded applications. When updating shared data structures, threads
must either access mutually exclusive portions of the data, or must lock and coordinate when
accessing the same data.

• Most real-life applications are multi-threaded. Some applications spawn a fixed number of
threads, and distribute the work of the application amongst them. For example, a web server
can have a fixed number of “worker” threads. When a request arrives from a client, a free thread
picks up the request and serves it. Having multiple threads also means that one thread can block
while serving a request, without impacting the server’s ability to serve another request.

• So far, our discussions of threads have referred to the threads that an application program cre-
ates. These are called user threads. Do these user threads always map to separate units of
execution at the kernel? That is, are all the threads in an application scheduled as independent
entities by the CPU scheduler? The answer is dependent on the architecture of the particu-
lar OS. The separate threads of execution that the kernel deals with (e.g., for the purpose of

6
scheduling) are called kernel threads. In some operating systems (e.g., Linux), there is a one-
to-one mapping between user threads and kernel threads. That is, when an application program
creates a thread using the pthreads library, Linux creates a new kernel object corresponding
to the thread. (In fact, Linux creates processes and threads much the same way, with the only
difference being the amount of information that is cloned during fork.) Thus, there is a one-to-
one mapping between the POSIX threads created by an application programs and kernel threads
managed by Linux. (Note that Linux also has some kernel threads that do not correspond to
any user threads, but whose sole purpose is to carry out kernel tasks.)

• However, there is not always a one-on-one mapping between user and kernel threads. In some
thread libraries on some operating systems, multiple user threads are multiplexed onto the same
kernel thread (i.e., many-to-one mapping). While the user program sees multiple threads, the
kernel only perceives one thread of execution. So, when one of the user threads blocks, the
other user threads that share the same kernel thread will also be blocked. A user-level library
takes care of scheduling the multiple user threads onto a single kernel thread. Such user-level
threads do not achieve all the benefits of multiprogramming. However, user threads are easier to
create and manage, and enable the application to handle and switch between many concurrent
operations at a time.

• Finally, there is also a middle ground between one-to-one and many-to-one mappings, which is
a many-to-many model. In some operating systems, the kernel multiplexes several user threads
onto a smaller number of kernel threads.

• Operations such as forking or handling signals becomes tricky in a multi-threaded application.


When forking a child, some operating systems fork all the threads in the process, while some
only fork the thread that made the system call. When handling signals, a multi-threaded appli-
cation can designate a particular thread to handle signals, or the OS may have a default policy
on which thread should receive the signal.

You might also like