为您推荐:
精华内容
最热下载
问答
  • 5星
    5.34MB qing_gee 2021-08-03 16:12:27
  • 5星
    37.7MB ZhanweiXue 2020-12-01 09:17:05
  • 5星
    29KB jia666666 2021-09-23 16:04:09
  • 5星
    127KB lunzilx 2021-01-11 16:02:37
  • 1、文件IO网络IO 1.1专有名词 之前一直对IO一知半解,所以整理下IO各种概念与IO读取数据的流程,先了解一下专有名词: (1)用户空间 / 内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的...

    1、文件IO与网络IO

    1.1专有名词

    之前一直对IO一知半解,所以整理下IO各种概念与IO读取数据的流程,先了解一下专有名词:

    (1)用户空间 / 内核空间
    现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
    操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

    (2)进程切换
    为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。

    (3)进程阻塞
    正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

    (4)文件描述符
    文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
    文件描述符在形式上是一个非负整数。实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统而windows为句柄的概念

    (5)、句柄
    所谓句柄实际上是一个数据,是一个Long (整长型)的数据。
    句柄是WONDOWS用来标识被应用程序所建立或使用的对象的唯一整数,WINDOWS使用各种各样的句柄标识诸如应用程序实例,窗口,控制,位图,GDI对象等等。WINDOWS句柄有点象C语言中的文件句柄。

    从上面的定义中的我们可以看到,句柄是一个标识符,是拿来标识对象或者项目的,它就象我们的姓名一样,每个人都会有一个,不同的人的姓名不一样,但是,也可能有一个名字和你一样的人。从数据类型上来看它只是一个16位的无符号整数。应用程序几乎总是通过调用一个WINDOWS函数来获得一个句柄,之后其他的WINDOWS函数就可以使用该句柄,以引用相应的对象。

    如果想更透彻一点地认识句柄,我可以告诉大家,句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是住留在内存的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果您真的这样认为,那么您就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?

    为了解决这个问题,Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时(Unload)又释放给系统。

    句柄地址(稳定)→记载着对象在内存中的地址────→对象在内存中的地址(不稳定)→实际对象,WINDOWS程序中并不是用物理地址来标识一个内存块,文件,任务或动态装入模块的,相反的,WINDOWS API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作

    (6)缓存I/O
    缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

    (7)网络分组
    分组的概念是大多数计算机网络都不能连续地传送任意长的数据,所以实际上网络系统把数据分割成小块,然后逐块地发送,这种小块就称作分组(packet)

    1.2普通IO与文件IO

    1.2.1普通文件IO,是对磁盘文件上的数据进行读写。

    IO读写经历的两个过程:
    阶段1 wait for data 等待数据准备
    阶段2 copy data from kernel to user 将数据从内核拷贝到用户进程中

    IO,其实意味着:数据不停地搬入搬出缓冲区而已(使用了缓冲区)。比如,用户程序发起读操作,导致“ syscall read ”系统调用,就会把数据搬入到 一个buffer中;用户发起写操作,导致 “syscall write ”系统调用,将会把一个 buffer 中的数据 搬出去(发送到网络中 or 写入到磁盘文件)。DMA(Direct Memory Access,直接内存存取,不需要CPU参与) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。

    整个IO过程的流程如下:

    1)程序员写代码创建一个缓冲区(这个缓冲区是用户缓冲区):哈哈。然后在一个while循环里面调用read()方法读数据(触发"syscall read"系统调用)

    byte[] b = new byte[4096];

    while((read = inputStream.read(b))>=0) {
    total = total + read;
    // other code…
    }
    2当执行到read()方法时,其实底层是发生了很多操作的: ①内核给磁盘控制器发命令说:我要读磁盘上的某某块磁盘块上的数据。–kernel issuing a command to the disk controller hardware to fetch the data from disk. ②在DMA的控制下,把磁盘上的数据读入到内核缓冲区。–The disk controller writes the data directly into a kernel memory buffer by DMA ③内核把数据从内核缓冲区复制到用户缓冲区。–kernel copies the data from the temporary buffer in kernel space 这里的用户缓冲区应该就是我们写的代码中 new 的 byte[] 数组。

    操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。

    对于操作系统而言,JVM只是一个用户进程,处于用户态空间中。而处于用户空间的进程是不能直接操作底层的硬件的。而IO操作就需要操作底层的硬件,比如磁盘。因此,IO操作必须得借助内核的帮助才能完成(中断,trap),即:CPU切换执行的进程会有用户态到内核态的切换。

    我们写代码 new byte[] 数组时,一般是都是“随意” 创建一个“任意大小”的数组。比如,new byte[128]、new byte[1024]、new byte[4096]…,即用户缓冲区,但是,对于磁盘块的读取而言,每次访问磁盘读数据时,并不是读任意大小的数据的,而是:每次读一个磁盘块或者若干个磁盘块(这是因为访问磁盘操作代价是很大的,而且我们也相信局部性原理) 因此,就需要有一个“中间缓冲区”–即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区。这也是为什么我们总感觉到第一次read操作很慢,而后续的read操作却很快的原因吧。因为,对于后续的read操作而言,它所需要读的数据很可能已经在内核缓冲区了,此时只需将内核缓冲区中的数据拷贝到用户缓冲区即可,并未涉及到底层的读取磁盘操作,当然就快了

    1.3网络IO是多网络数据进行读写,即从网卡NCI缓冲区读取数据。

    网络io分为多种,具体内容看如下链接: 五种网络IO,同步异步,阻塞非阻塞

    套接字soecktIO是一种IO,因为内核创建一个套接字相当于是创建一个文件描述符,对文件的读写其实就是IO操作。当我们进程种调用socket的read(),write()方法实际上是调用内核的read(),write()方法来进行数据的读取的与写入操作的。

    2、多路复用技术

    使用场景:java中的NIO的SelectorImpl类中就实现了IO的多路复用,从客户端链接到服务端的socket,在服务端都注册到Selector类上的一个列表里返回一个selectKey(代表选择器与socket的绑定关系,还有这个socket),然后selector调用内核的selec接口或则epllo接口,把socket列表(即文件描述符列表(文件描述符用一个long整形数值表示))与socket感兴趣的事件传到select接口的参数列表,由内核完成对socket列表的遍历,监听每个socket是否有相应的事件发生,如果有相应的socket感兴趣的事件发生,则把socket的数据结构相应的事件发生标识修改。再由selector调用selectionKeys()。返回所有的socket当前的状态,遍历selectionKeys。判断每个key的状态,然后进行事件的处理。

    多路复用技术:多路指多个socket,复用指用一个线程处理多个socket,或一个线程池处理多个socket多路复用技术的底层是使用select,poll,epoll三种操作系统提供的方法与NIO的特性。操作系统提供的多路复用的三种技术的区别

    2.1socket原理解释

    socket对于linux为一个文件描述符,对于windows系统为一个句柄。要建立两个不同主机间进行网络通信,就必须有一个为服务端,一个为客户端。
    服务端流程
    一、服务端启动先调用socket()内核接口创建一个文件描述符即ServerSocket,这个socket数据结构里没有远程客户端的地址与端口,(1)、调用bind接口这个socket是用来绑定本机地址与端口的 。(2)、然后调用listen接口,告诉内核在我这个ServerSocket上,监听是否有客户端的链接进来,内核就会建立两个队列,一个SYN队列,表示接受到请求,但未完成三次握手的连接,另一个是ACCEPT队列,表示已经完成了三次握手的队列,内核监听到有链接进来就根据情况放到这两个队列中。(3)调用accept()接口,这个接口会阻塞调用的线程(如何设置获取阻塞套接字即阻塞IO),直到内核的accept队列有值,然后内核返回新的socket,给我们调用的这个serverSocket, 这个返回的newSocket会在自己的数据接口里保存自己这个进程的地址与端口号还会保存远程客户端的进程与端口号。通过这个newsocket就可以与远程的客户端进行信息交互。

    二、客户端流程

    客户端的主机进程调用socket()内核接口创建一个描述符,(1)、调用connect()函数与服务端的进程建立链接。则客户端的socket数据结构里就会保存远程服务端的地址与端口,就可以进行消息的交互了。

    以下图片为TCP客户端与服务端建立的流程,来源与其他博客:
    图1
    UDP客户端与服务端建立的流程,图片来源其他博客:
    在这里插入图片描述
    更详细的解释看如下博文
    socket原理解释

    服务端的ServerSocketChannel调用accpet原理

    3、reactor模式

    Reactor模式(反应器模式)事件驱动结构的一种实现。是一种处理一个或多个客户端并发进行服务请求的。将服务端接收请求与事件处理分离,从而提高系统处理并发的能力,java的NIO的reactor模式是基于系统内核的多路复用技术实现的。

    4、javaNIO与再socket建立与多路复用技术的结合与reactor设计模式

    有实现源码:可以看看:
    https://www.jianshu.com/p/1cc249e7d88e

    java中的socketChannel注册到selector上后,会生成一个selectionKey。
    键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey对象的 cancel( )方法。可以通过调用 isValid( )方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey 将被返回。
    SelectionKey 类定义了四个便于使用的布尔方法来为您测试这些比特值:isReadable( ),isWritable( ),isConnectable( ), 和 isAcceptable( )。这个key中的属性为:
    在这里插入图片描述
    channel:当前那key绑定的channel
    selector:当前key绑定的selector,表示channel与selectro的一一对应关系
    interestOps:当前channel感性趣的事件即内核需要监听这个socketChannel发生的事件
    readyOps:当前key正在发生的事件。

    选择键包含两个操作集**,操作集为位运算值,每一位表示一种操作.**
    interestOps 集合:当前channel感兴趣的操作,此类操作将会在下一次选择器select操作时被交付,可以通过selectionKey.interestOps(int)进行修改.
    readyOps 集合:表示此选择键上,已经就绪的操作.每次select时,选择器都会对ready集合进行更新;外部程序无法修改此集合.

    selectionKey调用isReadable( ),isWritable( ),isConnectable( ), 和 isAcceptable( )这些方法时,都是那readyOps值与定义好的位来做 与运算
    在这里插入图片描述OP_WRITE等在selectionKey中定义了今天变量

    public static final int OP_ACCEPT = 1 << 4;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_READ = 1 << 0;

    java中的NIO的SelectorImpl类中就实现了IO的多路复用,从客户端链接到服务端的socket,在服务端都注册到Selector类上的一个列表里返回一个selectKey(代表选择器与socket的绑定关系,还有这个socket),然后selector调用内核的selec接口或则epllo接口,把socket列表(即文件描述符列表(文件描述符用一个long整形数值表示))与socket感兴趣的事件传到select接口的参数列表,由内核完成对socket列表的遍历,监听每个socket是否有相应的事件发生,如果有相应的socket感兴趣的事件发生,则把socket的数据结构相应的事件发生标识修改。再由selector调用selectionKeys()。返回所有的socket当前的状态,遍历selectionKeys。判断每个key的状态,然后进行事件的处理。

    如下代码来源与其他博客:

    `import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectableChannel;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;

    import javax.swing.text.html.HTMLDocument.Iterator;

    /**

    • Simple echo-back server which listens for incoming stream connections and

    • echoes back whatever it reads. A single Selector object is used to listen to

    • the server socket (to accept new connections) and all the active socket

    • channels.

    • @author zale (zalezone.cn)
      /
      public class SelectSockets {
      public static int PORT_NUMBER = 1234;
      public static void main(String[] argv) throws Exception
      {
      new SelectSockets().go(argv);
      }
      public void go(String[] argv) throws Exception
      {
      int port = PORT_NUMBER;
      if (argv.length > 0)
      { // 覆盖默认的监听端口
      port = Integer.parseInt(argv[0]);
      }
      System.out.println("Listening on port " + port);
      ServerSocketChannel serverChannel = ServerSocketChannel.open();// 打开一个未绑定的serversocketchannel
      ServerSocket serverSocket = serverChannel.socket();// 得到一个ServerSocket去和它绑定
      Selector selector = Selector.open();// 创建一个Selector供下面使用
      serverSocket.bind(new InetSocketAddress(port));//设置server channel将会监听的端口
      serverChannel.configureBlocking(false);//设置非阻塞模式
      serverChannel.register(selector, SelectionKey.OP_ACCEPT);//将ServerSocketChannel注册到Selector
      while (true)
      {
      // This may block for a long time. Upon returning, the
      // selected set contains keys of the ready channels.
      int n = selector.select();
      if (n == 0)
      {
      continue; // nothing to do
      }
      java.util.Iterator it = selector.selectedKeys().iterator();// Get an iterator over the set of selected keys
      //在被选择的set中遍历全部的key
      while (it.hasNext())
      {
      SelectionKey key = (SelectionKey) it.next();
      // 判断是否是一个连接到来
      if (key.isAcceptable())
      {
      ServerSocketChannel server =(ServerSocketChannel) key.channel();
      SocketChannel channel = server.accept();
      registerChannel(selector, channel,SelectionKey.OP_READ);//注册读事件
      sayHello(channel);//对连接进行处理
      }
      //判断这个channel上是否有数据要读
      if (key.isReadable())
      {
      readDataFromSocket(key);
      }
      //从selected set中移除这个key,因为它已经被处理过了
      it.remove();
      }
      }
      }
      // ----------------------------------------------------------
      /
      *

      • Register the given channel with the given selector for the given
      • operations of interest
        /
        protected void registerChannel(Selector selector,SelectableChannel channel, int ops) throws Exception
        {
        if (channel == null)
        {
        return; // 可能会发生
        }
        // 设置通道为非阻塞
        channel.configureBlocking(false);
        // 将通道注册到选择器上
        channel.register(selector, ops);
        }
        // ----------------------------------------------------------
        // Use the same byte buffer for all channels. A single thread is
        // servicing all the channels, so no danger of concurrent acccess.
        //对所有的通道使用相同的缓冲区。单线程为所有的通道进行服务,所以并发访问没有风险
        private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        /
        *
      • Sample data handler method for a channel with data ready to read.
      • 对于一个准备读入数据的通道的简单的数据处理方法
      • @param key

      A SelectionKey object associated with a channel determined by
      the selector to be ready for reading. If the channel returns
      an EOF condition, it is closed here, which automatically
      invalidates the associated key. The selector will then
      de-register the channel on the next select call.

      一个选择器决定了和通道关联的SelectionKey object是准备读状态。如果通道返回EOF,通道将被关闭。
      并且会自动使相关的key失效,选择器然后会在下一次的select call时取消掉通道的注册
      /
      protected void readDataFromSocket(SelectionKey key) throws Exception
      {
      SocketChannel socketChannel = (SocketChannel) key.channel();
      int count;
      buffer.clear(); // 清空Buffer
      // Loop while data is available; channel is nonblocking
      //当可以读到数据时一直循环,通道为非阻塞
      while ((count = socketChannel.read(buffer)) > 0)
      {
      buffer.flip(); // 将缓冲区置为可读
      // Send the data; don’t assume it goes all at once
      //发送数据,不要期望能一次将数据发送完
      while (buffer.hasRemaining())
      {
      socketChannel.write(buffer);
      }
      // WARNING: the above loop is evil. Because
      // it’s writing back to the same nonblocking
      // channel it read the data from, this code can
      // potentially spin in a busy loop. In real life
      // you’d do something more useful than this.
      //这里的循环是无意义的,具体按实际情况而定
      buffer.clear(); // Empty buffer
      }
      if (count < 0)
      {
      // Close channel on EOF, invalidates the key
      //读取结束后关闭通道,使key失效
      socketChannel.close();
      }
      }
      // ----------------------------------------------------------
      /
      *

      • Spew a greeting to the incoming client connection.
      • @param channel

      The newly connected SocketChannel to say hello to.
      */
      private void sayHello(SocketChannel channel) throws Exception
      {
      buffer.clear();
      buffer.put(“Hi there!\r\n”.getBytes());
      buffer.flip();
      channel.write(buffer);
      }
      }`

    展开全文
    qq_33670476 2019-06-17 00:30:33
  • IO和网络通讯基础概念 首先要从冯诺依曼说起,由他提出的计算机体系结构: 计算器控制(CPU…) ——> 主存(内存…) ——> 输入输出(硬盘、网卡、显示器、键盘…) IO说白了就是输入输出,宏观角度讲可以分为IO...
    IO和网络通讯基本概念

    首先要从冯诺依曼说起,由他提出的计算机体系结构:

    计算器控制(CPU…) ——> 主存(内存…) ——> 输入输出(硬盘、网卡、显示器、键盘…)

    IO说白了就是输入输出,宏观角度讲可以分为IO设备和IO接口两个部分,IO设备就是输入输出设备,IO设备的工作方式可以由程序进行控制的。IO接口可以理解为 “计算机和其他计算机”,或者 ,“程序与计算机的IO设备”之间的传输接口。

    IO它对于任何计算机系统都非常关键,因为所有 I/O 的主体实际上是内置在操作系统中的。程序一般是调用系统为它们完成大部分的工作。

    网络通讯,就相当于一台计算机给另外一台计算机传输数据,中间的过程就叫做通信,也就是通过IO接口输入输出到另一台计算机,这个就叫做网络IO,可以把网络通讯理解为IO的一种,很多人会把网络IO和文件IO的概念区分开,其实他俩是一样的,只不过是通过不同的方式把数据输入输出到了不同的地方。

    操作IO的模式也有很多种,有BIO、NIO之类的,这些可以对应到Java中的类来加深我们的概念。

    java.io包基于流模型实现,提供File抽象、输入输出流等IO的功能。交互方式是同步、阻塞的方式,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞。java.io包的好处是代码比较简单、直观,缺点则是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈

    java.net包下提供的部分网络API,比如Socket、ServerSocket、HttpURLConnection
    也可以被归类到同步阻塞IO类库,因为网络通信同样是IO行为

    在Java 1.4中引入了NIO框架(java.nio 包),提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用IO程序,同时提供更接近操作系统底层的高性能数据操作方式

    在Java7中,NIO有了进一步的改进,也就是NIO2,引入了异步非阻塞IO方式,也被称为AIO(Asynchronous IO),异步IO操作基于事件和回调机制

    以上提到的NIO、BIO、AIO、多路复用等概念会在后文详细介绍

    详细说IO和网络通讯前,还需要先有一个文件描述符的概念,这是一个实际可以看到的东西,我们先用Linux和java来说下计算机如何运行程序的。后面再来看这个文件描述符是什么鬼

    文件描述符

    我们编写的代码是放在某一个输出输入设备当中,当程序被执行的时候,代码会被加载到主存中,最终代码变成计算机指令,再由计算机控制完成计算等等其他操作。即程序的运行是由内存和计算机控制完成运行的。最终程序执行是对系统内核的调用,系统内核再对硬件进行调用。

    以java代码来讲,java文件编译后是class字节码文件,字节码文件也是普通的文本文件,只不过里面是字节,存储序列化的东西,这个字节码会运行在C语言写的JVM虚拟机或者其他java进程里面,JVM或java进程会把字节码解析成系统指令再去调用系统的API完成java程序要做的事。(正是因为不同的JVM虚拟机可以把java代码解析成对应的系统指令,所以java才可以跨平台)

    说完这些之后我们来看看如何调用Linux系统内核的API,在Linux系统中,有一个思想就是“一切皆文件”,包括输入输出设备都被看做文件,比如把打印机抽象为文件,那么我们向打印机文件写入的话,就会把我们写入的打印出来。那么如何去控制这个打印机这个文件帮我们输出打印呢?其实就是怎么操作系统提供的API的事。

    在Java中可以通过调用对象去做事情。在Linux当中没有对象的概念,而是都抽象为文件了,那么我们对想操作的设备,或是想操作的连接都变成了一个占位符,其实就是个数字,它的专业名词就叫做“文件描述符”,就像java中对象的引用变量一样。内核正是利用文件描述符来访问文件,打开或者新建文件时,内核会返回一个文件描述符,读写文件也需要使用文件描述符来指定待读写的文件。

    百度百科:文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统而windows为句柄的概念

    如上介绍可能还是有些抽象,后面会实际看到这个东西,并操作它,然后我们再铺垫个东西,学一下怎么查看linux系统内核提供的API文档,方便后面学习

    Linux API文档

    在Linux系统当中可以通过安装 帮助程序 和 帮助页 插件查看一些系统内核API或命令

    yun install man man-pages
    

    比如我们查看怎么读取一个文件,读就是read,2就代表第二类,即我们查询的是操作系统内核对程序的系统调用

    man 2 read
    

    read
    因为Linux是C语言写的,这里可以看到调用和java类似,有参数,fd这个数值类型的参数就是file descriptor,也就是文件操作符,这个操作符怎么得到呢?前面也提到过打开或者新建文件时,内核会返回一个文件描述符

    比如说read之前需要先打开一个文件

    man 2 open
    

    在这里插入图片描述
    在这里插入图片描述
    我们可以看到传入一个文件名称,返回的就是一个file descriptor 文件操作符

    除了open以外还有socket也能得到文件操作符

    man 2 socket
    

    在这里插入图片描述
    在这里插入图片描述

    结尾

    到现在就有一个IO的基本概念了,可以把文档插件装上,试一下,后面还会一直用到,下章使用文件描述符模拟浏览器获取网页,带大家感受通信,可以关注我的公众号,里面有完整系列的文章,回复关键字“资源”,可以免费领取架构师、大数据、AI等课程
    在这里插入图片描述

    原文链接:https://mp.weixin.qq.com/s?__biz=MzI5MDk1Mzc3MQ==&mid=100000012&idx=1&sn=cf49e575b6305d37ff541018d181b674&chksm=6c194e485b6ec75e6834b6a71d34d23896b8a6bbffb4d70a1519b656cf3d7cb34b06899a94fb&scene=25#wechat_redirect

    展开全文
    qq_40373699 2020-06-15 20:52:23
  • 3星
    2KB ganzhihong3 2013-03-23 11:03:16
  • 52KB viola110 2019-03-20 09:27:04
  • 2.16MB tr_ainiyangyang 2018-07-26 09:27:32
  • 先来了解下什么是标准IO以及文件IO。 标准IO:标准I/O是ANSI C建立的一个标准I/O模型,是一个标准函数包stdio.h头文件中的定义,具有一定的可移植性。标准IO库处理很多细节。例如缓存分配,以优化长度执行...

    一、先来了解下什么是文件I/O和标准I/O:

    文件I/O:文件I/O称之为不带缓存的IO(unbuffered I/O)。不带缓存指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与os绑定,特定于linix或unix平台。

    标准I/O:标准I/O是ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存。

    (1)全缓存:当填满标准I/O缓存后才进行实际的I/O操作。 
    (2)行缓存:当输入或输出中遇到新行符时,标准I/O库执行I/O操作。 
    (3)不带缓存:stderr就是了。

    二、二者的区别

          文件I/O 又称为低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。标准I/O被称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境中有标准I/O库,标准I/O就可以使用。(Linux 中使用的是GLIBC,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O)。

          通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准I/O可以看成是在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。

          文件I/O中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。而标准I/O中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。

    三、最后来看下他们使用的函数

     

    标准IO

    文件IO(低级IO)

    打开

    fopen,freopen,fdopen

    open

    关闭

    fclose

    close

    getc,fgetc,getchar
    fgets,gets
    fread

    read

    putc,fputc,putchar
    fputs,puts,
    fwrite

    write

    1.fopen与open

    标准I/O使用fopen函数打开一个文件:

    FILE* fp=fopen(const char* path,const char *mod)

    其中path是文件名,mod用于指定文件打开的模式的字符串,比如"r","w","w+","a"等等,可以加上字母b用以指定以二进制模式打开(对于 *nix系统,只有一种文件类型,因此没有区别),如果成功打开,返回一个FILE文件指针,如果失败返回NULL,这里的文件指针并不是指向实际的文 件,而是一个关于文件信息的数据包,其中包括文件使用的缓冲区信息。

    文件IO使用open函数用于打开一个文件:

    int fd=open(char *name,int how);

    与fopen类似,name表示文件名字符串,而how指定打开的模式:O_RDONLY(只读),O_WRONLY(只写),O_RDWR (可读可写),还有其他模式请man 2 open。成功返回一个正整数称为文件描述符,这与标准I/O显著不同,失败的话返回-1,与标准I/O返回NULL也是不同的。

    2.fclose与close

    与打开文件相对的,标准I/O使用fclose关闭文件,将文件指针传入即可,如果成功关闭,返回0,否则返回EOF
    比如:

    if(fclose(fp)!=0)  
                printf("Error in closing file");

    而文件IO使用close用于关闭open打开的文件,与fclose类似,只不过当错误发生时返回的是-1,而不是EOF,成功关闭同样是返回0。C语言用error code来进行错误处理的传统做法。

    3. 读文件getc,fscanf,fgets和read

    标 准I/O中进行文件读取可以使用getc,一个字符一个字符的读取,也可以使用gets(读取标准io读入的)、fgets以字符串单位进行读取(读到遇 到的第一个换行字符的后面),gets(接受一个参数,文件指针)不判断目标数组是否能够容纳读入的字符,可能导致存储溢出(不建议使用),而fgets使用三个参数:
    char * fgets(char *s, int size, FILE *stream);

    第一个参数和gets一样,用于存储输入的地址,第二个参数为整数,表示输入字符串的最大长度,最后一个参数就是文件指针,指向要读取的文件。最 后是fscanf,与scanf类似,只不过增加了一个参数用于指定操作的文件,比如fscanf(fp,"%s",words)
    文件IO中使用read函数用于读取open函数打开的文件,函数原型如下:

    ssize_t numread=read(int fd,void *buf,size_t qty);

    其中fd就是open返回的文件描述符,buf用于存储数据的目的缓冲区,而qty指定要读取的字节数。如果成功读取,就返回读取的字节数目(小于等于qty)

    4. 判断文件结尾

    如果尝试读取达到文件结尾,标准IO的getc会返回特殊值EOF,而fgets碰到EOF会返回NULL,而对于*nix的read函数,情况有所不 同。read读取qty指定的字节数,最终读取的数据可能没有你所要求的那么多(qty),而当读到结尾再要读的话,read函数将返回0.

    5. 写文件putc,fputs,fprintf和write

    与读文件相对应的,标准C语言I/O使用putc写入字符,比如:

    putc(ch,fp);

    第一个参数是字符,第二个是文件指针。而fputs与此类似:

    fputs(buf,fp);

    仅仅是第一个参数换成了字符串地址。而fprintf与printf类似,增加了一个参数用于指定写入的文件,比如:

    fprintf(stdout,"Hello %s.\n","dennis");

    切记fscanf和fprintf将FILE指针作为第一个参数,而putc,fputs则是作为第二个参数。

    在文件IO中提供write函数用于写入文件,原型与read类似:

    ssize_t result=write(int fd,void *buf ,size_t amt);

    fd是文件描述符,buf是将要写入的内存数据,amt是要写的字节数。如果写入成功返回写入的字节数,通过result与amt的比较可以判断是否写入正常,如果写入失败返回-1

    6. 随机存取fseek()、ftell()和lseek()

    标准I/O使用fseek和ftell用于文件的随机存取,先看看fseek函数原型

    int fseek(FILE *stream, long offset, int whence);

    第一个参数是文件指针,第二个参数是一个long类型的偏移量(offset),表示从起始点开始移动的距离。第三个参数就是用于指定起始点的模式,stdio.h指定了下列模式常量:

    SEEK_SET            文件开始处 
    SEEK_CUR            当前位置 
    SEEK_END            文件结尾处

    看几个调用例子: 
     fseek(fp,0L,SEEK_SET); //找到文件的开始处 
     fseek(fp,0L,SEEK_END); //定位到文件结尾处 
     fseek(fp,2L,SEEK_CUR); //文件当前位置向前移动2个字节数

    而ftell函数用于返回文件的当前位置,返回类型是一个long类型,比如下面的调用:

    fseek(fp,0L,SEEK_END);//定位到结尾 
            long last=ftell(fp); //返回当前位置

    那么此时的last就是文件指针fp指向的文件的字节数。

    与标准I/O类似,*nix系统提供了lseek来完成fseek的功能,原型如下:

    off_t lseek(int fildes, off_t offset, int whence);

    fildes是文件描述符,而offset也是偏移量,whence同样是指定起始点模式,唯一的不同是lseek有返回值,如果成功就 返回指针变化前的位置,否则返回-1。whence的取值与fseek相同:SEEK_SET,SEEK_CUR,SEEK_END,但也可以用整数 0,1,2相应代替。

     

    四、系统调用与库函数

             上面我们一直在讨论文件I/O与标准I/O的区别,其实可以这样说,文件I/O是系统调用、标准I/O是库函数,看下面这张图:

     

    POSIX:Portable Operating System Interface  可移植操作系统接口

    ANSI:American National Standrads Institute  美国国家标准学会

    1、系统调用

           操作系统负责管理和分配所有的计算机资源。为了更好地服务于应用程序,操作系统提供了一组特殊接口——系统调用。通过这组接口用户程序可以使用操作系统内核提供的各种功能。例如分配内存、创建进程、实现进程之间的通信等。

           为什么不允许程序直接访问计算机资源?答案是不安全。单片机开发中,由于不需要操作系统,所以开发人员可以编写代码直接访问硬件。而在32位嵌入式系统中通常都要运行操作系统,所以开发人员可以编写代码直接访问硬件。而在32位嵌入式系统中通常都要运行操作系统,程序访问资源的方式都发生了改变。操作系统基本上都支持多任务,即同时可以运行多个程序。如果允许程序直接访问系统资源,肯定会带来很多问题。因此,所有软硬件资源的管理和分配都有操作系统负责。程序要获取资源(如分配内存,读写串口)必须由操作系统来完成,即用户程序向操作系统发出服务请求,操作系统收到请求后执行相关的代码来处理。

           用户程序向操作系统提出请求的接口就是系统调用。所有的操作系统都会提供系统调用接口,只不过不同的操作系统提供的系统调用接口各不相同。Linux 系统调用接口非常精简,它继承了Unix 系统调用中最基本的和最有用的部分。这些系统调用按照功能大致可分为进程控制、进程间通信、文件系统控制、存储管理、网络管理、套接字控制、用户管理等几类。

    2、库函数

          库函数可以说是对系统调用的一种封装,因为系统调用是面对的是操作系统,系统包括Linux、Windows等,如果直接系统调用,会影响程序的移植性,所以这里使用了库函数,比如说C库,这样只要系统中安装了C库,就都可以使用这些函数,比如printf()  scanf()等,C库相当于对系统函数进行了翻译,使我们的APP可以调用这些函数;

    3、用户编程接口API

         前面提到利用系统调用接口程序可以访问各种资源,但在实际开发中程序并不直接使用系统调用接口,而是使用用户编程接口(API)。为什么不直接使用系统调用接口呢?

    原因如下:

    1)系统调用接口功能非常简单,无法满足程序的需求。

    2)不同操作系统的系统调用接口不兼容,程序移植时工作量大。

        用户编程接口通俗的解释就是各种库(最重要的就是C库)中的函数。为了提高开发效率,C库中实现了很多函数。这些函数实现了常用的功能,供程序员调用。这样一来,程序员不需要自己编写这些代码,直接调用库函数就可以实现基本功能,提高了代码的复用率。使用用户编程接口还有一个好处:程序具有良好的可移植性。几乎所有的操作系统上都实现了C库,所以程序通常只需要重新编译一下就可以在其他操作系统下运行。

        用户编程接口(API)在实现时,通常都要依赖系统调用接口。例如,创建进程的API函数fork()对应于内核空间的sys_fork()系统调用。很多API函数西亚我哦通过多个系统调用来完成其功能。还有一些API函数不要调用任何系统调用。

         在Linux 中用户编程接口(API)遵循了在Unix中最流行的应用编程界面标准——POSIX标准。POSIX标准是由IEEE和ISO/IEC共同开发的标准系统。该标准基于当时想用的Unix 实践和经验,描述了操作系统的系统调用编程接口(实际上就是API),用于保证应用程序可以在源代码一级商多种操作系统上运行。这些系统调用编程接口主要是通过C库(libc )实现的。


    展开全文
    big_bit 2016-07-01 21:44:17
  • 18KB weixin_26900991 2021-06-12 03:42:53
  • 311KB weixin_38723373 2020-09-21 13:20:51
  • 1. 缓存IO 缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。 读操作:操作系统...

    1. 缓存IO

     

           缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。

           读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。

           写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。

           缓存I/O的优点:1)在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;2)可以减少读盘的次数,从而提高性能

           缓存I/O的缺点:在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。

     

     

    2. 直接IO

     

           直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。

           直接IO的缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓存。通常直接IO与异步IO结合使用,会得到比较好的性能。(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待)

    下图分析了写场景下的DirectIO和BufferIO:

     

     首先,磁盘IO主要的延时是由(以15000rpm硬盘为例): 机械转动延时(机械磁盘的主要性能瓶颈,平均为2ms) + 寻址延时(2~3ms) + 块传输延时(一般4k每块,40m/s的传输速度,延时一般为0.1ms) 决定。(平均为5ms)

    而网络IO主要延时由: 服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时 决定。(一般为几十到几千毫秒,受环境干扰极大)

    所以两者一般来说网络IO延时要大于磁盘IO的延时。

    用Redis作缓存是因为,Redis就是设计来做缓存的阿。

    Reids作缓存的几大优势:

    1, 简单的K-V式数据存储方式,单一的 get set 模式比传统SQL性能提升显著

    2, 纯in mem db 形式,将数据缓存在内存中,减少服务器磁盘IO时间。

    更新一下数据源:

    ref :

    《大型网站技术架构:核心原理与案例分析》 

     

    作者:李晨曦
    链接:https://www.zhihu.com/question/47589908/answer/114768530
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    Google的Jeff Dean给的一些数据(一个talk的ppt, "Designs, Lessons and Advice from Building Large Distributed Systems" 23页),可以看到1Gbps的网络比硬盘的bandwidth高了很多,记住这些数据对设计高性能系统和对系统的性能估算很有帮助。

    L1 cache reference 0.5 ns

    Branch mispredict 5 ns

    L2 cache reference 7 ns

    Mutex lock/unlock 25 ns

    Main memory reference 100 ns

    Compress 1K bytes with Zippy 3,000 ns

    Send 2K bytes over 1 Gbps network 20,000 ns

    Read 1 MB sequentially from memory 250,000 ns

    Round trip within same datacenter 500,000 ns

    Disk seek 10,000,000 ns

    Read 1 MB sequentially from disk 20,000,000 ns

    Send packet CA->Netherlands->CA 150,000,000 ns
     
     

    PIO与DMA

    有必要简单地说说慢速I/O设备和内存之间的数据传输方式。

    • PIO
      我们拿磁盘来说,很早以前,磁盘和内存之间的数据传输是需要CPU控制的,也就是说如果我们读取磁盘文件到内存中,数据要经过CPU存储转发,这种方式称为PIO。显然这种方式非常不合理,需要占用大量的CPU时间来读取文件,造成文件访问时系统几乎停止响应。

    • DMA
      后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接进行磁盘和内存的数据交换。在DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传送即可,DMA控制器通过系统总线来传输数据,传送完毕再通知CPU,这样就在很大程度上降低了CPU占有率,大大节省了系统资源,而它的传输速度与PIO的差异其实并不十分明显,因为这主要取决于慢速设备的速度。

    可以肯定的是,PIO模式的计算机我们现在已经很少见到了。

    标准文件访问方式

    图片描述

    具体步骤:

    当应用程序调用read接口时,操作系统检查在内核的高速缓存有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回,如果没有,则从磁盘中读取,然后缓存在操作系统的缓存中。

    应用程序调用write接口时,将数据从用户地址空间复制到内核地址空间的缓存中,这时对用户程序来说,写操作已经完成,至于什么时候再写到磁盘中,由操作系统决定,除非显示调用了sync同步命令。
    图片描述

    内存映射(减少数据在用户空间和内核空间之间的拷贝操作,适合大量数据传输)

    Linux内核提供一种访问磁盘文件的特殊方式,它可以将内存中某块地址空间和我们要指定的磁盘文件相关联,从而把我们对这块内存的访问转换为对磁盘文件的访问,这种技术称为内存映射(Memory Mapping)。

    操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的

    内存映射是指将硬盘上文件的位置与进程逻辑地址空间中一块大小相同的区域一一对应,当要访问内存中一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。

    使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用

    图片描述

    访问步骤

    图片描述

    在大多数情况下,使用内存映射可以提高磁盘I/O的性能,它无须使用read()或write()等系统调用来访问文件,而是通过mmap()系统调用来建立内存和磁盘文件的关联,然后像访问内存一样自由地访问文件。
    有两种类型的内存映射,共享型和私有型,前者可以将任何对内存的写操作都同步到磁盘文件,而且所有映射同一个文件的进程都共享任意一个进程对映射内存的修改;后者映射的文件只能是只读文件,所以不可以将对内存的写同步到文件,而且多个进程不共享修改。显然,共享型内存映射的效率偏低,因为如果一个文件被很多进程映射,那么每次的修改同步将花费一定的开销。

    直接I/O(绕过内核缓冲区,自己管理I/O缓存区)

    在Linux 2.6中,内存映射和直接访问文件没有本质上差异,因为数据从进程用户态内存空间到磁盘都要经过两次复制,即在磁盘与内核缓冲区之间以及在内核缓冲区与用户态内存空间。
    引入内核缓冲区的目的在于提高磁盘文件的访问性能,因为当进程需要读取磁盘文件时,如果文件内容已经在内核缓冲区中,那么就不需要再次访问磁盘;而当进程需要向文件中写入数据时,实际上只是写到了内核缓冲区便告诉进程已经写成功,而真正写入磁盘是通过一定的策略进行延迟的。

    然而,对于一些较复杂的应用,比如数据库服务器,它们为了充分提高性能,希望绕过内核缓冲区,由自己在用户态空间实现并管理I/O缓冲区,包括缓存机制和写延迟机制等,以支持独特的查询机制,比如数据库可以根据更加合理的策略来提高查询缓存命中率。另一方面,绕过内核缓冲区也可以减少系统内存的开销,因为内核缓冲区本身就在使用系统内存。

    应用程序直接访问磁盘数据,不经过操作系统内核数据缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。这种方式通常是在对数据的缓存管理由应用程序实现的数据库管理系统中。
    直接I/O的缺点就是如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘进行加载,这种直接加载会非常缓慢。通常直接I/O跟异步I/O结合使用会得到较好的性能。

    图片描述

    访问步骤

    图片描述

    Linux提供了对这种需求的支持,即在open()系统调用中增加参数选项O_DIRECT,用它打开的文件便可以绕过内核缓冲区的直接访问,这样便有效避免了CPU和内存的多余时间开销

    顺便提一下,与O_DIRECT类似的一个选项是O_SYNC,后者只对写数据有效,它将写入内核缓冲区的数据立即写入磁盘,将机器故障时数据的丢失减少到最小,但是它仍然要经过内核缓冲区。

    sendfile/零拷贝(网络I/O,kafka用到此特性)

    普通的网络传输步骤如下:

    1)操作系统将数据从磁盘复制到操作系统内核的页缓存中
    2)应用将数据从内核缓存复制到应用的缓存中
    3)应用将数据写回内核的Socket缓存中
    4)操作系统将数据从Socket缓存区复制到网卡缓存,然后将其通过网络发出

    图片描述

    1、当调用read系统调用时,通过DMA(Direct Memory Access)将数据copy到内核模式
    2、然后由CPU控制将内核模式数据copy到用户模式下的 buffer中
    3、read调用完成后,write调用首先将用户模式下 buffer中的数据copy到内核模式下的socket buffer中
    4、最后通过DMA copy将内核模式下的socket buffer中的数据copy到网卡设备中传送。

    从上面的过程可以看出,数据白白从内核模式到用户模式走了一圈,浪费了两次copy,而这两次copy都是CPU copy,即占用CPU资源。

    sendfile

    图片描述

    通过sendfile传送文件只需要一次系统调用,当调用 sendfile时:
    1、首先通过DMA copy将数据从磁盘读取到kernel buffer中
    2、然后通过CPU copy将数据从kernel buffer copy到sokcet buffer中
    3、最终通过DMA copy将socket buffer中数据copy到网卡buffer中发送
    sendfile与read/write方式相比,少了 一次模式切换一次CPU copy。但是从上述过程中也可以发现从kernel buffer中将数据copy到socket buffer是没必要的。

    为此,Linux2.4内核对sendfile做了改进,下图所示
    图片描述
    改进后的处理过程如下:
    1、DMA copy将磁盘数据copy到kernel buffer中
    2、向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
    3、DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡上。
    经过上述过程,数据只经过了2次copy就从磁盘传送出去了。(事实上这个Zero copy是针对内核来讲的,数据在内核模式下是Zero-copy的)。
    当前许多高性能http server都引入了sendfile机制,如nginx,lighttpd等。

    FileChannel.transferTo(Java中的零拷贝)

    Java NIO中FileChannel.transferTo(long position, long count, WriteableByteChannel target)方法将当前通道中的数据传送到目标通道target中,在支持Zero-Copy的linux系统中,transferTo()的实现依赖于 sendfile()调用。

    图片描述

    传统方式对比零拷贝方式:

    图片描述

    整个数据通路涉及4次数据复制和2个系统调用,如果使用sendfile则可以避免多次数据复制,操作系统可以直接将数据从内核页缓存中复制到网卡缓存,这样可以大大加快整个过程的速度。

    大多数时候,我们都在向Web服务器请求静态文件,比如图片、样式表等,根据前面的介绍,我们知道在处理这些请求的过程中,磁盘文件的数据先要经过内核缓冲区,然后到达用户内存空间,因为是不需要任何处理的静态数据,所以它们又被送到网卡对应的内核缓冲区,接着再被送入网卡进行发送。

    数据从内核出去,绕了一圈,又回到内核,没有任何变化,看起来真是浪费时间。在Linux 2.4的内核中,尝试性地引入了一个称为khttpd的内核级Web服务器程序,它只处理静态文件的请求。引入它的目的便在于内核希望请求的处理尽量在内核完成,减少内核态的切换以及用户态数据复制的开销。

    同时,Linux通过系统调用将这种机制提供给了开发者,那就是sendfile()系统调用。它可以将磁盘文件的特定部分直接传送到代表客户端的socket描述符,加快了静态文件的请求速度,同时也减少了CPU和内存的开销。

    在OpenBSD和NetBSD中没有提供对sendfile的支持。通过strace的跟踪看到了Apache在处理151字节的小文件时,使用了mmap()系统调用来实现内存映射,但是在Apache处理较大文件的时候,内存映射会导致较大的内存开销,得不偿失,所以Apache使用了sendfile64()来传送文件,sendfile64()是sendfile()的扩展实现,它在Linux 2.4之后的版本中提供。

    这并不意味着sendfile在任何场景下都能发挥显著的作用。对于请求较小的静态文件,sendfile发挥的作用便显得不那么重要,通过压力测试,我们模拟100个并发用户请求151字节的静态文件,是否使用sendfile的吞吐率几乎是相同的,可见在处理小文件请求时,发送数据的环节在整个过程中所占时间的比例相比于大文件请求时要小很多,所以对于这部分的优化效果自然不十分明显


    转:https://www.cnblogs.com/sunsky303/p/8962628.html

    展开全文
    u010313909 2018-06-21 19:07:16
  • codingToMaster 2020-09-26 21:57:37
  • jjjndk1314 2018-12-13 14:50:50
  • weixin_39969060 2020-12-08 10:16:15
  • 103KB weixin_42155721 2021-02-02 21:15:23
  • lxwfly 2020-07-10 17:16:29
  • weixin_39701768 2021-02-28 10:42:09
  • wangweijundeqq 2019-03-14 11:15:05
  • 16KB weixin_44230419 2021-01-20 20:49:51
  • 64KB weixin_42181888 2021-05-23 20:59:36
  • q1449516487 2018-08-06 14:54:00
  • qq_38812184 2019-08-09 20:13:22
  • yaomingyang 2020-05-12 19:18:34
  • lexiaowu 2019-08-07 21:44:29
  • gelong_bokewang 2019-03-08 17:41:24
  • y277an 2019-08-02 08:00:00
  • plychoz 2017-08-17 18:13:54
  • a724888 2017-07-10 15:57:48
  • mingongge 2019-07-08 10:01:52
  • longgeqiaojie304 2019-09-13 11:13:20
  • BtB5e6Nsu1g511Eg5XEg 2018-10-30 19:05:23
  • weixin_42100694 2021-02-14 23:03:45

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 483,994
精华内容 193,597
关键字:

文件io和网络io的区别