Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Linux Process Management: Execution, Permissions, and Inter-Process Communication

Notes 2

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 uid but can change when executing setuid programs, 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 euid but for group permissions, modifiable via setgid().
  • 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:

  1. PID: The child receives a unique Process ID.
  2. PPID: The child's Parent Process ID is set to the parent's PID.
  3. Return Value: fork returns the child's PID to the parent, and 0 to the child. A return of -1 indicates 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

  1. The kernel allocates a new task_struct and copies the parent's content into it. Memory page are shared using Copy-on-Write (COW) optimization rather than immediate duplication.
  2. Essential data like PID and PPID are updated. Signal handlers and file descriptor tables are initialized.
  3. 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:

  1. Invocation: User programs trigger a software interrupt (e.g., int 0x80, syscall) along with a system call number.
  2. Mode Switch: The CPU transitions to kernel mode, saving user-space registers.
  3. Dispatch: The kernel uses the system call number as an index in the sys_call_table to execute the corresponding handler.
  4. 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: return from main, calling exit() (flushes stdout buffers), 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 setpgid to 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:

  1. Fork and exit the parent, ensuring the child is not a process group leader.
  2. Call setsid() to create a new session and detach from the controlling terminal.
  3. Change the working directory to root (chdir("/")).
  4. Reset the file mode mask (umask(0)).
  5. 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;
}

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.