POSIX menyediakan dua syscall fundamental untuk membuat proses baru: fork yang menduplikasi proses pemanggil menjadi dua proses identik (parent dan child), dan exec yang mengganti image program saat ini dengan program lain dari file. Kombinasi fork + exec adalah cara Unix menjalankan program baru—shell menggunakan pola ini setiap kali kita mengetik perintah. Pada chapter ini kita membuat child dengan fork, menunggu child selesai dengan waitpid, mengganti image child dengan execl, dan akhirnya menjalankan beberapa child paralel untuk membandingkan eksekusi berurutan vs paralel.

fork: Membuat Proses Baru #

fork() adalah satu-satunya cara POSIX membuat proses baru (selain beberapa varian khusus seperti vfork dan clone di Linux). Setelah fork, ada dua proses yang menjalankan kode yang sama: parent dan child. Cara membedakannya adalah nilai kembali fork—di parent, ia berisi PID child; di child, ia berisi 0.

c
/* File: fork_dasar.c - fork() dasar: parent dan child */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void) {
    /* flush stdout sebelum fork agar child tidak mewarisi buffer kotor */
    fflush(stdout);

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork gagal");
        return 1;
    }

    if (pid == 0) {
        /* Kode ini berjalan di child */
        printf("[CHILD]  PID=%d, PPID=%d\n", getpid(), getppid());
        fflush(stdout);
        printf("[CHILD]  selesai.\n");
        fflush(stdout);
        _exit(7);   /* exit code 7; _exit tidak flush stdio */
    } else {
        /* Kode ini berjalan di parent */
        printf("[PARENT] PID=%d, child PID=%d\n", getpid(), pid);
        int status;
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("[PARENT] child exit code = %d\n", WEXITSTATUS(status));
        }
        printf("[PARENT] selesai.\n");
    }
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o fork_dasar fork_dasar.c
timeout 5 ./fork_dasar

Output yang Dihasilkan (PID bervariasi antar eksekusi):

text
[CHILD]  PID=12795, PPID=12794
[CHILD]  selesai.
[PARENT] PID=12794, child PID=12795
[PARENT] child exit code = 7
[PARENT] selesai.

Mari kita telaah baris-baris kode di atas:

  1. Baris 8 (fflush(stdout);): Buffer stdout di-flush sebelum fork. Tanpa ini, child mewarisi buffer yang belum ditulis—bila child juga memanggil printf lalu keluar, pesan parent bisa tercetak dua kali (satu saat parent akhirnya flush, satu saat child exit).
  2. Baris 10 (pid_t pid = fork();): Membuat proses baru. Setelah pemanggilan, ada dua proses dengan kode identik. Memory layout (heap, stack, file descriptors) di-copy-on-write—child melihat snapshot memory parent saat fork.
  3. Baris 16 (if (pid == 0)): Cabang child. Di child, fork() selalu mengembalikan 0. Cabang ini menulis PID dan PPID child, lalu keluar dengan kode 7.
  4. Baris 24 (_exit(7)): Di child, gunakan _exit (dengan underscore), bukan exit. _exit langsung memanggil kernel tanpa flush stdio buffer atau memanggil atexit handler—tepat untuk child setelah fork.
  5. Baris 27 (else): Cabang parent. Variabel pid di parent berisi PID child.
  6. Baris 29 (waitpid(pid, &status, 0)): Parent memblokir hingga child dengan PID pid selesai. Status keluar disimpan di status. Argumen ketiga 0 berarti default (wait blocking).
  7. Baris 30-32 (WIFEXITED(status), WEXITSTATUS(status)): Macro untuk membaca status. WIFEXITED true bila child berakhir dengan return/exit normal. WEXITSTATUS mengambil 8-bit exit code yang dilewatkan ke _exit/exit—dalam kasus ini 7.

exec: Mengganti Image Program #

fork saja jarang berguna—biasanya kita ingin child menjalankan program lain. Fungsi keluarga exec (execl, execv, execlp, execvp, execle, execve) mengganti image program saat ini dengan program dari file. Bila exec sukses, kode setelah pemanggilan tidak pernah dieksekusi—image program lama hilang.

c
/* File: fork_exec.c - fork + exec: ganti image child dengan /bin/echo */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void) {
    fflush(stdout);
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork gagal");
        return 1;
    }

    if (pid == 0) {
        /* Child: ganti image program dengan /bin/echo */
        printf("[CHILD]  PID=%d sebelum exec\n", getpid());
        fflush(stdout);
        execl("/bin/echo", "echo", "Halo", "dari", "child!", NULL);

        /* execl tidak kembali kecuali gagal */
        perror("execl gagal");
        _exit(127);
    } else {
        printf("[PARENT] PID=%d, child PID=%d\n", getpid(), pid);
        int status;
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            int code = WEXITSTATUS(status);
            printf("[PARENT] child exit code = %d\n", code);
        }
    }
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o fork_exec fork_exec.c
timeout 5 ./fork_exec

