Managing Long-Running System Calls To Avoid Process Blocking

The Problem of Blocked Processes

When a process makes a system call that blocks, it can lead to poor system performance and responsiveness. System calls like read(), write(), and open() may need to wait for I/O resources to become available before they can complete. During this wait time, the process is said to be “blocked” as it sits idle, preventing execution of other processes.

A blocked process continues occupying system memory resources while idle. With multiple concurrent processes making blocking system calls, available CPU cycles go unused leading to underutilization of computing power.

User interfaces and networked services relying on these blocked processes freeze or lag, creating a poor user experience. Blocked processes degrade overall system interactivity.

How System Calls Can Block Processes

On Linux and Unix systems, many system calls can block process execution when accessing slow I/O devices or waiting on availability of limited resources.

For example, a call to read() blocks when trying to read input that is not yet available. The process waits until the requested input data becomes available from the file or network socket. Any delay in the arrival of the input causes the process to remain blocked from further computation or execution.

Similarly, a call to write() blocks when the output buffer is full and unable to accept more data. This commonly occurs when writing output to a slow device such as a disk or terminal. The process enters a paused state until the destination I/O resource can start accepting more data.

Functions like open() and close() can also incur blocking delays when accessing remote file systems, slow networks, or waiting on file locks.

In all cases of I/O related blocking calls, the CPU sits ideal while the process wastes valuable execution cycles waiting for I/O completion. This leads to poor utilization of computing resources and creates lag for applications.

Using Non-Blocking I/O to Prevent Blocking

Non-blocking I/O allows a process to perform a system call without getting paused if the underlying operation cannot complete immediately. This prevents the process from entering an inactive waiting state when accessing slow secondary storage or unavailable network resources.

With non-blocking calls, control returns to the calling process almost immediately. At a later stage when the previously unavailable I/O resources become ready, the process can retry the incomplete operation. This style of breaking up long I/O operations into incremental non-blocking calls is termed asynchronous I/O.

Asynchronous non-blocking I/O allows processes to remain active and responsive by avoiding calls that could stall execution. By continuously polling and retrying non-blocking operations, processes can progress with other computational tasks while I/O proceeds in the background.

Implementing Non-Blocking Calls with fcntl()

The Linux and Unix fcntl() system call enables switching standard I/O functions into non-blocking mode for a file or socket descriptor.

For example, making the following fcntl() call on a file descriptor fd sets the O_NONBLOCK flag. Subsequent read() or write() calls on fd become non-blocking.

fcntl(fd, F_SETFL, O_NONBLOCK); 

This prevents the process from getting paused if data is unavailable, instead returning control immediately to retry again later. Other flags instead of O_NONBLOCK can request asynchronous I/O.

Once a descriptor enters non-blocking mode, it remains that way until changed by another fcntl() call. Non-blocking status persists across multiple operations on that descriptor.

Example Code for Non-Blocking read()

Here is sample C code demonstrating non-blocking read() usage after making the descriptor non-blocking with fcntl():

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int fd = open("input.txt", O_RDONLY);
fcntl(fd, F_SETFL, O_NONBLOCK); 

char buf[MAX_BUF];
int nread;

do {
  nread = read(fd, buf, MAX_BUF); 
} while (nread == -1 && errno == EAGAIN);

This repeats the non-blocking read() call in a loop until some data is returned or another error occurs. The process remains active between repeated read attempts, preventing blocking pauses.

For writing non-blocking output, O_NONBLOCK would be set on the output file descriptor before cyclic write() attempts.

Alternatives to Non-Blocking I/O

While non-blocking I/O avoids process blocking, the busy waiting cycles waste CPU resources. Polling a descriptor repeatedly also increases load on underlying I/O subsystems.

Alternative asynchronous I/O methods avoid direct polling by relying on external notifications when long running I/O completes in the background. This allows processes to execute other tasks after initiating asynchronous I/O.

POSIX platforms support asynchronous I/O cancellation, scheduling, and completion notification through the aio_* family of functions defined in <aio.h>.

In Linux, asynchronous mechanism map I/O operations to threads in userspace or kernel. These execute the long running I/O asynchronously while processes continue other work. Examples of Linux specific asynchronous I/O interfaces includes POSIX asynchronous I/O, io_submit()/io_getevents, and libaio.

Leveraging Asynchronous I/O with aio_read()

The POSIX aio_read() function allows asynchronously scheduling file read operations that trigger callback notifications when complete. This prevents processes from blocking while waiting on I/O.

First an aiocb (asynchronous I/O control block) data structure instance defines the asynchronous read parameters like file descriptor, buffer location, and callback function.

Invoking aio_read() with this aiocb structure queues the read operation without blocking. The process continues other work while asynchronous reads proceed in parallel.

An external notification gets generated upon read completion through a signal or callback function execution. By avoiding direct polling, processes utilizing aio_read() prevent blocking while enjoying responsive asynchronous I/O.

Example Async I/O Implementation

Here is sample code doing asynchronous read from a file descriptor fd using POSIX aio interface:

#include <aio.h>
#include <stdio.h>
#include <errno.h>

void io_completion_handler(io_context_t ctx) {
  // checked completed asynchronously queued ops    
}   

...

int fd = open("file.txt", O_RDONLY);

struct aiocb my_aiocb;
my_aiocb.aio_fildes = fd;
my_aiocb.aio_buf = buffer;
my_aiocb.aio_nbytes = MAX_BUF;
my_aiocb.aio_ctx = custom_context; 

if (aio_read(&my_aiocb) == -1) {
  // handle error
} 

// other work continues in background

...

// async notification through callback 
my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
my_aiocb.aio_sigevent.sigev_notify_function = io_completion_handler;

...

// wait for asynchronous ops to complete
aio_suspend();  

This allows deferred processing of read results without blocking the main process flow when initiating reads. Callbacks handle I/O completion asynchronously.

Choosing the Right Strategy for Your Needs

Blocking system calls degrade interactive application performance through process blocking delays. Both non-blocking and asynchronous I/O allow overlap of processing with long runing I/O.

Non-blocking I/O offers simple control flow without external dependencies. But busy polling wastes resources. Asynchronous I/O minimizes resource wastage through external completion notifications.

For simplicity and portability across UNIX platforms, non-blocking I/O is easiest to integrate. Asynchronous I/O requires heavier OS specific configuration but enables delegating I/O handling to background threads or kernel management.

For compute intensive processes, like multimedia coding or scientific workloads, asynchronous I/O decouples communication from computation more cleanly. Non-blocking I/O suits simpler networked programs.

Combining non-blocking sockets for light data communication with asynchronous large file operations or device I/O provides a robust blend without blocking processes.

Leave a Reply

Your email address will not be published. Required fields are marked *