IO相关

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值