Files and I/O

Table of Contents

1. Design Philosophy

POSIX has a set of design philosophies that define the standard for UNIX-based operating systems:

  1. Uniformity: The basic UNIX philosophy is that everything is a “file”. In other words, the same identical file interface is used for files on disk, devices, networking, IPC, etc. The file interface is based on four fundamental system calls: open(), read(), write(), and close(). There is also the additional ioctl() for custom configuration that doesn’t fit.
  2. Open Before Use: Before any I/O operation, the “device” must be opened before using. This process allows the OS to perform various checks and bookkeeping operations before the device can be used.
  3. Byte Oriented: All data from devices are addressed in bytes, even for devices with different block sizes.
  4. Buffered Read/Writes: Data from devices are stored in a kernel buffer and returned on request. Similarly, outgoing data to devices is also stored in a kernel buffer until the device is ready. Buffering allows the same interface for devices with different read block sizes and write speeds.
  5. Explicit Close: close() must be explicitly called upon a device when an appication is done with it. Similar to open, this allows the OS to perform various bookkeeping operations.

2. Filesystems

The filesystem abstraction is used to represent files on disk. A file is a named collection of data in a file system (a sequence of bytes). We can also store some amount of file metadata with information about the file.

A directory is a “folder” containing files and possibly other directories. The path defined by directories uniquely identifies a file or directory.

Every process has a current working directory (CWD). All relative paths are relative to this CWD, which can be changed with the chdir() system call.

3. File API

3.1. Streams

At the high level, we operate on streams: unformatted sequences of bytes, with a position. Streams in C a represented by a pointer to a FILE data structure, which are returned by functions such as fopen().

Three predefined streams are opened implicitly when the program is executed:

  • FILE *stdin — normal source of input
  • FILE *stdout — normal source of output
  • FILE *stderr — diagnostics and errors

High level operations on streams are buffered in user memory, which means that printing, sleeping, and then printing again may actually cause everything to be printed out at once. However, low level operations on file descriptors are immediate.

3.2. Reading and Writing

C also gives us several different functions to read and write to files:

// character oriented
int fputc(int c, FILE *fp ); // rtn c or EOF on err
int fputs(const char *s, FILE *fp ); // rtn > 0 or EOF
int fgetc(FILE * fp );
char *fgets(char *buf, int n, FILE *fp );

// block oriented
size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

// formatted
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int fscanf(FILE *restrict stream, const char *restrict format, ... );

3.3. Pointer Positioning

The file stream also includes a pointer to the current position in the file. These below functions can help with positioning the pointer:

int fseek(FILE *stream, long int offset, int whence);
long int ftell(FILE *stream);
void rewind(FILE *stream);

For fseek(), the offset is interpreted based on the whence argument, which can be set the SEEK_SET (from beginning), SEEK_CUR, or SEEK_END (from end).

3.4. File Syscalls

The high level C functions presented above are built upon a low level file I/O system call interface:

int open(const char *filename, int flags, mode_t mode);
int creat(const char *filename, mode_t mode);
int close(int filedes);
ssize_t read(int filedes, void *buffer, size_t maxsize);
ssize_t write(int filedes, const void *buffer, size_t size);
off_t lseek(int filedes, off_t offset, int whence);
int dup(int oldfd);
int dup2(int oldfd, int newfd);

3.5. File Descriptors

Opening a file with open() returns a file descriptor, an integer that represents an open file. STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO are pre-opened standard descriptors that can be used as macros.

File descriptors are indices into a file descriptor table (FDT). Each process has its own FDT stored in kernel memory, where each entry points to a file description, which is a data structure inside of the kernel that store information about where to find the data on disk and the current position within the file.

3.5.1. Aliased File Descriptions

Suppose that we open a file description in a process, and then we fork that process. Then, the file descriptor of that open file is copied to the FDT of the new process, and the open file description is aliased with the file description of the original process. Thus, changing the file description of the open file in the new process also changes the file description for the original process. The open file description will remain alive until no file descriptors point to it.

4. Device Drivers

Device drivers are device-specific code in the kernel that interacts directly with the device hardware. It supports the standard system call interface so that the kernel can interact easily with different device drivers, with special device-specific configuration supported with the ioctl() syscall.

Device drivers are typically divided into two pieces. The top half is accessed via system calls, which implements a set of standard calls like open(), close(), read(), etc — this is the kernel’s interface to the device driver. The bottom half runs as a interrupt routine which gets input or transfers next block of output.

5. Inter-Process Communication (IPC)

Recall that a process has a protected address space with one or more threads that by default cannot share information between processes. Thus, communication between processes must be set up explicitly using a variety of abstractions, such as streams, files, sockets, and pipes.

6. Sockets

Communication across the world looks like file I/O! The interface underpinning this are sockets, which is an abstraction for one endpoint of a network connection. It is the same abstraction for any kind of network: whether local, the Internet (TCP/IP, UDP/IP), and even legacy systems (OSI, Appletalk, IPX).

A socket acts just like a file with a file descriptor. We can then connect sockets together to perform communication across the network. Each socket represents a queue, where write() adds to the output queue (data destined for the other side), and read() removes from the input queue (data destined for this side).

6.1. TCP/IP

We have a hostname (e.g. www.eecs.berkeley.edu) that resolves into some kind of IP address (128.32.244.172), and then finally a port number that identifies a socket. Port numbers are allocated as follows:

  • 0-1023 are “well-known” or “system” ports that require superuser privileges
  • 1024-49151 are “registered” ports assigned by IANA for specific services
  • 49152-65535 are “dynamic” or “private”, and are automatically allocated as “ephemeral ports”

To setup a connection over TCP/IP, the client side must first request a connection to the server via a server socket. If the server accepts, then it will create a new socket to handle communication with the client. A 5-tuple identifies each connection:

  1. Source IP address
  2. Destination IP address
  3. Source port number
  4. Destination port number
  5. Protocol (always TCP here)
Last modified: 2026-09-15 14:56