zoukankan      html  css  js  c++  java
  • 从OS的层次理解网络I/O模型

    基本概念

    传统IO的种类

    • InputStream、OutputStream 基于字节流操作的 IO
    • Write、Reader基于字符流的IO
    • File基于磁盘操作的IO
    • Socket基于网络操作的IO

    内核空间与用户空间

    图片名称
    - 内核负责网络与文件数据的读写 - 用户程序通过系统调用获得网络和文件的数据

    内核态与用户态的切换

    //当前线程处于用户态
    String str = "string";
    int x = 2;
    //切换至内核态
    FileOutputStream fop = new FileOutputStream(new File("a.txt"));
    OutputStreamWrite out = new OutputStreamWrite(fop, "GBK");
    out.write("....");
    out.append('
    ');
    out.close();
    //用户态
    int y = x + 2;
    

                         

    图片名称
    - 程序为读写数据不得不发生系统调用。 - 通过系统调用接口,线程从用户态切换到内核态,内核读写数据后,再切换回来。 - 进程或线程的不同空间状态。

    socket通信
                       

    图片名称
    1. 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
    2. 数据传输的过程:
      建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
    3. 如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
    • 客户端
    public class EchoClient {
    	public static int DEFAULT_PORT = 9999;
    	public static void main(String[] args) throws IOException {
    		int port;
            try {
    			port = Integer.parseInt(args[0]);
    		} catch(RuntimeException e) {
            	port = DEFAULT_PORT;
    		}
    		Socket socket = new Socket("127.0.0.1", port);
    		//键盘输入
    		BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
    		//Socket输出流,自动刷新
    		PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            //Socket输入流,读取服务端的数据并返回的大写数据
    		BufferedReader buffin = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line = null;
            while((line = buff.readLine()) != null) {
            	if("stop".equals(line)) {
            		break;
    			}
            	out.println(line);
    			// 读取服务端返回的一行大写数据
    			System.out.println(buffin.readLine());
    		}
    
    	}
    }
    

    也可使用linux下的nc命令代替客户端

    • 服务端
    public class EchoServer {
    	public static int DEFAULT_PORT = 9999;
    
    	public static void main(String[] args){
    		int port;
    		try {
    			port = Integer.parseInt(args[0]);
    		} catch(RuntimeException e){
    			port = DEFAULT_PORT;
    		}
    
    		try {
    			ServerSocket serverSocket = new ServerSocket(port);
    			Socket clientSocket = serverSocket.accept();
    			String ip = clientSocket.getInetAddress().getHostAddress();
    			System.out.println("port : " + port + '	' + "ipaddress : " + ip);
    			//server 输出流对应client输入流,反之亦然
    			PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
    			BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    			String inputline ;
    			while((inputline = in.readLine()) != null){
    				System.out.println(inputline);
    				out.println(inputline.toUpperCase());
    			}
    		} catch (IOException e) {
    			System.out.println("Exception caught when trying to listen on port" + port + "or listening for a connection");
    			e.printStackTrace();
    		}
    	}
    }
    

    同步与异步

    描述的是用户线程与内核的交互方式或者说关注的是消息通信机制:

    • 同步是指用户线程发起 I/O 请求后需要等待(堵塞)或者轮询(非堵塞)内核 I/O 操作完成后才能继续执行;
    • 异步是指用户线程发起 I/O 请求后仍继续执行,当内核 I/O 操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

    堵塞与非堵塞

    关注的是用户线程调用内核 I/O 操作时,用户线程等待I/O操作完成前是否能做其他的事情:

    • 阻塞是指 I/O 操作需要彻底完成后才返回到用户空间;
    • 非阻塞是指 I/O 操作被调用后立即返回给用户一个状态值,无需等到 I/O 操作彻底完成。

    阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度,同步与异步主要是从消息机制角度来说,这两组概念组合为四种情况,下面举几个网上的例子:

    • 同步堵塞 李华点火烧水,中间啥事也没干,并一直等到水开(阻塞),水开了李华关火(同步)
    • 同步非堵塞(轮询方式) 李华点火烧水,中间去看了电视,时不时看看水开了嘛(非阻塞),水开了李华关火(同步)
    • 异步堵塞 李华使用电水壶烧水,并一直等待电水壶烧水(阻塞),中间啥也没干,水开了自动断电(异步)。
    • 异步非堵塞 李华使用电水壶烧水,然后去看电视了(非阻塞),没有再管烧水壶,水开了自动断电(异步)。

    IO模型演进

    IO操作发生时会经历两个阶段:

    1. 用户进程等待系统内核数据准备
    2. 将数据从内核拷贝到用户进程中

    下面简单介绍常见的五种 I/O 模型:

    1. 阻塞 I/O
    2. 非阻塞 I/O
    3. I/O 复用(select 和 poll)
    4. 信号驱动I/O(SIGIO)
    5. 异步 I/O

    本节中将recvfrom函数视为系统调用。一般recvfrom函数的实现都有一个从应用程序进程中运行到内核中运行的切换,一段时间后再跟一个返回应用进程的切换。

    阻塞 I/O

    请求无法立即完成则保持阻塞。

    图片名称
    - 等待数据就绪。网络I/O的情况就是等待远端数据陆续抵达;磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。 - 数据复制。出于系统安全考虑,用户态的程序没有权限直接读取内核态内存,因此内核负责把内核态内存中的数据复制一份到用户态内存中。

    进程阻塞的整段时间是指从调用recvfrom函数开始到它返回的这段时间,当进程返回成功提示时,应用进程开始处理数据报。

    非阻塞 I/O
                   

    图片名称
    - socket设置为NONBLOCK(非阻塞)就是告诉内核,当所请求的I/O操作无法完成时,不要让进程进入睡眠状态,而是立刻返回一个错误码(EWOULDBLOCK),这样请求就不会阻塞; - I/O操作函数将不断地测试数据是否已经准备好,如果没有准备好,则继续测试,直到数据准备好为止。在整个I/O请求的过程中,虽然用户线程每次发起I/O请求后可以立即返回,但是为了等到数据,仍需轮询、重复请求,而这是对CPU时间的极大浪费。 - 数据准备好了,从内核复制到用户空间。

    I/O 复用(异步堵塞I/O)

    I/O 多路复用会用到 select 或者 poll 函数,这两个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

    图片名称
    从流程上看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select函数的优势是用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select来读取被激活的socket,达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    I/O复用模型使用Reactor设计模式实现了这一机制。

    调用select或poll函数的方法由一个用户态线程负责轮询多个socket,直到阶段1的数据就绪,再通知实际的用户线程执行阶段2的复制操作。通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现阶段1的异步化。

    信号驱动I/O

    图片名称
    > 首先,我们允许socket进行信号驱动I/O,并通过调用sigaction来安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好后,进程会收到一个SIGIO信号,可以在信号处理函数中调用recvfrom来读取数据报,并通知主循环数据已准备好被处理,也可以通知主循环,让它来读取数据报。

    异步 I/O

    调用 aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。
    异步I/O模型使用Proactor设计模式实现了这一机制。
                  

    图片名称
    异步I/O模型告知内核:当整个过程(包括阶段1和阶段2)全部完成时,通知应用程序来读数据。

    几种 I/O 模型的比较

    前四种模型的区别是阶段1不相同,阶段2基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步 I/O 的两个阶段都不同于前四个模型。

    同步 I/O 操作引起请求进程阻塞,直到 I/O 操作完成。异步 I/O 操作不引起请求进程阻塞。

                  

    图片名称

    关于这些模型的具体实现我打算放到Java I/O中进行讨论。

  • 相关阅读:
    Android WifiDisplay分析一:相关Service的启动
    Android4.2以后,多屏幕的支持 学习(一)
    第十七篇 --ANDROID DisplayManager 服务解析一
    Android Wi-Fi Display(Miracast)介绍
    Ubuntu下 Astah professional 6.9 安装
    JAVA调用c/c++代码
    Application Fundamentals
    说说Android应用的persistent属性
    Tasks and Back stack 详解
    Activity的四种launchMode
  • 原文地址:https://www.cnblogs.com/skills/p/13321995.html
Copyright © 2011-2022 走看看