Chapter 25 — Process Termination

Chapter 25 — Process Termination
Part 2 of 5  |  Details of Process Termination — What the Kernel Does
10
Kernel Actions
3
Code Examples
7
Interview Q&As

Series Navigation: Part 1: _exit() & exit()| Part 2: Kernel Cleanup Details| Part 3: Exit Handlers – atexit()| Part 4: Exit Handlers – on_exit()| Part 5: fork() + stdio Buffers

What You Will Learn

When any process terminates — whether normally (via _exit() / exit()) or abnormally (killed by a signal) — the kernel performs a fixed set of cleanup actions. Understanding these actions is critical for writing robust multi-process programs, avoiding resource leaks, and debugging orphaned processes.

Keywords in this part:

file descriptors file locks System V shared memory shm_nattch semadj SIGHUP controlling terminal POSIX named semaphores POSIX message queues orphaned process group SIGCONT mlock() / mlockall() mmap() foreground process group

§ Overview — All 10 Kernel Cleanup Actions at a Glance

These actions happen for both normal and abnormal termination:

# Resource / Action What the kernel does
1 File descriptors, directory streams, message catalogs, conversion descriptors All are closed
2 File locks (fcntl / flock) All locks held by this process are released
3 System V shared memory segments Detached; shm_nattch decremented by 1
4 System V semaphores with semadj semadj value added to semaphore value (undo)
5 Controlling terminal (if this is the controlling process) SIGHUP sent to foreground process group; terminal disassociated from session
6 POSIX named semaphores Closed as if sem_close() were called
7 POSIX message queues Closed as if mq_close() were called
8 Orphaned process groups with stopped processes SIGHUP + SIGCONT sent to all processes in the group
9 Memory locks (mlock() / mlockall()) All memory locks established by this process are removed
10 Memory mappings (mmap()) All mappings established by this process are unmapped

Action 1 — File Descriptors & Related Descriptors Are Closed

When a process terminates, the kernel closes all open file descriptors. This includes regular file FDs, socket FDs, pipe FDs, and also:

  • Directory streams opened with opendir()
  • Message catalog descriptors opened with catopen()
  • Conversion descriptors opened with iconv_open()

Closing file descriptors is a kernel operation — it happens even if _exit() is used (which skips userspace stdio flushing).

Key distinction: Closing a file descriptor is kernel-side. Flushing a stdio buffer (like stdout) is userspace-side. _exit() skips the userspace flush but the kernel still closes the underlying FD.

Action 2 — File Locks Are Released

As a consequence of closing file descriptors, any file locks held by the process are automatically released. This applies to:

  • fcntl() advisory locks (POSIX record locks)
  • flock() advisory locks (BSD-style)

This is important for server processes: a crash or unexpected exit automatically releases locks, so other waiting processes can proceed.

Action 3 — System V Shared Memory Segments Are Detached

If the process had attached any System V shared memory segments (using shmat()), they are detached on process exit. The kernel decrements the shm_nattch counter of each segment by 1.

Shared Memory Segment shm_nattch before exit shm_nattch after exit
shmid = 100 3 2 (decremented)

The shared memory segment itself is not deleted — it persists until explicitly removed with shmctl(shmid, IPC_RMID, NULL). Detaching just unmaps it from the process’s address space.

Action 4 — System V Semaphore semadj Values Are Applied

When a process uses System V semaphores with the SEM_UNDO flag in semop(), the kernel maintains a per-process semadj (semaphore adjustment) value for each semaphore it touched.

On process exit, the kernel adds each semadj value back to the corresponding semaphore’s value — effectively undoing any semaphore operations the process had performed. This prevents semaphore deadlocks when a process dies while holding semaphore resources.

Semaphore value before process’s semop() Process called semop(-1) (lock) semadj stored Value after process dies (undo applied)
1 → value becomes 0 (locked) +1 1 (unlocked again!)

Action 5 — SIGHUP Sent When Controlling Process Exits

Each terminal session has a controlling process (usually the login shell). If that specific process (the session leader / controlling process) exits:

  • The kernel sends SIGHUP to every process in the terminal’s foreground process group.
  • The terminal is disassociated from the session.

This is why background shell jobs (e.g., ./server &) receive SIGHUP and die when you close the terminal — unless protected with nohup, setsid(), or disown.

