© Sri Manikanta Palakollu 2021
S. M. PalakolluPractical System Programming with Chttps://doi.org/10.1007/978-1-4842-6321-1_5

5. Process and Signals

Sri Manikanta Palakollu1 
(1)
freelance, Hanuman Junction, Hanuman Junction, 521105, Andhra Pradesh, India
 

Processes play a major role on an operating system. When you execute a computer program in your system, it is done with a process. Without processes, you aren’t able to do any activity on an OS. In this chapter, you look at processes and how to perform various tasks. You also see various types of processes that can occur during the execution of a program.

Signals are interrupts or traps (a trap is a fault) that raise an event when an exception occurs. It is very handy to be able to detect exceptions and interrupts caused by the system or a program. Signals are more helpful when working with core system-level applications. This chapter discusses the following topics.
  • Introduction to process environments

  • Linux subsystems

  • Process creation

  • A zombie process

  • An orphan process

  • System calls for process management

  • Signals and their types

  • System calls for signal management

Introduction to the Process Environment

An executing program is considered a process. To get a deeper understanding of a process, you need to be familiar with the process environment. Let’s consider the internal working mechanisms of a normal C program that is subjected to the kernel for execution. You are already know that every C program execution starts with the main() function; but behind the scenes, a special start-up routine is called by the kernel before calling the main() function.

When you compile C code, an executable is generated by the compiler. This executable program contains the starting address of the start-up routine set up by the linker when the program is executed. But when ASLR (address space layout randomization) is enabled, the startup routine address is unpredictable. ASLR is a memory protection mechanism that resolves buffer overflow issues by randomizing the location. This startup routine usually takes a kernel. That type of argument is called a command-line argument .

Let’s start with some basics and work toward a deeper understanding. A typical C program main function look likes the following.
int main(int argc, char *argv[]);
It contains two parameters that take command-line arguments.
  • argc takes an integer type as an argument that contains the number of command-line arguments passed by the programmer. The parameters that are passing to the command line should be space separated. This means if you pass hello world, it is considered two different arguments, but hello_world is a single argument. If you want to pass a spaced single argument, it is advisable to pass it inside double quotes (i.e., “hello world”), which is also considered a single argument.

  • argv takes a character array type as an argument. It deals with the array of pointers that point to the argument values.

Let’s look at a simple C program that prints all the command-line values that are passed by the programmer explicitly.
#include<stdio.h>
int main(int argc, char * argv[]){
   printf("Number of Arguments Passed: %d ", argc);
   // This loop prints the all the command line values
   // that are passed through the program.
   for(int i=0;i<argc;i++){
       printf("%s ", argv[i]);
   }
   return 0;
}
The output of the program look like Figure 5-1.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig1_HTML.jpg
Figure 5-1

Output of C program for command-line arguments

The program is named “Command Line Arguments.c”. The gcc compiler compiles this program. After the compilation is done, the programmer run the program. This program was run with five types of command-line values. A loop to print all the command-line values was written. This program prints all the passed arguments and the number of arguments. There is an odd behavior that you can observe in the output: there were six arguments passed because it counted the executable file value as well.

Environment List

Your operating system has an environment list of items stored in an array of character pointers. A process environment has an environment list. An environ is a character pointer variable that points to an environment list. You can access this variable data with the extern keyword. The syntax is as follows; also see Figure 5-2.
extern char **environ;
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig2_HTML.jpg
Figure 5-2

Environment variables list

  • Environ is an environment pointer that points to the environment list, which consists of string data.

  • The environment list consists of predefined variables and custom process variables. All the predefined values are in uppercase format.

  • The format of an environment list is name=value.

  • The executing program is also present under this environment list variable.

Here’s an example.
#include<stdio.h>
int main(){
   extern char **environ;
   char **environment_list = environ;
   /* This code Helps us to prints the all the
      Environments available in the operating system.
   */
   while(*environment_list != NULL){
       printf("%s ", *environment_list);
       environment_list++;
   }
   return 0;
}

This program prints all the environment variables in your operating system and the variables defined in your current session/program. The list of values that are printed by the program contain your program execution file as well.

This program shows the predefined and working process items listed in the environment list. Finally, you can see the program execution path, which is assigned to the _ variable. The running program’s instance is available in a running processes list. This proves every program under execution is considered a process.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig3_HTML.jpg
Figure 5-3

Output of the environment variable list using C program

Memory Layout of a C Program

The memory layout of a C program typically consists of various block items. Each block has a specific task to do within the running program. To get a clear view of the memory layout in a C program, let’s look at memory layout in the pictorial representation shown in Figure 5-4.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig4_HTML.jpg
Figure 5-4

Memory layout

The entire memory layout is divided into several blocks. Each block has a separate functionality associated with it.

Command-Line Arguments

The command-line argument block accepts all the values explicitly passed by the programmer. This block also contains environment variables.

Stack

A stack is used for static allocation in a program. It stores all the automatic variables. The function call’s results are stored in the stack area, but you can’t estimate or predict exactly where a function call’s results will store. It depends on the hardware architecture. Function call results are ABI (application binary interface) dependent. The values stored in the stack are directly stored in RAM (random-access memory). Access time for items in the stack space is very fast.

Heap

A heap is used for dynamic memory allocation. Allocation of memory is done at runtime and accessing the items present in a heap space is slower than in a stack space. The size of the heap is limited to the size of your virtual memory.

Uninitialized Data

