The previous Java NIO analysis (1): Unix network model has described five classic I/O models. Modern enterprise scenarios are generally high concurrency, high traffic and long connection. Assuming sufficient hardware resources, how to increase the upper limit of the connection that can be accepted by a single application? Let’s start with a bit of history

The emergence of the UNIX

Back in the mid-1960s, it was the era of batch tasks, where you had to do a bunch of jobs in sequence, and then you had to do the next one. For example, you can’t listen to music and write a blog at the same time, or play a Teacher X movie at the same time. Then came the revolutionary idea of time-sharing, where each job was allowed only a small slice of CPU time to execute code, and if the CPU was fast enough, it looked like a bunch of jobs were running in parallel.

Time-sharing idea undoubtedly greatly reduce the written code and obtain the execution result of time, in the 70 s, some want to invent a better, more users, time-sharing environment to perform the most common tasks, such as the execution procedures that need a lot of CPU, a large number of disk access, etc., the environment, then evolved into Unix

At that time, the program blocked under the following conditions:

  • Waiting for the CPU
  • Wait for disk I/O
  • Waiting for user input
  • Wait for shell command results or terminal results

Pipe was one of the few real IPC devices of the time. At the time, however, a maximum of 20 FDS could be opened per process, and a maximum of 20 processes could be opened per user. There was little need for IPC and complex I/O.

In the early days of Unix, there was no concept of fd reuse. If you logged in to a Unix system remotely over SSH, the system would handle both user input and user output. This was done with the cu command, which created two processes, one for reading and one for writing. Because I/O was blocked, you had to run two processes to read and write at the same time.

Socket

By 1983, BSD4.2 was released, along with the BSD Socket API and TCP/IP stack that we know today. Socket solves the communication problem between different processes not necessarily in the same machine, and is an effective IPC means. Socket combined with TCP/IP protocol also solves the problem of network communication between computers.

However, reading and writing FDS would still block, so if you were processing two sockets, you might block reading socket1, and the data in socket2 would be lost in time for processing.

Along with the SocketAPI comes the well-known select system call, or I/O multiplexing implementation. I/O multiplexing by using a system function, such as SELECT, can wait for multiple FDS to be readable, writable, etc.

Before select, common Unix networking programs were written like this (accept-and-fork model)

listenfd = bind();
while(1) {
 fd = accept(listenfd);
 if (fork() == 0) {
   close(fd);

   // 具体的处理代码
   ...
   ...

   exit(0); // 处理完子进程退出
 }
 
 // 关闭fd避免fd泄露
 close(fd)
}
Copy the code

Accept-and-fork is a huge drain on system resources because every time a new process is started, a new stack needs to be opened, virtual memory needs to be allocated, and state cannot be shared between multiple processes due to the lack of IPC means, which is a disaster for server programs.

Select

You can ask the kernel which FDS are ready, and then send the system call to read the data. The process of reading the FDS is blocking. Using Select can avoid meaningless blocking, so that even a single process can handle multiple socket FDS

At that time, Bell LABS had a product called BLIT, which was a multi-user real-time terminal, similar to what we have today, and it required applications to be able to process both read and write at the same time, like CU, which relied on two processes to achieve something similar to multiplexing. Can only be called a hack, with a select can be truly multiplexed socket fd. This way you can easily handle socket fd reads and writes in the application process without meaninglessly blocking the thread.

Non-blockingapi was later released, but unlike Select, which helps you multiplex, non-BlockingAPI means that you can read a FD without blocking while waiting for the data to be prepared and copied by the kernel, while the process can do other things

A non-blocking function like select is also available, but it is the application layer that performs the select function. If the polling descriptor is not ready, the read of a fd will return an errno, indicating that there is no data to read. The downside of polling is that it wastes a lot of CPU time, because read makes system calls and the process switches from application mode to kernel mode, wasting resources each time.

Select also starts an Inetd process that can be reused by multiple processes. At that time, a maximum of 20 FDS could be opened for a process, so select set the maximum value of fd_set to 1024, which seemed to be inexhaustible at the time.


  1. Unix Time-Sharing System: A Retrospective