Controlling Process exits
Kernel sends SIGHUP
All foreground process group members receive SIGHUP
Practical note: Default action of SIGHUP is to terminate the process. Long-running daemons re-interpret SIGHUP as a “reload configuration” signal by catching it.

Action 6 — POSIX Named Semaphores Are Closed

Any POSIX named semaphores (opened with sem_open()) that are open in the terminating process are closed as if sem_close() were called on each.

The semaphore itself is not deleted — it remains in the kernel (visible via /dev/shm or /proc/sys/fs/mqueue) until explicitly removed with sem_unlink(). Closing just decrements the open count for that process.

Action 7 — POSIX Message Queues Are Closed

Similarly, any POSIX message queues (opened with mq_open()) that are open in the process are closed as if mq_close() were called. Like named semaphores, the queue persists until mq_unlink() is called.

Action 8 — Orphaned Process Groups Get SIGHUP + SIGCONT

A process group becomes orphaned when the last process in the group whose parent is in a different process group exits. If there are any stopped processes in that newly-orphaned group, the kernel sends SIGHUP followed immediately by SIGCONT to all processes in the group.

Why both signals?

  • SIGHUP: notifies the processes that their group is now orphaned.
  • SIGCONT: wakes up stopped processes so they can handle SIGHUP (a stopped process cannot receive and handle signals).

Process group becomes orphaned + has stopped members
Kernel sends SIGHUP to all members
Kernel sends SIGCONT to all members

Action 9 — Memory Locks Are Removed

Processes with real-time or low-latency requirements sometimes lock pages into RAM to prevent them being swapped out, using:

  • mlock(addr, len) — lock a specific address range
  • mlockall(flags) — lock all current and/or future pages of the process

On process exit, all such memory locks are automatically removed by the kernel. The physical pages are returned to the normal page replacement pool.

Action 10 — Memory Mappings (mmap()) Are Unmapped

Any memory mappings created by the process via mmap() are automatically unmapped on process exit.

  • For MAP_SHARED + file-backed mappings: dirty pages are flushed to the underlying file.
  • For MAP_PRIVATE mappings: changes are discarded (copy-on-write pages are freed).
  • For MAP_SHARED anonymous mappings (used for IPC between parent-child): the mapping is released from this process’s address space but the physical pages remain as long as another process shares them.

Coding Examples

Example 1 — File Lock Automatically Released on Process Exit

Two processes compete for an exclusive fcntl lock on a file. When the locking process exits, the kernel automatically releases the lock and the waiting process gets it immediately.

/* demo_lock_release.c
 * Compile : gcc -Wall -o demo_lock_release demo_lock_release.c
 * Run     : ./demo_lock_release
 *
 * The child acquires an exclusive lock, sleeps 2 seconds, then exits.
 * The parent waits for the lock.  The kernel releases the child's lock
 * automatically on child exit — the parent then acquires it.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

static void set_lock(int fd, short type)
{
    struct flock fl;
    fl.l_type   = type;          /* F_WRLCK or F_UNLCK */
    fl.l_whence = SEEK_SET;
    fl.l_start  = 0;
    fl.l_len    = 0;             /* 0 = lock entire file */

    if (fcntl(fd, F_SETLKW, &fl) == -1) {   /* blocking lock */
        perror("fcntl F_SETLKW");
        exit(EXIT_FAILURE);
    }
}

int main(void)
{
    int  fd;
    pid_t pid;

    fd = open("/tmp/locktest.tmp", O_RDWR | O_CREAT | O_TRUNC, 0600);
    if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }

    pid = fork();
    if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid == 0) {
        /* ---- CHILD: acquires lock, sleeps, then exits ---- */
        set_lock(fd, F_WRLCK);
        printf("Child  (PID %d): acquired exclusive lock\n", getpid());
        printf("Child  (PID %d): holding for 2 seconds ...\n", getpid());
        sleep(2);
        printf("Child  (PID %d): exiting — kernel releases lock automatically\n",
               getpid());
        /* _exit() here — kernel closes fd and releases fcntl lock */
        _exit(EXIT_SUCCESS);
    }

    /* ---- PARENT: tries to acquire lock (will block until child exits) ---- */
    printf("Parent (PID %d): waiting to acquire exclusive lock ...\n", getpid());
    set_lock(fd, F_WRLCK);   /* blocks here until child exits */
    printf("Parent (PID %d): got the lock after child exited!\n", getpid());

    set_lock(fd, F_UNLCK);   /* release */
    waitpid(pid, NULL, 0);
    close(fd);
    unlink("/tmp/locktest.tmp");
    return EXIT_SUCCESS;
}
Expected output: Parent blocks, child acquires lock and sleeps 2s, child exits → kernel releases lock → parent immediately acquires lock. This proves Action 2 from the cleanup list.

