IO
程序分类
计算机程序包含两大类:
- 内核程序Kernel
- 用户程序App
IO成本
操作系统开机首先加载到内存中的是内核程序Kernel,内核管理了计算机的所有硬件的调度
内核加载完成后会注册一个GDT(全局描述符表),将Kernel的内存空间与App的内存空间区分出来,即内核空间和用户空间
注册完成后Kernel开启保护模式,禁止App访问内核空间
CPU指令集分为range0-range3四个级别,作用在内核空间的指令是range0级别的,作用在用户空间的指令是range3级别的
App不能直接访问硬件,如果要访问硬件需通过内核进行访问
内核访问硬件的方法称为系统调用(SystemCall,简称SC)
App通过**(软)中断**的方式进行SC
结论:App要使用IO需要调用Kernel,即需要成本的
系统调用
试验
-
试验程序
public class TestSocket { public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(9876); System.out.println("step1: new ServerSocket(9876)..."); while(true) { Socket client = server.accept(); System.out.println("step2: client accept at " + client.getPort()); new Thread(() -> { try { InputStream in = client.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); while(true) { System.out.println(reader.readLine()); } } catch(Exception e) { e.printStackTrace(); } }).start(); } } }
-
追踪系统调用
strace -ff -o ./ts java TestSocket
-ff
追加的方式-o
输出到文件./ts
文件目录及文件名的前缀java TestSocket
TestSocket程序启动后可以看到所有线程的追踪日志文件
-rw-r--r-- 1 root root 9340 Jun 5 16:52 ts.2871 -rw-r--r-- 1 root root 181038 Jun 5 16:52 ts.2872 -rw-r--r-- 1 root root 915 Jun 5 16:52 ts.2873 -rw-r--r-- 1 root root 926 Jun 5 16:52 ts.2874 -rw-r--r-- 1 root root 891 Jun 5 16:52 ts.2875 -rw-r--r-- 1 root root 866 Jun 5 16:52 ts.2876 -rw-r--r-- 1 root root 74456 Jun 5 17:00 ts.2877 -rw-r--r-- 1 root root 1049 Jun 5 16:52 ts.2878 -rw-r--r-- 1 root root 1084 Jun 5 16:52 ts.2879 -rw-r--r-- 1 root root 945 Jun 5 16:52 ts.2880 -rw-r--r-- 1 root root 20342 Jun 5 17:00 ts.2881 -rw-r--r-- 1 root root 18469 Jun 5 17:00 ts.2882 -rw-r--r-- 1 root root 17177 Jun 5 17:00 ts.2883 -rw-r--r-- 1 root root 980 Jun 5 16:52 ts.2884 -rw-r--r-- 1 root root 1470625 Jun 5 17:00 ts.2885
-
查找主线程pid
因为主线程打印了含"step1"的代码,以此为关键字搜索
[root@Centos-33 ~]# grep 'step1' ./ts.* ./ts.2872:write(1, "step1: new ServerSocket(9876)...", 32) = 32
可以确定主线程的PID=2872
-
查看主线程的文件描述符
[root@Centos-33 fd]# cd /proc/2872/fd && ll total 0 lrwx------ 1 root root 64 Jun 5 17:05 0 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 5 17:05 1 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 5 17:05 2 -> /dev/pts/1 lr-x------ 1 root root 64 Jun 5 17:05 3 -> /usr/java/jdk1.8.0_121/jre/lib/rt.jar lrwx------ 1 root root 64 Jun 5 17:05 4 -> socket:[72346401] lrwx------ 1 root root 64 Jun 5 17:05 5 -> socket:[72346403]
L7-L8 主线程建立的Socket连接,分别是IPV4和IPV6的
-
连服务
nc localhost 9876
-
再查看fd
[root@Centos-33 fd]# cd /proc/2872/fd && ll total 0 lrwx------ 1 root root 64 Jun 5 17:05 0 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 5 17:05 1 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 5 17:05 2 -> /dev/pts/1 lr-x------ 1 root root 64 Jun 5 17:05 3 -> /usr/java/jdk1.8.0_121/jre/lib/rt.jar lrwx------ 1 root root 64 Jun 5 17:05 4 -> socket:[72346401] lrwx------ 1 root root 64 Jun 5 17:05 5 -> socket:[72346403] lrwx------ 1 root root 64 Jun 5 17:12 6 -> socket:[72352839]
fd#6 是nc建立的新的socket连接
-
追踪日志中多了一条
step2: client accept at 40984
-
线程日志
[root@Centos-33 ~]# cd ~ && grep '40984' ./ts.2872 accept(5, {sa_family=AF_INET6, sin6_port=htons(40984), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6 write(1, "step2: client accept at 40984", 29) = 29
完整过程描述
作为一个服务端应用程序,必须开启监听状态,程序与Kernel将发生如下系统调用
- socket SC,得到一个 fd,例如fd#5
- bind SC,绑定端口
- listen SC,开启监听状态
- accept SC,得到客户端连接,因为客户端什么时候连接不确定,因此accept是阻塞状态
- 如果有一个客户端连接了服务端,会新建一个fd,如fd#6
- clone SC,克隆线程进行读取fd#6的操作
- read/recvfrom SC,克隆出的线程读取fd#6数据
BIO
概念
上述过程中,主线程负责监听客户端接入,每有一个客户端接口,都会抛出一个线程分别对应处理客户端请求,因为accept和read都会发生阻塞,因此称为BIO
问题
- 每个线程都要进行系统调用
- 线程会消耗资源(栈独立)
- 线程多的话需要频繁切换CPU,效率低
根本原因
因为主线程是阻塞的,所以需要抛出线程
NIO
概念
- 在操作系统级别,NIO指的是NonblockingIO,非阻塞IO
- 在java中,NIO指的是NewIO,新一代的IO模型,包括channel、buffer、selector
开启内核非阻塞
Kernel socket方法有一个参数开启非阻塞,开启之后,连接客户端和读取数据就可以在同一个线程中执行了,详细情况执行命令:
man 2 socket
Kernel有两个socket方法,分别是1类和2类,此处看2类
SOCK_NONBLOCK 参数开启非阻塞
问题&解决
问题
C10K 即:有n个客户端接入之后,每一次循环体内都需要进行O(n)次的recvfrom SC,观察客户端是否有请求数据,如果发送数据的客户端占比很低,说明有很多次的SC没有读取到数据,造成严重的资源浪费
解决
减少系统调用,需要Kernel支持
Kernel增加了select SC
[root@cehome-test11 ~]# man 2 select
.....................
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible).
A file descriptor is considered ready if it is possible to perform the corresponding I/O operation (e.g., read(2)) without blocking.
.....................
翻译:
select()和pselect()允许一个程序监视多个文件描述符,等待直到一个或多个文件描述符“准备好”进行某类I/O操作(例如,可能的输入)。
如果一个文件描述符可以在不阻塞的情况下执行相应的I/O操作(例如,读取(2))则认为该文件描述符已准备就绪。
拓展知识
paxos算法论文 https://www.iteblog.com/archives/2337.html#id47