Appearance
[中文版本](/posts/c教程/zh-CN/07-贴近系统/063-File Descriptors and openreadwrite)
Now that you can create and manage processes, let’s explore how those processes communicate with files, devices, and even each other, through file descriptors.
File descriptors (FDs) are one of the simplest yet most powerful abstractions in Unix and C. Everything, files, pipes, sockets, terminals, is represented by a small integer handle. Once you understand how to open, read, write, and close file descriptors, you can interact with any I/O system on a Unix machine.
Step 1. What Is a File Descriptor?
A file descriptor is an integer that identifies an open resource in your process. Every process starts with three open descriptors by default:
| FD | Symbolic Name | Description |
|---|---|---|
| 0 | STDIN_FILENO | Standard input (keyboard) |
| 1 | STDOUT_FILENO | Standard output (screen) |
| 2 | STDERR_FILENO | Standard error (screen) |
Each time you open a file, socket, or pipe, the kernel gives you the lowest unused FD.
Step 2. Opening Files with open()
You can open files directly using the system call layer, instead offopen() from stdio.
#include <fcntl.h> // open
#include <unistd.h> // close, read, write
#include <stdio.h> // perror
int main(void) {
int fd = open("data.txt", O_RDONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
printf("File descriptor: %d\n", fd);
close(fd);
}Compile and run:
gcc open_demo.c -o open_demo
./open_demoOutput example:
File descriptor: 3Step 3. Reading from a File
read(fd, buffer, size) reads raw bytes into a memory buffer.
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = open("data.txt", O_RDONLY);
if (fd == -1) return 1;
char buf[64];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Read %zd bytes: %s\n", n, buf);
}
close(fd);
}Output:
Read 12 bytes: Hello worldStep 4. Writing to a File
write(fd, buffer, size) writes raw bytes from memory to a file descriptor.
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void) {
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
const char msg[] = "Writing from C using write()\n";
write(fd, msg, strlen(msg));
close(fd);
}This overwritesout.txt with your message. Flags:
O_WRONLY→ write-onlyO_CREAT→ create if it doesn’t existO_TRUNC→ truncate (clear) existing contents
The final argument0644 sets Unix permissions:
- Owner can read/write,
- Group/others can read.
Step 5. Append and Non-blocking Modes
You can combine flags using bitwise OR:
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);O_APPEND moves the file offset to the end before every write, ideal for logs.
You can also open files as non-blocking:
int fd = open("pipe", O_RDONLY | O_NONBLOCK);Useful for I/O on sockets or named pipes.
Step 6. Duplicating Descriptors
You can duplicate an FD usingdup() ordup2(). This is how redirection works (> in shells).
Example:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void) {
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // redirect stdout to file
close(fd);
printf("This goes into output.txt!\n");
}Afterdup2, everything printed to stdout goes tooutput.txt.
Run and inspect:
./redir_demo
cat output.txtOutput:
This goes into output.txt!Step 7. Offsets and lseek()
You can move around inside a file usinglseek(fd, offset, whence).
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = open("data.txt", O_RDONLY);
lseek(fd, 5, SEEK_SET); // move to byte 5
char buf[16];
read(fd, buf, 10);
buf[10] = '\0';
printf("Chunk: %s\n", buf);
close(fd);
}whence can be:
SEEK_SET(from start)SEEK_CUR(from current)SEEK_END(from end)
Step 8. Error Checking and Return Values
All system calls return:
- A nonnegative value → success
-1→ error (checkerrno)
Example:
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
int fd = open("missing.txt", O_RDONLY);
if (fd == -1)
fprintf(stderr, "Error: %s\n", strerror(errno));
}Output:
Error: No such file or directoryTiny Code: Copy File Using System Calls
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int src = open("source.txt", O_RDONLY);
int dst = open("copy.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (src == -1 || dst == -1) {
perror("open failed");
return 1;
}
char buf[256];
ssize_t n;
while ((n = read(src, buf, sizeof(buf))) > 0)
write(dst, buf, n);
close(src);
close(dst);
return 0;
}Compile and run:
gcc copy.c -o copy
./copyNowcopy.txt is identical tosource.txt, using pure syscalls.
Step 9. Reading from STDIN and Writing to STDOUT
You can useread(0, ...) andwrite(1, ...) directly for console I/O.
#include <unistd.h>
int main(void) {
char buf[64];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}Run:
./echo_demo
hello worldOutput:
hello worldThat’s the essence of every shell command.
Step 10. Why It Matters
File descriptors unify I/O across everything:
- Regular files
- Pipes and sockets
- Devices and terminals
They let you control exactly how data flows in and out of your program, a foundation for system tools, servers, and OS-level programming.
Once you understand these primitives, you can build your own versions of tools likecat,tee, and even simple shells.
Try It Yourself
- Write a mini
catclone usingread()andwrite(). - Use
dup2()to redirect both stdout and stderr to a file. - Add error messages using
perror()and handleEINTR. - Use
lseek()to skip the first N bytes of a file before printing. - Implement a simple file appender with
O_APPEND.
Next, you’ll use these file descriptors to make processes communicate, building pipes and redirection, the same mechanisms shells use to connect commands likels | grep.
