Skip to main content

Overview

File I/O in C can be performed at two levels: high-level (using stdio.h functions like fopen, fread) and low-level (using system calls like open, read, write). This guide focuses on low-level file I/O operations that interact directly with the operating system.
Low-level file I/O uses file descriptors (integers) instead of FILE pointers, giving you direct control over system calls.

System Calls vs Standard Library

System Calls

  • Direct kernel interaction
  • Use file descriptors (int)
  • Functions: open, read, write, close
  • Unbuffered I/O
  • More control, more responsibility

Standard Library

  • Wrapper around system calls
  • Use FILE pointers
  • Functions: fopen, fread, fwrite, fclose
  • Buffered I/O
  • Easier to use, less control

Required Headers

main.h
#ifndef FILEIO
#define FILEIO

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

ssize_t read_textfile(const char *filename, size_t letters);
int create_file(const char *filename, char *text_content);
int append_text_to_file(const char *filename, char *text_content);

#endif /*FILEIO*/

Core File Operations

Opening Files

The open() system call opens a file and returns a file descriptor:
int fd = open(filename, flags, mode);
Common flags:
  • O_RDONLY - Open for reading only
  • O_WRONLY - Open for writing only
  • O_RDWR - Open for reading and writing
  • O_CREAT - Create file if it doesn’t exist
  • O_TRUNC - Truncate file to zero length
  • O_APPEND - Append to end of file
File modes (when creating):
  • 0600 - Owner can read and write
  • 0644 - Owner can read/write, others can read
  • 0664 - Owner and group can read/write, others can read

Reading from Files

The read() system call reads data into a buffer:
ssize_t bytes_read = read(fd, buffer, count);

Writing to Files

The write() system call writes data from a buffer:
ssize_t bytes_written = write(fd, buffer, count);

Closing Files

Always close file descriptors when done:
int result = close(fd);

File Descriptors

Every process has three standard file descriptors:
DescriptorNameValuePurpose
STDIN_FILENOStandard input0Read user input
STDOUT_FILENOStandard output1Write normal output
STDERR_FILENOStandard error2Write error messages
File descriptors are non-negative integers. A return value of -1 indicates an error.

Implementation Examples

Reading a Text File

0-read_textfile.c
#include "main.h"

/**
 * read_textfile - reads a text file and prints it to the POSIX standard output
 * @filename: filename.
 * @letters: numbers of letters printed.
 * Return: numbers of letters it could read and print
 */
ssize_t read_textfile(const char *filename, size_t letters)
{
	int fd;
	ssize_t nrd, nwr;
	char *buf;

	if (!filename)
		return (0);

	fd = open(filename, O_RDONLY);

	if (fd == -1)
		return (0);

	buf = malloc(sizeof(char) * (letters));
	if (!buf)
		return (0);

	nrd = read(fd, buf, letters);
	nwr = write(STDOUT_FILENO, buf, nrd);

	close(fd);

	free(buf);

	return (nwr);
}
Key points:
  1. Validates filename is not NULL
  2. Opens file in read-only mode
  3. Allocates buffer for reading
  4. Reads specified number of bytes
  5. Writes to standard output
  6. Properly cleans up resources

Creating a File

1-create_file.c
#include "main.h"

/**
 * create_file - creates a file
 * @filename: filename.
 * @text_content: content writed in the file.
 *
 * Return: 1 if it success. -1 if it fails.
 */
int create_file(const char *filename, char *text_content)
{
	int fd;
	int nletters;
	int rwr;

	if (!filename)
		return (-1);

	fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0600);

	if (fd == -1)
		return (-1);

	if (!text_content)
		text_content = "";

	for (nletters = 0; text_content[nletters]; nletters++)
		;

	rwr = write(fd, text_content, nletters);

	if (rwr == -1)
		return (-1);

	close(fd);

	return (1);
}
Flags explained:
  • O_CREAT - Create the file if it doesn’t exist
  • O_WRONLY - Open for writing only
  • O_TRUNC - Truncate to zero length if file exists
  • 0600 - Permissions: owner read/write only
Permissions 0600 mean only the file owner can read and write. Use 0644 to allow others to read.

Appending to a File

2-append_text_to_file.c
#include "main.h"

/**
 * append_text_to_file - appends text at the end of a file
 * @filename: filename.
 * @text_content: added content.
 * Return: 1 if the file exists. -1 if the fails does not exist
 * or if it fails.
 */
int append_text_to_file(const char *filename, char *text_content)
{
	int fd;
	int nletters;
	int rwr;

	if (!filename)
		return (-1);

	fd = open(filename, O_WRONLY | O_APPEND);

	if (fd == -1)
		return (-1);

	if (text_content)
	{
		for (nletters = 0; text_content[nletters]; nletters++)
			;

		rwr = write(fd, text_content, nletters);

		if (rwr == -1)
			return (-1);
	}

	close(fd);

	return (1);
}
Key difference from create_file:
  • Uses O_APPEND flag instead of O_TRUNC
  • Doesn’t use O_CREAT - file must already exist
  • Returns 1 even if text_content is NULL (file exists check)

Copying Files

A complete file copy implementation with error handling:
3-cp.c
#include "main.h"
#include <stdio.h>

/**
 * error_file - checks if files can be opened.
 * @file_from: file_from.
 * @file_to: file_to.
 * @argv: arguments vector.
 * Return: no return.
 */