Example 2 — System V Shared Memory shm_nattch Counter

Shows that attaching shared memory increments shm_nattch, and on process exit it is decremented. We read shm_nattch using shmctl(IPC_STAT).

/* demo_shm_nattch.c
 * Compile : gcc -Wall -o demo_shm_nattch demo_shm_nattch.c
 * Run     : ./demo_shm_nattch
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>

static void print_nattch(int shmid, const char *label)
{
    struct shmid_ds info;
    if (shmctl(shmid, IPC_STAT, &info) == -1) {
        perror("shmctl IPC_STAT");
        return;
    }
    printf("  [%s] shm_nattch = %lu\n", label, (unsigned long)info.shm_nattch);
}

int main(void)
{
    int   shmid;
    void *addr;
    pid_t pid;

    /* Create a 4 KB shared memory segment */
    shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
    if (shmid == -1) { perror("shmget"); exit(EXIT_FAILURE); }

    /* Parent attaches it */
    addr = shmat(shmid, NULL, 0);
    if (addr == (void *)-1) { perror("shmat"); exit(EXIT_FAILURE); }

    print_nattch(shmid, "After parent attaches");   /* shm_nattch = 1 */

    pid = fork();
    if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid == 0) {
        /*
         * Child inherits parent's mapping.
         * After fork, the child has its own attachment,
         * so shm_nattch becomes 2.
         */
        print_nattch(shmid, "Child after fork       ");  /* shm_nattch = 2 */
        printf("  Child exiting now ...\n");
        /* On child exit, kernel detaches shm → shm_nattch decremented */
        _exit(EXIT_SUCCESS);
    }

    waitpid(pid, NULL, 0);       /* wait for child to die */
    print_nattch(shmid, "After child exited     ");  /* shm_nattch = 1 again */

    /* Cleanup */
    shmdt(addr);
    print_nattch(shmid, "After parent detaches  ");  /* shm_nattch = 0 */
    shmctl(shmid, IPC_RMID, NULL);                    /* delete segment */
    printf("  Shared memory segment deleted.\n");
    return EXIT_SUCCESS;
}
Expected output:
After parent attaches: shm_nattch = 1
Child after fork: shm_nattch = 2
After child exited: shm_nattch = 1 ← kernel decremented on child exit
After parent detaches: shm_nattch = 0

Example 3 — SIGHUP on Controlling Process Exit

Demonstrates that child processes in the foreground process group receive SIGHUP when the session leader (controlling process) exits. We catch SIGHUP to prove it arrives.

/* demo_sighup.c
 * Compile : gcc -Wall -o demo_sighup demo_sighup.c
 * Run     : ./demo_sighup
 *
 * Parent creates a child, then the child installs a SIGHUP handler.
 * Parent (session leader) exits → kernel sends SIGHUP to foreground group.
 *
 * NOTE: For a real controlling-terminal SIGHUP, you would need to run
 * this as a session leader (the shell is usually the session leader).
 * This demo simulates the concept using kill(0, SIGHUP) from the parent.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

static volatile sig_atomic_t got_sighup = 0;

static void sighup_handler(int sig)
{
    (void)sig;
    got_sighup = 1;
    /* async-signal-safe: write() only */
    write(STDOUT_FILENO, "  Child: received SIGHUP!\n", 26);
}

