Thread berbagi memori sehingga komunikasi antar thread mudah—cukup tulis ke variabel shared. Proses tidak berbagi memori; untuk berkomunikasi, mereka perlu mekanisme Inter-Process Communication (IPC). POSIX menyediakan beberapa mekanisme IPC: pipe (channel satu arah antar proses yang berkerabat), FIFO (file khusus yang berperilaku seperti pipe tapi bisa dipakai proses manapun), shared memory (segmen memori yang dipetakan ke address space dua proses), dan message queue. Pada chapter ini kita fokus pada pipe—membuat channel komunikasi antara parent dan child setelah fork, lalu membangun pipeline shell klasik ls | wc -l dari dalam program C.

pipe: Channel Satu Arah Antar Proses #

Fungsi pipe() membuat sepasang file descriptor: pipefd[0] untuk membaca, pipefd[1] untuk menulis. Data yang ditulis ke pipefd[1] muncul di pipefd[0] dengan urutan FIFO (first-in, first-out). Setelah fork, kedua proses mewarisi kedua file descriptor—tetapi untuk komunikasi yang benar, masing-masing proses harus menutup end yang tidak dipakai.

c
/* File: pipe_dasar.c - komunikasi parent-child dengan pipe */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(void) {
    int pipefd[2];   /* pipefd[0]: baca, pipefd[1]: tulis */

    if (pipe(pipefd) != 0) {
        perror("pipe");
        return 1;
    }
    fflush(stdout);

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

    if (pid == 0) {
        /* CHILD: menulis ke pipe */
        close(pipefd[0]);   /* tutup end baca yang tidak dipakai */

        const char *pesan = "Halo Parent, ini Child!";
        write(pipefd[1], pesan, strlen(pesan));
        close(pipefd[1]);   /* tutup end tulis - sinyal EOF ke pembaca */

        _exit(0);
    } else {
        /* PARENT: membaca dari pipe */
        close(pipefd[1]);   /* tutup end tulis yang tidak dipakai */

        char buf[128];
        ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = '\0';
            printf("[PARENT] menerima %zd byte: %s\n", n, buf);
        }
        close(pipefd[0]);

        waitpid(pid, NULL, 0);
        printf("[PARENT] selesai.\n");
    }
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
[PARENT] menerima 23 byte: Halo Parent, ini Child!
[PARENT] selesai.

Mari kita telaah baris-baris kode di atas:

  1. Baris 9 (pipe(pipefd)): Membuat pipe. Setelah pemanggilan sukses, pipefd[0] adalah file descriptor untuk membaca, pipefd[1] untuk menulis. Pipe ini disimpan di process table kernel—bukan di filesystem.
  2. Baris 17 (close(pipefd[0]) di child): Child hanya butuh menulis, jadi tutup end baca. Penting: bila child tidak menutup end baca, parent tidak akan pernah menerima EOF—read akan memblokir selamanya karena kernel menganggap masih ada writer yang potensial (child sendiri).
  3. Baris 19 (write(pipefd[1], pesan, strlen(pesan))): Menulis pesan ke pipe. Bila pipe penuh (kapasitas default Linux 64 KB), write memblokir hingga ada pembaca yang mengosongkan.
  4. Baris 20 (close(pipefd[1]) di child): Setelah selesai menulis, tutup end tulis. Ini mengirim sinyal EOF ke pembaca—read di parent akan kembali 0 setelah membaca semua data.
  5. Baris 24-29 (parent membaca): Parent membaca hingga n > 0 (ada data) atau n == 0 (EOF). Setelah itu, tutup end baca dan tunggu child.

Komunikasi Dua Arah dengan Dua Pipe #

Pipe bersifat satu arah—untuk komunikasi dua arah, kita butuh dua pipe: satu untuk parent→child, satu untuk child→parent. Pola ini disebut bidirectional IPC dan banyak dipakai pada program yang memerlukan request-response.