Output yang Dihasilkan:

text
[CHILD]  PID=12746 sebelum exec
Halo dari child!
[PARENT] PID=12745, child PID=12746
[PARENT] child exit code = 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 17 (execl("/bin/echo", "echo", "Halo", "dari", "child!", NULL)): Mengganti image program child dengan /bin/echo. Argument pertama adalah path program. Argument kedua ("echo") menjadi argv[0] di program baru—biasanya sama dengan nama program. Argument berikutnya ("Halo", "dari", "child!") menjadi argv[1], argv[2], argv[3]. Daftar argument wajib diakhiri NULL sebagai sentinel.
  2. Suffix l pada execl: Berarti list—argument dilewatkan satu per satu. Varian lain: execv (vector/array), execlp (list + cari di PATH), execvp (vector + cari di PATH). Untuk shell, execvp adalah pilihan umum karena menerima argv[] yang sudah di-parse dari input user.
  3. Baris 20 (perror("execl gagal"); _exit(127);): Bila execl kembali, pasti gagal—sukses tidak pernah kembali. Konvensi exit code 127 artinya “command not found” (sama seperti shell). Tanpa baris ini, bila exec gagal, child akan lanjut mengeksekusi kode parent yang seharusnya tidak boleh dijalankan dua kali.
  4. Output Halo dari child! berasal dari program /bin/echo yang baru—bukan dari printf. Ini membuktikan image program sudah berganti.

Menjalankan Banyak Child Paralel #

Salah satu kekuatan fork adalah parent dapat membuat banyak child, lalu menunggu semuanya. Karena setiap child adalah proses terpisah dengan PID sendiri, kernel scheduler menjalankan mereka paralel pada mesin multicore. Program berikut membuat tiga child yang masing-masing tidur 1 detik—total waktu sekitar 1 detik, bukan 3 detik.

c
/* File: fork_paralel.c - menjalankan banyak child secara paralel */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>

static double sekarang_detik(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

int main(void) {
    const int N = 3;
    double mulai = sekarang_detik();
    pid_t child_pids[3];

    for (int i = 0; i < N; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            return 1;
        }
        if (pid == 0) {
            /* Child: tidur 1 detik, lalu exit */
            struct timespec ts = { 1, 0 };
            nanosleep(&ts, NULL);
            _exit(i + 10);   /* exit code unik per child */
        }
        child_pids[i] = pid;
    }

    /* Parent: tunggu semua child */
    for (int i = 0; i < N; i++) {
        int status;
        pid_t pid = waitpid(child_pids[i], &status, 0);
        if (WIFEXITED(status)) {
            printf("[PARENT] child PID %d exit code = %d\n",
                   pid, WEXITSTATUS(status));
        }
    }
    double selesai = sekarang_detik();
    printf("[PARENT] total waktu = %.2f detik (paralel)\n", selesai - mulai);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o fork_paralel fork_paralel.c
timeout 5 ./fork_paralel

Output yang Dihasilkan (urutan child bisa berbeda):

text
[PARENT] child PID 12775 exit code = 10
[PARENT] child PID 12776 exit code = 11
[PARENT] child PID 12777 exit code = 12
[PARENT] total waktu = 1.00 detik (paralel)

Mari kita telaah baris-baris kode di atas:

  1. Baris 18 (pid_t pid = fork(); di loop): Setiap iterasi membuat satu child. Parent menyimpan pid ke array child_pids. Setelah loop, ada 4 proses: 1 parent + 3 child, semuanya menjalankan kode yang sama tetapi di cabang berbeda (parent lanjut loop, child keluar loop karena pid == 0).
  2. Baris 28 (_exit(i + 10)): Exit code child ke-i adalah i + 10—jadi child pertama exit 10, kedua 11, ketiga 12. Pola ini membantu parent mengidentifikasi child mana yang selesai.
  3. Baris 35 (waitpid(child_pids[i], &status, 0)): Parent menunggu child ke-i spesifik. Karena kita tunggu sesuai urutan array, output juga muncul dalam urutan array. Untuk aplikasi yang tidak peduli urutan, gunakan wait(&status) yang mengembalikan salah satu child yang sudah mati.
  4. Total waktu 1.00 detik: Bila ketiga child berjalan sequential, total butuh 3 detik. Karena kernel menjadwalkan mereka paralel (mesin kita punya 2 core), total hanya ~1 detik—durasi child terlama.

Perbandingan fork vs pthread_create #

Kapan pilih fork dan kapan pthread_create? Berikut ringkasan praktis:

Aspekforkpthread_create
Isolasi memoryYa—address space terpisahTidak—berbagi address space
PembuatanMahal (~1 ms karena copy-on-write setup)Murah (~10-100 µs)
KomunikasiIPC: pipe, socket, shared memoryLangsung lewat variabel shared
Crash isolationTinggi—child crash tidak memengaruhi parentRendah—thread crash mematikan seluruh proses
Cocok untukMenjalankan program eksternal, server isolationParalelisme dalam satu program