UNIX Network Programming Volume 1 (Third Edition)
Chapter 6 I/O
6.1 an overview of the
In Section 5.12, we saw that the TCP client processes two inputs simultaneously: the standard input and the TCP socket. The problem we had was that the client blocked the fgets call (on standard input) and the server process was killed. Server TCP correctly sends a FIN to client TCP, but the client process is blocking the read from the standard input and doesn’t see the end-of-file character until it reads from the socket (possibly a long time later). We need the ability to be notified if one or more I/O conditions are met (for example, the input is ready to be read, or the descriptor can take on more output). This capability, called I/O multiplexing, is supported by the functions SELECT and poll, and we also introduce the newer POSIx.lg variant (called PSELECT). I/O multiplexing is typically used in the following network applications:
Summary: The implication of this paragraph is that the TCP client processes two inputs simultaneously, but not sequentially, in parallel. So I/O multiplexing is essentially waiting for the data to be ready and then issuing a system call to tell the process it’s ready to copy. The process is still blocked while waiting for the data to be ready, so theoretically I/O multiplexing is still classified as a blocked process.
- When customers work with multiple descriptors (typically interactive input and network sockets), they must use I/O multiplexing, as described in the previous paragraph.
- It is possible, but rare, for a single client to handle multiple sockets simultaneously. In the context of a Web customer in Section 15.5, we give an example of using SELECT.
- If a TCP server handles both listening and connected sockets, I/O multiplexing is also typically used, as described in Section 6.8.
- If a server handles both TCP and UDP, it will typically use I/O multiplexing as well, as we’ll see in Section 8.15.
- If a server handles multiple services or protocols (for example, the Inetd daemon we will describe in Section 12.5), I/O multiplexing is typically used. I/O reuse is not limited to network programming; many formal applications require this technique.
6.2 I/O model
Before we get to the select and poll functions, we need to step back
- Blocking I/O
- Non-blocking I/O
- I/O multiplexing (SELECT and poll)
- Signal driven I/O(SIGIO)
- Asynchronous I/O(POSIX.1 AIO_ series of functions)
You may want to skim this section for the first time and come back to it later when the different I/O models are covered in more detail.
As we have shown in all the examples in this section, an input operation generally has two distinct stages:
- Wait for the data to be ready.
- Copy data from kernel to process.
For an input operation on a socket, the first step is typically to wait for the data to arrive on the network, and when the packet arrives, it is copied to some buffer in the kernel, and the second step is to copy the data from the kernel buffer to the application buffer.
6.2.1 Blocking I/O Model
The most popular I/O model is the blocking I/O model, which has been used by all the examples in this book so far. By default, all sockets are blocked. Using the datagram socket as an example, we have the situation in example Figure 6.1.
In this example, we use UDP rather than TCP because with UDP the concept of data readiness is simpler: whether the entire datagram has been received or not, whereas with TCP it is much more complex, with many additional variables such as the lowwater mark of the socket to be considered.
In the example in this section, we treat the function recvfrom as a system call because we are considering the difference between the application process and the kernel. No matter how the function recvfrom is implemented (as a system call in kernels from Berkeley, or as a function that calls the system call getMsg in the system V kernel). There is usually a switch from running in an application process to running in the kernel, followed some time later by a switch back to the application process.
Original map of the book:
In Figure 6.1, the process calls recvFROM, which does not return until the datagram arrives and is copied to the application buffer or an error occurs. The most common error is a signal break during system calls, as described in Section 5.9. The entire period of time when a process is blocked is the time between the call to recvfrom and its return. When the process returns a success indication, the application process starts processing datagrams.
Note: RecvFROM (receive data through socket), then understand the concept: blocking and non-blocking refers to whether the process needs to wait if the data is not ready, which is equivalent to the implementation difference inside the function, namely whether to return directly or wait ready when not ready.
6.2.2 Non-blocking I/O model
When we set a socket to non-blocking, we inform the kernel that an error should be returned when the packet for the requested I/O operation is not ready. We’ll cover non-blocking I/O in more detail in Chapter 15, but to illustrate the example we’re considering, a summary description is given in Figure 6.2.
The first three calls to RecvFROM still return no data, so the kernel immediately returns an EWOULDBLOCK error, and the fourth time recvFROM is called, the datagram is ready and copied to the application buffer, recvfrom returns a success indication, and then we process the data.
When an application process calls recvfrom on a non-blocking descriptor loop like this, it is called polling. Application processes constantly query the kernel to see if an operation is ready, which can be a huge waste of CPU performance, but this pattern indication is only encountered occasionally, usually on a system dedicated to a particular function.
6.2.3 I/O Multiplexing Model
With I/O multiplexing, we can call SELECT or poll and block on one of the two system calls rather than on the actual I/O system call. Figure 6.3 is a summary of the I/O multiplexing model.
We block the SELECT call and wait for the datagram socket to be readable. When SELECT returns a socket readable condition, recvFROM is called to copy the datagram into the application buffer.
Comparing Figure 6.3 with Figure 6.1 does not seem to show any advantage, and in fact, the requirement for two system calls instead of one seems to be somewhat inferior due to the use of the system call SELECT. However, as we will see later in this chapter, the advantage of using SELECT is that we can wait for multiple descriptors to be ready.
6.2.4 Signal-driven I/O model
We can also use signals to have the kernel notify us with signal SIGIO when the descriptor is ready. We call this method signal-driven I/O, which is summarized in Figure 6.4.
First, we allow the socket to do signal-driven I/O(which we’ll discuss in Section 22.2) and install a signal handler through the system call SIGAction. This system call returns immediately, and the process continues to work, which is non-blocking. When the datagram is ready to be read, a SIGIO signal is generated for the process. We can then call recvfrom in the signal handler to read the datagram and notify the main loop that the data is ready to be processed (which is exactly what we did in Section 22.3). You can also tell the main loop to read the datagram.
No matter what we do with SIGIO signals, the benefit of this mode is that it does not block while waiting for datagrams to arrive. The main loop can continue executing, just waiting for the signal handler to tell it; Either the data is ready to be processed, or the datagram is ready to be read.
6.2.5 Asynchronous I/O Model
Asynchronous I/O is new in the 1993 version of POSIX.1 (the “real-time” extension). We let the kernel start the operation and notify us when the entire operation is complete, including copying data from the kernel to our own buffer. Because this model is not widely used, it is not discussed in this book. The main difference between this model and the signal-driven model described in the previous section is that signal-driven I/O lets the kernel tell us when an I/O operation can be started, whereas asynchronous I/O lets the kernel tell us when the I/O operation is complete. Figure 6.5 shows an example.
We call the function aio_read(Posix asynchronous I/O functions start with aio_ or lio_), passing the descriptor to the kernel, the buffer pointer, the buffer size (the same three arguments as read), the file offset (similar to Lseek), and telling the kernel how to notify us when the entire operation is complete. This system call returns immediately and our process does not block and wait for the I/O operation to complete. In this example, unlike the signal-driven I/O model, we assume that the kernel is required to generate a signal when the operation is complete until the data has been copied to the application buffer.
As explained in this book, few systems support posiX.1’s asynchronous I/O model. For example, we are not sure whether the system supports this model on the socket. We use it here only as an example to compare with the signal-driven I/O model.
Figure 6.6 shows a comparison of the five different I/O models mentioned above. It shows that the main difference between the first four models is in the first stage, because the second stage of the first four devils is essentially the same: the process blocks the recvFROM call as data is copied from the kernel to the caller’s buffer. Then, neither phase of the asynchronous I/O model processing is used for the first four models.
Synchronous I/O and asynchronous I/O
Posix.1 defines these two terms as follows:
Synchronous I/O operations cause the requesting process to block until the I/O operation is complete. Asynchronous I/O operations do not block the requesting process. According to the above definition, our first four models ———— blocking I/O model, I/O multiplexing model, and signal-driven I/O model are synchronous I/O models because true I/O operations (RECvFROM) block the process and only the asynchronous I/O model matches this asynchronous I/O definition.