void error_file(int file_from, int file_to, char *argv[])
{
	if (file_from == -1)
	{
		dprintf(STDERR_FILENO, "Error: Can't read from file %s\n", argv[1]);
		exit(98);
	}
	if (file_to == -1)
	{
		dprintf(STDERR_FILENO, "Error: Can't write to %s\n", argv[2]);
		exit(99);
	}
}

/**
 * main - check the code for Holberton School students.
 * @argc: number of arguments.
 * @argv: arguments vector.
 * Return: Always 0.
 */
int main(int argc, char *argv[])
{
	int file_from, file_to, err_close;
	ssize_t nchars, nwr;
	char buf[1024];

	if (argc != 3)
	{
		dprintf(STDERR_FILENO, "%s\n", "Usage: cp file_from file_to");
		exit(97);
	}

	file_from = open(argv[1], O_RDONLY);
	file_to = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC | O_APPEND, 0664);
	error_file(file_from, file_to, argv);

	nchars = 1024;
	while (nchars == 1024)
	{
		nchars = read(file_from, buf, 1024);
		if (nchars == -1)
			error_file(-1, 0, argv);
		nwr = write(file_to, buf, nchars);
		if (nwr == -1)
			error_file(0, -1, argv);
	}

	err_close = close(file_from);
	if (err_close == -1)
	{
		dprintf(STDERR_FILENO, "Error: Can't close fd %d\n", file_from);
		exit(100);
	}

	err_close = close(file_to);
	if (err_close == -1)
	{
		dprintf(STDERR_FILENO, "Error: Can't close fd %d\n", file_from);
		exit(100);
	}
	return (0);
}
Exit codes:
  • 97 - Wrong number of arguments
  • 98 - Cannot read from source file
  • 99 - Cannot write to destination file
  • 100 - Cannot close file descriptor
The buffer size of 1024 bytes is a good balance between memory usage and performance. Larger buffers can improve performance for large files.

Error Handling

Checking Return Values

Always check return values from system calls:
int fd = open(filename, O_RDONLY);
if (fd == -1)
{
    perror("Error opening file");
    return (-1);
}

Using dprintf for Error Messages

dprintf() writes directly to a file descriptor:
dprintf(STDERR_FILENO, "Error: Can't read from file %s\n", filename);
This is useful when you need to write errors while avoiding buffered I/O.

File Permissions

Permission Modes

File permissions are specified in octal:
0600 = rw-------  (owner: read+write)
0644 = rw-r--r--  (owner: read+write, others: read)
0664 = rw-rw-r--  (owner+group: read+write, others: read)
0666 = rw-rw-rw-  (all: read+write)
0755 = rwxr-xr-x  (owner: all, others: read+execute)

Permission Breakdown

DigitBinaryPermission
0000--- (none)
1001--x (execute)
2010-w- (write)
3011-wx (write + execute)
4100r-- (read)
5101r-x (read + execute)
6110rw- (read + write)
7111rwx (all)
For sensitive files, use 0600. For shared files, use 0644 or 0664.

Best Practices

1

Always Check Return Values

Every system call can fail. Check for errors:
if (fd == -1) { /* handle error */ }
if (bytes_read == -1) { /* handle error */ }
if (close(fd) == -1) { /* handle error */ }
2

Close File Descriptors

Always close files when done to prevent resource leaks:
close(fd);
Even if an error occurs, close the file before returning.
3

Free Allocated Memory

If you allocate buffers with malloc(), free them:
free(buffer);
4

Use Appropriate Buffer Sizes

Common buffer sizes:
  • Small reads: 256-512 bytes
  • General purpose: 1024-4096 bytes
  • Large files: 8192+ bytes
5

Set Proper Permissions

Choose appropriate file permissions:
  • Private files: 0600
  • Shared readable files: 0644
  • Shared writable files: 0664

Common Pitfalls

Problem:
int fd = open(filename, O_RDONLY);
read(fd, buffer, size); // fd might be -1!
Solution:
int fd = open(filename, O_RDONLY);
if (fd == -1)
    return (-1);
Problem: Leaking file descriptors leads to “too many open files” errors.Solution: Always close files, even in error paths:
if (error_occurred)
{
    close(fd);
    return (-1);
}
Problem: read() and write() may transfer fewer bytes than requested.Solution: Loop until all bytes are transferred:
while (bytes_remaining > 0)
{
    ssize_t n = write(fd, buf + bytes_written, bytes_remaining);
    if (n == -1)
        return (-1);
    bytes_written += n;
    bytes_remaining -= n;
}
Problem:
fd = open(filename, O_WRONLY); // Can't create new file
Solution:
fd = open(filename, O_WRONLY | O_CREAT, 0644); // Creates if needed

Performance Considerations

Buffer Size

Larger buffers reduce system call overhead but use more memory. Typical: 1024-8192 bytes.

Unbuffered I/O

System calls are unbuffered. For many small operations, consider using standard library (stdio.h).

Minimize System Calls

Each system call has overhead. Batch operations when possible.

Error Handling

Proper error handling prevents resource leaks and undefined behavior.

Compilation

gcc -Wall -pedantic -Werror -Wextra -std=gnu89 file_io.c -o file_io
Recommended flags:
  • -Wall - Enable all warnings
  • -Werror - Treat warnings as errors
  • -Wextra - Additional warnings
  • -pedantic - Strict ISO C compliance

Build docs developers (and LLMs) love