files and sockets
Files and Sockets
Learn how operating systems represent files and network communication through descriptors, how applications perform reliable input and output, and how partial transfers, blocking, concurrency and resource ownership affect system correctness.
Introduction
Files and sockets are fundamental input/output resources. Files provide access to persistent or device-backed data, while sockets provide communication between processes on the same machine or across a network.
On POSIX systems, applications commonly access both resources through file
descriptors. This unified interface allows functions such as
read(), write() and close() to work
with several kinds of operating-system-managed resources.
Applications use files and sockets to:
- Read configuration and application data
- Write logs and reports
- Persist records
- Transfer files
- Communicate between processes
- Accept client connections
- Send requests and responses
- Stream messages and events
Core idea: Files and sockets expose similar descriptor- based operations, but their behaviour is not identical. A regular file has persistent content and a file position, while a stream socket carries a sequence of bytes whose message boundaries must be defined by the application protocol.
In your System Design curriculum, Files and Sockets is Topic 2.6 under Computer Systems, Linux and Concurrency. It follows synchronization, race conditions and deadlocks, and prepares learners for Linux process and network diagnostic tools.
Prerequisites
| # | Prerequisite | Why It Is Needed |
|---|---|---|
| 1 | CPU, memory, disk and network costs | File and socket operations can wait for storage, devices or remote systems. |
| 2 | Processes versus threads | Descriptors belong to process environments and can be accessed by several threads. |
| 3 | Synchronization | Concurrent access to shared descriptors and buffers requires coordination. |
| 4 | Race conditions and deadlocks | Concurrent file and socket operations can create unsafe ordering and blocking defects. |
| 5 | C pointers and arrays | Low-level I/O functions transfer data through memory buffers. |
| 6 | Basic error handling | Every I/O operation can fail, transfer partially or be interrupted. |
What Is a File?
A regular file is a named filesystem object containing a sequence of bytes. The filesystem also maintains metadata describing the object.
File metadata can include:
- File type
- File size
- Owner and group identifiers
- Permission bits
- Modification and access timestamps
- Filesystem-specific identifiers
- Links to the underlying object
Simplified Filesystem Relationship
Pathname
|
v
Directory Entry
|
v
Filesystem Object Metadata
|
v
File Data Blocks
A pathname identifies a route through directories. After a file is opened, the process normally performs I/O using the returned descriptor rather than repeating pathname resolution for every operation.
File Descriptors
A file descriptor is a non-negative integer used by a process to refer to an open kernel-managed I/O resource.
File descriptors can refer to:
- Regular files
- Directories
- Pipes
- Devices
- Network sockets
- Local-domain sockets
- Other platform-specific resources
Standard Descriptors
| Descriptor | Name | Purpose |
|---|---|---|
| 0 | Standard input | Default input source |
| 1 | Standard output | Default normal output destination |
| 2 | Standard error | Default diagnostic output destination |
Descriptor Relationship
Application Process
Descriptor 0 ----> Standard Input
Descriptor 1 ----> Standard Output
Descriptor 2 ----> Standard Error
Descriptor 3 ----> Open File
Descriptor 4 ----> Network Socket
Descriptor 5 ----> Pipe
Descriptor scope: Descriptor values belong to a process context. Descriptor 4 in one process can refer to a completely different resource from descriptor 4 in another process.
File Descriptor vs FILE Stream
C supports standard I/O streams through the FILE type, while
POSIX systems provide lower-level descriptor operations.
| Area | FILE Stream | File Descriptor |
|---|---|---|
| Interface | ISO C standard library | POSIX operating-system interface |
| Representation | Pointer to a FILE object |
Non-negative integer |
| Opening | fopen() |
open() or socket() |
| Reading | fread(), fgets() |
read() |
| Writing | fwrite(), fprintf() |
write() |
| Closing | fclose() |
close() |
| Buffering | Normally managed by the C library | Application and kernel buffering must be considered |
Do not close the same underlying resource independently through both interfaces unless ownership and conversion behaviour are carefully defined.
Opening a File
The POSIX open() function opens a filesystem object and
returns a file descriptor.
#include <fcntl.h>
int descriptor =
open(
"input.txt",
O_RDONLY
);
Common flags include:
| Flag | Meaning |
|---|---|
O_RDONLY |
Open for reading |
O_WRONLY |
Open for writing |
O_RDWR |
Open for reading and writing |
O_CREAT |
Create the file when it does not exist |
O_EXCL |
With creation, fail if the path already exists |
O_TRUNC |
Truncate an eligible existing file |
O_APPEND |
Perform writes using append semantics |
O_NONBLOCK |
Request nonblocking behaviour where supported |
Data-safety warning: Opening an existing file with truncation can remove its current content immediately. Validate the destination and use an appropriate temporary-file or replacement strategy when preserving recoverable data matters.
Reading from a Descriptor
The POSIX read() function requests bytes from an open
descriptor.
#include <unistd.h>
unsigned char buffer[4096];
ssize_t amount =
read(
descriptor,
buffer,
sizeof buffer
);
Interpreting the Result
| Result | Meaning |
|---|---|
| Positive value | The number of bytes placed in the buffer |
| Zero | End of file, or orderly end of a stream according to resource semantics |
| Negative value | The operation failed and error information is available through the platform mechanism |
A successful read can return fewer bytes than requested. Correct programs process the returned byte count rather than assuming the buffer was filled.
Writing to a Descriptor
const char message[] =
"Hello, file descriptor!\n";
ssize_t amount =
write(
descriptor,
message,
sizeof message - 1
);
A successful write can report fewer bytes than requested. Applications requiring completion must continue from the first unwritten byte.
Complete Write Helper
#include <errno.h>
#include <stddef.h>
#include <unistd.h>
static int write_all(
int descriptor,
const void *data,
size_t size)
{
const unsigned char *bytes =
data;
size_t written = 0;
while (written < size) {
ssize_t result =
write(
descriptor,
bytes + written,
size - written
);
if (result > 0) {
written +=
(size_t)result;
continue;
}
if (result == -1 &&
errno == EINTR) {
continue;
}
return 0;
}
return 1;
}
For nonblocking descriptors, a temporary unavailable condition requires an event-waiting or retry policy rather than an uncontrolled immediate loop.
Complete File-copy Example
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static int write_all(
int descriptor,
const void *data,
size_t size)
{
const unsigned char *bytes =
data;
size_t written = 0;
while (written < size) {
ssize_t result =
write(
descriptor,
bytes + written,
size - written
);
if (result > 0) {
written +=
(size_t)result;
continue;
}
if (result == -1 &&
errno == EINTR) {
continue;
}
return 0;
}
return 1;
}
static int copy_file(
const char source_name[],
const char destination_name[])
{
int source =
open(
source_name,
O_RDONLY
);
if (source == -1) {
perror(
"Unable to open source"
);
return 0;
}
int destination =
open(
destination_name,
O_WRONLY |
O_CREAT |
O_TRUNC,
0644
);
if (destination == -1) {
perror(
"Unable to open destination"
);
close(source);
return 0;
}
unsigned char buffer[8192];
int succeeded = 1;
for (;;) {
ssize_t amount =
read(
source,
buffer,
sizeof buffer
);
if (amount > 0) {
if (!write_all(
destination,
buffer,
(size_t)amount)) {
perror(
"Unable to write"
);
succeeded = 0;
break;
}
continue;
}
if (amount == 0) {
break;
}
if (errno == EINTR) {
continue;
}
perror(
"Unable to read"
);
succeeded = 0;
break;
}
if (close(source) == -1) {
succeeded = 0;
}
if (close(destination) == -1) {
succeeded = 0;
}
return succeeded;
}
int main(
int argc,
char *argv[])
{
if (argc != 3) {
fprintf(
stderr,
"Usage: %s SOURCE DESTINATION\n",
argv[0]
);
return EXIT_FAILURE;
}
return
copy_file(
argv[1],
argv[2]
)
? EXIT_SUCCESS
: EXIT_FAILURE;
}
Compile and Run
cc -std=c17 -Wall -Wextra -Wpedantic file_copy.c -o file_copy
./file_copy source.txt destination.txt
This educational example handles partial writes and interrupted operations. A production copy utility can additionally preserve metadata, avoid unsafe destination replacement, validate source and destination identity, and define durability requirements.
File Position
Regular-file descriptors commonly have a current file offset. Reads and writes can advance that offset.
File bytes:
0 1 2 3 4 5 6 7 8 9
^
Current offset
POSIX lseek() can reposition the offset for supported
resources.
off_t position =
lseek(
descriptor,
0,
SEEK_SET
);
Pipes and stream sockets do not support ordinary random-access seeking.
Buffered and Unbuffered I/O
The terms buffered and unbuffered require context. Low-level descriptor I/O can still pass through kernel caches and device buffers. Standard C streams add user-space library buffering.
Application
|
| fwrite()
v
C Library Buffer
|
| write()
v
Kernel / Filesystem Cache
|
v
Storage Device
Buffering can:
- Combine several small operations
- Reduce system-call frequency
- Improve sequential transfer efficiency
- Delay visibility of buffered output
- Require explicit flushing at selected boundaries
Flush vs Durable Storage
Moving data from an application buffer to the kernel is not necessarily the same as persisting the data to stable storage.
Application Buffer
|
| fflush() or write()
v
Operating-system Buffer
|
| Filesystem and device persistence
v
Durable Storage
The required guarantee should be based on the application's data-loss and recovery requirements. Stronger durability can add latency and reduce write throughput.
What Is a Socket?
A socket is a communication endpoint. A successful POSIX
socket() call returns a file descriptor referring to that
endpoint.
#include <sys/socket.h>
int socket_descriptor =
socket(
AF_INET,
SOCK_STREAM,
0
);
The main arguments select:
- The communication domain or address family
- The socket type and communication semantics
- The protocol, explicitly or through a default selection
Common Socket Types
| Socket Type | Communication Model | Important Property |
|---|---|---|
SOCK_STREAM |
Connection-oriented byte stream | Provides a sequenced stream without preserving application message boundaries |
SOCK_DGRAM |
Datagram communication | Preserves datagram boundaries but does not provide stream-style connection guarantees |
SOCK_SEQPACKET |
Sequenced packet communication | Preserves records with connection-oriented semantics where supported |
Address Families
| Family | Purpose |
|---|---|
AF_INET |
IPv4 network communication |
AF_INET6 |
IPv6 network communication |
AF_UNIX |
Local communication on the same operating system |
Address-family availability and behaviour depend on the target operating system.
TCP Server Lifecycle
| Operation | Purpose |
|---|---|
socket() |
Create the communication endpoint |
bind() |
Associate the socket with a local address |
listen() |
Mark the stream socket as a passive listener |
accept() |
Accept a pending connection and return a connected socket |
read()/recv() |
Receive bytes from the connection |
write()/send() |
Send bytes through the connection |
close() |
Release a descriptor reference |
TCP Client Lifecycle
Name resolution can return several candidate addresses. A robust client evaluates applicable candidates and applies a documented connection deadline and error policy.
Minimal POSIX TCP Server
The following educational server accepts one IPv4 connection, reads one block of bytes and echoes the received bytes back to the client.
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
enum
{
SERVER_PORT = 8080,
BUFFER_CAPACITY = 4096
};
static int send_all(
int socket_descriptor,
const void *data,
size_t size)
{
const unsigned char *bytes =
data;
size_t sent = 0;
while (sent < size) {
ssize_t result =
send(
socket_descriptor,
bytes + sent,
size - sent,
0
);
if (result > 0) {
sent +=
(size_t)result;
continue;
}
if (result == -1 &&
errno == EINTR) {
continue;
}
return 0;
}
return 1;
}
int main(void)
{
int listener =
socket(
AF_INET,
SOCK_STREAM,
0
);
if (listener == -1) {
perror(
"socket"
);
return EXIT_FAILURE;
}
int reuse = 1;
if (setsockopt(
listener,
SOL_SOCKET,
SO_REUSEADDR,
&reuse,
sizeof reuse) == -1) {
perror(
"setsockopt"
);
close(listener);
return EXIT_FAILURE;
}
struct sockaddr_in address;
memset(
&address,
0,
sizeof address
);
address.sin_family =
AF_INET;
address.sin_addr.s_addr =
htonl(INADDR_ANY);
address.sin_port =
htons(SERVER_PORT);
if (bind(
listener,
(const struct sockaddr *)
&address,
sizeof address) == -1) {
perror(
"bind"
);
close(listener);
return EXIT_FAILURE;
}
if (listen(
listener,
16) == -1) {
perror(
"listen"
);
close(listener);
return EXIT_FAILURE;
}
puts(
"Waiting for one connection..."
);
int client =
accept(
listener,
NULL,
NULL
);
if (client == -1) {
perror(
"accept"
);
close(listener);
return EXIT_FAILURE;
}
unsigned char buffer[
BUFFER_CAPACITY
];
ssize_t amount;
do {
amount =
recv(
client,
buffer,
sizeof buffer,
0
);
} while (
amount == -1 &&
errno == EINTR
);
int succeeded = 1;
if (amount > 0) {
succeeded =
send_all(
client,
buffer,
(size_t)amount
);
} else if (amount == -1) {
perror(
"recv"
);
succeeded = 0;
}
if (close(client) == -1) {
succeeded = 0;
}
if (close(listener) == -1) {
succeeded = 0;
}
return
succeeded
? EXIT_SUCCESS
: EXIT_FAILURE;
}
Compile
cc -std=c17 -Wall -Wextra -Wpedantic tcp_server.c -o tcp_server
This example handles only one connection and one received block. A production server additionally requires protocol framing, size limits, authentication where needed, deadlines, concurrent-connection control, graceful shutdown, overload handling and observability.
Minimal POSIX TCP Client
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
enum
{
SERVER_PORT = 8080
};
static int send_all(
int socket_descriptor,
const void *data,
size_t size)
{
const unsigned char *bytes =
data;
size_t sent = 0;
while (sent < size) {
ssize_t result =
send(
socket_descriptor,
bytes + sent,
size - sent,
0
);
if (result > 0) {
sent +=
(size_t)result;
continue;
}
if (result == -1 &&
errno == EINTR) {
continue;
}
return 0;
}
return 1;
}
int main(void)
{
int connection =
socket(
AF_INET,
SOCK_STREAM,
0
);
if (connection == -1) {
perror(
"socket"
);
return EXIT_FAILURE;
}
struct sockaddr_in server;
memset(
&server,
0,
sizeof server
);
server.sin_family =
AF_INET;
server.sin_port =
htons(SERVER_PORT);
if (inet_pton(
AF_INET,
"127.0.0.1",
&server.sin_addr) != 1) {
fprintf(
stderr,
"Invalid server address.\n"
);
close(connection);
return EXIT_FAILURE;
}
if (connect(
connection,
(const struct sockaddr *)
&server,
sizeof server) == -1) {
perror(
"connect"
);
close(connection);
return EXIT_FAILURE;
}
const char request[] =
"Hello from the client.";
if (!send_all(
connection,
request,
sizeof request - 1)) {
perror(
"send"
);
close(connection);
return EXIT_FAILURE;
}
unsigned char response[4096];
ssize_t amount;
do {
amount =
recv(
connection,
response,
sizeof response,
0
);
} while (
amount == -1 &&
errno == EINTR
);
if (amount > 0) {
if (!write_all(
STDOUT_FILENO,
response,
(size_t)amount) ||
!write_all(
STDOUT_FILENO,
"\n",
1)) {
close(connection);
return EXIT_FAILURE;
}
} else if (amount == -1) {
perror(
"recv"
);
close(connection);
return EXIT_FAILURE;
}
if (close(connection) == -1) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Compilation note: The client uses the same
write_all() helper shown earlier. Include that helper in the
client source file before compiling.
TCP Is a Byte Stream
A TCP stream delivers an ordered sequence of bytes. It does not preserve the
boundaries of individual application send() calls.
Sender:
send("HELLO")
send("WORLD")
Receiver can observe:
recv() -> "HELLOWORLD"
or:
recv() -> "HEL"
recv() -> "LOWOR"
recv() -> "LD"
or another valid segmentation.
The application protocol must define how the receiver identifies complete messages.
Message Framing
Common framing strategies include:
| Strategy | Description | Important Concern |
|---|---|---|
| Fixed length | Every record has the same documented size | Can waste space or limit flexibility |
| Delimiter | A marker separates messages | Delimiter escaping and maximum length are required |
| Length prefix | A header states the following payload length | The length must be validated before allocation |
| Self-describing format | A parser determines where one message ends | Parsing limits and malformed-input handling are required |
| Connection closure | End of stream marks the end of one payload | Suitable only for selected one-payload protocols |
Length-prefixed Message
+----------------------+-------------------------+
| 4-byte payload size | Payload bytes |
+----------------------+-------------------------+
A receiver should:
- Read the complete fixed-size header.
- Decode the length using the documented byte order.
- Reject lengths above the protocol limit.
- Allocate or select a bounded buffer.
- Read exactly the stated payload length.
- Validate the message contents.
Read-exactly Helper
#include <errno.h>
#include <stddef.h>
#include <sys/socket.h>
static int receive_exactly(
int socket_descriptor,
void *destination,
size_t size)
{
unsigned char *bytes =
destination;
size_t received = 0;
while (received < size) {
ssize_t result =
recv(
socket_descriptor,
bytes + received,
size - received,
0
);
if (result > 0) {
received +=
(size_t)result;
continue;
}
if (result == 0) {
return 0;
}
if (errno == EINTR) {
continue;
}
return 0;
}
return 1;
}
This blocking helper waits until all requested bytes arrive, the peer closes the stream or an error occurs. Production use should also apply an appropriate deadline.
Blocking I/O
A blocking operation can suspend the calling thread until data, connection progress, resource capacity or an error becomes available.
Thread calls recv()
|
+--> Data available: return bytes
|
+--> Peer closed: return end-of-stream result
|
+--> Error: report failure
|
+--> No data: thread waits
Blocking I/O can provide simple control flow, but every blocked connection consumes associated state and possibly a thread.
Nonblocking I/O
In nonblocking mode, an operation that cannot make immediate progress returns a temporary unavailable result instead of waiting indefinitely.
Attempt read
|
+--> Data available: process bytes
|
+--> End of stream: close connection
|
+--> Temporary unavailable:
wait for readiness through an event mechanism
|
+--> Permanent error:
close or recover according to policy
Nonblocking I/O requires careful state management for partially received and partially sent messages.
I/O Readiness
POSIX systems provide mechanisms that allow an application to wait for readiness across several descriptors.
Examples include:
select()poll()- Platform-specific scalable event facilities
Readiness means an operation may make progress without blocking in the expected way. It does not guarantee that an entire application message can be read or written in one operation.
Partial Socket Writes
A successful send() can accept only part of the application
buffer. The application must retain the unsent bytes.
Application message: 10,000 bytes
send() accepts: 4,000 bytes
Remaining:
6,000 bytes beginning at offset 4,000
In an event-driven server, the remaining bytes are commonly stored in connection-specific output state and retried when the socket becomes writable.
End of Stream
For a stream socket, a receive result of zero indicates that the peer has performed an orderly closure of its sending side.
Peer sends bytes
|
v
Peer closes sending direction
|
v
Receiver drains remaining bytes
|
v
recv() returns 0
End of stream is not the same as an empty application message. The protocol must define whether closure is expected at that point.
Half-close
A stream socket can support closing one communication direction while retaining the other.
Client:
Send request
Shutdown client sending direction
Continue receiving response
Server:
Read until client end of stream
Generate response
Send response
Close connection
POSIX shutdown() controls selected communication directions.
The application protocol must define whether half-close is supported.
Datagram Sockets
Datagram sockets send and receive individual datagrams rather than one continuous byte stream.
Sender:
Datagram A
Datagram B
Datagram C
Receiver:
Receives individual datagrams,
subject to transport and network behaviour.
Datagram applications must consider:
- Loss
- Duplication
- Reordering
- Maximum datagram size
- Truncation when the receive buffer is too small
- Sender identity
- Application-level retry and deduplication where required
Regular Files vs Stream Sockets
| Area | Regular File | Stream Socket |
|---|---|---|
| Primary purpose | Persistent byte storage | Communication byte stream |
| Random access | Generally possible through file offsets | Not ordinarily seekable |
| End condition | End of stored file content | Peer closes the sending direction |
| Message boundaries | Defined by file format | Must be defined by the application protocol |
| Failure source | Filesystem, device, permissions or capacity | Local networking, remote peer and network path |
| Concurrency concern | Offsets, append behaviour and multiple writers | Interleaved sends, receive ownership and connection state |
Concurrent Descriptor Access
Threads within one process can access the same descriptor, but safe behaviour requires an explicit ownership model.
Shared File Offset
Thread A reads from shared descriptor
Thread B reads from shared descriptor
Both operations affect the shared open-file position
according to the descriptor relationship and platform semantics.
Possible designs include:
- One owner thread performs all I/O.
- A mutex protects position-dependent operations.
- Position-explicit operations are used when supported.
- Each worker opens an independent descriptor when appropriate.
Interleaved Socket Writes
Thread A sends message header A.
Thread B sends message header B.
Thread A sends payload A.
Thread B sends payload B.
Peer observes a byte stream that does not match
the intended complete-message ordering.
A connection should normally have one output owner or a synchronized output queue that preserves protocol framing.
Descriptor Ownership
For every descriptor, define:
- Who creates or receives it
- Who is allowed to read it
- Who is allowed to write it
- Who closes it
- Whether ownership can be transferred
- Whether several threads can access it
- How shutdown is communicated
- How errors are reported
Ownership rule: A descriptor should have one documented close owner. Closing a descriptor while another thread is using it can lead to unsafe lifetime and descriptor-reuse defects.
Duplicated Descriptors
POSIX duplication functions can create several descriptor numbers referring to the same underlying open resource relationship.
Descriptor 3 ----+
+--> Open Resource
Descriptor 8 ----+
Closing one descriptor releases that descriptor reference. The underlying resource remains open while another descriptor reference still exists.
Duplicated descriptors can share resource state such as a file position, depending on how they were created.
Descriptors Across fork()
A child process created through POSIX fork() inherits descriptor
references according to platform semantics.
Before fork:
Parent Descriptor 3 ---> Open File
After fork:
Parent Descriptor 3 ---+
+--> Related Open File Description
Child Descriptor 3 ----+
Parent and child must define which process retains each descriptor. Unneeded copies should be closed so end-of-stream and resource-release behaviour are not delayed unexpectedly.
File Locking
File locks can coordinate cooperating processes, but locking semantics vary by platform and interface.
A file-lock design should define:
- The protected file or byte range
- Shared vs exclusive access
- Whether locks are advisory or mandatory
- The process or descriptor ownership model
- Lock ordering across several files
- Behaviour after process termination
- Timeout and recovery policies
A lock works only when every participating writer follows the same protocol.
Safe File Replacement
Directly truncating the active file before writing can destroy the only recoverable copy when the write fails.
Safer Replacement Pattern
Write new content to a temporary file
|
v
Check every write
|
v
Apply the required durability operation
|
v
Close successfully
|
v
Validate temporary content where required
|
v
Replace the destination according to platform rules
|
v
Preserve or clean up backup according to policy
Replacement atomicity, durability and metadata behaviour depend on the filesystem, operating system and whether source and destination are on the same filesystem.
Timeouts and Deadlines
Network operations should not wait indefinitely when the application has a finite request deadline.
A timeout policy should define:
- Connection-establishment timeout
- Read timeout
- Write timeout
- Complete-operation deadline
- Retry ownership
- Maximum retry attempts
- Behaviour after uncertain completion
- Cancellation and cleanup
A timeout does not prove that the remote operation failed. A request or write may have reached the peer even if the response was not observed.
Interrupted Operations
Some blocking POSIX calls can return an interruption error when a signal is handled.
ssize_t amount;
do {
amount =
read(
descriptor,
buffer,
sizeof buffer
);
} while (
amount == -1 &&
errno == EINTR
);
Retrying is appropriate only when the operation contract and application cancellation policy permit it.
Broken Connections
Socket operations can fail because the peer closed the connection, the network path failed, a deadline expired or the local system rejected the operation.
The application should:
- Check every operation result
- Distinguish temporary from permanent conditions
- Stop using connections in an invalid state
- Release associated buffers and application state
- Record useful diagnostics without exposing sensitive payloads
- Retry only when the business operation is safe
Security Considerations
File and socket code should validate:
- Path and filename policy
- File type
- Ownership and permissions
- Symbolic-link behaviour
- Input length
- Message framing
- Protocol version
- Numeric fields and byte order
- Authentication and authorization
- Resource-consumption limits
Never Trust a Network Length
uint32_t payload_size =
receive_length();
unsigned char *payload =
malloc(payload_size);
enum
{
MAXIMUM_PAYLOAD_SIZE =
1024 * 1024
};
uint32_t payload_size =
receive_length();
if (payload_size == 0 ||
payload_size >
MAXIMUM_PAYLOAD_SIZE) {
return PROTOCOL_ERROR;
}
unsigned char *payload =
malloc(payload_size);
if (payload == NULL) {
return ALLOCATION_ERROR;
}
Backpressure
Backpressure prevents a fast producer from creating unlimited buffered work for a slower consumer.
Fast Client
|
v
Bounded Input Buffer
|
+--> Capacity available:
| accept more data
|
+--> Buffer full:
stop reading, delay,
reject or close according to policy
Backpressure controls include:
- Bounded buffers
- Bounded queues
- Read suspension
- Request limits
- Connection limits
- Rate limiting
- Flow-control mechanisms
- Load shedding
Important Metrics
| Metric | What It Helps Explain |
|---|---|
| Open descriptor count | Resource consumption and possible descriptor leaks |
| File read and write rate | Storage workload |
| File I/O latency | Time spent waiting for storage operations |
| Active connection count | Current network concurrency |
| Connection-accept rate | Incoming connection workload |
| Connection-establishment failures | Listener, network or resource problems |
| Bytes sent and received | Network transfer demand |
| Partial-send queue size | Slow peers and output backpressure |
| Read and write timeout rate | Slow or failed communication |
| Protocol errors | Malformed, incompatible or abusive traffic |
Linux Diagnostic Commands
List Open Descriptors
ls -l /proc/PROCESS_ID/fd
List Process Files and Sockets
lsof -p PROCESS_ID
Display Socket Summary
ss -s
Display Listening TCP Sockets
ss -ltnp
Display TCP Connections
ss -tnp
Trace File and Network System Calls
strace -f -e trace=file,network ./application
Observe Storage Activity
iostat -xz 1
Command availability, permissions and output vary by Linux distribution and environment. Diagnostic tools can add overhead and should follow the approved operational process.
Common Files and Sockets Mistakes
Assuming One Read Returns the Complete Message
Reads can return fewer bytes than requested. Stream protocols require explicit framing and accumulation.
Assuming One Write Sends Everything
Writes and sends can complete partially. Preserve and retry the remaining bytes according to the I/O mode.
Ignoring End of Stream
A zero-byte stream read has a defined closure meaning and should not be treated as temporary absence of data.
Using TCP Without Message Framing
TCP preserves byte order, not application message boundaries.
Leaking Descriptors
Every successfully opened or accepted descriptor requires a documented close owner and cleanup path.
Closing a Descriptor While Another Thread Uses It
Coordinate descriptor lifetime and shutdown across participating threads.
Allowing Concurrent Socket Writes Without Framing Control
Separate writers can interleave protocol segments in an invalid order.
Waiting Without a Deadline
Slow or failed peers can retain connections, threads and buffers indefinitely.
Trusting Lengths Received from the Network
Validate every externally supplied size before allocation, copying or indexing.
Using Unbounded Buffers
A slow consumer can cause memory growth when incoming data is not bounded.
Confusing Flush with Durability
Moving data from one buffer layer does not automatically prove stable persistence.
Checking a Path and Using It Later
The filesystem object identified by a pathname can change between a separate check and use operation.
Recommended Test Cases
| Test | Expected Evidence |
|---|---|
| Empty file | The application handles immediate end of file correctly |
| Large file | The application processes data incrementally without unbounded memory use |
| Partial write | Remaining bytes are preserved and written correctly |
| Interrupted operation | The application follows its retry or cancellation policy |
| Storage full | The write fails safely without claiming successful persistence |
| Client sends one byte at a time | The server accumulates the framed message correctly |
| Several messages in one receive | The parser extracts every complete message and preserves remaining bytes |
| Oversized length prefix | The connection is rejected without unbounded allocation |
| Peer closes during a message | The incomplete message is rejected and resources are released |
| Slow client | Deadlines and bounded buffers prevent indefinite resource consumption |
| Concurrent shutdown | No thread accesses a closed or reused descriptor unsafely |
| Descriptor leak test | Open-descriptor count returns to the expected baseline |
Files and Sockets Best Practices
Recommended Practices
- Check every open, read, write, send, receive and close result.
- Expect partial reads and writes.
- Handle interrupted operations according to the operation policy.
- Define message framing explicitly for stream protocols.
- Validate lengths before allocating or copying data.
- Apply maximum request, message and file sizes.
- Assign one close owner to every descriptor.
- Coordinate shared descriptor access across threads.
- Use one output owner or synchronized output queue per connection.
- Apply connection, read, write and operation deadlines.
- Use bounded buffers and queues.
- Distinguish temporary unavailability from permanent errors.
- Treat a socket timeout as an uncertain remote outcome when applicable.
- Close unneeded inherited or duplicated descriptors.
- Avoid direct truncation when recoverable file replacement is required.
- Separate flushing from durability guarantees.
- Monitor descriptor counts, connection states, errors and timeouts.
- Test fragmented, combined, malformed and incomplete input.
Practice Exercise
Build a length-prefixed TCP echo service using POSIX sockets.
Requirements
- The server listens on a configurable port.
- Each message begins with a four-byte unsigned payload length.
- The length is encoded in network byte order.
- The maximum payload size is one megabyte.
- The server reads the complete header before decoding it.
- The server rejects a zero or oversized payload.
- The server reads the exact payload length.
- The server returns the same framed message.
- Partial sends are handled correctly.
- Peer closure during a message is treated as incomplete input.
- Every accepted descriptor is closed exactly once.
- The implementation records useful errors without logging payload content.
Protocol Format
Request:
+----------------------+---------------------------+
| 4-byte payload size | Payload |
+----------------------+---------------------------+
Response:
+----------------------+---------------------------+
| 4-byte payload size | Same payload |
+----------------------+---------------------------+
Required Validation
| Condition | Expected Behaviour |
|---|---|
| Complete valid frame | Echo the complete frame |
| Header arrives in fragments | Accumulate until all four bytes are received |
| Payload arrives in fragments | Accumulate until the declared length is received |
| Several frames arrive together | Parse each complete frame in sequence |
| Payload length exceeds limit | Reject the frame before allocation |
| Peer closes during payload | Reject the incomplete frame and close the connection |
| Send completes partially | Continue from the first unsent byte |
| Operation exceeds deadline | Release connection resources according to policy |
Frequently Asked Questions
What is a file descriptor?
A file descriptor is a non-negative integer used by a POSIX process to identify an open kernel-managed I/O resource.
Is a socket a file descriptor?
On POSIX systems, a successful socket-creation operation returns a file descriptor referring to the communication endpoint.
What is the difference between a file and a socket?
A regular file provides persistent byte storage and commonly supports seeking. A socket provides communication and follows the semantics of its selected socket type and protocol.
Why can read() return fewer bytes than requested?
The resource can currently provide only part of the requested amount. Correct code processes the returned count and continues according to its framing or file-processing rules.
Why can write() or send() complete partially?
The operating system can accept fewer bytes than requested because of buffer capacity, I/O mode or resource conditions.
Does TCP preserve messages?
No. TCP provides a byte stream. The application protocol must define and parse message boundaries.
What does recv() returning zero mean?
For a stream socket, it indicates orderly end of the peer's sending direction after previously received bytes have been consumed.
What is the difference between blocking and nonblocking I/O?
Blocking I/O can suspend the calling thread until progress is possible. Nonblocking I/O reports temporary unavailability so the application can wait through an event mechanism or perform other work.
Can several threads use the same socket?
They can access a shared descriptor, but the application must coordinate connection state, message framing, sending, receiving and closure.
Does close() guarantee file durability?
File visibility, buffering and stable-storage guarantees depend on the operating system, filesystem, device and selected durability operations. The application must define and implement the required guarantee.
Why are timeouts required for sockets?
A peer or network path can stop making progress while retaining application threads, descriptors and buffers.
What comes after files and sockets?
The next topic is Linux process and network tools, including
top, vmstat, iostat,
lsof, ss and related diagnostic workflows.
Key Takeaway
POSIX systems expose files, sockets, pipes and other I/O resources through descriptors. Correct I/O code checks every result, handles partial transfers, defines ownership and closes resources exactly once. Regular files and stream sockets share descriptor operations but have different semantics: files provide persistent content and offsets, while stream sockets provide ordered bytes without application message boundaries. Define framing, limits, deadlines, backpressure, concurrency rules and recovery behaviour before treating an I/O path as production-ready.