The kernel assigns the data present in this segmented block to an arithmetic zero or the NULL pointer before the program starts executing. This block is also called a BSS block, which is a block started by symbol. Global and static variables that don’t have any explicit initialization in the program are stored in this data block. This block contains only uninitialized data.

Initialized Data

Global and static variables initialized by the programmer with predefined values in the program are stored in the initialized data block.

Text

The text block contains the machine code/instructions the CPU needs to execute.

Process Termination Methodologies

A process is terminated normally or abnormally based on the program flow or unexpected interrupts. The termination of a process is done in the following ways.
  • When a main() function returns the value, the process is terminated.

  • When you call an exit() function, which is available in the stdlib.h library, to terminate a process.

  • When you call the _Exit() or _exit() functions available in stdlib.h and unistd.h, respectively, to terminate a process.

  • When you call pthread_exit to terminate the process.

  • When you call an abort() function to abnormally terminate the process.

  • When the programmer raises a signal, the process is terminated abnormally if the custom handler or built-in signal handler is not available. But you can handle the signals with a custom/built-in signal handler.

  • Thread cancellation requests are also responsible for process termination. A thread cancellation request is the termination of a thread before its job is done in the process.

  • Any I/O failure/interrupt leads to process termination. For example, if the process is waiting for input from a scanner but the scanner is not working, this leads to process termination. If there is any custom exception handler code available, this situation is handled easily without the process being terminated.

  • In some situations, a child process is terminated because of a parent process request.

  • A process is terminated when it is trying to access unallocated or unauthorized resources. For example, when a process tries to execute a program that doesn’t have execution permissions, it leads to process termination. When a program tries to access memory that it does not own, it leads to process termination.

The process environment consists of the environment List, memory layout, and process termination. Memory layout deals with how program data is organized in the system memory for better access. In contrast, an environment list deals with storing all the processes that are running on an operating system. Finally, process termination methodologies terminate a process normally or abnormally, based on the programmer’s requirements. Abnormally terminating a process is done when something unexpectedly happens to a program, so the programmer kills the process abnormally.

Environment Variables

Every process has an environment block that contains environment variables. An environment variable is a dynamic variable that deals with the processes and programs in an operating system.

Every operating system has an environment list and variables. These variables store the system process data/system-related path data. The operations that perform in environment variables are create, modify, delete, and save. There are two types of environment variables.
  • User-level environment variables

  • System-level environment variables

User-Level Environment Variables

User-level environment variables belong to a specific user in an operating system.

System-Level Environment Variable

The variables in a system-level environment can be accessed by every user in the system.

Environment Variable Examples

Table 5-1 shows some of the predefined system-level variables available in every Linux/Unix-based operating system.
Table 5-1

List of Environment Variables

Variable

Description

PWD

It prints the present working directory.

HOME

It prints the default path to the user’s home directory.

SHELL

It prints the location of the shell used by the user.

UID

It prints the user’s unique ID.

HOSTNAME

It displays the computer’s hostname.

Accessing an Environment Variable

To read a value variable, you need to pass the command to the terminal as follows.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig5_HTML.jpg
Figure 5-5

Accessing environment variables using CLI (command-line interface )

Syntaxecho $VARIABLE_NAME

Exampleecho $HOME
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig6_HTML.jpg
Figure 5-6

Creating environment variable using CLI

Note

Variable names are case sensitive. You need to be very careful when accessing data from a variable. The name needs to match exactly to get data from the system.

Setting a New Environment Variable

You can create your own environment variables with the following syntax.

SyntaxVAR_NAME=VALUE

ExampleMY_VARIABLE=/Users/Home

Note

The key point to remember in declaring a variable is that there is no space between the variable name and the value, as shown in the syntax. If there is a space between the name and the value, an error is thrown.

In bash, there is a built-in command named export. If you want to set the environment variable permanently, the export command is useful. This method sets the environment variable for temporary purposes only. It is not available once the terminal session is closed. The export command exports the variable to the permanent system environment variables list, which is not deleted until you delete it explicitly.

Deleting Environment Variables

Deleting an environment variable is done with the unset command.

Syntax ➜ unset VARIABLE_NAME

Example ➜ unset MY_VARIABLE
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig7_HTML.jpg
Figure 5-7

Deleting environment variable using CLI

Note

If you try to access an environment variable that was deleted, you get NULL as a result.

Accessing Environment Variables in C

C provides a built-in getenv() function that retrieves system variable information in a C program. The return type of this function is a pointer to the value in the environment. It takes the character value as an argument and returns the results if there is a variable in the environment list; otherwise, it prints the NULL value. This function is available in the stdlib.h library.
char *getenv(const char *name);
Here’s an example.
#include<stdio.h>
#include<stdlib.h>
int main(){
   char environment_name[50] ;
   printf("Enter the Environment name: ");
   scanf("%s", environment_name);
   printf("Environment Value: %s ", getenv(environment_name));
   return 0;
}
Figure 5-8 shows the output.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig8_HTML.jpg
Figure 5-8

Printing the environment variable using C

Setting a New Environment Variable Using C

C provides a built-in function named setenv() that creates a new environment variable. It is available in the stdlib.h library. The return type of this function is an integer. It returns 0 for the successful creation of an environment variable; it returns –1 for any errors.
int setenv(const char *envname,
           const char *envval,
           int overwrite);
  • envname takes the name of the variable that you want to create as an environment variable.

  • envval takes the environment variable value that you want to assign to the created value.

  • overwrite takes the integer value as argument (i.e., either 0 or 1). A 0 doesn’t overwrite an existing variable value; 1 overwrites the value. If the variable already exists in the environment, a non-zero value overwrites it.

