IO多路复用底层原理及源码解析

基本概念

1. 关于linux文件描述符

在Linux中,一切都是文件,除了文本文件、源文件、二进制文件等,一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件。例如,stdin 称为标准输入文件,它对应的硬件设备一般是键盘,stdout 称为标准输出文件,它对应的硬件设备一般是显示器。对于所有的文件,都可以使用 read() 函数读取数据,使用 write() 函数写入数据。

“一切都是文件”的思想极大地简化了程序员的理解和操作,使得对硬件设备的处理就像普通文件一样。所有在Linux中创建的文件都有一个 int 类型的编号,称为文件描述符(File Descriptor)。使用文件时,只要知道文件描述符就可以。例如,stdin 的描述符为 0,stdout 的描述符为 1。

在Linux中,socket 也被认为是文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件 I/O 相关的函数。可以认为,两台计算机之间的通信,实际上是两个 socket 文件的相互读写。

文件描述符有时也被称为文件句柄(File Handle),但“句柄”主要是 Windows 中术语,所以本教程中如果涉及到 Windows 平台将使用“句柄”,如果涉及到 Linux 平台将使用“描述符”。

2. Socket

socket IP+Port

一台计算机(一个IP地址)最多65535个port

socket是两端的 是四元组 只要满足可以唯一确认,区分每一个socket对应关系即可

ip:port + ip:port

网络上两个程序通过一个双向的通信连接实现数据的交换 这个连接的一端称为一个socket。

本质是一个API

一个Socket有两个内核缓冲区和一个等待队列

{% asset_img image-20210208212926670.png 发送与接收 %}

Socket的真正实例在内核中

可以在服务端的内核获取客户端Socket 比如在select函数中 在java socket Demo中

都是在服务端获取到客户端的socket 通过这些socket与客户端的socket(这些socket某方面就成了服务端的socket) 进行服务端与客户端的通信

客户端发送报文到服务端 服务端网卡发出中断请求 执行中断处理程序 解析出报文 报文中有ip:port 找到对应socket 将数据发送到对应socket的输入缓冲区 然后进程读取socket的输入缓存区即可。

更多见unix网络编程

3. I/O

I/O 输入/输出 重要

两种:

  • 文件系统IO
  • 网卡Socket IO 用于网络通信 在这个上面有IO模型 IO模型向上延伸支持编程模型

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

缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

IO模型前置知识是计算机组成 中断等

4. 用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

5. 进程切换

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

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  2. 更新PCB信息。
  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  4. 选择另一个进程执行,并更新其PCB。
  5. 更新内存管理的数据结构。
  6. 恢复处理机上下文。

注:总而言之就是很耗资源,具体的可以参考这篇文章:进程切换

6. 进程阻塞

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

7. DMA设备

DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于[ CPU ](https://baike.baidu.com/item/ CPU /120556)的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

在本篇文章中 DMA设备将网卡收到的报文放到内存的网卡缓冲区中

补充文章/视频

  1. 不可不知的socket和TCP连接过程

  2. 【动画】当我们在读写Socket时,我们究竟在读写什么? https://juejin.cn/post/6844903629233766414

  3. Linux IO模式及 select、poll、epoll详解https://segmentfault.com/a/1190000003063859

    基本概念中有一些摘自这篇文章 建议阅读这篇文章

  4. 网络包的流转 https://plantegg.github.io/2019/05/08/%E5%B0%B1%E6%98%AF%E8%A6%81%E4%BD%A0%E6%87%82%E7%BD%91%E7%BB%9C–%E7%BD%91%E7%BB%9C%E5%8C%85%E7%9A%84%E6%B5%81%E8%BD%AC/

  5. linux select等 视频补充:https://www.bilibili.com/video/BV1Lk4y117gC?from=search&seid=13832656714087589503

  6. 面试视频与记录:http://zhuuu.work/2020/08/17/Linux/Linux-06-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/

    https://www.bilibili.com/video/BV12i4y1G7UK/?spm_id_from=333.788.videocard.5

  7. Linux TCP/IP协议栈 数据发送接收流程:https://network.51cto.com/art/201909/603780.htm

    https://segmentfault.com/a/1190000008836467

    https://segmentfault.com/a/1190000008926093

    linux网络之数据包的接受过程:https://www.jianshu.com/p/e6162bc984c8

  8. Unix网络编程 第六章

基础知识点1:Linux操作系统中断

1.1 什么是系统中断?(软中断/硬中断)

一个小故事讲中断请求和中断处理程序 中断流程

你正在打游戏 饿了 要吃东西 要点外卖(吃东西任务还处于休眠状态) 正在打BOSS 快打死了 此时外卖小哥敲门(中断请求IRQ) 需要去拿外卖 (CPU收到IRQ要求执行中断处理程序 先讲当前任务存档)

拿外卖 放到桌子上 中断处理程序做的

然后中断处理程序将吃东西任务可运行起来 可以执行了

中断可以使CPU执行可运行的任务 不必一直等着不可运行的任务 可将不可运行的任务挂起

  1. 可将等待外部设备数据的任务/进程(吃东西的任务) 放到设备相关的等待队列中

  2. 当设备的数据来了后 硬件会给CPU发起中断请求(外卖小哥敲门)

  3. CPU收到中断请求后 把当前任务存档(打游戏存档) 立马执行中断处理程序 中断处理程序将数据放到相关的缓冲区中

  4. 然后将等待缓冲区数据的等待队列中的进程(吃东西) 转移到运行队列中 说明这个进程可以执行了

  5. 中断处理程序结束

  6. 恢复挂起之前的进程的状态(打游戏) 但此时CPU不止执行打游戏的任务 可以执行吃东西的任务了 可以边打游戏边吃东西

    {% asset_img image-20210205152933932.png 中断示例 %}

硬中断

硬件发起的中断 外部设备对CPU的中断 典型的异步中断 由外部设备产生的 可能发生在任意时间

例子:

计算机网卡设备接收到一组报文后 报文会被 DMA设备 转移到内存条的一块空间内(内存上的网卡缓冲区) 然后网卡会向CPU发生中断信号IRQ 若此时CPU正在执行进程C 那么需要将进程C的各种寄存器保存到 [^2]进程描述符 (一块引用进程空间的东西 保留用户态下CPU的状态 将CPU上的瞬时数据保存起来)当前进程用户态切换到内核态 因为接下来执行的程序(中断处理程序)与用户程序无关了 中断处理程序是内核的程序 需要调用内核

CPU收到中断信号后会执行网卡对应的中断处理程序

中断处理程序执行结束后 CPU从内核态切换回用户态(从进程C的进程描述符恢复CPU的现场 恢复寄存器中保存的数据等)进程C继续执行自己的代码段

用户态切换到内核态:

每个进程都有两块堆栈 一块堆栈是用户态堆栈 其中保留用户态代码下声明的变量 数据等

一块堆栈是内核态堆栈,程序可能需要 [^3] 系统调用 申请一些资源时 系统调用的程序也声明变量等数据 这些数据在内核态堆栈保留

异步中断大部分情况下与当前占用CPU的进程没有直接关系 更像是一种事件驱动模型 驱动关联的进程 进行下一步工作

软中断

CPU产生的 硬中断服务程序对内核的中断

例子:

CPU在执行一个代码段 当执行到一个1/0的错误代码 CPU检查到这行指令有问题后 会发起软中断 让当前进程从用户态切换到内核态 保留一些数据 应用程序恢复到用户态 检查某个寄存器的区域 说明产生错误 应用程序有机会改错误或者输出异常信息或直接结束

系统调用就是借助软中断完成的 即0x80中断(十六进制的80) 对应一种中断处理程序

不论是软中断还是硬中断 每一种中断都对应一种中断处理程序 都有一个系统编号

1.2 系统中断,内核会做什么?

见上面网卡的例子

1.3 硬件中断触发的过程?(ps:8259A芯片中断控制器的工作流程)

见图:

可编程中断控制器:https://www.processon.com/view/link/5f5b1d071e08531762cf00ff

图中CPU上的INTR中断引脚 接收中断信号的

CPU可接很多硬件 每个硬件一个引脚不现实 需要中断控制器

中断控制器连接硬件设备 每个硬件设备插在主板上 有一根电线连在中断控制器上

当电线上电流发生变化(见微机原理)则产生中断了 中断控制器会代理这个设备向CPU发出中断请求

8259A中断控制器:

默认有八个接口 但设备不止八个 可以用一个接口再连一个中断控制器 可以级联

{% asset_img image-20210210211413558.png 中断控制器级联 %}

中断控制器内部

中断请求寄存器 若某个设备发起中断请求 在中断请求寄存器中会保存此设备的请求信号

可以在其中放多个设备的请求信号

然后到优先级解析器 中断是有优先级的 一个CPU每次只能执行一个中断处理程序

优先级解析器根据设备编号IRQ 进行排序 IRQ值低的优先

正在服务寄存器

比如

  1. CPU正在处理键盘的中断请求 则正在服务寄存器中则保存IRQ1

  2. CPU处理完键盘的中断后 正在服务寄存器中的值会清空

  3. 中断控制器发现 正在服务寄存器 处于空闲状态 会从优先级解析器中优先级高的信号 给CPU CPU进行处理

  4. CPU通过数据总线将正在处理的信号写到正在服务寄存器中

图中的IRQ中断程序入口映射表 在内核中

操作系统启动系统时 就会将设备编号IRQ与中断处理程序入口绑定起来 因为在中断控制器中只能发送编号 CPU只能收到对应的编号 收到编号后要知道去调用哪一个中断处理程序

{% asset_img image-20210205181909024.png IRQ中断程序入口映射表 %}

基础知识2:Socket基础

2.1 JAVA SocketDemo

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
 
/**
 * 基于TCp的Socket通信,实现用户登录
 * 服务器端
 */
public class Server {
   
    public static void main(String[] args) {
   
 
        try  {
   
            //创建一个服务器socket,即serversocket,指定绑定的端口,并监听此端口
            ServerSocket serverSocket = new ServerSocket(8888);
            //调用accept()方法开始监听,等待客户端的连接
            System.out.println("***服务器即将启动,等待客户端的连接***");
            //得到连接上服务端的客户端的socket
            Socket socket = serverSocket.accept();
            //获取输入流,并读入客户端的信息 拿到客户端的输入流和输出流
            //输入流是客户端想要传递给服务端的数据 
            //输出流是交给服务器 服务器可以通过输出流把数据给客户端
            
            InputStream in = socket.getInputStream(); //字节输入流
            InputStreamReader inreader = new InputStreamReader(in); //把字节输入流转换为字符流
            BufferedReader br = new BufferedReader(inreader); //为输入流添加缓冲
            String info = null;
            while((info = br.readLine())!=null){
   
                System.out.println("我是服务器,客户端说:"+info);
    
            }
            socket.shutdownInput();//关闭输入流<

你可能感兴趣的:(Linux,计算机那些事儿,操作系统,linux,IO多路复用,NIO,epoll,内核)