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
#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:
File Descriptors
Every process has three standard file descriptors:
Descriptor Name Value Purpose STDIN_FILENOStandard input 0 Read user input STDOUT_FILENOStandard output 1 Write normal output STDERR_FILENOStandard error 2 Write error messages
File descriptors are non-negative integers. A return value of -1 indicates an error.
Implementation Examples
Reading a Text File
#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:
Validates filename is not NULL
Opens file in read-only mode
Allocates buffer for reading
Reads specified number of bytes
Writes to standard output
Properly cleans up resources
Creating a File
#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, 0 600 );
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
#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:
#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, 0 664 );
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:
open() error checking
read() error checking
write() error checking
close() error checking
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
Digit Binary Permission 0 000 --- (none)1 001 --x (execute)2 010 -w- (write)3 011 -wx (write + execute)4 100 r-- (read)5 101 r-x (read + execute)6 110 rw- (read + write)7 111 rwx (all)
For sensitive files, use 0600. For shared files, use 0644 or 0664.
Best Practices
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 */ }
Close File Descriptors
Always close files when done to prevent resource leaks: Even if an error occurs, close the file before returning.
Free Allocated Memory
If you allocate buffers with malloc(), free them:
Use Appropriate Buffer Sizes
Common buffer sizes:
Small reads: 256-512 bytes
General purpose: 1024-4096 bytes
Large files: 8192+ bytes
Set Proper Permissions
Choose appropriate file permissions:
Private files: 0600
Shared readable files: 0644
Shared writable files: 0664
Common Pitfalls
Forgetting to check return values
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 );
Not closing file descriptors
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, 0 644 ); // Creates if needed
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