Linux Process Management: Execution, Permissions, and Inter-Process Communication
A process is an executing program and the fundamental unit of resource allocation in an operating system. The kernel manages processes by maintaining a doubly linked list of task_struct structures, which contain all process information.
Process Permissions
Process privileges are governed by several identifiers and flags:
- uid (User ID): Identifies the user who launched the process.
- euid (Effective User ID): Determines the actual permissions used during file access or system calls. It usually matches the
uidbut can change when executingsetuidprograms, allowing a process to temporarily assume the file owner's privileges. - gid (Group ID): Specifies the primary group of the process owner.
- egid (Effective Group ID): Operates similar to
euidbut for group permissions, modifiable viasetgid(). - Sticky Bit: A directory permission flag (denoted as
t, e.g.,drwxrwxrwt). When applied, only the file owner, directory owner, or root can delete or rename files within that directory, preventing users from removing others' files in shared spaces like/tmp.
Creating Processes
fork System Call
fork creates a new process by duplicating the calling process. The newly created child process is nearly identical to the parent, inheriting memory space and file descriptors. Key distinctions include:
- PID: The child receives a unique Process ID.
- PPID: The child's Parent Process ID is set to the parent's PID.
- Return Value:
forkreturns the child's PID to the parent, and0to the child. A return of-1indicates failure.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t proc_id = fork();
printf("Execution point\n");
if (proc_id == 0) {
printf("Child Context - PID: %d, PPID: %d\n", getpid(), getppid());
} else if (proc_id > 0) {
printf("Parent Context - PID: %d, PPID: %d\n", getpid(), getppid());
sleep(2);
} else {
fprintf(stderr, "Fork unsuccessful\n");
exit(EXIT_FAILURE);
}
return 0;
}
Under the Hood of fork
- The kernel allocates a new
task_structand copies the parent's content into it. Memory page are shared using Copy-on-Write (COW) optimization rather than immediate duplication. - Essential data like PID and PPID are updated. Signal handlers and file descriptor tables are initialized.
- The new process is placed in the scheduler's ready queue.
Steps 1 and 2 must be atomic (non-preemptible) to prevent race conditions, constituting the "top half" of the system call. Step 3 is preemptible, acting as the "bottom half".
System Call Architecture
System calls facilitate user-to-kernel space transitions:
- Invocation: User programs trigger a software interrupt (e.g.,
int 0x80,syscall) along with a system call number. - Mode Switch: The CPU transitions to kernel mode, saving user-space registers.
- Dispatch: The kernel uses the system call number as an index in the
sys_call_tableto execute the corresponding handler. - Return: Results are passed back, and execution resumes in user mode.
This context switch makes system calls slower than regular function calls.
File Descriptors and Buffering in fork
While logical user-space segments (stack, heap, data) are copied, the underlying physical memory is shared until modified (COW). Standard I/O buffers (FILE*) are also duplicated. If printf("Hello"); (without a newline) is called before fork, the unwritten buffer is copied to the child, causing both processes to print "Hello" eventually.
Kernel-space file objects, however, are shared. File descriptors in parent and child point to the same kernel file object, meaning their write offsets are shared—behaving similarly to dup.
exec Function Family
The exec family replaces the current process image with a new executable. It clears existing data segments, stacks, and heaps, loads the new program into the code segment, and resets the program counter. If exec succeeds, subsequent code in the calling program does not execute.
execl and execv
execl takes a variable argument list, while execv accepts an array of pointers.
// sum_program.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 3) return 1;
int val1 = atoi(argv[1]);
int val2 = atoi(argv[2]);
printf("Total: %d\n", val1 + val2);
return 0;
}
// launcher.c
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Launcher PID: %d\n", getpid());
char *arguments[] = {"./sum_program", "15", "25", NULL};
execv("./sum_program", arguments);
// This line is unreachable if execv succeeds
perror("execv failed");
return 1;
}
Resource Reclamation: wait and waitpid
When a child terminates, it becomes a zombie, retaining some kernel resources until the parent reads its exit status. wait and waitpid are used to collect this status and free resources.
- Orphan Process: If the parent terminates first, the child is adopted by
init(PID 1), which automatically reaps it. - Zombie Process: If the child terminates but the parent never calls
wait, it remains a zombie. Killing the parent resolves the zombies by turning them into orphans.
wait
pid_t wait(int *status); blocks until any child terminates. The status integer encodes exit information, inspectable via macros like WIFEXITED and WEXITSTATUS. Note that exit values are modulo 256; returning -1 yields 255.
waitpid
pid_t waitpid(pid_t pid, int *status, int options); provides finer control:
pid == -1: Wait for any child.pid > 0: Wait for a specific child.WNOHANG: Return immediately if no child has exited (non-blocking). This option is often combined with a polling loop to prevent blocking while ensuring resources are eventually collected.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t child_id = fork();
if (child_id == 0) {
printf("Child running, PID: %d\n", getpid());
sleep(2);
exit(0);
} else {
int exit_info;
while (1) {
pid_t res = waitpid(child_id, &exit_info, WNOHANG);
if (res == 0) {
printf("Child still busy...\n");
sleep(1);
} else if (res > 0) {
if (WIFEXITED(exit_info)) {
printf("Child normal exit, code: %d\n", WEXITSTATUS(exit_info));
} else if (WIFSIGNALED(exit_info)) {
printf("Child killed by signal: %d\n", WTERMSIG(exit_info));
}
break;
}
}
}
return 0;
}
Process Termination
- Normal:
returnfrommain, callingexit()(flushesstdoutbuffers), or_exit()/_Exit()(terminates immediately without flushing). - Abnormal: Calling
abort(), or receiving a fatal signal from the kernel or another process.
Process Groups and Sessions
- Process Group: A collection of related processes. The Group ID (PGID) equals the PID of the group leader. Child processes inherit their parent's group. Use
setpgidto change groups; a non-leader can start a new group. - Session: A collection of process groups. A session leader creates the session and typically interacts with a controlling terminal. If the terminal closes, all processes in the session receive a SIGHUP.
Daemon Processes
Daemons are background processes detached from any terminal, ensuring continuous operation even after a session closes. They typically end with d (e.g., sshd).
To create a daemon:
- Fork and exit the parent, ensuring the child is not a process group leader.
- Call
setsid()to create a new session and detach from the controlling terminal. - Change the working directory to root (
chdir("/")). - Reset the file mode mask (
umask(0)). - Close standard file descriptors (0, 1, 2) and redirect logging to
syslog.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <syslog.h>
#include <time.h>
void init_daemon() {
if (fork() != 0) exit(0);
setsid();
chdir("/");
umask(0);
for (int fd = 0; fd < 3; fd++) close(fd);
}
int main() {
init_daemon();
while (1) {
time_t now = time(NULL);
struct tm *timeinfo = localtime(&now);
syslog(LOG_INFO, "Daemon heartbeat: %02d:%02d:%02d",
timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
sleep(5);
}
return 0;
}
Inter-Process Communication (IPC)
IPC breaks process isolation to allow data sharing. Common mechanisms include pipes, shared memory, semaphores, message queues, and signals.
popen
popen is a standard C library function that creates a pipe, forks a process, and invokes the shell. It provides a simpler alternative to manually setting up pipes and exec.
FILE *popen(const char *command, const char *type);
"r": The calling process reads the command's standard output."w": The calling process writes to the command's standard input.
// read_example.c
#include <stdio.h>
int main() {
FILE *pipe_in = popen("./data_gen", "r");
char buffer[256];
if (fgets(buffer, sizeof(buffer), pipe_in) != NULL) {
printf("Fetched: %s", buffer);
}
pclose(pipe_in);
return 0;
}
// write_example.c
#include <stdio.h>
int main() {
FILE *pipe_out = popen("./data_consumer", "w");
fprintf(pipe_out, "100 200\n");
pclose(pipe_out);
return 0;
}