CEF427 Chapter1
CEF427 Chapter1
Inter Process Communication (IPC) is a mechanism that involves communication of one process
with another process. This usually occurs only in one system.
Communication can be of two types −
• Between related processes initiating from only one process, such as parent and child
processes.
Following are some important terms that we need to know before proceeding further on this topic.
Pipes − Communication between two related processes. The mechanism is half duplex meaning the
first process communicates with the second process. To achieve a full duplex i.e., for the second
process to communicate with the first process another pipe is required.
FIFO − Communication between two unrelated processes. FIFO is a full duplex, meaning the first
process can communicate with the second process and vice versa at the same time.
Message Queues − Communication between two or more processes with full duplex capacity. The
processes will communicate with each other by posting a message and retrieving it out of the queue.
Once retrieved, the message is no longer available in the queue.
Shared Memory − Communication between two or more processes is achieved through a shared
piece of memory among all processes. The shared memory needs to be protected from each other by
synchronizing access to all the processes.
Semaphores − Semaphores are meant for synchronizing access to multiple processes. When one
process wants to access the memory (for reading or writing), it needs to be locked (or protected) and
released when the access is removed. This needs to be repeated by all the processes to secure data.
Signals − Signal is a mechanism to communication between multiple processes by way of
signalling. This means a source process will send a signal (recognized by number) and the
destination process will handle it accordingly.
pid_t getpid(void);
This call returns the process ID of the calling process which is guaranteed to be unique. This call is
always successful and thus no return value to indicate an error.
Each process has its unique ID called process ID that is fine but who created it? How to get
information about its creator? Creator process is called the parent process. Parent ID or PPID can be
obtained through getppid() call.
The system call getppid() returns the Parent PID of the calling process.
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
This call returns the parent process ID of the calling process. This call is always successful and thus
no return value to indicate an error.
Let us understand this with a simple example.
Following is a program to know the PID and PPID of the calling process.
File name: processinfo.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
int mypid, myppid;
printf("Program to know PID and PPID's information\n");
mypid = getpid();
myppid = getppid();
printf("My process ID is %d\n", mypid);
printf("My parent process ID is %d\n", myppid);
printf("Cross verification of pid's by executing process commands on shell\n");
system("ps -ef");
return 0;
}
On
compilation and execution of the above program, following will be the output.
Note − The “C” library function system() executes a shell command. The arguments passed to
system() are commands executed on shell. In the above program, command is “ps”, which gives
process status.
The complete information about all running processes and other system related information are
accessible from proc file system available at /proc location.
Now that we have seen how to get the basic information of process and its parent process, it is time
to look into the details of process/program information.
What exactly is process image? Process image is an executable file required while executing the
program. This image usually contains the following sections −
Code segment is a portion of object file or program’s virtual address space that consists of
executable instructions. This is usually read-only data segment and has a fixed size.
Data segment is of two types.
• Initialized
• Un-initialized
Initialized data segment is a portion of the object file or program’s virtual address space that
consists of initialized static and global variables.
Un-initialized data segment is a portion of the object file or program’s virtual address space that
consists of uninitialized static and global variables. Un-initialized data segment is also called BSS
(Block Started by Symbol) segment.
Data segment is read-write, since the values of variables could be changed during run time. This
segment also has a fixed size.
Stack segment is an area of memory allotted for automatic variables and function parameters. It
also stores a return address while executing function calls. Stack uses LIFO (Last-In-First-Out)
mechanism for storing local or automatic variables, function parameters and storing next address or
return address. The return address refers to the address to return after completion of function
execution. This segment size is variable as per local variables, function parameters, and function
calls. This segment grows from a higher address to a lower address.
Heap segment is area of memory allotted for dynamic memory storage such as for malloc() and
calloc() calls. This segment size is also variable as per user allocation. This segment grows from a
lower address to a higher address.
Let us now check how the segments (data and bss segments) size vary with a few sample programs.
Segment size is known by executing the command “size”.
Initial program
File: segment_size1.c
#include<stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
In the following program, an uninitialized static variable is added. This means uninitialized segment
(BSS) size would increase by 4 Bytes. Note − In Linux operating system, the size of int is 4 bytes.
Size of the integer data type depends on the compiler and operating system support.
File: segment_size2.c
#include<stdio.h>
int main() {
static int mystaticint1;
printf("Hello World\n");
return 0;
}
In the following program, an initialized static variable is added. This means initialized segment
(DATA) size would increase by 4 Bytes.
File: segment_size3.c
#include<stdio.h>
int main() {
static int mystaticint1;
static int mystaticint2 = 100;
printf("Hello World\n");
return 0;
}
In the following program, an initialized global variable is added. This means initialized segment
(DATA) size would increase by 4 Bytes.
File: segment_size4.c
#include<stdio.h>
In the following program, an uninitialized global variable is added. This means uninitialized
segment (BSS) size would increase by 4 Bytes.
File: segment_size5.c
#include<stdio.h>
Execution Steps
Compilation
Lionel $ gcc segment_size1.c -o segment_size1
Lionel $ gcc segment_size2.c -o segment_size2
Lionel $ gcc segment_size3.c -o segment_size3
Lionel $ gcc segment_size4.c -o segment_size4
Lionel $ gcc segment_size5.c -o segment_size5
Execution/Output
Lionel size segment_size1 segment_size2 segment_size3 segment_size4 segment_size5
text data bss dec hex filename
878 252 8 1138 472 segment_size1
878 252 12 1142 476 segment_size2
878 256 12 1146 47a segment_size3
878 260 12 1150 47e segment_size4
878 260 16 1154 482 segment_size5
Lionel
Till now we know that whenever we execute a program then a process is created and would be
terminated after the completion of the execution. What if we need to create a process within the
program and may be wanted to schedule a different task for it. Can this be achieved? Yes, obviously
through process creation. Of course, after the job is done it would get terminated automatically or
you can terminate it as needed.
Process creation is achieved through the fork() system call. The newly created process is called the
child process and the process that initiated it (or the process when execution is started) is called the
parent process. After the fork() system call, now we have two processes - parent and child
processes. How to differentiate them? Very simple, it is through their return values.
After creation of the child process, let us see the fork() system call details.
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
Creates the child process. After this call, there are two processes, the existing one is called the
parent process and the newly created one is called the child process.
The fork() system call returns either of the three values −
• Negative value to indicate an error, i.e., unsuccessful in creating the child process.
• Returns a positive value for the parent process. This value is the process ID of the newly
created child process.
Let us consider a simple program.
File name: basicfork.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
fork();
printf("Called fork() system call\n");
return 0;
}
Execution Steps
Compilation
gcc basicfork.c -o basicfork
Execution/Output
Called fork() system call
Called fork() system call
Note − Usually after fork() call, the child process and the parent process would perform different
tasks. If the same task needs to be run, then for each fork() call it would run 2 power n times, where
n is the number of times fork() is invoked.
In the above case, fork() is called once, hence the output is printed twice (2 power 1). If fork() is
called, say 3 times, then the output would be printed 8 times (2 power 3). If it is called 5 times, then
it prints 32 times and so on and so forth.
Having seen fork() create the child process, it is time to see the details of the parent and the child
processes.
int main() {
pid_t pid, mypid, myppid;
pid = getpid();
printf("Before fork: Process id is %d\n", pid);
pid = fork();
if (pid < 0) {
perror("fork() failure\n");
return 1;
}
// Child process
if (pid == 0) {
printf("This is child process\n");
mypid = getpid();
myppid = getppid();
printf("Process id is %d and PPID is %d\n", mypid, myppid);
} else { // Parent process
sleep(2);
printf("This is parent process\n");
mypid = getpid();
myppid = getppid();
printf("Process id is %d and PPID is %d\n", mypid, myppid);
printf("Newly created process id or child pid is %d\n", pid);
}
return 0;
}
• Normally, using _exit() system call (or _Exit() system call) or exit() library function.
The difference between _exit() and exit() is mainly the cleanup activity. The exit() does some
cleanup before returning the control back to the kernel, while the _exit() (or _Exit()) would return
the control back to the kernel immediately.
Consider the following example program with exit().
void exitfunc() {
printf("Called cleanup function - exitfunc()\n");
return;
}
int main() {
atexit(exitfunc);
printf("Hello, World!\n");
exit (0);
}
Note : In the C Programming Language, the atexit function registers a function as a termination
function which is called if the program terminates normally.
When calling the atexit function more than once, the last function to be registered is the first
function that will be called when the program is terminated normally.
Syntax
The syntax for the atexit function in the C Language is:
int atexit(void (*func)(void));
Parameters or Arguments
func
The function to register as a termination function.
Returns
The atexit function returns zero if successful and a nonzero value if unsuccessful
void exitfunc() {
printf("Called cleanup function - exitfunc()\n");
return;
}
int main() {
atexit(exitfunc);
printf("Hello, World!\n");
_exit (0);
}
As we have seen, whenever we create a child process from a program using fork, the following
happens −
#include<stdio.h>
int main() {
int pid;
pid = fork();
// Child process
if (pid == 0) {
system("ps -ef");
sleep(10);
system("ps -ef");
} else {
sleep(3);
}
return 0;
}
Following are the variants of system calls to monitor the child process/es −
• wait()
• waitpid()
• waitid()
The wait() system call would wait for one of the children to terminate and return its termination
status in the buffer as explained below.
#include <sys/types.h>
#include <sys/wait.h>
This call returns the process ID of the terminated child on success and -1 on failure. The wait()
system call suspends the execution of the current process and waits indefinitely until one of its
children terminates. The termination status from the child is available in status.
Let us modify the previous program, so that the parent process now waits for the child process.
#include<stdio.h>
int main() {
int pid;
int status;
pid = fork();
// Child process
if (pid == 0) {
system("ps -ef");
sleep(10);
system("ps -ef");
return 3; //exit status is 3 from child process
} else {
sleep(3);
wait(&status);
printf("In parent process: exit status from child is decimal %d, hexa %0x\n", status, status);
}
return 0;
}
The wait() system call has limitation such as it can only wait until the exit of the next child. If we
need to wait for a specific child it is not possible using wait(), however, it is possible using
waitpid() system call.
The waitpid() system call would wait for specified children to terminate and return its termination
status in the buffer as explained below.
#include <sys/types.h>
#include <sys/wait.h>
The above call returns the process ID of the terminated child on success and -1 on failure. The
waitpid() system call suspends the execution of the current process and waits indefinitely until the
specified children (as per pid value) terminates. The termination status from the child is available in
the status.
The value of pid can be either of the following −
• < -1 − Wait for any child process whose process group ID is equal to the absolute value
of pid.
• -1 − Wait for any child process, which equals to that of wait() system call.
• 0 − Wait for any child process whose process group ID is equal to that of the calling
process.
• >0 − Wait for any child process whose process ID is equal to the value of pid.
By default, waitpid() system call waits only for the terminated children but this default behavior can
be modified using the options argument.
Now let us consider a program as an example, waiting for a specific process with its process id.
/* Filename: waitpid_test.c */
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main() {
int pid;
int pids[3];
int status;
int numprocesses = 0;
int total_processes = 3;
while (numprocesses < total_processes) {
pid = fork();
// Child process
if (pid == 0) {
printf("In child process: process id is %d\n", getpid());
sleep(5);
return 4;
} else {
pids[numprocesses] = pid;
numprocesses++;
printf("In parent process: created process number: %d\n", pid);
}
}
Now, let us check for waitid() system call. This system call waits for the child process to change
state.
#include <sys/wait.h>
The above system call waits for the child process to change the state and this call suspends the
current/calling process until any of its child process changes its state. The argument ‘infop’ is to
record the current state of the child. This call returns immediately, if the process has already
changed its state.
The value of idtype can be either of the following −
• P_PID − Wait for any child process whose process ID is equal to that of id.
• P_PGID − Wait for any child process, whose process group ID is equal to that of id.
• The options argument is to specify which state changes and this can be formed with bitwise
OR operation with the below-mentioned flags −
• WCONTINUED − Returns the status of any child that was stopped and has been continued.
• WSTOPPED − Waits for the process of any child that has stopped, upon receipt of the
signal and returns the status.
This call returns 0, if it returns due to a change of the state of one of its children and WNOHANG is
used. It returns –1, in case of error and sets the appropriate error number.
/* Filename: waitid_test.c */
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main() {
int pid;
int pids[3];
int status;
int numprocesses = 0;
int total_processes = 3;
siginfo_t siginfo;
while (numprocesses < total_processes) {
pid = fork();
// Child process
if (pid == 0) {
printf("In child process: process id is %d\n", getpid());
sleep(5);
return 2;
} else {
pids[numprocesses] = pid;
numprocesses++;
printf("In parent process: created process number: %d\n", pid);
}
}
After execution and compilation of the above program, following is the result.
In this section, we will get familiar with Process Groups, Sessions and Job Control.
Process Group − Process group is a collection of one or more processes. A process group
constitutes of one or more processes sharing the same process group identifier (PGID). A process
group ID (PGID) is of the same type (pid_t) as the process ID. A process group has a process group
leader, which is the process that creates the group and whose process ID becomes the process group
ID of the group.
• Shell script (in BASH) to perform basic commands (date, echo, sleep and cal) named
basic_commands.sh
date
echo "Now sleeping for 10 seconds, so that testing job control functionality is smooth"
sleep 10
cal
#!/bin/bash
#process_status.sh
ps
echo "Now sleeping for 20 seconds, so that testing job control functionality is smooth"
sleep 20
ps
Use chmod command to give the file the execute permissions. By default, the normal file would get
only read and write permissions and not execute permissions.
To stop the current running process, you need to enter CTRL+Z. This gives you a job number. The
job can be resumed either in the foreground or the background. If needed, to resume the job in the
foreground use ‘fg’ command. If needed, to resume the job in the background, use ‘bg’ command.
By using this, it would run only the last stopped process. What if you want to start other than the
last stopped process? Just use the job number after fg or bg (say bg %2 or bg %3, etc). If the
running job is in the background, you can run any other tasks in the foreground. To get the list of
jobs, use command, jobs. It is also possible to terminate the process either with CTRL+C or kill
command. You can pass the job number while using the kill command.
Check the following output which demonstrates stopping the jobs, moving the jobs from the
foreground to the background and vice versa, terminating the jobs, etc
The process needs certain resources such as CPU and memory to perform the tasks. Now we will
look into the related commands and system calls to know the information on resource utilization
and monitoring. Also there are certain limits by default for each process on the resources, and if
required the limits can be enhanced to accommodate the application requirements.
Following are the essential system or process resources information using commands −
The top command continuously displays the usage of system resources. If any process puts the
system in some kind of hang state (consuming more of CPU or Memory) it is possible to note the
process information and take appropriate action (such as killing the related process).
The ps command
$ ps
The ps command provides information about all the running processes. This helps to monitor and
control the processes.
The vmstat command reports the statistics of virtual memory subsystem. It reports the information
of processes (waiting to run, sleeping, runnable processes, etc.), memory (virtual memory
information such as free, used, etc.), swap area, IO devices, system information (number of
interrupts, context switches) and CPU (user, system and idle time).
The lsof command prints the list of open files of all the current running processes, including system
processes.
• System calls related to accessing and setting resource limits viz., getrlimit(), setrlimit(),
prlimit().
The system call getrusage() returns the information on the system resource usage. This can include
information on self, children, or calling thread using flags RUSAGE_SELF,
RUSAGE_CHILDREN, RUSAGE_THREAD for the “who” variable. After the call, it returns the
information in the structure rusage.
This call would return “0” on success and “-1” on failure.
Let us look at the following sample program.
/* Filename: sysinfo_getrusage.c */
#include<stdio.h>
#include<sys/time.h>
#include<sys/resource.h>
void main(void) {
struct rusage res_usage;
int retval;
retval = getrusage(RUSAGE_SELF, &res_usage);
if (retval == -1) {
perror("getrusage error");
return;
}
printf("Details of getrusage:\n");
printf("User CPU time (seconds) is %d\n", (int)res_usage.ru_utime.tv_sec);
printf("User CPU time (micro seconds) is %d\n", (int)res_usage.ru_utime.tv_usec);
printf("Maximum size of resident set (kb) is %ld\n", res_usage.ru_maxrss);
printf("Soft page faults (I/O not required) is %ld\n", res_usage.ru_minflt);
printf("Hard page faults (I/O not required) is %ld\n", res_usage.ru_majflt);
printf("Block input operations via file system is %ld\n", res_usage.ru_inblock);
printf("Block output operations via file system is %ld\n", res_usage.ru_oublock);
printf("Voluntary context switches are %ld\n", res_usage.ru_nvcsw);
printf("Involuntary context switches are %ld\n", res_usage.ru_nivcsw);
return;
}
Let us now look at the system calls related to accessing and setting resource limits.
#include <sys/time.h>
#include <sys/resource.h>
The system call getrlimit() gets the resource limits in structure rlimit by inputting the resource one
needs such as RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_STACK, etc.
The system call setrlimit() sets the resource limits as mentioned in the rlimit structure as far as
within the limits.
The system call prlimit() is used for varius purposes, such as either for retrieving the current
resource limits or for updating the resource limits to new values.
The structure rlimit contains two values −
/* Filename: sysinfo_getrlimit.c */
#include<stdio.h>
#include<sys/time.h>
#include<sys/resource.h>
void main(void) {
struct rlimit res_limit;
int retval;
int resources[] = {RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_STACK};
int max_res;
int counter = 0;
printf("Details of resource limits for NOFILE, NPROC, STACK are as follows: \n");
max_res = sizeof(resources)/sizeof(int);
while (counter < max_res) {
retval = getrlimit(resources[counter], &res_limit);
if (retval == -1) {
perror("getrlimit error");
return;
}
printf("Soft Limit is %ld\n", res_limit.rlim_cur);
printf("Hard Limit (ceiling) is %ld\n", res_limit.rlim_max);
counter++;
}
return;
}
Let us consider another example with getrlimit() system call but now with prlimit() system call.
/* Filename: sysinfo_prlimit.c */
#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/resource.h>
void main(void) {
struct rlimit res_limit;
int retval;
int resources[] = {RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_STACK};
int max_res;
int counter = 0;
printf("Details of resource limits for NOFILE, NPROC, STACK using prlimit are as follows: \n");
max_res = sizeof(resources)/sizeof(int);
while (counter < max_res) {
retval = prlimit(getpid(), resources[counter], NULL, &res_limit);
if (retval == -1) {
perror("prlimit error");
return;
}
printf("Soft Limit is %ld\n", res_limit.rlim_cur);
printf("Hard Limit (ceiling) is %ld\n", res_limit.rlim_max);
counter++;
}
return;
}