Episode ini menyentuh lapisan sistem operasi: system call dan wrapper libc, process creation dengan fork, exec, dan wait, signals, pipes, dan inter-process communication, serta file descriptors dengan select dan poll untuk event-driven I/O.

Program C tidak berjalan sendiri di atas perangkat keras; ia berjalan di atas sistem operasi yang menyediakan layanan lewat system call. Episode 18 membawa kalian ke level sistem programming: menciptakan proses, berkomunikasi antar proses, dan menangani I/O yang banyak sekaligus.
Kita mulai dari system call dan wrapper libc, lalu fork, exec, dan wait untuk manajemen proses, kemudian signals dan pipes sebagai alat komunikasi antar proses. Terakhir, select dan poll memungkinkan satu program menangani banyak file descriptor tanpa memblokir.
Ini adalah materi yang menghubungkan C dengan kernel — bahasa yang sama yang menulis Unix dan Linux kembali digunakan untuk berinteraksi dengannya.
Aplikasi tidak memanggil kernel secara langsung, melainkan memanggil fungsi libc yang menjadi wrapper. Misalnya open, read, write, dan close memanggil system call dengan nama yang sama, ditambah menangani errno dan parameter. Wrapper ini menyeragamkan antarmuka dan menangani detail arsitektur.
Semua I/O Linux berjalan lewat file descriptor, bilangan yang mewakili file, socket, atau pipe terbuka:
#include <fcntl.h>
#include <unistd.h>
int main(void) {
int fd = open("catatan.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
return 1;
}
write(fd, "data dari system call\n", 22);
close(fd);
return 0;
}
EOF
gcc -Wall -Wextra syscall.c -o syscall && ./syscallPanggilan open("catatan.txt", O_WRONLY | O_CREAT, 0644) membuka file dan mengembalikan descriptor, write menulis byte, dan close melepaskannya. O_CREAT membuat file jika belum ada, dan 0644 adalah izin default. Ini dasar dari seluruh I/O Linux, termasuk socket di episode 12.
fork menyalin proses saat ini menjadi dua: proses induk dan anak, keduanya melanjutkan dari titik yang sama tetapi menerima nilai kembalian berbeda:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
printf("proses anak\n");
return 0;
}
waitpid(pid, NULL, 0);
printf("proses induk selesai\n");
return 0;
}pid_t pid = fork() mengembalikan 0 di anak dan PID anak di induk. waitpid(pid, NULL, 0) membuat induk menunggu anak selesai, mencegah zombie process. Tanpa wait, proses anak yang selesai meninggalkan jejak di tabel proses.
exec menggantikan gambar proses saat ini dengan program lain. Kombinasi klasik: fork membuat anak, lalu anak memanggil exec untuk menjalankan program baru:
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
execlp("ls", "ls", "-l", NULL);
return 1;
}
waitpid(pid, NULL, 0);
return 0;
}execlp("ls", "ls", "-l", NULL) memanggil ls -l di dalam proses anak. Argumen pertama adalah nama program, yang kedua adalah nama yang muncul di argv[0], lalu argumen berikutnya, dan NULL sebagai penutup. Jika exec gagal, fungsi mengembalikan kontrol — inilah mengapa ada return 1 setelahnya.
Signals adalah notifikasi asinkron ke proses: SIGINT saat Ctrl+C, SIGTERM saat diminta berhenti. Program bisa menangkap signal dan menjalankan handler:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) {
write(2, "menerima sinyal\n", 16);
}
int main(void) {
signal(SIGTERM, handler);
pause();
return 0;
}signal(SIGTERM, handler) mendaftarkan handler untuk sinyal terminasi, dan pause menahan proses sampai ada sinyal. Handler sebaiknya hanya memanggil fungsi async-signal-safe seperti write, bukan printf.
Pipe menghubungkan output satu proses ke input proses lain, seperti perintah cat file | grep pola di shell:
#include <stdio.h>
#include <unistd.h>
int main(void) {
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
close(fd[0]);
write(fd[1], "halo dari anak\n", 15);
return 0;
}
close(fd[1]);
char buf[64];
read(fd[0], buf, sizeof(buf));
printf("%s", buf);
return 0;
}pipe(fd) menyediakan dua descriptor: fd[1] untuk menulis dan fd[0] untuk membaca. Anak menulis, induk membaca. Menutup sisi yang tidak dipakai adalah aturan penting untuk mencegah kebuntuan.
Server yang harus melayani banyak koneksi tidak bisa memblokir di satu read. select dan poll memantau banyak descriptor sekaligus dan memberi tahu mana yang siap dibaca:
#include <poll.h>
#include <stdio.h>
int main(void) {
struct pollfd fds[1];
fds[0].fd = 0;
fds[0].events = POLLIN;
int siap = poll(fds, 1, 5000);
if (siap > 0 && (fds[0].revents & POLLIN)) {
printf("stdin siap dibaca\n");
}
return 0;
}struct pollfd fds[1] mendeklarasikan array descriptor yang dipantau, dan poll(fds, 1, 5000) menunggu sampai ada yang siap atau timeout 5 detik. Ini pola dasar event-driven I/O yang dipakai server berskala besar — menunggu tanpa menghabiskan satu thread per koneksi.
Tip
Perhatikan perbedaan pola: satu thread per koneksi menangani concurrency dengan simpel tetapi mahal di sumber daya. select, poll, atau epoll memungkinkan satu thread menangani ribuan koneksi — pilihan penting untuk server produksi.
Inti yang harus dibawa pulang:
Di episode 19 selanjutnya kita akan membahas tooling modern dan build automation — toolchain GCC, Clang, dan MSVC, static analysis, sanitizer, dan linting dalam workflow, build automation dengan make, CMake, dan CI pipelines, serta packaging binary dan dasar cross-compilation.