In the second part of this series, we introduced system calls and explained how they act as the gateway between user applications and the kernel. Now, we’ll focus on one of the most fundamental areas: file input and output (I/O). Files are central to Linux. In fact, Linux treats almost everything as a file, regular files, directories, sockets, pipes, devices they all share a common abstraction: the file descriptor.
1. File Descriptors
A file descriptor (FD) is a small integer that represents an open file (or other resource).
By convention:
- 0 → Standard input (stdin)
- 1 → Standard output (stdout)
- 2 → Standard error (stderr)
Example: Writing to stdout without using printf:
#include <unistd.h>
int main()
{
const char msg[] = "Hello stdout via write()!\n";
write(1, msg, sizeof(msg) - 1);
return 0;
}
2. Opening and Closing Files
open()
#include <fcntl.h>
int fd = open(const char *pathname, int flags, mode_t mode);
- pathname → file path
- flags → access mode (O_RDONLY, O_WRONLY, O_RDWR, plus modifiers like O_CREAT, O_APPEND)
- mode → permissions (used if O_CREAT is given, e.g., 0644)
If successful, open() returns a non-negative file descriptor. On error, it returns -1 and sets errno.
Example:
int fd = open("notes.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0)
{
perror("open failed");
}
close()
#include <unistd.h>
int close(int fd);
Closes a file descriptor. Always close FDs you no longer need.
3. Reading and Writing
read()
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- Reads up to count bytes from fd into buf.
- Returns number of bytes read, 0 on EOF or -1 on error.
write()
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- Writes up to count bytes from buf to fd.
- Returns number of bytes written or -1 on error.
4. File Positioning
Each open file has a file offset, the current position in the file. You can move it with lseek().
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
- SEEK_SET: set position to offset
- SEEK_CUR: move relative to current position
- SEEK_END: move relative to end of file
Example: jumping to the end of a file:
lseek(fd, 0, SEEK_END);
5. File Metadata
System calls like stat, fstat, and lstat retrieve information about files.
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
These populate a struct stat with details like file size, permissions, timestamps and type.
Example:
struct stat st;
if (stat("notes.txt", &st) == 0)
{
printf("File size: %ld bytes\n", st.st_size);
}
6. Creating and Removing Files/Directories
- creat(path, mode) → legacy equivalent of open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
- mkdir(path, mode) → create a directory
- unlink(path) → remove a file
- rmdir(path) → remove a directory (must be empty)
Example:
mkdir("mydir", 0755);
unlink("oldfile.txt");
7. Example: Implementing a Mini cp
Here’s a simple file copy program using system calls:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
fprintf(stderr, "Usage: %s <source> <dest>\n", argv[0]);
return 1;
}
int src = open(argv[1], O_RDONLY);
if (src < 0)
{
perror("open source");
return 1;
}
int dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dst < 0)
{
perror("open dest");
close(src);
return 1;
}
char buf[4096];
ssize_t n;
while ((n = read(src, buf, sizeof(buf))) > 0)
{
if (write(dst, buf, n) != n)
{
perror("write error");
close(src);
close(dst);
return 1;
}
}
if (n < 0)
{
perror("read error");
}
close(src);
close(dst);
return 0;
}
This program demonstrates open, read, write and close working together to implement a core Unix utility.
8. TL/DR
- Files and resources are accessed through file descriptors.
- open() creates a file descriptor, while close() releases it.
- read() and write() perform I/O using file descriptors.
- lseek() manipulates the file offset for random access.
- Metadata about files can be retrieved with stat and related calls.
- Many higher-level tools (cp, cat, etc.) are built directly on these system calls.
At this point, you should be comfortable with low-level file operations in Linux.
Next, in Part 3: Process Management System Calls, we’ll explore how new processes are created and managed with fork, execve and wait.