Here’s an example.
#include<stdio.h>
#include<stdlib.h>
int main(){
   char variable_name[15];
   char variable_value[255];
   int overwrittenValue;
   printf("Enter your Variable name:");
   scanf("%s", variable_name);
   printf("Enter the Variable Value: ");
   scanf("%s", variable_value);
   // 1 ---> Represents the Overridden of Value.
   // 0 ---> Doesn't override the value
   printf("Enter the Overridden Value: ");
   scanf("%d", &overwrittenValue);
   // Returns 0 --> On Success || -1 on failure
   int status = setenv(variable_name, variable_value, overwrittenValue);
   if(status == 0){
       printf("Environment variable Created Successfully.! ");
   }else if(status == -1){
       printf("Environment variable Created Successfully.! ");
   }
   return 0;
}
Figure 5-9 shows the output.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig9_HTML.jpg
Figure 5-9

Output of environment variable creation in C

The successful creation of a variable prints a successful message; otherwise, an error message prints.

Deleting an Environment Variable

C provides a built-in function named unsetenv() to clear the environment variable. It is available in the stdlib.h library. The return type of this function is an integer. It returns 0 on the successful deletion of the variable; otherwise, it returns –1.
int unsetenv(const char *name);

The name variable takes the environment variable name, which you want to delete.

Here’s an example.
#include<stdio.h>
#include<stdlib.h>
int main(){
   char variable_name[50];
   printf("Enter the variable to Delete:");
   scanf("%s",variable_name);
   // Returns 0 --> On Success || -1 on failure
   int status = unsetenv(variable_name);
   if(status == 0){
       printf("Environment Variable is Deleted Successfully.! ");
   }else{
       printf("Unable to Delete the Environment variable. ");
   }
   return 0;
}
Figure 5-10 shows the output.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig10_HTML.jpg
Figure 5-10

Output of environment variable deletion using C

Kernel Support for Processes

The kernel is the most important component. It manages all the operations in an operating system. The kernel handles process management and file management as well. In modern computers, multiple processes run simultaneously to execute user tasks and system tasks. These processes require several resources, which include memory, processor time, and hardware resources. The tasks and activities that are done through a kernel are depicted in Figure 5-11.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig11_HTML.jpg
Figure 5-11

Linux kernel-level subsystem

Process Scheduler

The process scheduler schedules programs that are constantly running in the OS and delivers resources within a minimum response time to all programs. This is done with scheduling algorithms. The process scheduler uses two types of algorithms.
  • Preemptive scheduling algorithm

  • Non-preemptive scheduling algorithm

Preemptive Scheduling Algorithm

  • In a preemptive scheduling algorithm , the process is interrupted before the completion of the process task.

  • Starvation occurs after adding a high-priority process to the queue.

  • CPU utilization is high in preemptive scheduling. In preemptive scheduling, you can keep the CPU as busy as possible with multiple processes.

  • Resources are allocated for a limited time.

Non-Preemptive Scheduling Algorithm

  • In a non-preemptive scheduling algorithm, a process is not interrupted until its task are finished.

  • CPU utilization is low. The CPU does not allow other processes to utilize resources.

  • The process utilizes resources until the task is done.

Memory Manager

The memory manager is responsible for managing memory in the operating system.
  • It deals with the implementation of virtual memory, demand paging, and memory allocation for kernel-level space and user-level space programs.

  • It maps the files required to run a process.

  • It effectively manages interprocess communication tasks.

Virtual File System

A virtual file system is an abstract layer of a concrete file system.
  • It acts as a bridge between various file systems, like Windows and macOS. This file system easily communicates with other OS file systems.

  • It accesses different types of files from various file systems in a uniform way.

  • It transparently handles data from network storage devices.

Network Unit

A network unit handles all network activities in the system.
  • It manages certain types of protocols used by network hardware to transfer data between systems.

  • It manages all the network hardware drivers in a system to establish effective communication.

Process Creation

Creating your own process within a program is done with a fork() system call. A newly created process is called a child process, and the process that is initiated to create the new process is considered a parent process. When a fork() system call creates a process, it creates two processes (i.e., parent and child). The diagram shown in Figure 5-12 indicates that the parent process/main process calls the fork() system call to create a process. By default, two subprocesses are created (i.e., parent and child process). A process may create another process for specific work. The creation process is called a parent process, and the created processes are called child processes. A parent process can have many child processes, but a child process has only one parent process.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig12_HTML.jpg
Figure 5-12

Mechanism of process creation

The process created to perform particular operations does a specific job in its life cycle. Before the creation of the process done, it undergoes four steps.
  1. 1.

    Programmer requests the process be created by the program

     
  2. 2.

    System initialization

     
  3. 3.

    Batch job initialization

     
  4. 4.

    Execution of the fork() system call by the running process

     
