I/O from an application perspective

It is Zion
5 min readDec 12, 2020

The essence of IO operations is the mutual copying of user space buffers and kernel space buffers.

It generally consists of two steps:

  1. wait for network data to reach NIC (read-ready) / wait for NIC to be writable (write-ready) -> read / write to kernel buffer
  2. copy data from kernel buffer -> user space (read) / copy data from user space -> kernel buffer (write)

In Linux, the objects of IO are files (because everything is a file).
IO for normal files involves DMA, disk and CPU. For socket files sendfile technology is also included.

Synchronous IO and Asynchronous IO

From the point of view of program execution, IO is divided into synchronous IO and asynchronous IO.

To determine whether an I/O model is synchronous or asynchronous, it depends on the second step: whether data copying between user and kernel space blocks the current process, if so, it is synchronous I/O, otherwise, it is asynchronous I/O.

this pic from Unix network programming Chapter

Based on this principle, there is only one asynchronous I/O model among these five I/O models: asynchronous I/O. The rest are all synchronous I/O models.

Here is a brief description of these 5 IO models.

Blocking IO

When an application calls recvfrom to read data, its system call returns when it knows that the packet has been copied to the application buffer or sent in error, and waits during this time.
The process is blocked from the time of the call to the time of the return is called blocking IO.

nonBlocking IO

non-blocking IO returns an EWOULDBLOCK error if there is no data in the buffer when the application calls recvfrom to read the data, and does not keep the application waiting. The error flag is returned immediately when there is no data, which means that if the application wants to read data it needs to keep calling recvfrom requests (I think it’s an infinite loop internally) until it reads the data it wants.

IO multiplexing

First think about a problem, the application reads data from the TCP buffer. If in a concurrent environment, there may be N people sending messages to application. In this case our application will have to create multiple threads to read the data, and each thread will call recvfrom itself to read the data.

In this case, application needs to create millions of threads to read the data, and because the application threads do not know when the data will be read, they must keep sending recvfrom requests to the kernel to read the data in order to ensure that the messages are read in time.

So the question arises, so many threads keep calling recvfrom to request data, not to mention that the server can not carry so many threads, even if it can carry then it is obvious that this way is not too wasteful of resources, threads are valuable resources of our operating system, a large number of threads used to read data, then it means that the threads can do other things will be less.

So, is there a way to monitor multiple file descriptors by a single thread (the Linux system takes all network requests to a socket file descriptor), so that only one or a few threads can complete the operation of data status interrogation, and then allocate the corresponding thread to read data when there is data ready, so that you can save a lot of thread resources out, this is the idea of the IO reuse model.

The idea of the IO reuse model is that the system provides a function that can monitor multiple file descriptor operations at the same time, this function is often referred to as select, poll, epoll function, with this function, the application thread can monitor multiple file descriptors at the same time by calling this function, this function monitors the file descriptors as long as any one of the data state is ready, this function will return readable state, then the interrogation thread to notify the data processing thread, the corresponding thread then launched a recvfrom request to read the data.

So the basic pattern is that one thread is used to call select, epoll, etc., and another thread is used to call recvfrom to process the data.

The process blocks on the select operation by passing one or more file descriptors to select. select helps us to detect if multiple file descriptors are ready, when there are file descriptors ready, select returns the data readable status and the application then calls recvfrom to read the data.

Signal-drive IO

The multiplexed IO model solves the problem that a thread can monitor multiple file descriptors, but select uses polling to monitor multiple file descriptors, by constantly polling the file descriptors readable state to know if the data is readable, and the mindless polling is a bit violent, because in most cases the polling is invalid, so can I not always ask you if the data is ready, can the application send a request to notify the application when the data is ready, so the signal-driven IO model is derived.

When the sigaction is called, a SIGIO signal contact is established, and when the kernel data is ready, the thread is notified of the readable status of the data through the SIGIO signal, and when the thread receives the signal of the readable status, it then initiates a recvfrom request to the kernel to read the data, because under the signal-driven IO model, the application thread can signal the monitor and then So in this way, an application thread can also monitor multiple file descriptors at the same time.

Asynchronous IO

Can there be a one-and-done way where I just send a request and I tell the kernel I want to read the data and then I don’t care about anything and then the kernel does all the rest for me?

Some people have designed a scheme in which the application simply sends a read request to the kernel, tells the kernel that it wants to read the data and returns immediately; the kernel receives the request and establishes a signal contact, and when the data is ready, the kernel will actively copy the data from the kernel to the user space. We call this one-and-done model the asynchronous IO model.

The main difference between this model and the signal-driven model is that the signal-driven IO is only informed by the kernel that it is appropriate to start the next IO operation, while the asynchronous IO model is informed by the kernel when the operation is completed and that the asynchronous IO does not block the thread during the second step of the IO.

Aobut asynchronous blocking

After understanding all the IO models, let’s look at synchronous blocking and synchronous non-blocking, which differ only in the first step, but are the same in that they both block in the second step and require the application to monitor the entire data completion process itself. And why asynchronous non-blocking and not asynchronous blocking afterwards? Because in the asynchronous model, the request is sent and returned immediately without any subsequent process, so it is not destined to block, so there is only the asynchronous non-blocking model.

--

--