int main(void)
{
    pid_t pid;

    pid = fork();
    if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid == 0) {
        /* ---- CHILD ---- */
        struct sigaction sa;
        sa.sa_handler = sighup_handler;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        sigaction(SIGHUP, &sa, NULL);

        printf("  Child (PID %d): waiting for SIGHUP ...\n", getpid());
        /* wait until SIGHUP arrives */
        while (!got_sighup)
            pause();

        printf("  Child (PID %d): SIGHUP handled, exiting cleanly.\n",
               getpid());
        exit(EXIT_SUCCESS);
    }

    /* ---- PARENT: simulate controlling process exit ---- */
    sleep(1);  /* give child time to install handler */
    printf("Parent (PID %d): sending SIGHUP to process group (simulates exit)\n",
           getpid());
    /*
     * kill(0, sig) sends signal to every process in the same process group.
     * In a real terminal scenario the kernel does this when the session
     * leader (shell) exits.
     */
    kill(0, SIGHUP);
    waitpid(pid, NULL, 0);
    printf("Parent: done.\n");
    return EXIT_SUCCESS;
}
Key point: In a real terminal session, closing the terminal window or the shell exiting triggers the kernel to send SIGHUP to the entire foreground process group. Use nohup ./myprogram & or call setsid() to make a daemon immune to SIGHUP.

Interview Questions & Answers

Q1. What kernel-side cleanup happens when any process terminates?

Answer: The kernel closes all open file descriptors (including directory streams, message catalog descriptors, conversion descriptors); releases all file locks; detaches any System V shared memory segments (decrementing shm_nattch); applies semadj values for System V semaphores; sends SIGHUP to the foreground process group if this was the controlling process; closes POSIX named semaphores and message queues; sends SIGHUP+SIGCONT to any newly-orphaned process groups that have stopped members; removes memory locks; and unmaps all mmap() mappings.

Q2. What is shm_nattch and how does process exit affect it?

Answer: shm_nattch is a kernel counter in the shmid_ds structure that tracks how many processes currently have a System V shared memory segment attached to their address space. When a process exits (or explicitly calls shmdt()), the kernel decrements shm_nattch for each attached segment. The segment is not deleted on exit — it remains until shmctl(IPC_RMID) is called.

Q3. What is the purpose of semadj and how does it help on process exit?

Answer: When a process uses System V semaphores with the SEM_UNDO flag, the kernel tracks a semadj (adjustment) value per semaphore. Each semop() with SEM_UNDO updates the semadj with the negated operation value. On process exit, the kernel adds all outstanding semadj values back to their semaphores, effectively undoing the process’s semaphore operations. This prevents other processes from being permanently blocked on a semaphore that a dead process was holding.

Q4. When is SIGHUP sent by the kernel on process termination?

Answer: SIGHUP is sent by the kernel in two situations during process termination: (1) When the controlling process (session leader) of a terminal exits — SIGHUP is sent to all processes in the terminal’s foreground process group, and the terminal is disassociated from the session. (2) When a process exits and this causes a process group to become orphaned while that group has stopped members — SIGHUP followed by SIGCONT is sent to all members of the newly-orphaned group.

Q5. Are POSIX named semaphores deleted when a process exits?

Answer: No. On process exit, POSIX named semaphores are only closed (as if sem_close() were called). The semaphore object itself persists in the kernel until explicitly deleted with sem_unlink(). This is similar to a file — closing the file descriptor does not delete the file.

Q6. Why does the kernel send SIGCONT after SIGHUP to an orphaned process group?

Answer: A stopped process cannot receive and act on signals while it is stopped (SIGSTOP / SIGTSTP). If the kernel only sent SIGHUP, stopped members of the orphaned group would never process it and would remain blocked forever. Sending SIGCONT first wakes them up (resumes execution), allowing them to receive and handle the subsequent SIGHUP signal.

Q7. What happens to mmap() MAP_SHARED file mappings when a process exits?

Answer: The kernel unmaps all mmap() regions when a process exits. For MAP_SHARED file-backed mappings, any dirty pages are written back to the underlying file (the mapping is flushed). For MAP_PRIVATE mappings, copy-on-write pages modified by the process are discarded — the original file is not changed. For MAP_SHARED anonymous mappings shared between parent/child via fork, the physical pages remain as long as another process still has the mapping.

Next: Part 3 — Exit Handlers with atexit()

Learn how to register automatic cleanup functions that run when your process calls exit() — including registration order, reverse calling order, limitations, and full code examples.

Go to Part 3 → EmbeddedPathashala Home

Leave a Reply

Your email address will not be published. Required fields are marked *