The built-in fork() system call creates its own process. The return type of this system call is an integer. It returns the three types of values. If the child process is created successfully, it returns 0. The fork() system call internally creates a copy of the process that calls it. If the parent process is successfully created, it returns a positive value. If the process is unable to create it, a negative value is returned. The syntax of the fork() system call is
int fork(void)
The internal workings of the fork() system call is demonstrated in the diagram shown in Figure 5-13. The fork() system call returns one of three values: a negative value for an error, a zero for creating a child process, and a positive value for creating a parent process. When the process ID is zero, the child process is executed, and the parent process is in a waiting state. After the child process execution is completed, the parent resumes the execution. This doesn’t mean that the parent process always waits for the child process to complete its execution. You can make the parent process wait for the child process execution. The parent process terminates once its assigned work is completed. The hierarchy may vary from program to program. All the created processes share the same memory allocated to the program but have a different address space. Figure 5-13 is a simple example of creating a process using the fork() system call.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig13_HTML.jpg
Figure 5-13

Internal mechanism of fork() system call

When a process is created with a fork() system call, two processes (i.e., child and copy of the parent processes) are created. When the main program creates parent and child processes, they try to execute simultaneously. This achieves concurrency in the program.

Here’s an example.
#include <stdio.h>
#include <unistd.h>
int main() {
  int pid = fork();
  if(pid > 0){
      printf("Parent Process is created ");
  }else if(pid == 0){
      printf("Child Process is created ");
   }
  return 0;
}
Figure 5-14 shows the output.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig14_HTML.jpg
Figure 5-14

Output of the program on process creation using C

Zombie Process

A zombie process is any process that has finished executing, but entry to the process is available in the process table for reporting to the parent process. A process table is a data structure that stores all the process-related information in an operating system. A process that is not removed from the process table is considered a zombie. The parent process removes the process entry with the exit status of the child process.

Here’s an example.
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main() {
   pid_t child_pid = fork();
   // Parent process
   if (child_pid > 0){
       printf("In Parent Process.! ");
       // Making the Parent Process to Sleep for some time.
       sleep(10);
   }else{
       printf("In Child process.! ");
       exit(0);
   }
    return 0;
}

In this program, the fork() function creates a new child process. If the child_process value is greater than zero, it is a parent process. If the child process ID is equal to zero, it is a child process. If it is a child process, the program is terminated; otherwise, the parent process is under execution in a sleep state. Meanwhile, the child process is terminated, but the process ID is in the process table, making the child process a zombie.

Orphan Process

A process that does not have a parent process is an orphan process. A child process becomes an orphan when either of the following occurs.
  • When the task of the parent process finishes and terminates without terminating the child process.

  • When an abnormal termination occurs in the parent process.

Here’s an example.
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main() {
   pid_t child_pid = fork();
   // Parent process
   if (child_pid > 0){
       printf("In Parent Process.! ");
   }else{
       printf("In Child process.! ");
       // Making the Child Process to Sleep for some time.
       sleep(10);
       printf("After Sleep Time");
   }
    return 0;
}

In this program, the parent process completes its execution and exits while the child process is in execution, so it is considered an orphan process. If there is no parent for a process, then that process is adopted by the init process.

System Calls for Process Management

When you are working with a process for a task, it is good to know how to manage the processes effectively. Until now, you have seen fork() system calls create a process. This section looks at various types of system calls that manage process activities effectively. The following system calls manage processes.
  • vfork

  • exec

  • wait

  • waitpid

  • kill

  • exit

  • _Exit

vfork System Call

A vfork system call creates a new process, but the behavior is undefined in certain circumstances. If the process is created using a vfork system call, the parent process is blocked until the child block is executed. In the vfork system call, the child process shares a common address space as the current calling process. Since they share the common address space, changes in the code are visible to other processes. The return type of this system call is an integer. When a child is successfully created, it returns 0 and the child process ID to the parent process. If any error occurs, it returns –1.

The following shows the syntax.
pid_t vfork(void)

It takes zero arguments but creates the child process and blocks the parent process.

Here’s an example.
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
   pid_t status;
   status = vfork();
   printf("Process is Executing: %d ", getpid());
   if(status == 0){
       printf("Process is executing: %d ", getpid());
       exit(0);
   }
   return 0;
}
This code explains the working mechanisms of the vfork system call. Initially, the child process is created and executes its task after the parent process is executed.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig15_HTML.jpg
Figure 5-15

Output of process creation using vfork() system call

Note

The vfork() system call is removed from POSIX standards due to its undefined behavior in certain circumstances.

exec System Call Family

The exec system call family replaces the currently running process with a new process. But the original process identifier remains the same, and all the internal details, such as stack, data, and instructions. The new process replaces the executables. This function call family runs binary executables and shell scripts. Figure 5-16 shows the workings of the exec system call.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig16_HTML.jpg
Figure 5-16

Working mechanism of exec system call

There are several system calls of the same family type available in the unistd.h library. They create a new process or execute another binary executable. The family of the exec system call functions include the following.
  • execl

  • execlp

  • execle

  • execv

  • execvp

  • execve

execl( )

This system call takes the first and second parameter as a path of the binary executable. and the remaining parameters are the ones that you need to pass as based on your interest; that is, optional parameters or flags that are required for the executable program and purpose followed by a NULL value. This system call is available in the unistd.h library. The return type of this function is an integer. If the execution is unsuccessful, it returns –1; otherwise, it returns nothing.

The following shows the syntax.
int execl(const char *path, const char *arg, ..., NULL)
  • path takes the binary executable with the complete path.

  • arg also takes the binary executable path as an argument.

  • [...] considers the variable number of arguments, which means you can pass any number of arguments.

  • NULL is the default parameter, which the execl function’s last parameter should be.

