Di POSIX, hampir semua sumber daya I/O—file biasa, pipe, socket, device, terminal—diabstraksikan sebagai file descriptor: integer kecil yang menjadi indeks ke tabel di kernel proses. Kita sudah memakai file descriptor secara implisit di chapter sebelumnya (pipe mengembalikan fd, socket mengembalikan fd). Pada chapter ini kita melihat operasi low-level langsung: open, read, write, close, dan dup2. Memahami file descriptor adalah kunci untuk menulis kode yang efisien dan untuk membaca dokumentasi syscall POSIX.
Anatomi File Descriptor #
Setiap proses memiliki tabel file descriptor di kernel. Tiga entri pertama sudah terisi saat proses dimulai:
- fd 0 (
STDIN_FILENO): standard input - fd 1 (
STDOUT_FILENO): standard output - fd 2 (
STDERR_FILENO): standard error
Setiap pemanggilan open, pipe, atau socket yang berhasil mendapat fd baru—biasanya nomor terkecil yang belum terpakai. Saat fd ditutup dengan close, slot di tabel dibebaskan dan bisa dipakai fd baru.
/* File: fd_table.c - menunjukkan alokasi fd */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
printf("stdin fd = %d\n", STDIN_FILENO);
printf("stdout fd = %d\n", STDOUT_FILENO);
printf("stderr fd = %d\n", STDERR_FILENO);
/* Buka beberapa file - fd akan terus bertambah */
int fd1 = open("/tmp/fd_a.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int fd2 = open("/tmp/fd_b.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int fd3 = open("/tmp/fd_c.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
printf("fd1 = %d, fd2 = %d, fd3 = %d\n", fd1, fd2, fd3);
/* Tutup fd2 - fd number bebas untuk dipakai ulang */
close(fd2);
printf("fd2 (%d) ditutup\n", fd2);
int fd4 = open("/tmp/fd_d.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
printf("fd4 = %d (reuses fd2)\n", fd4);
/* Cleanup */
close(fd1); close(fd3); close(fd4);
unlink("/tmp/fd_a.txt"); unlink("/tmp/fd_b.txt");
unlink("/tmp/fd_c.txt"); unlink("/tmp/fd_d.txt");
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o fd_table fd_table.c
./fd_tableOutput yang Dihasilkan:
stdin fd = 0
stdout fd = 1
stderr fd = 2
fd1 = 3, fd2 = 4, fd3 = 5
fd2 (4) ditutup
fd4 = 4 (reuses fd2)Mari kita telaah baris-baris kode di atas:
- Baris 7-9 (
STDIN_FILENO, dll.): Konstanta dari<unistd.h>.STDIN_FILENOselalu0,STDOUT_FILENOselalu1,STDERR_FILENOselalu2. Konvensi ini berlaku di semua sistem POSIX. - Baris 12-14 (
open(...)x3): Setiapopenyang sukses mengembalikan fd terkecil yang belum terpakai. Karena0,1,2sudah dipakai std streams, fd pertama yang baru adalah3, lalu4, lalu5. - Baris 18 (
close(fd2)): Menutup fd4. Slot tabel di kernel dibebaskan. File/tmp/fd_b.txttetap ada di filesystem—closehanya melepas association fd-file, tidak menghapus file. - Baris 21 (
open(...)): Membuka file baru. Karena fd4sekarang bebas, kernel mengembalikan4lagi. Inilah mengapa output menunjukkanfd4 = 4. - Cleanup:
closesemua fd yang masih terbuka, laluunlinkfile demo untuk membersihkan filesystem. Lupaclosemenyebabkan kebocoran fd—bisa mencapai batasulimit -n(default 1024 di Linux).
Operasi Low-Level: open, read, write, close #
Empat fungsi inti untuk I/O file di POSIX:
/* File: fd_io.c - operasi low-level: open, read, write, close */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void) {
const char *path = "/tmp/fd_demo.txt";
/* Buka file untuk tulis (create + truncate, mode 0644) */
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open write");
return 1;
}
printf("File dibuka untuk tulis, fd=%d\n", fd);
const char *pesan = "Halo, ini ditulis dengan write()!\n";
ssize_t ditulis = write(fd, pesan, strlen(pesan));
printf("Menulis %zd byte\n", ditulis);
close(fd);
printf("fd ditutup.\n");
/* Buka file untuk baca */
fd = open(path, O_RDONLY);
if (fd < 0) {
perror("open read");
return 1;
}
printf("File dibuka untuk baca, fd=%d\n", fd);
char buf[128];
ssize_t dibaca = read(fd, buf, sizeof(buf) - 1);
if (dibaca > 0) {
buf[dibaca] = '\0';
printf("Membaca %zd byte: %s", dibaca, buf);
}
close(fd);
/* Hapus file demo */
unlink(path);
printf("File %s dihapus.\n", path);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o fd_io fd_io.c
./fd_ioOutput yang Dihasilkan:
File dibuka untuk tulis, fd=3
Menulis 34 byte
fd ditutup.
File dibuka untuk baca, fd=3
Membaca 34 byte: Halo, ini ditulis dengan write()!
File /tmp/fd_demo.txt dihapus.Mari kita telaah baris-baris kode di atas:
- Baris 9 (
open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)): Membuka file untuk ditulis.O_WRONLYmode tulis saja.O_CREATbuat file bila belum ada.O_TRUNChapus isi bila sudah ada. Argumen ketiga0644adalah permission mode (owner: read+write, group: read, other: read)—hanya dipakai bila file baru dibuat. - Baris 16 (
write(fd, pesan, strlen(pesan))): Menulisstrlen(pesan)byte dari bufferpesanke fd. Mengembalikan jumlah byte yang benar-benar ditulis. Bila return lebih kecil dari yang diminta, terjadi partial write—umum pada pipe atau socket. Untuk file biasa, write biasanya menulis semua byte yang diminta. - Baris 17 (
ssize_t ditulis = ...): Tipessize_tadalah signed size_t—bisa negatif untuk menandakan error. Selalu cek return value: bila-1, bacaerrnountuk diagnosa. - Baris 21 (
close(fd)): Menutup fd. Setelah ini,fdtidak valid dan bisa dipakai ulang olehopenberikutnya. - Baris 25 (
open(path, O_RDONLY)): Membuka file untuk dibaca. Tidak perlu argumen mode karena tidak membuat file baru. - Baris 31 (
read(fd, buf, sizeof(buf) - 1)): Membaca hinggasizeof(buf) - 1byte. Mengembalikan jumlah byte yang dibaca,0bila EOF, atau-1bila error. Kita kurangi 1 untuk menyisakan tempat null terminator. - Baris 38 (
unlink(path)): Menghapus file dari filesystem.unlinkmenghapus nama file; bila tidak ada proses lain yang membuka file, kernel benar-benar menghapus data. Bila ada fd yang masih terbuka, file tetap hidup hingga fd terakhir ditutup—ini adalah mekanisme anonymous file yang dipakai untuk temporary file.
dup2: Mengalihkan File Descriptor #
Fungsi dup2(old_fd, new_fd) menggandakan old_fd ke nomor new_fd. Bila new_fd sudah terbuka, ia ditutup dulu sebelum diganti. Trik ini dipakai untuk redirect stdin/stdout/stderr—tepat seperti yang dilakukan shell saat menulis > file.txt.
/* File: fd_dup2.c - dup2 untuk redirect stdout ke file */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void) {
int fd = open("/tmp/fd_redirect.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("open"); return 1; }
/* Simpan stdout lama */
int stdout_lama = dup(STDOUT_FILENO);
/* Redirect stdout ke file */
dup2(fd, STDOUT_FILENO);
close(fd);
/* printf sekarang menulis ke file, bukan terminal */
printf("Baris ini masuk ke file, bukan terminal.\n");
fflush(stdout);
/* Kembalikan stdout ke terminal */
dup2(stdout_lama, STDOUT_FILENO);
close(stdout_lama);
printf("Baris ini tampil di terminal.\n");
/* Baca file untuk verifikasi */
fd = open("/tmp/fd_redirect.txt", O_RDONLY);
char buf[128];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Isi file: %s", buf);
}
close(fd);
unlink("/tmp/fd_redirect.txt");
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o fd_dup2 fd_dup2.c
./fd_dup2Output yang Dihasilkan:
Baris ini tampil di terminal.
Isi file: Baris ini masuk ke file, bukan terminal.Mari kita telaah baris-baris kode di atas:
- Baris 9 (
int fd = open(...)): Membuka file target. Dapatkan fd baru (misalnya3). - Baris 13 (
int stdout_lama = dup(STDOUT_FILENO)):dupmenggandakan fd1(stdout) ke fd baru (misalnya4). Sekarang fd1dan fd4menunjuk ke entri tabel yang sama: terminal. Ini cara menyimpan referensi stdout asli sebelum kita meng-overwrite fd1. - Baris 16 (
dup2(fd, STDOUT_FILENO)): Menggandakan fd3(file) ke fd1(stdout). Setelah ini,printfyang menulis ke fd1sebenarnya menulis ke file. - Baris 17 (
close(fd)): Menutup fd3yang asli. Tabel fd1sekarang menunjuk ke file—referensi fd3sudah tidak diperlukan. - Baris 20-21 (
printf(...); fflush(stdout);):printfke stdout (fd1) sekarang masuk ke file.fflushmemaksa buffer library C ditulis ke kernel—penting sebelum kita kembalikan stdout, agar pesan tidak tertinggal di buffer. - Baris 24 (
dup2(stdout_lama, STDOUT_FILENO)): Kembalikan fd1ke terminal (meng-overwrite referensi file yang sebelumnya dipasang). Sekarangprintfkembali ke terminal. - Baris 28-32 (baca file verifikasi): Membaca file yang baru saja ditulis, untuk membuktikan bahwa
printfbaris 20 benar-benar masuk ke file.
Batas dan Manajemen fd #
Setiap proses punya batas jumlah fd yang bisa dibuka—default 1024 di Linux, dapat ditingkatkan lewat ulimit -n atau setrlimit(RLIMIT_NOFILE, ...). Untuk server yang menangani ribuan koneksi, batas ini bisa menjadi kendala—kernel Linux menyediakan epoll untuk efisiensi monitoring fd dalam jumlah besar.