zoukankan      html  css  js  c++  java
  • 网络通信IO的演变过程(一)(一个门外汉的理解)

    以前从来不懂IO的底层,只知道一个大概,就是输入输出的管道怼到一起,然后就可以传输数据了。
    最近看了周志垒老师的公开课后,醍醐灌顶。
    所以做一个简单的记录。

    0 计算机组成原理相关

    0.1. 计算机的基本组成大家都了解一点,如下图,当操作系统启动的时候,首先进入内存的除了BIOS,然后就是Linux内核程序。

    • 内核暂时先理解成系统程序,比如我们想通过键盘获取到用户的输入,想打开网卡录取视频。这些硬件是受系统保护的,只能交给内核控制。不可能把控制权交给用户程序。
    • 用户程序如果想访问硬件,只能用户调用内核暴露的一些调用,我们称这个为系统调用。
    • 操作系统启动的时候,会把内核程序所在的地址空间设为绝对安全的空间,这个空间称为内核空间,这种机制称为保护模式。其他的空间即提供给用户程序使用,称为用户空间。比如JVM,QQ,微信对内核来说都是用户App。
    • 操作系统启动后,OS会在一个叫做GDT(全局描述符表)的表里标识出内核空间的位置。

    0.2. 中断

    假设在单核CPU的电脑上,安装一个操作系统,系统中运行了N多的程序,包括内核程序和用户程序。
    此时在一个瞬间CPU只会执行一个程序。

    CPU是怎么进行各个进程的调度的呢?

    Step1:

    • 晶振1秒钟震动1千次或者1万次,晶振每震荡一次,都会传递一个时钟中断给CPU,假设当前运行的是用户程序1,晶振震动之后,CPU会把用户程序1的数据(现场)从CPU的高速寄存器中缓存到这个用户程序的空间中。

    Step2:

    • 操作系统启动的时候,会有一个中断向量表(中断表述符表IDT(Interrupt Descriptor Table)),OS启动时,内核程序注册的。
    • 中断产生了之后,存在一个中断回调程序。这个回调程序是由内核注册的。比如这里的:时钟中断产生之后,CPU把现场保存了,CPU本不知道下一步要干什么。但是在OS启动的时候,内核已经注册了,说“CPU,当你收到时钟中断之后,先保护现场,然后来调用我内核程序中的一个调用,这个调用叫'进程调度'”。这个注册的东西就是注册中断向量表。代表“某一个中断发生了,CPU应该去这个表上查它下一步应该干啥”
    • 所以当时钟中断产生之后,用户程序现场被保存下来,然后CPU调用内核的“进程调度”这个方法。

    Step3:

    • CPU调用内核的“进程调度”后,内核告诉CPU,现在可以执行用户程序2了,CPU就去执行用户程序2了,因为之前可能用户程序2被调度过,所以中断后的进程调度第一步就是恢复现场。

    所以这里的中断会导致1:CPU的保护现场和恢复现场,会使CPU高速寄存器和内存之间的数据传递,会有消耗。
    所以这里的中断会导致2:CPU进行系统调用"进程调度"的次数会有消耗。
    【证明01:程序运行的越多,单位时间内,CPU浪费在内核调度上的时间会变多,浪费在寄存器与内存数据传递的时间会变多,真正运行程序的时间会变少。】

    0.3. 软中断(陷阱)

    假设用户程序想访问电脑的摄像头或者硬盘,此时用户程序会调用内核的系统调用。这种调用我们称为软中断或者陷阱(INT x80)

    从CPU的角度考虑一下这个调用,CPU首先在执行用户的程序,但是用户程序写了一行“读取网卡输入”的代码,这个时候用户的编程语言的编译器会在指令里加入一个软中断int x80,表示需要进行系统调用。当CPU读取到int x80的指令的时候,内核之前已经注册了中断向量表(中断表述符表IDT(Interrupt Descriptor Table)),系统调用处理程序 system_call() 的入口地址放在系统的中断表述符表IDT(Interrupt Descriptor Table)中,Linux系统初始化时,由 trap_init() 将其填写完整,其设置系统调用处理程序的语句为:

    set_system_gate(0x80, &system_call)

    经过初始化以后,每当执行 int 0x80 指令时,产生一个异常使系统陷入内核空间并执行128号异常处理程序,即系统调用处理程序 system_call() 。

    这一列骚操作会让CPU在用户态和内核态之间转换。所以依然会让CPU在单位时间产生更大的消耗。

    【证明02:当程序调用了系统调用访问外设这种操作,会让CPU消耗更多的时间在用户态和内核态之间】

    1. BIO (BlockingIO)

    先上代码:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class TestBio {
        public static void main(String[] args) throws Exception {
            ServerSocket serverSocket = new ServerSocket(8090); //监听端口8090,本机作为服务端
            System.out.println("step1 new ServerSocket(8090)");
            while(true) {
                Socket client = serverSocket.accept(); //一旦客户端连接上来,新开辟一个线程处理客户端输入
                System.out.println("client port :" + client.getPort());
    
                new Thread(new Runnable() {
                    Socket ss;
    
                    public Runnable setSS(Socket s) {
                        ss = s;
                        return this;
                    }
    
                    @Override
                    public void run() {
                        try {
                            InputStream is = ss.getInputStream();
                            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
                            while(true) {
                                System.out.println(reader.readLine());
                            }
                        } catch(IOException e) {
                            e.printStackTrace();
                        }
                    }
                }.setSS(client)).start();
            }
        }
    }
    

    这段代码非常之简单,不多做解释。

    利用自己的虚拟机进入Linux系统,把这份程序拷贝到Linux环境下运行。再利用strace命令跟踪java bio在linux系统中的调用过程。

    什么是strace?

    按照strace官网的描述, strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。
    strace底层使用内核的ptrace特性来实现其功能。

    [root@hadoop-senior code]# strace -ff -o out java TestBio执行结果:

    [root@hadoop-senior code]# strace -ff -o out java TestBio
    step1 new ServerSocket(8090)
    

    可以理解,此时服务器端(即自己的虚拟机)已经在8090监听来自客户端的连接。但是目前还没有任何连接建立,所以打印完输出文字之后就阻塞在这里。

    通过ll查看strace命令自动生成的线程跟踪文件:

    [root@hadoop-senior code]# ll
    total 252
    -rw-r--r-- 1 root root   9424 Jun 26 15:23 out.6228
    -rw-r--r-- 1 root root 177741 Jun 26 15:23 out.6229
    -rw-r--r-- 1 root root   2123 Jun 26 15:23 out.6230
    -rw-r--r-- 1 root root    931 Jun 26 15:23 out.6231
    -rw-r--r-- 1 root root   1066 Jun 26 15:23 out.6232
    -rw-r--r-- 1 root root    975 Jun 26 15:23 out.6233
    -rw-r--r-- 1 root root   5003 Jun 26 15:23 out.6234
    -rw-r--r-- 1 root root   3705 Jun 26 15:23 out.6235
    -rw-r--r-- 1 root root    931 Jun 26 15:23 out.6236
    -rw-r--r-- 1 root root  17321 Jun 26 15:23 out.6237
    -rw-r--r-- 1 root root   1092 Jun 26 14:29 TestBio$1.class
    -rw-r--r-- 1 root root   1128 Jun 26 14:29 TestBio.class
    -rw-r--r-- 1 root root   1326 Jun 26 14:28 TestBio.java
    [root@hadoop-senior code]# 
    

    可以发现生成了很多out开头的线程跟踪文件,通过less或者tail -f查看这些文件。
    通过jpsnetstat -natp查看服务器各个端口使用情况。

    因为java是一个多线程的程序,查看最大size的文件:out.6229,这个文件跟踪的即是main方法的输出。前面4位数字是这个文件的行号,不用在意。

       2421 socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 5
       2422 setsockopt(5, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
       2423 fcntl(5, F_GETFL)                       = 0x2 (flags O_RDWR)
       2424 fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
       2425 setsockopt(5, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
       2426 lseek(3, 58578982, SEEK_SET)            = 58578982
       2427 read(3, "PK34
    10!260365J324317366B3412341226", 30) = 30
       2428 lseek(3, 58579034, SEEK_SET)            = 58579034
       2429 read(3, "312376272276004%
    1032	733
    	34
    	357"..., 737) = 737
       2430 lseek(3, 58572442, SEEK_SET)            = 58572442
       2431 read(3, "PK34
    10!260365J355177E>Q31Q3135", 30) = 30
       2432 lseek(3, 58572501, SEEK_SET)            = 58572501
       2433 read(3, "312376272276004015
    Y217722010221
    2222
    2232247"..., 6481) = 6481
       2434 lseek(3, 58571940, SEEK_SET)            = 58571940
       2435 read(3, "PK34
    10!260365J0216322;2711271137", 30) = 30
       2436 lseek(3, 58572001, SEEK_SET)            = 58572001
       2437 read(3, "31237627227600427
    31772172416<init>1"..., 441) = 441
       2438 lseek(3, 59057764, SEEK_SET)            = 59057764
       2439 read(3, "PK34
    1033260365J_M312337\"\"33", 30) = 30
       2440 lseek(3, 59057821, SEEK_SET)            = 59057821
       2441 read(3, "3123762722760041222
    20314
    4315	203167317
    320"..., 8796) = 8796
       2442 lseek(3, 59053916, SEEK_SET)            = 59053916
       2443 read(3, "PK34
    1033260365J245h2203312741627416.", 30) = 30
       2444 lseek(3, 59053992, SEEK_SET)            = 59053992
       2445 read(3, "312376272276004x7J
    26K
    26L	26M
    30"..., 3772) = 3772
       2446 bind(5, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
       2447 listen(5, 50)                           = 0
       2448 write(1, "step1 new ServerSocket(8090)", 28) = 28
       2449 write(1, "
    ", 1)                       = 1
       2450 lseek(3, 58689145, SEEK_SET)            = 58689145
       2451 read(3, "PK34
    1020260365Jy271LV2416241625", 30) = 30
       2452 lseek(3, 58689196, SEEK_SET)            = 58689196
       2453 read(3, "3123762722760041345
    6127	233130	233131	233132	"..., 13985) = 13985
       2454 accept(5, 
    

    关键来了
    new ServerSocket(8090) 代表的第2421,2422,2446,2447 这几行。
    意思就是

       2421 socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 5            //创建socket,定义为文件描述符5
       2422 setsockopt(5, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0         //设置socket属性,如Ipv4或者Ipv6
       2446 bind(5, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0    //把文件描述符5绑定到8090端口
       2447 listen(5, 50)                                            //监听5文件描述符
    

    serverSocket.accept() 代表的是2454 accept(5,
    这个accept系统调用是阻塞的,表示如果没有客户端连接上来,那么这个方法会一直阻塞着等待。

    新开一个xshell客户端,输入:
    [root@hadoop-senior ~]# nc localhost 8090
    第一个客户端连接上来,可以看到客户端的连接端口号是40368

    [root@hadoop-senior code]# strace -ff -o out java TestBio
    step1 new ServerSocket(8090)
    client port :40368
    

    再次查看less -N out.6229文件,可以发现下面的新增内容:

       2455 accept(5, {sa_family=AF_INET6, sin6_port=htons(40368), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
       2456 fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)
       2457 fcntl(6, F_SETFL, O_RDWR)               = 0
       2458 write(1, "client port :40368", 18)      = 18
       2459 write(1, "
    ", 1)                       = 1
       2460 stat("/opt/code/TestBio$1.class", {st_mode=S_IFREG|0644, st_size=1092, ...}) = 0
       2461 open("/opt/code/TestBio$1.class", O_RDONLY) = 7
       2462 fstat(7, {st_mode=S_IFREG|0644, st_size=1092, ...}) = 0
       2463 stat("/opt/code/TestBio$1.class", {st_mode=S_IFREG|0644, st_size=1092, ...}) = 0
       2464 read(7, "312376272276004H
    16#	
    $
    %&7'7(
    "..., 1024) = 1024
       2465 read(7, "%3130237530732733377f17341735"..., 68) = 68
       2466 close(7)                                = 0
       2467 lseek(3, 62390550, SEEK_SET)            = 62390550
       2468 read(3, "PK34
    1016260365J9267215270R6R6;", 30) = 30
       2469 lseek(3, 62390639, SEEK_SET)            = 62390639
       2470 read(3, "312376272276004:7!
    v"	10#
    1$	v"..., 1618) = 1618
       2471 mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f584016d000
       2472 clone(child_stack=0x7f584026cff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CL
       2472 ONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f584026d9d0, tls=0x7f584026d700, child_tidp
       2472 tr=0x7f584026d9d0) = 6333
       2473 futex(0x7f583c009454, FUTEX_WAIT_PRIVATE, 21, NULL) = 0
       2474 futex(0x7f583c009428, FUTEX_WAIT_PRIVATE, 2, NULL) = 0
       2475 futex(0x7f583c009428, FUTEX_WAKE_PRIVATE, 1) = 0
       2476 accept(5, 
    

    我们来看下面这关键的三行,

       2455 accept(5, {sa_family=AF_INET6, sin6_port=htons(40368), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
       ...
       2472 clone(child_stack=0x7f584026cff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CL
       2472 ONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f584026d9d0, tls=0x7f584026d700, child_tidp
       2472 tr=0x7f584026d9d0) = 6333
       ...
       2476 accept(5, 
    

    第一行之前被阻塞在accept(5, 这一时刻,当客户端连接进来的时候,系统调用被唤醒,然后因为我们的java程序写的是new了一个线程去处理这个客户端,所以看到一个clone命令
    第二行就是clone命令,代表的是创建一个线程,去处理这个客户端的输入。
    第三行就是继续阻塞,去接受新客户端的连接。

    下面简单说一下新线程中的读取客户端输入的过程:
    当使用nc命令连接服务端之后:strace新创建了一个out.6333文件,这个文件的输出就是新线程接受客户端连接的那个动作

    [root@hadoop-senior code]# ll
    total 5776
    -rw-r--r-- 1 root root    9424 Jun 26 15:23 out.6228
    -rw-r--r-- 1 root root  179455 Jun 26 15:38 out.6229
    -rw-r--r-- 1 root root  268997 Jun 26 15:50 out.6230
    -rw-r--r-- 1 root root     931 Jun 26 15:23 out.6231
    -rw-r--r-- 1 root root    1066 Jun 26 15:23 out.6232
    -rw-r--r-- 1 root root     975 Jun 26 15:23 out.6233
    -rw-r--r-- 1 root root   58561 Jun 26 15:50 out.6234
    -rw-r--r-- 1 root root   57379 Jun 26 15:50 out.6235
    -rw-r--r-- 1 root root     931 Jun 26 15:23 out.6236
    -rw-r--r-- 1 root root 5277429 Jun 26 15:50 out.6237
    -rw-r--r-- 1 root root    1782 Jun 26 15:49 out.6333
    -rw-r--r-- 1 root root    1092 Jun 26 14:29 TestBio$1.class
    -rw-r--r-- 1 root root    1128 Jun 26 14:29 TestBio.class
    -rw-r--r-- 1 root root    1326 Jun 26 14:28 TestBio.java
    

    查看out,6333,可以看到如果客户端没有输入的时候,服务器阻塞在recvfrom(6,, 当我从nc命令行分别输入2次:"123"、"456"之后,
    recvfrom(6, "123 ", 8192, 0, NULL, NULL) = 4 会从阻塞状态变为执行。从而读取到客户端的输入。

          1 set_robust_list(0x7f584026d9e0, 0x18)   = 0
          2 gettid()                                = 6333
          3 rt_sigprocmask(SIG_BLOCK, NULL, [QUIT], 8) = 0
          4 rt_sigprocmask(SIG_UNBLOCK, [HUP INT ILL BUS FPE SEGV USR2 TERM], NULL, 8) = 0
          5 rt_sigprocmask(SIG_BLOCK, [QUIT], NULL, 8) = 0
          6 futex(0x7f583c009454, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7f583c009450, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
          7 futex(0x7f583c009428, FUTEX_WAKE_PRIVATE, 1) = 1
          8 sched_getaffinity(6333, 32,  { 1, 0, 0, 0 }) = 32
          9 sched_getaffinity(6333, 32,  { 1, 0, 0, 0 }) = 32
         10 mmap(0x7f584016d000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f584016d000
         11 mprotect(0x7f584016d000, 12288, PROT_NONE) = 0
         12 lseek(3, 30051273, SEEK_SET)            = 30051273
         13 read(3, "PK34
    1020260365J24w067E3E327", 30) = 30
         14 lseek(3, 30051326, SEEK_SET)            = 30051326
         15 read(3, "312376272276004-	634
    735	3236
    3733
    "..., 837) = 837
         16 lseek(3, 30053056, SEEK_SET)            = 30053056
         17 read(3, "PK34
    1033260365J+346oD255
    255
     ", 30) = 30
         18 lseek(3, 30053118, SEEK_SET)            = 30053118
         19 read(3, "312376272276004242
    YZ
    -[	,\	,]	"..., 3501) = 3501
         20 futex(0x7f583c0b1954, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7f583c0b1950, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
         21 futex(0x7f583c0b1928, FUTEX_WAKE_PRIVATE, 1) = 1
         22 recvfrom(6, "123
    ", 8192, 0, NULL, NULL) = 4
         23 ioctl(6, FIONREAD, [0])                 = 0
         24 write(1, "123", 3)                      = 3
         25 write(1, "
    ", 1)                       = 1
         26 recvfrom(6, "456
    ", 8192, 0, NULL, NULL) = 4
         27 ioctl(6, FIONREAD, [0])                 = 0
         28 write(1, "456", 3)                      = 3
         29 write(1, "
    ", 1)                       = 1
         30 recvfrom(6,
    

    总结:

    无论BIO、NIO、多路复用,在linux系统下的网络通信都离不开: socket、bind、listen这三个系统调用。

    传统BIO的底层调用流程就是这样:最大的特点:一个线程处理一个连接

    accept是阻塞的
    recvfrom是阻塞的

    因为这俩哥们儿阻塞,所以BIO称为Blocking IO。

  • 相关阅读:
    Daily Build[called heart beat]
    JS判断浏览器类型与版本
    Dependency Injection in ASP.NET MVC
    Splash Screen(短时间弹出框,信息显示一次)
    Mock Framework
    sites for debugging script
    Types in Javascript(jQuery)
    推荐10个超棒的jQuery工具 提示插件
    WebClient 下载文件
    sqlserver导入Excel数据 总是报错:错误 0xc020901c: 数据流任务 1: 输出“Excel 源输出”(55) 上的 输出列“T2”(64) 出错。返回的列状态是:“文本被截断,或者一个或多个字符在目标代码页中没有匹配项
  • 原文地址:https://www.cnblogs.com/1626ace/p/13193435.html
Copyright © 2011-2022 走看看