Here’s an example.
#include <unistd.h>
int main() {
 char *binary_path = "/bin/ls";
 char *arg1 = "-l";
 char *arg2 = "-a";
 char *arg3 = ".";
  // System call to perform the ls -la operation in the
  // CWD (Current Working Directory)
 execl(binary_path, binaryPath, arg1, arg2, arg3, NULL);
 return 0;
}
This program shows a long list of all the files and directories, including the hidden ones and the execl system call (see Figure 5-17). The advantage of this program is that with one process identifier, another process is also executed.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig17_HTML.jpg
Figure 5-17

Output for the execl system call

execlp( )

This system call is a bit more advanced than the execl() system call. It does not require the path for the binary built-in executable, but for custom executables, it does require the path to execute. The return type of this system call is an integer. It returns –1 if any error occurs and returns anything for successful execution.

The following shows the syntax.
int execlp(const char *path, const char *arg, ..., NULL)
  • path takes the binary executable with the complete path.

  • arg also takes the binary executable path as an argument.

  • [...] considers the variable number of arguments, which means you can pass any number of arguments.

  • NULL is the default parameter, which the execl function’s last parameter should be.

Here’s an example.
#include <unistd.h>
int main() {
 char *binary_executable = "ls";
 char *arg1 = "-la";
 char *arg2 = ".";
  // System call to perform the ls -la operation in the
 // CWD (Current Working Directory)
 execlp(binary_executable, binary_executable, arg1, arg2, NULL);
 return 0;
}
The output of this program is the same as the execl() system call program that prints the long listing of the current working directory (see Figure 5-18).
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig18_HTML.jpg
Figure 5-18

Output for execlp() system call

execle( )

This system call works similarly to the execl() system call. The major difference is that you can pass your own environment variables as an array. You can access the environment variables from the envp constant array pointer. The return type of this system call is an integer. It returns –1 on an error and returns anything for the successful execution of the executable.

The following shows the syntax.
int execle(const char *path,
           const char *arg,
           ..., NULL,
           char * const envp[])
  • path takes the binary executable with the complete path.

  • arg also takes the binary executable path as an argument.

  • [...] considers the variable number of arguments, which means you can pass any number of arguments.

  • NULL is the default parameter, which the execl function’s last parameter should be.

  • envp is an environment pointer variable that lets you access the environment variables from the array. The last element of the array is a NULL value.

Here’s an example.
#include <unistd.h>
int main() {
 char *binary_path = "/bin/zsh";
 char *arg1 = "-c";
 char *arg2 = "echo "Visit $HOSTNAME:$PORT from your browser."";
 char *const envp[] = {"HOSTNAME=www.netflix.com", "PORT=80", NULL};
    // execle() System call can able to access
   // the envp environment variables.
 execle(binary_path, binary_path, arg1, arg2, NULL, envp);
 return 0;
}
The output for this code is a statement to visit the URL in the browser. This is done by accessing the environment variables with the echo statement within a C program.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig19_HTML.jpg
Figure 5-19

Output for execle() system call

execv( )

This execv() system call is slightly different from this all three system calls. In this system call you can pass your parameters as an argv array that you want to execute. The last element of this array is a NULL value. The return type of this system call is an integer value. It returns –1 on an error and returns nothing on success.

The following shows the syntax.
int execv(const char *path, char *const argv[])
  • The path argument points to the path of the executable that is being executed.

  • argv is the second argument. It is a NULL-terminated array of character pointers.

Here’s an example.
#include<stdio.h>
#include<unistd.h>
int main() {
       //A null terminated array of character pointers
       char *args[]={"./hello",NULL};
       execv(args[0],args);
   return 0;
}
In this code, when execv() system call is executed, it calls the ./hello binary executable, which contains the simple hello world program, and is executed.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig20_HTML.jpg
Figure 5-20

Output for execv() system call

execvp( )

This system call works the as same as the execv() system call. The major difference is that you don’t need to pass the path for system executables like an execlp() system call. The execvp() system call tries to find the path of the file in an operating system.

In the following example, the ls command is a program name. The execvp() system call automatically finds its path in the system and performs the action.

The following shows the syntax.
int execvp (const char *file, char *const argv[])
  • file points to the executable file name associated with the file being executed.

  • argv is a NULL-terminated array of character pointers that contain the executables information.

Here’s an example.
#include<stdio.h>
#include<unistd.h>
int main() {
       char *program_name = "ls";
       //A null terminated array of character pointers
       char *args[]={program_name,"-la", ".", NULL};
       execvp(program_name,args);
   return 0;
}
In this code, the execvp() system call calls the built-in ls command, displaying all the contents in a directory. External parameters like -la with . means that it performs a long-list operation by displaying the hidden details of the current directory. This operation simply refers to the ls -la, where . is an external parameter that considers the current working directory of the program.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig21_HTML.jpg
Figure 5-21

Output for execvp() system call

execve( )
This system call works the same as the execle() system call. You can pass the environment variables, and those variables can access it from your program.
int execve(
            const char *file,
            char *const argv[],
            char *const envp[]
          )
Here’s an example.
#include <unistd.h>
int main() {
 char *binary_path = "/bin/bash";
 // Argument Array
 char *const args[] = {binary_path, "-c", "echo "Visit $HOSTNAME:$PORT from your browser."", NULL};
// Environment Variable Array
 char *const env[] = {"HOSTNAME=www.netflix.com", "PORT=80", NULL};
 execve(binary_path, args, env);
 return 0;
}
This code is the same as the execle() system call output, as shown in Figure 5-22.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig22_HTML.jpg
Figure 5-22

Output for execve() system call

