Brief on Linux process
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
It is impossible to cover the whole aspect of process in 1 or 2 articles.
In this article, we will try to explore the concepts such as process creation, process death ,init process, zombie process, process group etc. in detail.
I already covered the basic of process like definition and difference with thread in earlier chapter in previous article :Thread/Process in Linux | LinkedIn
Process Creation:
In Linux, a process is a program or application that is executing. Each process has its own memory space, file descriptors, and a unique process ID (PID). A process can be created in Linux using the fork() system call. The fork() system call creates a new child process that is a copy of the calling parent process. The child process inherits some attributes of its parent, such as the environment and open file descriptors.
In the Linux kernel, processes are handled by the scheduler, which is responsible for allocating CPU time to processes. Each process is represented by a task_struct data structure, which contains information such as the process's state, priority, and resources.
The task_struct data structure is a kernel data structure used to represent a process in the Linux kernel. The task_struct data structure is defined in the Linux kernel source code in the file "sched.h".
Here is a list of some of the important fields of the task_struct data structure:
The scheduler uses a scheduling algorithm to determine which process should run next. The Linux kernel uses a scheduler called the Completely Fair Scheduler (CFS) which is designed to provide a fair distribution of CPU time among all processes.
The size of the task_struct data structure in Linux kernel depends on the version of the kernel and the architecture of the system, and it's also affected by the kernel configuration options. The size of task_struct can vary from few kilobytes to several hundred kilobytes.
It's important to note that the task_struct data structure is a kernel data structure and it's not a part of the Application Programming Interface (API) that is exposed to user-space programs. Therefore, it's not possible to know the exact size of task_struct without looking into the kernel source code and the configuration options of your specific system.
Additionally, the size of task_struct can also vary depending on the features enabled in the kernel such as CPU and memory hotplug, kernel threads and real-time scheduling, and other features that may increase the size of the task_struct.
The init process is the first process that is created when the Linux system boots up. It has the process ID (PID) of 1 and is responsible for initializing and starting other processes. The init process is the parent of all other processes on the system, and it is responsible for reaping the resources of any zombie that are created.
In Linux the task list, which is used to keep track of all the tasks (processes and threads) that are currently running on the system, is represented by a doubly linked list.
Each task_struct contains a set of pointers that link it to the next and previous task_struct in the list, forming a doubly linked list. This allows the kernel to easily traverse the list to perform operations such as scheduling, context switching, and process management.
The list is represented by init_task which is an instance of task_struct and is the head of the list. This init_task is created during the initialization of the kernel, and it serves as the parent of all other tasks in the system. Each task in the system is represented by a task_struct struct, and they are linked together by next and prev pointers.
The kernel uses this doubly linked list to traverse through all the tasks, and perform operations such as scheduling, context switching, and process management. The kernel uses the task list to keep track of the state of each task (such as running, stopped, or zombie) and to decide which task should be executed next. It also uses the list to manage the resources of each task, such as memory, file descriptors, and signal handlers.
In summary, the task list in Linux is represented by a doubly linked list, where each task is represented by a task_struct struct, which contains a set of pointers that link it to the next and previous task_struct in the list. This allows the kernel to easily traverse the list to perform operations such as scheduling, context switching, and process management.
The init process also starts other processes such as system daemons and services, and it is responsible for shutting down the system cleanly when it receives a shutdown signal. The init process also monitors the system and restarts any services that may have failed.
When a new process is created, it is added to a list of runnable processes, called the run queue. The scheduler selects the next process to run from the run queue based on the process's priority and the scheduling algorithm in use.
The scheduler also supports multi-tasking, allowing multiple processes to run concurrently on a single CPU. This is achieved by using time slicing, where each process is given a small time slice of the CPU to execute its instructions, and then it is preempted and moved back to the run queue. The process is then later rescheduled to continue its execution.
The Linux kernel also supports real-time scheduling, which allows processes with real-time constraints to be scheduled with a higher priority than normal processes. This helps to ensure that real-time processes are not delayed by other processes.
Additionally, the kernel also provides a feature called 'cgroups' which is used to manage and limit resources for a group of processes. This allows to guarantee a certain level of resources for a specific set of processes and prevent one process from monopolizing the resources and affecting the performance of the other processes.
The Linux kernel manages processes using the scheduler, which allocates CPU time to processes based on their priority and the scheduling algorithm in use. The kernel also provides features such as time slicing, real-time scheduling and cgroups to manage resources and guarantee a certain level of resources for a specific set of processes.
As discussed, earlier process can be created in Linux using the fork() system call. The fork() system call returns twice, once in the parent process and once in the child process. In the parent process, the return value is the PID of the child process, while in the child process, the return value is 0. This allows the parent and child processes to differentiate between them and take appropriate actions.
Process Death:
In Linux, a process can die for several reasons, such as completion of execution, receiving a signal, or a crash. When a process dies, its resources are freed, and its process ID (PID) becomes available for reuse.
Death Handling:
When a process creates a child process, it may want to wait for the child process to complete before continuing its own execution. The wait() system call is used for this purpose. The wait() system call blocks the execution of the calling process until one of its child processes dies or was stopped by a signal or resumed by a signal..
The wait() system call returns the exit status of the child process and the child's process ID (PID). The exit status of a process can be used to determine if the process completed successfully or if it encountered an error.
Another important system call related to process death handling is waitpid(). This system call works similarly to wait(), but allows the caller to specify which child process to wait for, using the child's process ID (PID).
Additionally, there's also waitid() which allows the caller to wait for a specific child process and also get more information about the state of the child.
All of these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal. In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then terminated the child remains in a "zombie" state (see NOTES below).
If a child has already changed state, then these calls(wait and waitpid ) return immediately. Otherwise they block until either a child changes state or a signal handler interrupts the call (assuming that system calls are not automatically restarted using the SA_RESTART flag of sigac-tion(2)).
When we are discussing the process it is important to discuss about "zombie"
"zombie or orphan" process is a process that has completed execution but its parent process has not yet called the wait() system call to release its resources. When a process completes execution, it enters the "zombie" state, and its process ID (PID) and other resources are not freed until the parent process calls the wait() system call.
When a process becomes a zombie it still consumes some system resources such as process ID, but it no longer consumes any CPU time and zombie proce gets re-parented by the Init process. A zombies process occupies memory because the kernel keeps a task_struct data structure for each process, which contains information about the process such as its state, priority, and resources.
The task_struct data structure takes up a certain amount of memory, and if there are too many zombies processes, the memory consumed by the task_struct data structures can add up and cause the system to run out of memory. This can lead to performance degradation and in severe cases, system crash.
The kernel keeps some minimal information about the process in memory, such as its exit status, until the parent process retrieves it via the wait() system call.
What is the problem having the many Zombie process?
Having too many zombie processes can cause a few problems as below :
领英推荐
It's important to note that the number of zombies process should be kept to a minimum. If a parent process is not properly handling the child process, it could lead to zombies process buildup. In such cases, it's important to identify the root cause of the problem and fix it to prevent the accumulation of zombies processes.
It's also important to have a proper exception handling and recovery mechanism in the parent process that can handle the child process death and release the resources. This can prevent accumulation of zombies process in the system.
Below is example of fork(), wait() and waitPid()
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid;
int status;
pid = fork();
if (pid == 0) {
printf("I am the child process, my PID is %d\n", getpid());
_exit(0);
} else if (pid > 0) {
wait(&status);
// waitpid(pid,&status,0); // Uncomment this like if waitpid need to be used
printf("I am the parent process, my PID is %d and my child's PID is %d\n", getpid(), pid);
if (WIFEXITED(status)) {
printf("Child process terminated normally with status %d\n", WEXITSTATUS(status));
}
} else {
printf("Fork failed!\n");
}
return 0;
}
In this example, the `waitpid()` system call is used in the parent process to wait for a specific child process (specified by the PID `pid`) to terminate. The `waitpid()` system call takes three arguments: the PID of the child process to wait for, a pointer to an integer to store the exit status of the child process, and a set of options. In this example, the options are set to 0, which means that the call will block until the child process specified by the PID terminates.
The `waitpid()` system call is more flexible than the `wait()` system call, it allows the parent process to wait for a specific child process to terminate and it allows the parent process to specify options to control the behavior of the call.
In both examples, it is important to wait for child process termination, otherwise, the child process will become a "zombie" process, which can cause memory leak and other issues.
There is one more way to handle the child's death by using the SIGCHLD signal, Ex below.
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
// Signal handler for SIGCHLD to handle terminated child processes
void sigchld_handler(int sig) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("Child process %d terminated with status %d\n", pid, status);
}
}
// Function to initialize SIGCHLD signal handling
void setup_signal_handler() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
}
// Function to create a child process
pid_t create_child(int sleep_time, int child_num) {
pid_t pid = fork();
if (pid == 0) { // Child process
printf("I am child process %d, my PID is %d\n", child_num, getpid());
sleep(sleep_time); // Simulate work
_exit(0);
} else if (pid < 0) { // Fork failed
perror("Fork failed");
}
return pid; // Return child PID to the parent
}
// Function to run parent process logic
void run_parent_process() {
printf("I am the parent process, my PID is %d\n", getpid());
while (1) {
sleep(1);
printf("Parent process is doing some work...\n");
}
}
int main() {
setup_signal_handler(); // Set up SIGCHLD handling
// Create child processes
pid_t pid1 = create_child(3, 1);
pid_t pid2 = create_child(6, 2);
pid_t pid3 = create_child(9, 3);
if (pid1 > 0 && pid2 > 0 && pid3 > 0) {
run_parent_process(); // Run parent logic
}
return 0;
}
/*
I am the parent process, my PID is 4283
I am child process 1, my PID is 4284
I am child process 2, my PID is 4285
I am child process 3, my PID is 4286
Parent process is doing some work...
Parent process is doing some work...
Child process 4284 terminated with status 0
Parent process is doing some work...
Parent process is doing some work...
Parent process is doing some work...
Child process 4285 terminated with status 0
Parent process is doing some work...
Parent process is doing some work...
Parent process is doing some work...
Child process 4286 terminated with status 0
Parent process is doing some work...
*/
IIn this example, the parent process sets up a signal handler for the `SIGCHLD` signal using the `sigaction()` system call. The `sigchld_handler()` function is specified as the signal handler and it is called when the parent process receives the `SIGCHLD` signal. The signal handler uses the `waitpid()` system call with the `WNOHANG` option to wait for child processes to terminate, and it prints a message when a child process terminates.
The `SIGCHLD` signal is useful for a parent process that wants to be notified when its child process terminates, instead of continuously polling the status of the child process. It also allows the parent process to be notified of multiple child process termination, in case of running many child processes in parallel.
It's important to note that, by default the `SIGCHLD` signal is ignored, if not handled properly it can cause zombie process, which can cause memory leak and other issues.
The waitpid() system call and the SIGCHLD signal are two different ways of handling child process termination in a parent process.
The waitpid() system call is used to wait for a specific child process to terminate and retrieve its exit status. The parent process can use the waitpid() system call to wait for a specific child process to terminate by specifying the child's process ID as an argument. The parent process can also specify options to control the behavior of the call, such as whether to block or to return immediately if the child process has not yet terminated.
On the other hand, the SIGCHLD signal is sent to the parent process when one of its child processes terminates or stops. The parent process can choose to handle the signal using the signal() or sigaction() system calls, and it can specify a signal handler function to be executed when the signal is received.
The waitpid() system call is more efficient when you need to wait for a specific child process to terminate and retrieve its exit status, it can be blocking if the parent process needs to wait for a specific child to complete.
Pid passed to waitpid() can have the following values:
Difference between wait and waitpid():
The SIGCHLD method is useful when the parent process want to handle multiple child process termination and doesn't need the exit status of each child process, it also allows the parent process to do other things while it's waiting for child processes to terminate.
In summary, the waitpid() system call is a blocking call that waits for a specific child process to terminate and retrieves its exit status, while the SIGCHLD signal is a non-blocking method that informs the parent process when one of its child processes terminates or stops, it allows the parent process to handle multiple child process termination.
Meaning of WNOHANG ,WIFEXITED and WEXITSTATUS
WNOHANG is an option that can be passed to the waitpid() system call in Linux. It is used to specify that the call should return immediately if the child process specified by the process ID has not yet terminated. This option is useful for parent processes that want to periodically check the status of a child process without blocking. It allows the parent process to continue doing other things while it's waiting for the child process to terminate.
WIFEXITED(status) is a macro in Linux that can be used to test the exit status of a child process. It takes an integer status as an argument, which is the exit status of the child process as returned by the wait(), waitpid() or waitid() system call.
WEXITSTATUS(status): It holds exit status if WIFEXITED is non-zero.
When to use wait/waitpid and when to use sigchld for receiving death of child process
The decision of whether to use the wait() or waitpid() system call or the SIGCHLD signal to handle child process termination in a parent process depends on the specific requirements of the program and the desired behavior of the parent process.
If the parent process needs to wait for a specific child process to terminate and retrieve its exit status, then the wait() or waitpid() system call is more appropriate. The wait() system call waits for any child process to terminate, while the waitpid() system call allows the parent process to wait for a specific child process to terminate by specifying its process ID.
If the parent process wants to handle multiple child process termination and doesn't need the exit status of each child process, then the SIGCHLD signal is more appropriate. The SIGCHLD signal is sent to the parent process when one of its child processes terminates or stops, and the parent process can specify a signal handler function to be executed when the signal is received. This allows the parent process to handle multiple child process termination in a non-blocking way, and it can continue to do other things while it's waiting for child processes to terminate.
It's also important to note that, if the parent process does not handle the SIGCHLD signal, it will be ignored by default, and the child process will become a zombie process, which can cause memory leak and other issues.
In summary, if the parent process needs the exit status of a specific child process, the waitpid() system call or wait() system call is the best option. If the parent process wants to handle multiple child process termination and doesn't need the exit status of each child process, the SIGCHLD signal is the best option.
How to inform the process is Realtime in Linux ?
In Linux, a process can be marked as a real-time process using the sched_setscheduler() system call. This system call sets the scheduling policy and parameters of a given process.
The scheduling policies available in Linux are:
To mark a process as a real-time process, the sched_setscheduler() system call is called with the process ID of the process and the desired scheduling policy (e.g. SCHED_FIFO or SCHED_RR) as arguments.
Here is an example of how the sched_setscheduler() system call can be used to mark a process as a real-time process:
#include <sched.h>
#include <unistd.h>
int main() {
struct sched_param param; param.sched_priority = 50;
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
perror("sched_setscheduler"); return -1;
} // Process code here return 0;
}
In this example, the sched_setscheduler() system call is called with the process ID of the current process (0), the SCHED_FIFO scheduling policy, and a sched_param structure with a priority of 50. This sets the scheduling policy of the current process to SCHED_FIFO, which is a real-time policy and sets the priority of the process to 50.
It's important to note that, in order to use real-time scheduling policies, the process should have the necessary privileges, in general, the process need to run as a superuser.
In conclusion, process creation, process death, and death handling are important concepts in the Linux operating system. The fork() system call is used to create new processes, the wait() system call is used to wait for child processes to complete, and the waitpid() and waitid() system calls are used to wait for specific child processes.
I try to cover the rest of the process topics such as demon process , process states, process group , process communication, execs families ,procs etc in next article .
Thank you for reading till end .
If you have any questions or suggestions, please leave a comment.
Referance:
nice