c
/* File: pipe_dua_arah.c - parent kirim soal, child jawab */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(void) {
    int pipe_soal[2];    /* parent -> child */
    int pipe_jawab[2];   /* child -> parent */

    if (pipe(pipe_soal) != 0 || pipe(pipe_jawab) != 0) {
        perror("pipe");
        return 1;
    }
    fflush(stdout);

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

    if (pid == 0) {
        /* CHILD: baca soal, hitung, kirim jawab */
        close(pipe_soal[1]);
        close(pipe_jawab[0]);

        char soal[64];
        ssize_t n = read(pipe_soal[0], soal, sizeof(soal) - 1);
        if (n > 0) {
            soal[n] = '\0';
            int angka = atoi(soal);
            int kuadrat = angka * angka;

            char jawab[32];
            int len = snprintf(jawab, sizeof(jawab), "%d", kuadrat);
            write(pipe_jawab[1], jawab, len);
        }
        close(pipe_soal[0]);
        close(pipe_jawab[1]);
        _exit(0);
    } else {
        /* PARENT: kirim soal, baca jawab */
        close(pipe_soal[0]);
        close(pipe_jawab[1]);

        const char *soal = "12";
        write(pipe_soal[1], soal, strlen(soal));
        close(pipe_soal[1]);    /* EOF ke child */

        char jawab[64];
        ssize_t n = read(pipe_jawab[0], jawab, sizeof(jawab) - 1);
        if (n > 0) {
            jawab[n] = '\0';
            printf("[PARENT] kuadrat %s = %s\n", soal, jawab);
        }
        close(pipe_jawab[0]);
        waitpid(pid, NULL, 0);
    }
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
[PARENT] kuadrat 12 = 144

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-9 (dua pipe): pipe_soal membawa pesan dari parent ke child. pipe_jawab membawa jawaban dari child ke parent. Kedua pipe independen—menulis ke satu tidak memengaruhi yang lain.
  2. Baris 23-26 (child menutup end yang tidak dipakai): Child menutup pipe_soal[1] (writer) dan pipe_jawab[0] (reader). Setelah ini, child hanya bisa membaca soal dan menulis jawab. Pola simetris dilakukan parent di baris 47-48.
  3. Baris 30 (read(pipe_soal[0], soal, ...)): Child membaca soal sebagai string. Bila parent belum menulis, read memblokir—ini adalah mekanisme sinkronisasi natural pipe. Tidak perlu mutex atau condition variable.
  4. Baris 35 (snprintf(jawab, ...)): Child memformat jawaban menjadi string. snprintf aman dipakai di child setelah fork karena tidak ada masalah async-signal-safety di sini (kita tidak di signal handler).
  5. Baris 49-50 (parent menulis soal lalu menutup pipe_soal[1]): Menutup writer mengirim EOF ke child. Child melihat EOF setelah membaca data, sehingga read kembali dengan jumlah byte yang dibaca (tidak menunggu selamanya).

Pipeline Shell: ls | wc -l dari C #

Pola paling kuat dari pipe adalah pipeline shell—menggabungkan beberapa program seperti puzzle. Kita dapat mereplikasi pipeline ls | wc -l dari dalam C dengan dua fork dan satu pipe. Triknya: pakai dup2 untuk mengubah stdout satu child menjadi end tulis pipe, dan stdin child lain menjadi end baca pipe.

c
/* File: pipe_pipeline.c - pipeline ls | wc -l dari C */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    int pipefd[2];
    if (pipe(pipefd) != 0) {
        perror("pipe");
        return 1;
    }
    fflush(stdout);

    pid_t pid_ls = fork();
    if (pid_ls == 0) {
        /* CHILD 1: jalankan ls, output ke pipe */
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[0]);
        close(pipefd[1]);
        execlp("ls", "ls", "-1", "/usr/include", NULL);
        _exit(127);
    }

    pid_t pid_wc = fork();
    if (pid_wc == 0) {
        /* CHILD 2: baca dari pipe, jalankan wc -l */
        dup2(pipefd[0], STDIN_FILENO);
        close(pipefd[0]);
        close(pipefd[1]);
        execlp("wc", "wc", "-l", NULL);
        _exit(127);
    }

    /* PARENT: tutup kedua end pipe */
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid_ls, NULL, 0);
    waitpid(pid_wc, NULL, 0);
    printf("[PARENT] pipeline selesai.\n");
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan (jumlah file bervariasi antar sistem):

text
370
[PARENT] pipeline selesai.

Mari kita telaah baris-baris kode di atas:

  1. Baris 12 (dup2(pipefd[1], STDOUT_FILENO) di child ls): dup2 menggantikan file descriptor STDOUT_FILENO (nomor 1, stdout standar) dengan pipefd[1] (end tulis pipe). Setelah ini, semua printf atau write ke stdout di program ls sebenarnya menulis ke pipe.
  2. Baris 13-14 (close(pipefd[0]); close(pipefd[1]);): Setelah dup2, kita menutup kedua descriptor asli. dup2 sudah membuat salinan pipefd[1] ke nomor 1, jadi descriptor asli tidak diperlukan lagi. Penting untuk menutup end tulis di child ls—bila tidak, parent tidak akan menerima EOF setelah ls selesai (karena kernel menganggap masih ada writer).
  3. Baris 16 (execlp("ls", "ls", "-1", "/usr/include", NULL)): execlp (akhiran p = cari di PATH) menjalankan program ls. Setelah exec, image program diganti—ls mengira stdout-nya adalah terminal, padahal sebenarnya pipe.
  4. Baris 22 (dup2(pipefd[0], STDIN_FILENO) di child wc): Simetri—stdin wc diganti dengan end baca pipe. wc membaca dari stdin (yang sebenarnya pipe) dan menghitung baris.
  5. Baris 32-33 (parent menutup kedua end): Parent tidak butuh pipe sama sekali. Menutup keduanya penting agar tidak ada referensi tersisa yang mencegah EOF.
  6. Output 370: Jumlah file di direktori /usr/include di sistem ini. Bisa berbeda di mesin lain—jumlah file tergantung paket yang terinstall.

FIFO: Pipe Bernama untuk Proses Tak Berkerabat #

Pipe anonim hanya bekerja antara proses yang berkerabat (parent-child). Untuk komunikasi antar proses tak berkerabat, gunakan FIFO—file khusus di filesystem yang berperilaku seperti pipe. FIFO dibuat dengan mkfifo(path, mode).

c
/* File: fifo_buat.c - membuat FIFO di filesystem */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/stat.h>

int main(void) {
    if (mkfifo("/tmp/fifo_demo", 0644) == -1) {
        perror("mkfifo");
        return 1;
    }
    printf("FIFO dibuat di /tmp/fifo_demo\n");
    printf("Coba dari terminal lain:\n");
    printf("  echo 'halo' > /tmp/fifo_demo\n");
    return 0;
}

Untuk menghindari demo yang menggantung (FIFO memblokir sampai ada pembaca dan penulis bersamaan), kita hanya menunjukkan cara membuat FIFO. Pembaca dapat menguji sendiri dengan dua terminal: satu menjalankan cat /tmp/fifo_demo, satu lagi menjalankan echo pesan > /tmp/fifo_demo.