Processes
1. Processes
Allows a single processor to run multiple programs "simultaneously".
- Provide illusion of concurrency.
- Isolation between processes.
- Simplicity of programming.
- Better resource management.
1.1 Types of Concurrency
- Pseudo Concurrency: MSingle processor is switched between processes by interleaving. This gives the illusion of concurrent execution.
- Real Concurrency: Multiple processors are used to run multiple processes simultaneously.
2. Context Switches
On a context switch, the processor switches from executing process
- OS may take periodic scheduling decisions.
- OS May switch processes in response to events/interrupts.
- Process
may be restarted later, therefore, all information concering the process, needed to restart safely, should be stored. - This data is stored in the process descriptor or process control block (PCB), which is kept in the process table.
2.1 Process Control Block (PCB)
Each process has its own virtual machine:
- Its own virtual CPU.
- Its own address space.
- Open file descriptors. So, when saving, we should store:
- Program counter (PC), page table register, stack pointer, ...
- Process management info (PID, state, priority, ...).
- File management info (root dir, working dir, open descriptors, ...).
2.2 Context Switch Cost
Context switches ar eexpensive. They require:
- Direct cost: Time to save and restore the context.
- Indirect cost:
- Cache misses.
- TLB misses (Translation Lookaside Buffer).
- Pipeline flushes.
It is important to avoid unnecessary context switches.
3. Process Creation & Termination
Processes are created at:
- System initialization.
- User request.
- System call by a running process.
Processes can be classified (without difference in implementation) as:
- Foreground - interact with users.
- Background - no user interaction (daemons).
There are three ways to terminate a process:
- Normal completion: Process finishes execution (done with a syscall).
- Abnormal completion: Process has run into an error or an unhandled exception.
- Aborted: Process is killed by another process (e.g. killed from the shell).
Some processes run in an endless loop and never terminate unless an error occurs.
4. Case Study: UNIX
UNIX processes are created using the fork() system call. It creates a new child process by making an exact copy of the parent process image. The function returns twice:
- Parent: Returns the child's PID.
- Child: Returns
.
On error, no child is created and
1#include <unistd.h> 2#include <stdio.h> 3 4int main () { 5 if (fork() != 0) 6 printf("Hello from parent!\n"); 7 else 8 printf("Hello from child!\n"); 9 10 printf("Hello from both!\n"); 11}
> Hello from parent!
> Hello from both!
> Hello from child!
> Hello from both!
5. Process Execution
Processes are executed with the execve function. This function takes:
- The path of the program to run.
- The argv (arguments) to pass to the program.
- The envp (environment variables) to pass to the program. This function changes the process image and runs the new process.
We can wait for process termination using the waitpid function. This function takes:
- The pid to wait for.
- The status to store the exit status of the process.
- The options to specify how to wait for the process.
This function suspends the execution of the calling process until the process with PID pid terminates normally or a signal is received. This funciton can wait for more than one child:
- pid = -1 waits for any child process.
- pid = 0 waits for any child process in the same process group as the caller.
- pid = -gid waits for any child with process group ID equal to the absolute value of pid.
It returns:
- pid of the terminated child process if successful.
- 0 if
WNOHANGwas specified and no child process was available. - -1 if an error occurred.
Both fork and exeve exist for simplicity in UNIX. In windows, CreateProcess() is used to create a new process. This function takes 10 parameters!
6. Process Termination
A process is terminated by calling exit. This returns an exit status to the parent pointer, which is stored within the status pointer argument in waitpid.
We can also terminate a process with kill(pid, sig) which sends signal sig to process pid.
7. Process Communication
Processes can communicate via:
- Files
- Signals (UNIX)
- Events, exceptions (Windows)
- Pipes
- Message Queues (UNIX)
- Mailslots (Wnidows)
- Sockets
- Shared memory
- Semaphores
7.1 Signals
Inter-Process Communication (IPC) mechanism is a signal-delivery system similar to hardware interrupts. It is used to notify a process that a particular event has occurred. A process may send a signal to another process if it has permission to do so. Signals are generated when:
- An exception occurs (e.g. divide by zero
SIGFPE, segment violationSIGSEGV). - When the kernel wants to notify the process of an event (e.g. a proceses writes to a closed pipe
SIGPIPE). - When certain key combinations are typed in the terminal (e.g. Ctrl+C
SIGINT). - Programmatically using the
killsyscall.
By default, most signals will terminate the process. However, the receiving process may choose to:
- Ignore it.
- Handle it with a signal handler
SIGKILL and SIGSTOP cannot be caught or ignored. An example of a signal handler can be seen below:
1#include <signal.h> 2#include <stdio.h> 3 4void my_handler(int sig) { 5 fprintf(stderr, "Caught SIGINT\n"); 6} 7 8int main(int argc, char *arvg[]) { 9 signal(SIGINT, my_handler); 10 while (1) {} 11}
7.2 Pipes
A pipe is a method of connecting the standard output of one process to the standard input of another. This allows for one-way communication betwen processes. This is widely used on the command line and in shell scripts:
1# List files and pipe the output to the less command 2ls | less 3 4# Count the number of lines in file.txt that contain hello 5cat file.txt | grep hello | wc -l
Unnamed pipes are created using the int pipe(int fd[2]) syscall. This creates a pipe and returns two file descriptors in fd:
fd[0]is the read end of the pipe.fd[1]is the write end of the pipe.
The sender should close the read end and the receiver should close the write end. If the receiver reads from an empty pipe, it will block until data is available. If the sender writes to a full pipe, it will block until data is read at the other end.
For example:
1int main(int argc, char *argv[]) { 2 int fd[2]; 3 char buf; 4 assert(argc == 2); 5 if (pipe(fd) == -1) exit(1); 6 7 if (fork() == 0) { 8 close(fd[0]); 9 write(fd[1], argv[1], strlen(argv[1])); 10 close(fd[1]); 11 waitpid(-1, NULL, 0); 12 } else { 13 close(fd[1]); 14 while (read(fd[0], &buf, 1) > 0) { 15 printf("%c", buf); 16 } 17 printf("\n"); 18 close(fd[0]); 19 } 20}
Persistent pipes can outlive processes which created them. They are stored on a file system, and any process can open it like a regular file. They can be created with the mkfifo command.