前面 Java NIO 分析 (1): Unix 网络模型 http://sound2gd.wang/2018/06/14/Java-NIO 分析 - 1-Unix 网络模型 / 讲过 5 种经典 I/O 模型,
现代企业的场景一般是高并发高流量, 长连接, 假设硬件资源充足, 如何提高应用单机能接受链接的上限?
先讲段历史
UNIX 的出现
20 世纪 60 年代中期, 那会儿还是批处理任务 https://en.wikipedia.org/wiki/Batch_processing 的天下, 也就是有一堆 job 一个个顺序做,
一个做完了才做下一个. 举个栗子, 你没法边听音乐边写博客, 也没法边下边播 x 老师的电影. 随后分时 https://en.wikipedia.org/wiki/Time-sharing 这革命性的理念提出来了, 每个 job 只允许占有一小段 CPU 时间片执行代码, 假如 cpu 处理的够快, 看起来就像是一堆 job 并行一样.
分时理念无疑极大地减少了写代码和获取代码执行结果的时间, 到了 70 年代, 有人提出要发明一种更好的, 多用户的, 分时的环境来执行大多数的共同任务, 比如 执行需要大量 CPU 计算的程序, 大量的磁盘访问等等, 这个环境后来就发展成了 https://en.wikipedia.org/wiki/Unix
当时, 程序阻塞的条件是:
等待 CPU
等待磁盘 I/O
等待用户输入
等待 shell 命令结果或者终端结果
在当时也没有多少真正的 IPC 手段, pipe 算是一个. 不过对于当时的情况来说, 一个进程最多只能打开 20 个 fd, 每个用户最多只能开 20 个进程
也没有多少 IPC 和复杂 I/O 的需求.
早期的 Unix 也没有 fd 复用的概念, 如果你 ssh 远程登录 Unix 系统, 系统要同时处理用户的输入, 还给用户输出. 当时是靠 cu 这个命令来实现的,
cu 会创建俩进程, 一个负责读一个负责写. 因为当时的 I/O 都是阻塞的, 如果要同时读写就得搞俩进程.
Socket
到了 1983 年, BSD4.2 发布的时候, 一起发布的还有我们今天耳熟能详的 BSD Socket API https://en.wikipedia.org/wiki/Berkeley_sockets 和 TCP/IP 协议栈.
Socket 解决了不一定在同一台机器的不同进程之间的通信问题, 是一种有效的 IPC 手段. Socket 结合 TCP/IP 协议还解决了计算机之间的网络通讯问题.
然而读写 fd 依然是阻塞的, 假如你要处理俩 socket, 那么可能在阻塞读 socket1 的时候, socket2 的数据因为来不及处理丢失了.
随着 SocketAPI 一起发布的还有大名鼎鼎的 select 系统调用, 也即 I/O 多路复用的实现. I/O 多路复用通过使用一个系统函数, 如 select, 可以同时等待多个 fd 的可读, 可写等状态.
在没有 select 之前, 一般的 unix 网络程序是这么写的 (accept-and-fork 模型)
- listenfd = bind();
- while(1) {
- fd = accept(listenfd);
- if (fork() == 0) {
- close(fd);
- // 具体的处理代码
- ...
- ...
- exit(0); // 处理完子进程退出
- }
- // 关闭 fd 避免 fd 泄露
- close(fd)
- }
accept-and-fork 是非常费系统资源的, 因为每启动一个新的进程, 就需要开辟新的栈, 分配虚拟内存等, 而且多个进程之间由于缺乏 IPC 手段,
状态难以共享, 对于服务端程序来说是灾难.
Select
Select 发布以后, I/O 就能复用了, 你可以询问内核哪些 fd 准备就绪了, 然后去发系统调用读数据,
读 fd 的过程是阻塞的, 使用 select 可以避免无意义的阻塞, 这样即使只有一个进程也可以处理多个 socket fd 的读写
当时贝尔实验室有个产品叫 blit, 是一种多用户实时终端, 和现在的 terminal 差不多, 这就要求应用能同时处理读和写, 像 cu 这种靠俩进程
来实现类似多路复用的效果, 只能称为一种 hack, 有了 select 就能真正多路复用 socket fd. 这样你就能在应用进程里方便的处理读和写 socket fd 而不
无意义的阻塞线程.
随后又发布了 Non-blockingAPI, 但是它和 select 不同, select 是帮助你多路复用, Non-blockingAPI 是指你在读 fd 的时候不会阻塞等待数据准备和内核拷贝数据的完成, 这个时候进程可以干别的事情
Non-Blocking 也可以实现类似 select 的功能, 不过那是应用层去做 select 的功能, 也就是应用层需要轮询描述符是都就绪, 假如没有就绪, 对 fd 的 read 会直接返回一个 errno, 代表没有数据可读等.
轮询的缺点是浪费了太多 CPU 时间, 因为 read 是要发系统调用的, 进程会从应用态切换到内核态, 每次这个切换过程都是资源的一种浪费.
select 还会启动多个进程能复用的一个 inetd 进程. 由于当时一个进程才能开最多 20 个 fd, 所以 select 才会设置
fd_set 的最大值是 1024, 在当时看来是远远用不完的.
Unix Time-Sharing System: A Retrospective https://ia801605.us.archive.org/33/items/bstj57-6-1947/bstj57-6-1947_text.pdf
来源: https://juejin.im/entry/5b4d57ece51d45191a0d4240