wait System Call

In some situations, a process needs to wait for resources or for other processes to complete execution. A common situation that occurs during the creation of a child process is that the parent process needs to wait or suspend until the child process execution is completed. After the child process execution completes, the parent process resumes execution. The work of the wait system call is to suspend the parent system call until its child process terminates. This wait system call is available in the sys/wait.h header file. The process ID is the return type of the wait system call. On successful termination of the child process, it returns the child process ID to the parent process. If the process doesn’t have any child processes, the initiated wait call does not affect the parent activity. It returns –1 if there are no child processes. If the parent process has multiple child processes, the wait() call returns the appropriate result to the parent when the child processes have terminated.

The following shows the syntax.
pid_t wait(int *status)
This system call takes the child status as an argument and returns the terminated child process ID. If you don’t want to give the child status, you can use the NULL value. The workings of the wait function are shown in the Figure 5-23 diagram.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig23_HTML.jpg
Figure 5-23

Working mechanism of wait() system call

Here’s an example.
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
 int main() {
   int status = fork();
   if (status == 0) {
       printf("Hello from child ");
       printf("Child work is Completed and terminating.! ");
   }else if(status > 0){
       printf("Hello from parent ");
       wait(NULL);
       printf("Parent has terminated ");
   }
   return 0;
}
In this program, the parent process is executed first, and then it enters a wait state. When the parent process enters a wait state, the child process enters the action to execute its assigned task. Once the child task is completed and terminated, the parent completes the remaining tasks that are assigned to it.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig24_HTML.jpg
Figure 5-24

Output of the C program for wait() system call

waitpid System Call

The waitpid() system call is an advanced version of the wait() system call. It takes three parameters as arguments. The first parameter takes the child process identifier. The second parameter deals with the status of the child process and stores the status code of the child process. The third parameter is an options parameter that takes several options to get the child process-related information. The values that are passed to this argument are built-in macros. The return type of the waitpid system call is a process ID. If an error occurs, it returns –1.

The following shows the syntax.
pid_t waitpid(pid_t pid, int *status, int options)

The following are options parameters.

  • WIFEXITED(status): It checks if the child exits normally or not.

  • WEXITSTATUS(status): It returns the status code when a child exits.

  • WIFSIGNALED(status): It informs the child exit status if the child exits because a signal was not caught.

  • WTERMSIG(status): It gives the number of terminating signals.

  • WIFSTOPPED(status): It returns the status information when the child stops execution.

  • WSTOPSIG(status): It returns the number of stop signals in a program.

  • WUNTRACED: It returns the child status that has stopped, but it doesn’t trace the child.

  • WNOHANG: It returns the status immediately if the child exits.

  • WCONTINUED: It returns the status code if a signal resumes the stopped child process.

Here’s an example.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
   int pid;
   int status;
   pid = fork();
   // Terminates the Child process .
   if(!pid){
       printf("My PID: %d ",getpid());
       _exit(0);
   }
   waitpid(pid,&status,WUNTRACED);
   if(WIFEXITED(status)) {
       printf("Exit Normally ");
       printf("Exit status: %d ",WEXITSTATUS(status));
       _exit(0);
   }else {
       printf("Exit NOT Normal ");
       _exit(1);
   }
   return 0;
}
In this code, you get the status of the child process that is being terminated explicitly. WEXITSTATUS returns the status of the exited child process. WUNTRACED untraces the exited child process.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig25_HTML.jpg
Figure 5-25

Output of C program using waitpid() system call

kill System Call

A kill system call kills processes and signals. Killing a signal or process is the termination of a program/process/signal. The return type of this kill system call is an integer value. It returns 0 on the successful execution of the system call; otherwise, it returns –1 for an error.

The following shows the syntax.
int kill(pid_t pid, int sig);
  • pid takes the process identifier of the process.

  • sig takes the built-in signal parameter that needs to send to the process.

Here’s an example.
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main(){
   int pid = fork();
   if(pid == 0){
       printf("Child PID: %d ",getpid());
   }else{
       printf("Parent PID: %d ", getppid());
   }
   sleep(2);
   kill(getpid(), SIGQUIT);
   return 0;
}
This code prints the process ID of the child and parent processes. The current process ID is set to the kill system call that kills the currently running program after sleeping for two seconds.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig26_HTML.jpg
Figure 5-26

Output of the C program for kill() system call

exit System Call

An exit system call exits the calling process without executing the rest of the code that is present in the program. It is available in the stdlib.h library. The return of this system call is void. It doesn’t return anything on execution.

The following shows the syntax.
void exit(int status)

status takes the value that is returned to the parent process.

Here’s an example.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
   int pid = fork();
   if(pid == 0){
       // Prints the Child Process ID.
       printf("Child Process ID: %d ", getpid());
       exit(0);
   }else{
       // Prints the Parent Process ID.
       printf("Parent Process Id: %d ", getppid());
       exit(0);
   }
  printf("Processes are exited and this line will not print ");
   return 0;
}
This code prints the parent and child process ID and exits the program without executing the last printf statement. This is because the exit() system call has exited the parent and child processes, and there is no process left to execute the last printf statement, so it doesn’t print to the console screen.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig27_HTML.jpg
Figure 5-27

Output of the C program for exit() system call

_Exit System Call

_Exit terminates the process normally, but it doesn’t perform any cleanup activity. This system call is available in the unistd.h library. The return type of this system call is void. It doesn’t return anything. After the process is terminated, the control is given to the host environment (currently running) in this system call.

