Table of Contents
Understanding processes in Linux is fundamental for working efficiently with the system. Processes are instances of executing programs and are a core part of any Unix-like operating system. Below is a detailed tutorial on Linux processes, including how to create and manage them using C programming language examples.
What is a Process?
In Linux, a process is a running instance of a program. Each process is assigned a unique Process ID (PID) and has its own address space, files, and resources. Processes can create other processes through a system call named fork()
.
Process identification
Identifying a process in C
on a Linux system can be done using the getpid()
and getppid()
functions. These functions are used to get the process ID (PID) of the current process and the PID of the parent process, respectively.
Getting the Current Process ID with getpid()
The getpid()
function returns the process ID of the calling process. This is useful for when you need to reference the current process programmatically.
Example:
#include <stdio.h>
#include <unistd.h>
int main() {
// Get the current process ID
pid_t pid = getpid();
// Print the process ID
printf("The current process ID is %d\n", pid);
return 0;
}
code execution on linux SOC:
Getting the Parent Process ID with getppid()
The getppid()
function returns the parent process ID of the calling process. This can be particularly useful in a child process created by fork()
, to reference the PID of the process that created it.
Example:
#include <stdio.h>
#include <unistd.h>
int main() {
// Fork a new process
pid_t pid = fork();
if (pid == 0) {
// In child process
printf("This is the child process. PID: %d, Parent PID: %d\n", getpid(), getppid());
} else if (pid > 0) {
// In parent process
printf("This is the parent process. PID: %d\n", getpid());
} else {
// Fork failed
fprintf(stderr, "Fork failed.\n");
return 1;
}
return 0;
}
code execution on linux SOC:
@startuml
!theme toy
participant "Main Process" as main
participant "fork()" as fork
participant "Child Process" as child
main -> fork : Calls fork()
alt pid == 0
== Child Process ==
fork -> child : In child process
child -> child : printf("This is the child process. PID: %d, Parent PID: %d\n", getpid(), getppid())
child -> main : Child process ends, return 0
else pid > 0
== Parent Process ==
fork -> main : In parent process (Main Process continues)
main -> main : printf("This is the parent process with PID: %d\n", getpid())
main -> main : Parent process ends, return 0
else
== Fork Failed ==
fork -> main : Fork failed
main -> main : fprintf(stderr, "Fork failed.\n")
main -> main : return 1
end
@enduml
In this example, the parent process forks a child process. Both processes then print their PID and, in the case of the child, also the parent's PID. This demonstrates how to identify and differentiate processes in a C
program running on Linux.
Creating a Process with fork()
The fork()
system call is used to create a new process by duplicating the current process. The new process is called the child process, and the original process is called the parent process.
Example:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("This is the child process with PID: %d\n", getpid());
} else if (pid > 0) {
// Parent process
printf("This is the parent process with PID: %d\n", getpid());
} else {
// Fork failed
fprintf(stderr, "Fork failed.\n");
return 1;
}
return 0;
}
code execution on linux SOC:
@startuml
!theme toy
participant "Main Process" as main
participant "fork()" as fork
participant "Child Process" as child
main -> fork : Calls fork()
alt pid == 0
== Child Process ==
fork -> child : In child process
child -> child : printf("This is the child process with PID: %d\n", getpid())
child -> main : Child process ends, return 0
else pid > 0
== Parent Process ==
fork -> main : In parent process (Main Process continues)
main -> main : printf("This is the parent process with PID: %d\n", getpid())
main -> main : Parent process ends, return 0
else
== Fork Failed ==
fork -> main : Fork failed
main -> main : fprintf(stderr, "Fork failed.\n")
main -> main : return 1
end
@enduml
Process Synchronization with wait()
To synchronize the parent process with the termination of its child processes, you can use the wait()
system call. This call allows the parent to wait for all of its children to terminate.
Example:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("Child process with PID %d\n", getpid());
} else if (pid > 0) {
// Parent process waits for the child to finish
wait(NULL);
printf("Child has finished: Parent PID %d\n", getpid());
} else {
// Fork failed
fprintf(stderr, "Fork failed.\n");
return 1;
}
return 0;
}
code execution on linux SOC:
@startuml
!theme toy
participant "Main Process" as main
participant "fork()" as fork
participant "Child Process" as child
participant "wait()" as wait
main -> fork : Calls fork()
alt pid == 0
== child Process ==
fork -> child : In child process
child -> child : printf("Child process with PID %d\n", getpid())
child -> main : Child process ends, return 0
else pid > 0
== Parent Process ==
fork -> main : In parent process
main -> wait : Calls wait(NULL)
wait -> main : Child has finished
main -> main : printf("Child has finished: Parent PID %d\n", getpid())
main -> main : Parent process ends, return 0
else
== Fork Failed ==
fork -> main : Fork failed
main -> main : fprintf(stderr, "Fork failed.\n")
main -> main : return 1
end
@enduml
@enduml
Executing a New Program with exec()
The exec()
family of functions replaces the current process image with a new process image. It is commonly used in conjunction with fork()
to replace the child process's image.
Example:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child process
char *args[] = {"/bin/ls", "-l", NULL};
execvp(args[0], args);
// If execvp returns, it must have failed.
printf("execvp failed\n");
} else if (pid > 0) {
// Parent process
wait(NULL); // Wait for the child to finish
} else {
// Fork failed
fprintf(stderr, "Fork failed.\n");
return 1;
}
return 0;
}
code execution on linux SOC:
@startuml
!theme toy
participant "Main Process" as main
participant "fork()" as fork
participant "Child Process" as child
participant "wait()" as wait
participant "execvp()" as execvp
main -> fork : Calls fork()
alt pid == 0
== Child Process ==
fork -> child : In child process
child -> execvp : execvp("/bin/ls", "-l")
execvp -> child : If execvp fails
child -> child : printf("execvp failed\n")
child -> child : Child process ends, return 0
else pid > 0
== Parent Process ==
fork -> main : In parent process
main -> wait : wait(NULL) for child to finish
wait -> child : Waiting...
child -> wait : Child has finished
wait -> main : Child has finished
main -> main : Parent process ends, return 0
else
== Fork Failed ==
fork -> main : Fork failed
main -> main : fprintf(stderr, "Fork failed.\n")
main -> main : return 1
end
@enduml
Zombie Processes :trollface:
Zombie processes occur when a child process has completed its execution but still has an entry in the process table. This happens because the operating system needs to keep some information about the process so the parent process can retrieve the child's exit status. A process becomes a zombie, or "defunct", if the parent process does not call wait()
(or a related method) to read its exit status. The entry is removed from the process table once the exit status is read, allowing the system to fully clean up the resources associated with the child process.
How Zombie Processes Occur
- Process Termination: When a child process finishes execution, it becomes a zombie until its parent reads its exit status.
- Parent Not Calling
wait()
: If the parent process fails to callwait()
, the child remains in a zombie state, occupying a slot in the process table but not executing or consuming other system resources besides that.
Example Demonstrating a Zombie Process
This C program demonstrates the creation of a zombie process by intentionally not calling wait()
in the parent process after the child process terminates.
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// Parent process
printf("Parent process, PID: %d\n", getpid());
printf("Child PID: %d\n", pid);
printf("Parent sleeping for 30 seconds...\n");
sleep(30); // Sleep to simulate delay in calling wait(), leaving child as zombie
} else if (pid == 0) {
// Child process
printf("Child process, PID: %d\n", getpid());
exit(0); // Child exits immediately
} else {
// Fork failed
fprintf(stderr, "Fork failed.\n");
return 1;
}
return 0;
}
code execution on linux SOC:
@startuml
!theme toy
participant "Main Process" as main
participant "fork()" as fork
participant "Child Process" as child
participant "sleep()" as sleep
main -> fork : Calls fork()
alt pid > 0
== Parent Process ==
fork -> main : In parent process
main -> main : printf("Parent process, PID: %d\n", getpid())
main -> main : printf("Child PID: %d\n", pid)
main -> main : printf("Parent sleeping for 30 seconds...\n")
main -> sleep : sleep(30)
sleep -> main : After 30 seconds
else pid == 0
== Child Process ==
fork -> child : In child process
child -> child : printf("Child process, PID: %d\n", getpid())
child -> child : exit(0)
else
== Fork Failed ==
fork -> main : Fork failed
main -> main : fprintf(stderr, "Fork failed.\n")
main -> main : return 1
end
@enduml
Observing a Zombie Process
While the parent process is sleeping (and before it exits, if it does not call wait()
), you can observe the zombie process by running the ps
command in another terminal:
ps aux | grep Z
This command filters processes to show those with a status of 'Z', which stands for "zombie". You will see the child process listed as a defunct process.
Handling Zombie Processes
To prevent processes from becoming zombies, the parent process must call wait()
or waitpid()
to collect the exit status of its terminated children. Alternatively, if a parent process exits before its children, the children are adopted by the init process (PID 1), which periodically calls wait()
to clean up any zombies it inherits.
Process Termination
A process can terminate normally by returning from its main function, calling exit()
, or being terminated by another process (e.g., with a signal).
Exiting a Process Example:
#include <stdlib.h>
int main() {
// Terminate the process
exit(0);
}
Release the resources of child processes asynchronously
The SIGCHLD
signal is used in Unix operating systems to indicate to a parent process that one of its child processes has finished, has been interrupted, or has resumed after being interrupted. By default, SIGCHLD
is ignored, meaning that the parent process does not get notified when its child processes terminate. However, a program can choose to handle SIGCHLD
to get notified about these events and take appropriate actions, such as using wait()
to collect the child's termination status and thus prevent zombie processes.
Handling SIGCHLD
is particularly useful in concurrent programs where the parent process spawns multiple child processes and needs to clean them up properly when they terminate, without blocking on a wait()
call for any specific child.
Example: Using SIGCHLD
to Handle Child Process Termination
The following example demonstrates how a parent process can use SIGCHLD
to be notified when a child process terminates and then clean up the zombie process by calling wait()
within the signal handler.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sigchld_handler(int signum) {
// Wait for all children that have terminated, non-blocking call
while (waitpid(-1, NULL, WNOHANG) > 0) {
// Child was cleaned up
}
}
int main() {
// Set up the SIGCHLD handler
struct sigaction sa;
sa.sa_handler = sigchld_handler; // Pointer to our handler function
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // Restart functions if interrupted by handler
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
// Create a child process
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("Child process PID: %d\n", getpid());
// Child exits immediately
exit(0);
} else if (pid > 0) {
// Parent process
printf("Parent process PID: %d, child PID: %d\n", getpid(), pid);
printf("Parent waiting for child to terminate...\n");
// Parent does some work, not blocking on wait()
sleep(10); // Simulate doing some work
printf("Parent finished work.\n");
} else {
// Fork failed
fprintf(stderr, "Fork failed.\n");
return 1;
}
return 0;
}
code execution on linux SOC:
@startuml
!theme toy
participant "Main Process" as main
participant "sigaction()" as sigaction
participant "fork()" as fork
participant "Child Process" as child
participant "sigchld_handler()" as handler
participant "sleep()" as sleep
main -> sigaction : Set up SIGCHLD handler
sigaction -> main : Handler set
main -> fork : Create a child process
alt pid == 0
== Child Process ==
fork -> child : In child process
child -> child : printf("Child process PID: %d\n", getpid())
child -> child : exit(0)
child -> handler : SIGCHLD sent to handler
handler -> handler : waitpid(-1, NULL, WNOHANG) > 0
handler -> handler : Child cleaned up
else pid > 0
== Parent Process ==
fork -> main : In parent process
main -> main : printf("Parent process PID: %d, child PID: %d\n", getpid(), pid)
main -> main : printf("Parent waiting for child to terminate...\n")
main -> sleep : sleep(10)
sleep -> main : After 10 seconds
main -> main : printf("Parent finished work.\n")
else
== Fork Failed ==
fork -> main : Fork failed
main -> main : fprintf(stderr, "Fork failed.\n")
main -> main : return 1
end
@enduml
How It Works:
- Signal Handler Setup: The
sigaction
struct andsigaction()
function are used to specify how theSIGCHLD
signal should be handled. The handler functionsigchld_handler()
is defined to callwaitpid()
with theWNOHANG
option. This meanswaitpid()
will return immediately if no child has exited, allowing the parent to clean up zombie processes without blocking. - Child Process Creation: The program creates a child process using
fork()
. The child process exits immediately, which generates aSIGCHLD
signal. - Signal Handling: Upon receiving
SIGCHLD
, the parent process's signal handler is invoked, which callswaitpid()
to clean up the zombie process created by the exiting child. - Non-blocking Behavior: The parent process continues its execution (simulated by
sleep(10)
) without being blocked by child processes waiting to be cleaned up.
This approach allows the parent process to be promptly informed about child process terminations and efficiently clean up without dedicating specific code in the main execution flow to wait for child process termination, thus improving the concurrency and responsiveness of the application.
Conclusion
These examples and explanations should give you a solid foundation in managing processes in Linux using C. Remember, processes are a critical part of Linux and understanding how to create, manage, and synchronize them is key to developing efficient applications.
🏷️ Author position : Embedded Software Engineer
🔗 Author LinkedIn : LinkedIn profile
Comments