什么是io?
在计算机系统中I/O就是输入(Input)和输出(Output)的意思,针对不同的操作对象,可以划分为磁盘I/O模型,网络I/O模型,内存映射I/O, Direct I/O、数据库I/O等,只要具有输入输出类型的交互系统都可以认为是I/O系统。
IO可以简单的认为就是“读写”。
在Linux系统中,计算机里面的程序(代码程序)都是作为文件存在硬盘里面的。开机之后,这些程序就会从硬盘加载到内存并经过处理,变成CPU可执行的格式,此时这些程序就变成了进程。
kernel(内核)是开机是第一个加载到内存的程序。其他的程序都是有内核帮忙加载进内存的。内核进程和其他进程都会按照时间片占有CPU并发的运行(后面说到内核就是说内核程序内核进程)。
内核kernel
内核会暴露一些系统调用system call(就是操作系统提供的函数),用户进程可以请求内核调用内核中的函数完成工作(用户进程本身无法调用系统调用,只有内核才可以执行系统调用,系统调用的代码保存在内核的内存中)。
系统调用的作用就是,让其他进程调用内核提供的接口函数,让内核程序帮这些进程去完成这个操作,而不用用户进程自己去做这些事。如果是用户进程自己去做这些事,就需要他们自己重新实现这些方法,很麻烦,所以内核提供的这些系统调用大大减少了用户进程的工作量。
为了防止其他程序知道内核所在的内存地址修改内核kernel中的指令,Linux提供了一种保护模式,内核进程是处于这种保护模式之下(这种保护模式的作用就是不让其他程序知道内核所在的内存地址,这样其他进程就无法访问内核和修改内核):当cpu执行内核进程中的指令时,内核可以访问其他进程的内存,但是cpu执行用户进程中的指令时,用户进程不能访问内核和其他用户进程的内存地址,只能访问自己这个进程的内存(也就是说,kernel可以访问和修改其他进程的内容,其他的进程只能访问和修改自己这个进程的内容)。
但是这个时候有点打脸,用户进程不能访问到内核又怎么调用到内核提供的系统调用(函数)呢?
这个时候就提出了系统中断的概念。
系统中断
所谓的中断其实就是告诉CPU停止运行当前的程序去做另一件事情,去做另一件事情有很多种情况
中断发生的情况有很多种
可能是时间片用完了,cpu就会接受到中断指令,于是cpu就停止当前程序的运行让下一个进程占用进行工作
也可能是程序中运行到sleep,yield这样的代码,运行到这样的代码也会发送中断指令给cpu,cpu就也会中断当前程序的运行,让其他进程运行
也可能是移动鼠标,鼠标这个硬件设备会发送一个中断指令给cpu,cpu就会中断当前进程的运行,然后发送一个io请求让鼠标移动。
中断是一个计算机指令,这个指令后面会跟一个数字,这些数字映射到一个存着回调函数的表中(这个表叫做中断向量表,是存在CPU的寄存器中的,只有当发起中断指令给cpu时,cpu才会往这个表里面查数字对应的回调函数),一个数字代表一个回调函数。所以中断指令后面跟着的数字代表cpu中断程序后,会做些什么操作,是去调度下一个程序还是去发起一个io请求之类的。
反正一句话:中断就是告诉cpu停下手里的工作去干另一件事,干完这件事之后你可以继续运行刚刚的程序,也可以去运行其他程序。
如果没有系统中断机制,那么cpu就会跑完一个程序再跑另一个程序或者干其他事情。那计算机就变成串行运行程序而不是并发运行。因此系统中断是计算机可以并发运行多个程序的关键。
现在回到其他进程怎么调用内核的系统调用。比如,我用python调用了一个write函数,这个write函数里面其实埋了一个中断指令(int 0x80,int就是中断指令,这是cpu才能识别的指令),当cpu运行到这个int 0x80的时候,就会找中断向量表对应0x80的回调函数并执行,回调函数会让cpu中断当前进程,并切换到内核进程(让内核进程占有cpu),内核再去调用系统调用中的write方法。
这个过程中,就由用户态的程序切换到了内核态,进入到了内核态就自然可以调内核中的函数了。不是用户进程去调内核的函数,而是切换到内核态后内核自己调自己进程的方法,是内核自己访问自己的内存。
io操作都会需要进行系统调用(调用内核提供的函数),所以io操作都需要系统中断(中断会使用户进程让出cpu),都得经过一个用户态内核态的切换。
所以io操作的成本比较高。
总结:系统调用需要进行系统中断,切换用户态和内核态,系统中断就要当前程序让出cpu停止工作,把cpu交给内核。
IO操作都需要内核执行系统调用。
用户空间和内核空间
系统分配给每个进程一个独立的、连续的、虚拟的内存空间,该大小一般是4G(是所有进程都放在这里面共用)。其中将高地址值的内存空间分配给内核使用,一般是1G,其他空间给用户程序使用(即所有进程共用这3G)。
linux下每个进程都被分配了用户空间和内核空间。
可以理解为用户空间和内核空间是存储在不同的内存空间中。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。也就是说,进程的切换是由内核控制的。
为什么要划分用户空间和内核空间?为了保证用户进程不能直接操作内核,保证内核的安全
用户态切换内核态的过程:(用户态与内核态切换本质是CPU在用户空间的内存和内核空间内存的切换)
Linux创建进程的时候,会给该进程分配两块空间:用户空间(用户栈)和内核空间(内核栈)。PCB进程控制块中保存着该进程的用户栈空间的地址和内核空间的地址。
CPU的寄存器中存储着当前用户程序的运行信息和上下文以及用户栈的地址。当cpu从用户态切换到内核态的时候(比如因为硬中断,硬件设备向CPU发起IRQ),首先会将用户程序的运行 信息和上下文存到PCB的进程描述符(有点类似与游戏存档),然后cpu寄存器记录的堆栈地址从用户堆栈的地址指向为内核堆栈的地址(这就是用户空间切换到内核空间),CPU查询中断向量表在内核中找到对应的中断处理程序,并加载到CPU的寄存器,然后执行中断处理程序。
此时可以说,CPU被内核进程给占用。
上面就是用户态切换为内核态的过程。
内核态切换为用户态就是一个反过来的过程。
什么情况下会进行用户态切换内核态:
1.系统调用(用户程序自己发起中断,软中断)
2.外部设备发起中断请求(硬中断)
3.用户程序异常
用户态切换内核态与进程间切换的区别
CPU在两个进程间的切换本质上是CPU在两块PCB内存间的切换,CPU会从读取某块PCB切换为读取另一块PCB的数据,然后进行运算。
而用户态切换到内核态是CPU从用户空间这块内存切换到内核空间这块内存。
所以二者都是CPU在不同内存间的切换。二者都需要进行用户程序的中断和上下文的保存。所以二者的耗时和成本基本相当。
系统中断的过程和分类
系统中断分为两种:硬中断和软中断
硬中断是由计算机硬件发起的中断,如网卡,鼠标,键盘和打印机等。硬中断可能发生在任何时期。
以网卡为例:当网卡接收到一个网络报文,报文由网卡的DMA(直接存储器访问)写入到内存(网卡缓冲区),网卡再向CPU发起一个中断请求(IRQ,interrupt request),CPU收到中断信号会停下当前用户进程的运行,做好上下文环境的保存(保存到PCB的进程描述符中)。之后CPU从用户态切换到内核态(CPU所保存的堆栈地址从用户空间切换到内核空间的堆栈地址),执行网卡的中断程序。之后会切换回进程的用户态,CPU从进程描述符中读取上下文继续工作。
软中断是正在运行的用户态进程产生的,最常见的软中断就是用户程序要进行IO操作的时候,此时用户进程的上下文环境会从CPU的寄存器写入到内存中(写入到进程的进程描述符中,不是写入到用户空间中)以保存上下文。之后CPU由用户态切换到内核态进行系统调用,再之后CPU会切换会刚才的用户态,加载上下文环境到CPU的寄存器中,然后继续用户进程的运行。除了io操作外,像sleep,yield代码也会产生软中断使当前进程让出CPU。
无论是硬中断还是软中断,每种系统中断都由各自不同的中断处理程序(即中断之后要执行的函数,要做的事情),例如系统调用他的中断处理程序就是 0x80 。像网卡,鼠标,键盘,硬盘都有它对应的中断处理程序。
这些中断处理程序的编号会以数字的形式存在CPU的中断向量表中。而中断处理程序的内容存在内核中,CPU会拿着这个编号去找内核中对应的中断处理程序来执行。
所以无论是硬中断还是软中断的io操作都需要进行用户态切换到内核态,因为要执行中断处理程序。
无论是磁盘IO还是网络IO,数据都要在内核空间的内存和用户空间内存之间拷贝传输。
以磁盘IO写入磁盘文件为例,数据不会直接从用户进程(用户空间)的内存直接写入磁盘,而是会先把数据从用户空间的内存拷贝到内核空间的缓冲区,再从内核缓冲区写入到磁盘。 而数据从内核缓冲区写入到磁盘的过程与用户进程是异步发生的,也就是说这个过程中用户进程完全可以干自己的事情而不用等待内核刷盘。
网络IO同理,无论是读还是写,数据也都会经过内核的缓冲区。
那么综合以上的所有概念,我们简单的描述进程进行IO写操作的整体过程:
1.用户进程发起系统中断指令给CPU,用户进程暂停运行(即将让出CPU)
2.CPU根据系统中断指令查询中断向量表找到对应的系统调用
3.CPU保存好用户进程的上下文,从用户态切换到内核态(CPU的堆栈指针从指向用户空间的内存地址变为指向内核空间的内存地址)
4.数据从用户空间的内存拷贝到内核空间的内存(缓冲区)
5.内核执行相应的系统调用将数据从内核缓冲区写入磁盘文件(磁盘IO)或者发送给网络对端(网络IO)
本文转载自: 张柏沛IT技术博客 > 从IO模型到协程(一) 什么是IO,用户进程与内核