The following shows the syntax.
void _Exit(int status)

status takes the value, which is returned to the parent process.

Here’s an example.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
   printf("Current Running Process ID: %d ", getpid());
   _Exit(0);
   printf("Nothing will execute ");
   return 0;
}

This code does not give any results. It simply terminates the process and returns control to the host environment.

Introduction to Signals

A signal is a software interrupt or an event generated by a Unix/Linux system in response to a condition or an action. There are several signals available in the Unix system. All signal mechanisms are implemented in the signals.h library. In this section, the signals.h library is used to create custom signals and to handle the signals that are created by the system. When a signal is raised, the kernel is guided as discussed next.

Catch the Signal

When the kernel raises a signal, you can create a custom routine to handle the signal. But to use your custom handling routines, the process needs to register the custom routine before the processed signal is delivered to the user space.

Ignore the Signal

When the program is raising a signal, and that signal has no effect, you go to the ignore case. This ignores the signal that does not affect the program, but you need to explicitly mention it before the signal is delivered. All signals can’t be ignored. The signals that have no effect on raising a signal can be ignored.

Default Action

When a program raises a signal, and that signal is neither caught nor ignored, it is handled by the default built-in signal handler that is defined by the system. It is an implicit system behavior meant for handling the signal. But a process can explicitly request to use the built-in signal handler in the program. Default handlers do not always terminate a process.

Every signal has certain attributes. The name and the signal number identify the signal very easily. Every signal has a certain functionality associated with it, which makes signals very handy. All the available built-in signals supported by the system can be printed with the kill command.
kill -l
The signals in Figure 5-28 are the signals that are supported by the Linux system.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig28_HTML.jpg
Figure 5-28

List of all the built-in signals

The commonly used signals and the functionalities are described in Table 5-2.
Table 5-2

Signals and Their Functionality

Signal Name

Signal Number

Signal Functionality

SIGHUP

1

Hang up a signal

SIGINT

2

Interrupt (Ctrl+C)

SIGQUIT

3

Quit (Ctrl+D)

SIGABRT

6

Process Abort

SIGKILL

9

Kills the process without cleanup activity

SIGUSR1

10

User-defined signal 1

SIGSEGV

11

Invalid Memory Segment Access

SIGUSR2

12

User-defined signal 2

SIGALRM

14

Alarm Signal

SIGTERM

15

Program/Software Termination Signal

SIGCHLD

17

Child process has stopped or exited

SIGCONT

18

Continue Execution

SIGSTOP

19

Stop Execution

SIGTSTP

20

Stop Signal

SIGTTIN

21

Background process trying to read

SIGTTOU

22

Background process trying to write

The actual list of signals may vary between Solaris and Linux. All the signal lists are available in the signal.h library. By using signals, you can set traps and interrupts. In the C standard library, there is a signal() system call that creates the signals. The return type of the signal system call is a pointer to a function that takes the single integer parameter and returns nothing (i.e., void). If successful, this system call returns the previous action. If any error occurs, it returns SIG_ERR to indicate the error. This system call also has a typedef version that is easy to read and understand. But in this chapter, you are dealing with the syntax of the original signal system call.

The following shows the syntax.
void (*signal(int sig, void (*function)(int)))(int)
  • sig takes the signal number. The signal number completely depends on the purpose and the type of signal you want to send.

  • function is a pointer that points to either the function implemented by the programmer or the built-in ones. These are the built-in functions.
    • SIG_DFL handles the signal by default. It is considered the default handling of signals, which means it sends the interrupt that is caused by the program.

    • SIG_IGN ignores the signal that is caused by the program.

Here’s an example.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void CUSTOM_HANDLER(int);
int main () {
   // SIGINT is used to intimate when any interrupt occurs to
  // the program.
  signal(SIGINT, CUSTOM_HANDLER);
  while(1) {
     printf("Hello World...! ") ;
     sleep(1);
  }
  return 0;
}
// This function will call when any signal interrupt occurs.
void CUSTOM_HANDLER(int signum) {
  printf("Caught signal %d, coming out from Program ", signum);
  exit(1);
}
This code prints “Hello World...!” an infinite number of times. If an interrupt occurs in the program, SIGINT immediately catches that signal and sends it to the CUSTOM_HANDLER function to handle it.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig29_HTML.jpg
Figure 5-29

Signal generation output

Types of Signals

In Unix/Linux, signals are classified into two types based on functionality: unreliable and reliable.

Unreliable Signals

Signals that doesn’t have any available installed signal handler and become lost means the process never knows about the signal that is being raised by the system. A process has very little control over unreliable signals. The process can catch a signal or ignore it, but blocking a signal is not possible with unreliable signals. A blocking operation means intimating the operating system explicitly to hold the signal for a certain time and releasing it when asked by the program.

Reliable Signals

Signals that are not lost in the system are reliable. The process has complete control and can catch, ignore, and block signals using system calls. These signals are the enhanced version in Unix-based system.

System Calls for Signals

There are different system calls available in the signal.h library that manipulates the signals. Signal manipulation can be done with the following system calls.
  • raise

  • kill

  • alarm

  • pause

  • abort

  • sleep

raise System Call

A raise system call raises a signal by the process itself. The return type of this system call is an integer. This system call returns zero on success and nonzero if a failure occurs.

The following shows the syntax.
int raise(int sig)

sig is the signal number that needs to be sent. This parameter depends on the type of signal you want to raise explicitly to the process itself. The signal numbers are from the built-in signals list.

Here’s an example.
#include <stdio.h>
#include<stdlib.h>
#include <signal.h>
void SIGNAL_HANDLER(int);
int main () {
   signal(SIGINT, SIGNAL_HANDLER);
   printf("Raising a new signal ");
   int status = raise(SIGINT);
   if(status != 0){
     printf("Something went wrong Unable to raise the new signal ");
   }
  return 0;
}
void SIGNAL_HANDLER(int signal) {
  printf("signal caught and handled gracefully ");
}
This code raises a signal for the running process, and that signal is handled by SIGNAL_HANDLER.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig30_HTML.jpg
Figure 5-30

Output of the raise system call

kill System Call

A kill system call sends signals to other processes as well itself. A kill system call can also kill processes. The killing/terminating of a signal is similar to killing/terminating a process in an operating system.

alarm System Call

In signals, there is an alarm clock facility that schedules the signal trap for the future. This system call is used by the process to schedule the SIG_ALARM signal. The return type of the alarm system call is an unsigned integer. It returns the number of seconds remaining in the set time that is to be delivered. If no alarm is set, it returns 0.

The following shows the syntax.
unsigned int alarm(unsigned int seconds)

seconds takes time in the form of seconds. The second’s value must be a positive number.

Here’s an example.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void raisedAlarm(int sig);
int main(){
   alarm(5);
   signal(SIGALRM, raisedAlarm);
   while(1){
       printf("Hello World...! ");
       sleep(1);
   }
   return 0;
}
void raisedAlarm(int sig){
   printf("The Alarm has Raised. ");
   exit(0);
}
This code raises the alarm after seconds of code execution. It is very handy to set signal traps for time-dependent applications. Since the exit() function is used in the raisedAlarm function, it terminates the program.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig31_HTML.jpg
Figure 5-31

Output of the alarm system call

pause System Call

The pause system call suspends the execution of a program until a signal occurs. The return type of a pause system call is an integer. It takes 0 parameters. It returns –1 on failure; otherwise, it returns the respective signal catching function.

The following shows the syntax.
int pause(void)
Here’s an example.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void SIGNAL_HANDLER(int);
int main(void){
   alarm(10);
   signal(SIGALRM, SIGNAL_HANDLER);
   if(alarm(7) > 0){
       printf("An alarm has been set already. ");
   }
   pause();
   printf("You will not see this text. ");
   return 1;
}
void SIGNAL_HANDLER(int signo){
   printf("Caught the signal with number: %d ", signo);
   exit(0);
}
This code catches the alarm signal interrupt. The remaining code below pause() does not work. This is because pause() suspends the current running program, but the alarm function and its handlers work parallelly. When SIGALRM is raised, the custom handler handles it.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig32_HTML.jpg
Figure 5-32

Output of pause system call

abort System Call

The abort system call terminates the program or process abnormally. This system call returns a void type. It takes zero parameters. This system call sends the SIGABRT signal to the process to terminate. This signal is not able to be overridden by other signals. This system call does not close all the files and pointers opened by the process since it causes an abnormal termination of the program.

The following shows the syntax.
void abort(void)
Here’s an example.
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
int main(){
   int status = fork();
   if(status == 0){
       printf("Child Process ID: %d ", getpid());
   }else if(status > 0){
       printf("Parent Process ID: %d ", getpid());
   }
   abort();
   printf("Due to abnormal termination this line will not execute. ");
   return 0;
}
This program creates a child and parent process. After creating processes, the remaining lines of code that are present below abort() are not executed. This is because abort terminates the program abnormally. But the output of the program may differ.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig33_HTML.jpg
Figure 5-33

Output of the abort system call

sleep System Call

This sleep system call sleeps the thread until the specified number of seconds have elapsed or a signal hits (which is not ignored). The return type of this system call is an unsigned integer. It returns 0 if the requested time has elapsed, or the number of seconds left to sleep if the call is interrupted by a signal handler.

The following shows the syntax.
unsigned int sleep(unsigned int seconds)

seconds takes the number of seconds that the process or thread wants to sleep as an argument.

Here’s an example.
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
   for(int i=0; i<5; i++){
       printf("Hello World. ");
       sleep(1);
   }
   return 0;
}
This code prints “Hello World” five times by sleeping for 1 second after every iteration of the loop.
../images/497677_1_En_5_Chapter/497677_1_En_5_Fig34_HTML.jpg
Figure 5-34

Output of sleep system call

Summary

In this chapter, you were introduced to the process environment, including how to create and terminate a process.
  • You looked at the environment variable and how to create it programmatically and by using commands.

  • You explored the memory layout of the C program and how things are stored in a computer’s memory.

  • Kernel support for the process teaches you about the Linux subsystem. In the Linux subsystem, you looked at various management schemes done by the Linux kernel internally.

  • The creation of processes achieves concurrency. You learned a lot about how to create processes using built-in system calls in C.

  • You learned that a process could become a zombie or an orphan.

  • You learned about the various system calls that are available for process management. The system calls include vfork(), wait(), waitpid(), kill(), execv family system calls, and exit(), and _Exit() system calls.

  • You saw signals and traps set in a program to create your custom interrupt in a program. You learned types of signals and system calls for signal management include abort(), sleep(), pause(), alarm(), raise(), and kill().

You now know the core concepts of process and signals in an operating system. You should be able to work with your custom applications and scripts in a very comfortable manner.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.143.239