操作系统概述

操作系统结构

操作系统核心的东西就是内核,我们比较常见的两款操作系统是Windows和Linux。

内核
计算机是由各种外部硬件设备组成的,比如内存、cpu、硬盘等,如果每个应用都要和这些硬件设备对接通信协议,那这样太累了,所以这个中间人就由内核来负责,让内核作为应用连接硬件设备的桥梁,应用程序只有关心与内核交互,不用关心硬件的细节。
image.png
现代操作系统,内核一般会提供4个基本能力:
1.管理进程、线程 决定哪个进程、线程使用CPU,也就是进程调度的能力;
2.管理内存 决定内存的分配和回收,也就是内存管理的能力;
3.管理硬件设备 为进程与硬件设备之间提供通信能力,也就是硬件通信能力;
4.提供系统调用 如果应用程序要运行更高权限的服务,那么需要有系统调用,它是用户程序与操作系统之间的接口。

内核具有很高的权限,可以控制CPU、内存、硬盘等硬件,而应用程序具有的权限很小,因此把操作系统分为两个区域:
1.内核空间 这个内存空间只有内核程序可以访问。
2.用户空间 这个内存空间专门给应用程序使用。
当程序使用用户空间时,我们常说该程序在用户态执行,而当程序使用内核空间时,程序则在内核态执行
Linux内核由如下几部分组成:内存管理、进程管理、设备驱动程序、文件系统和网络管理
image.png

计算机启动过程

在主板上,有一个东西叫ROM(Read Only Memory,只读存储器),上面早就固化了一些初始化的程序,也就是BIOS(Basic Input and Output System,基本输入输出系统)。然后操作系统会询问BIOS获取配置信息,然后把设备驱动程序加载到内存中,初始化表,创建所需的后台进程,并启动登录程序或GUI。

内存管理

虚拟内存

为什么要用虚拟内存?
直接操作内存的物理地址,想要同时在内存中运行两个程序是不可能的,如果第一个程序在2000的位置写入了一个新的值,将会抹除掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两个程序会立刻崩溃。
这里的关键是两个程序都引用了绝对物理地址
因此我们可以把进程所使用的地址隔离开,即让操作系统为每个进程分配独立的一套虚拟地址,每一个进程都有,而且都是同一套,各自玩各自的,互不干涉。但是有个前提是,每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的,操作系统已经把这些都安排好了。

操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。

于是,这里就引出了两种地址的概念:
我们程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)
操作系统引入了虚拟地址,进程持有的虚拟地址会通过CPU芯片中的内存管理单元(MMU)的映射关系来转换变成物理地址,然后再通过物理地址访问内存,如下图:
image.png

操作系统如何管理虚拟地址和物理地址之间的映射关系?
主要有两种方式,分别是内存分段和内存分页

内存分段

程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性,所以就用分段的形式把这些段分离出来。
在分段机制下的虚拟地址由两部分组成,段选择子段内偏移量

段选择子保存在段寄存器里,段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
虚拟地址中的段内偏移量应该位于0和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

例如:如果要访问段3中偏移量为500的虚拟地址,计算出的物理地址为,段3基地址7000+偏移量500=7500
image.png
内存分段解决了程序本身不需要关心具体的物理内存地址的问题,但也有一些不足之处:
1.内存碎片的问题 因为分段需要连续的内存空间,如果中间第一个100MB的程序关闭,释放掉了段1的内存,之后再关掉第二个50MB的程序,释放了段4的内存,这两个释放的内存起始地址不连续。另外再打开一个120MB的程序,那么就会运行失败,程序无法加载到内存
2.内存交换的效率低的问题 解决上述内存碎片的问题,就是内存交换,就是将占用的内存先写到硬盘上,然后再读回来,此时读回来的内存起始地址是与上一个内存末尾地址是连续的,要知道硬盘I/O速度是要比内存慢很多的,因此效率非常低

内存分页

解决了内存分段的内存碎片和内存交换效率低的问题。
本质就是把原来分段的粒度缩小到分页

分页就是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样连续并且尺寸固定的内存空间叫做(Page)。在Linux下,每一页的大小为4KB
虚拟地址与物理地址之间通过页表来映射,如下图:
image.png
采用了分页,那么释放的内存都是以页为单位释放的(页的整数倍),也就不会产生无法给进程使用的小内存

对于一个内存地址转换,需要三个步骤:
1.把虚拟内存地址,切分成页号和偏移量;
2.根据页号,从页表里面,查询对应的物理页号;
3.直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

简单分页

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25761560/1641007142882-dcc5f986-81a6-4e30-853e-bff5b4aa13b4.png#clientId=u737c1236-2500-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=507&id=u033e08f0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=787&originWidth=979&originalType=binary&ratio=1&rotation=0&showTitle=false&size=228280&status=done&style=none&taskId=u4d2d36bf-d3f3-4582-98bf-ffd34467fcc&title=&width=630.5)<br />简单分页有空间上的缺陷,在32位的环境下,虚拟地址空间共有4GB,假设一个页的大小是4KB(2^12),那么就需要大约100万(2^20)个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有4MB的内存来存储页表<br />那么,100个进程的话,就需要400MB的内存来存储页表,这是非常大的内存了。

多级页表

解决了上述简单分页页表占用内存过大的问题

  • 一级页表必须进行映射,由于出现了多级页表,一级页表的内容可以减少很多,只有4k
  • 多级页表只有在需要的时候,才会被创建

我的理解:类似mysql索引,2到3层就可以表示上千万条数据了,如果只用1层表示同样上千万条数据的话,内存占用太大,且必须是要占用的,虽然2到3层的索引占用的内存也很大,但是它是按需创建的,除了第一层必须占用内存外,其他的都是按需创建

      ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25761560/1641044349361-747a4767-6144-41bd-9339-a78fd3714aca.png#clientId=u69adfd9a-0e22-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=392&id=uafbe46b2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=529&originWidth=785&originalType=binary&ratio=1&rotation=0&showTitle=false&size=140769&status=done&style=none&taskId=u6e9a1d47-52d9-4dda-87a9-4407cd6b982&title=&width=581.5)

多级页表的问题在于,在查找物理地址的时候,需要一级一级查找,解决了空间上的问题,但是时间上会增加消耗,这个问题就需要缓存,把最常访问的页表项缓存到CPU里面。

页表缓存TLB

为了解决多级页表转换的速度
在CPU芯片中内置了TLB

Linux内存管理

image.png

程序所使用的地址,通常是没被段式内存管理映射的地址,称为逻辑地址
通过段式内存管理映射的地址,称为线性地址,也叫虚拟地址
逻辑地址是「段式内存管理」转换前的地址,线性地址则是「页式内存管理」转换前的地址。

在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:
image.png

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存;

虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
image.png

进程和线程

进程

运行中的程序,就被称为进程

我们把操作系统做某件事,抽象成一种概念,称之为一个任务。一个进程可以对应一个任务,也可以对应多个任务。

并行和并发
由于 CPU 速度非常快,这种多个任务不断切换,会给用户一种任务并行执行的错觉,这种也被称为是伪并行调度(并发)。既然有伪并行,那么也会有真并行。在现代计算机中,常见的CPU核数可以达到8核甚至更多,操作系统可将每一个核视为一个CPU,那么8核CPU就可以真并行执行8个任务。
image.png

进程的状态

五状态模型
image.png
如上图,进入就绪队列,其状态就会变为就绪态。各个状态之间的关系描述如下:
就绪—->运行:当操作系统内存在着调度程序,当需要运行一个新进程时,调度程序选择一个就绪态的进程,让其进入运行态。
运行—->就绪:运行态的进程,会占用CPU。每个进程会被分配一定的执行时间,当时间结束后,重新回到就绪状态。
运行—->阻塞:进程请求调用系统的某些服务,但是操作系统没法立即给它(比如这种服务可能要耗时初始化,比如I/O资源需要等待),那么它就会进入阻塞态。
阻塞—->就绪:当等待结束了,就由阻塞态进入了就绪态。
运行—->终止:当进程表示自己已经完成了,它会被操作系统终止。

当存在多个进程时,由于同一时间只能有一个进程在执行,那么如何去管理这一系列的处于阻塞态和就绪态的进程呢?一般来说,会使用就绪队列,和阻塞队列,让处于阻塞态和就绪态的进程进入队列,排队执行。

七状态模型
一旦排队的进程多了,对于有限的内存空间将会是极大的考验。为了解决内存占用问题,可以将一部分内存中的进程交换到磁盘中,这些被交换到磁盘的进程,会进入挂起状态
挂起状态可以分为两种:
1.阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
2.就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;
这两种挂起状态加上前面的五种状态,就变成了七种状态变迁,见如下图:
计算机操作系统 - 图11

进程的控制结构

操作系统中,是用进程控制块process control block,PCB)数据结构来描述进程的。
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
计算机操作系统 - 图12
每个 PCB 是如何组织的呢?
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:

  • 将所有处于就绪状态的进程链在一起,称为就绪队列
  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列

选择链表是因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。

进程的切换

当一个正在运行中的进程被中断,操作系统指定另一个就绪态的进程进入运行态,这个过程就是进程切换,也可以叫上下文切换。
切换过程一般涉及以下步骤:
1.保存处理器上下文环境:将CPU程序计数器和寄存器的值保存到当前进程的私有堆栈里
2.更新当前进程的PCB(包括状态更变)
3.将当前进程移到就绪队列或者阻塞队列
4.根据调度算法,选择就绪队列中一个合适的新进程,将其更改为运行态
5.更新内存管理的数据结构
6.新进程内对堆栈所保存的上下文信息载入到CPU的寄存器和程序计数器,占有CPU

发生进程上下文切换的场景?

  • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
  • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

进程调度

一旦操作系统把进程切换到运行状态,也就意味着该进程占用着 CPU 在执行,但是当操作系统把进程切换到其他状态时,那就不能在 CPU 中执行了,于是操作系统会选择下一个要运行的进程。
选择一个进程运行这一功能是在操作系统中完成的,通常称为调度程序scheduler)。
在进程的生命周期中,当进程从一个运行状态到另外一状态变化的时候,其实会触发一次调度。
把调度算法分为两类:

  • 非抢占式调度算法挑选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,也就是说不会理时钟中断这个事情。
  • 抢占式调度算法挑选一个进程,然后让该进程只运行某段时间,如果在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程。这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制

五种调度原则:

  • CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
  • 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
  • 周转时间:周转时间是进程运行和阻塞时间总和,一个进程的周转时间越小越好;
  • 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
  • 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。

说白了,这么多调度原则,目的就是要使得进程要「快」。

进程调度算法

先来先服务

先进就绪队列,则先被调度,先来先服务是最简单的调度算法。
当前面任务耗费很长时间执行,那么后面的任务即使只需要执行很短的时间,也必须一直等待。属于非抢占式

时间片轮转调度

每一个进程会被分配一个时间片,表示允许该进程在这个时间段运行,如果时间结束了,进程还没运行完毕,那么会通过抢占式调度,将CPU分配给其他进程,该进程回到就绪队列。由于进程的切换,需要耗费时间,如果时间片太短,频繁进行切换,会影响效率。如果进程时间片太长,有可能导致排后面的进程等待太长时间。因此时间片的长度,需要有大致合理的数值。(《现代操作系统》的观点是建议时间片长度在20ms~50ms)。
计算机操作系统 - 图13

最短作业优先

即进程按照作业时间长短排队,作业时间段的排前面先执行
计算机操作系统 - 图14
一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。

最短剩余时间优先

从就绪队列中选择剩余时间最短的进程进行调度。该算法可以理解最短作业优先和时间片轮转的结合。如果没有时间片,那么最短剩余时间其实就是最短作业时间,因为每个进程都是从头执行到尾。

优先级调度

即按照进程的优先级来调度
但是依然有缺点,可能会导致低优先级的进程永远不会运行。

多级反馈队列调度

多级反馈队列调度基于时间片轮转和优先级调度,设置多个就绪队列,赋予每个就绪队列优先级,优先级越高的队列进程的时间片越短
计算机操作系统 - 图15
可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。

进程间通信

每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
计算机操作系统 - 图16
进程间通信目的一般有共享数据,数据传输,消息通知,进程控制等。以 Unix/Linux 为例,介绍几种重要的进程间通信方式:共享内存,管道,消息队列,信号量,信号

管道

$ ps auxf | grep mysql
上面命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。
同时,我们得知上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁。
管道这种通信方式效率低,不适合进程间频繁地交换数据。当然,它的好处,自然就是简单,同时我们也很容易得知管道里的数据已经被另一个进程读取了。
对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。
计算机操作系统 - 图17

消息队列

管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
计算机操作系统 - 图18
缺点:
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销

共享内存

消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
计算机操作系统 - 图19

信号量

用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据

信号

信号一般用于一些异常情况下的进程间通信,是一种异步通信,它的数据结构一般就是一个数字

Socket

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。

实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

计算机操作系统 - 图20

线程

线程是进程当中的⼀条执⾏流程。
在早期的操作系统中都是以进程作为独⽴运⾏的基本单位,直到后⾯,计算机科学家们⼜提出了更⼩的能独⽴运⾏的基本单位,也就是线程

操作系统底层存在调度程序,调度程序可调度任务,而单线程进程,每个进程可以对应一个任务。现在,对于多线程的进程,每一个线程最终对于调度程序来说,都是一个任务,如下图(Linux系统)。因此也有一种流行的说法线程是CPU调度的基本单位计算机操作系统 - 图21

线程的上下文切换

线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位

  • 当进程只有一个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;

另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

线程上下文切换的是什么?
这还得看线程是不是属于同一个进程:

  • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

所以,线程的上下文切换相比进程,开销要小很多。

文件系统

内存只是暂时保存数据,想要持久保存数据,需要保存到硬盘上。数据是以文件的形式保存在硬盘上的,为了管理这些文件,需要考虑以下几点:
1.文件系统要有严格的组织形式,使得文件能够以块为单位进行存储。(类似于书架小格子)
2.文件系统中也要有索引区,用来方便查找一个文件分成的多个块都存放在了什么位置
计算机操作系统 - 图22
3.如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层
4.文件应该用文件夹的形式组织起来,方便管理和查询。(类似于分类)
5.Linux 内核要在自己的内存里面维护一套数据结构,来保存哪些文件被哪些进程打开和使用

文件系统的基本组成

一切皆文件

文件系统是操作系统中负责管理持久数据的子系统,说简单点,就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。
文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。
以liunx为例,最经典的一句话就是一切皆文件,不仅普通的文件和目录,就连块设备、管道、socket等,也都是统一交给文件系统管理的。
liunx系统会为每个文件分配两个数据结构:索引节点目录项。用来记录文件的元信息和目录层次结构。

1.索引节点,也就是 inode,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间
2.目录项,也就是 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存

索引节点唯一标识一个文件,而目录项纪录着文件名,所以目录项和索引节点的关系是多对一,一个文件可以有多个名字,如硬链接的实现就是多个目录项中的索引节点指向同一个文件。
注:目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录文件。

目录项和目录

虽然名字很相近,但是它们不是一个东西,目录是一个文件,持久化存储在磁盘,而目录项是内核的一个数据结构,缓存在内存。
如果查询目录频繁从硬盘中取,效率低,内核会把已经读过的目录,用目录项这个数据结构缓存在内存。
注:目录项这个数据结构不仅可以表示目录,也可以表示文件。

文件数据在磁盘的存储

磁盘最小的读写单位是扇区,扇区的大小只有512字节,如果每次读写都以这么小为单位,读写效率非常低。
文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),liunx中逻辑块的大小是4kb,就是一次性读写8个扇区,大大提高了磁盘的读写效率。
计算机操作系统 - 图23
索引节点是存储在硬盘上的数据,那么为了加速文件的访问,通常会把索引节点加载到内存中。
另外,磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。

  • 超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。
  • 索引节点区,用来存储索引节点;
  • 数据块区,用来存储文件或目录数据;

我们不可能把超级块和索引节点区全部加载到内存,这样内存肯定撑不住,所以只有当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同的:

  • 超级块:当文件系统挂载时进入内存;
  • 索引节点区:当文件被访问时进入内存;

设备管理

设备控制器

我们的电脑设备可以接非常多的输入输出设备,比如键盘、鼠标、显示器、网卡、硬盘、打印机、音响等等,每个设备的用法和功能都不同,那操作系统是如何把这些输入输出设备统一管理的呢?
为了屏蔽设备之间的差异,每个设备都有一个叫设备控制器(Device Control 的组件,比如硬盘有硬盘控制器、显示器有视频控制器等。
计算机操作系统 - 图24
设备控制器直接与硬件设备交互,CPU直接与设备控制器交互。

控制器有三类寄存器,它们分别是状态寄存器(Status Register命令寄存器(Command Register以及数据寄存器(Data Register
数据寄存器:CPU向I/O设备写入传输的数据。
命令寄存器:CPU发送一个指令,告诉I/O设备进行什么操作,完成后,会把状态寄存器里面的状态标记成完成。
状态寄存器:告诉CPU,现在已经工作还是已经完成,如果是在工作状态,则CPU再发送命令过来是没用的,直到工作已经完成,状态寄存器标记已完成,CPU才能发送下一个字符和命令。

另外, 输入输出设备可分为两大类 :块设备(Block Device字符设备(Character Device
块设备:把数据存储在固定大小的块中,每个块都有自己的地址,硬盘,usb等。通常传输的数据非常大,于是控制器设立了一个可读写的数据缓冲区。CPU写入数据到控制器的缓冲区时,当缓冲区数据有一部分后,才会发给设备,CPU从缓冲区读数据时,也需要缓冲区有一部分数据后,才拷贝到内存。这样可以减少I/O次数。
字符设备:以字符为单位发送或接受一个字符流,字符设备是不可寻址的,也没有任何寻道操作,鼠标。
端口I/O:每个控制寄存器被分配一个I/O端口,可以通过特殊的汇编指令操作这些寄存器,比如:in/out 类似指令。
内存映射I/O:将所有控制寄存器映射到内存空间,可以像读写内存一样读写数据缓冲区。

I/O控制方式

每种设备都有一个设备控制器,控制器相当于一个小型CPU,它可以自己处理一下事情,当CPU发送给设备一个指令,当设备完成之后如何通知CPU?
控制寄存器一般有一个状态标记位,用来标识输入或输出操作是否完成,第一种方式 轮询等待 方法,让CPU一只查询寄存器状态,直到完成,这种方式非常占用CPU时间。
第二种方式 中断,设备完成之后会由中断控制器通知CPU,一个中断产生,CPU会暂停当前工作,处理中断。中断分为两种,软中断 通过代码 INT 指令实现,硬中断 通过中断控制器触发。
但中断方式并不友好,频繁读写磁盘,CPU会被经常打断,对于这种情况,使用 DMA 功能解决,它可以在设备在CPU不参与的情况下,自行把设备I/O数据存放到内存,需要 DMA控制器 硬件支持。
计算机操作系统 - 图25

CPU对DMA控制器发送指令,告诉它读多少数据,读完放到内存什么位置。
DMA会把磁盘控制器的数据放入缓冲区,再放入内存,完成后DMA发送中断通知CPU。

设备驱动程序

虽然设备控制器屏蔽了设备的众多细节,但每种设备的控制器的寄存器、缓冲区等使用模式都是不同的,所以为了屏蔽设备控制器间的差异,引入了设备驱动程序
计算机操作系统 - 图26
设备控制器属于硬件,设备驱动程序属于操作系统。设备驱动程序提供统一的接口给操作系统,例如中断,CPU会处理设备驱动程序的中断函数,然后继续执行,设备驱动程序来和设备控制器交互。

通用块层

对于块设备,为了减少不同块设备的差异带来的影响,Linux 通过一个统一的通用块层,来管理不同的块设备。
通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层,它主要有两个功能:

  • 第一个功能,向上为文件系统和应用程序,提供访问块设备的标准接口,向下把各种不同的磁盘设备抽象为统一的块设备,并在内核层面,提供一个框架来管理这些设备的驱动程序;
  • 第二功能,通用层还会给文件系统和应用程序发来的 I/O 请求排队,接着会对队列重新排序、请求合并等方式,也就是 I/O 调度,主要目的是为了提高磁盘读写的效率。

键盘敲入字母时,期间发生了什么?

CPU 的硬件架构图:
image.png
当用户输入键盘字符,键盘控制器就会产生扫描码数据,并将其缓冲在键盘控制器的寄存器中,紧接着键盘控制器通过总线给 CPU 发送中断请求
CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下文,然后调用键盘的中断处理程序。中断处理程序是在键盘驱动程序中注册的,键盘的中断处理函数就是从键盘控制器的缓冲区中读取。显示出结果后,恢复被中断进程的上下文

网络系统

一台机器将自己想要表达的内容,按照某种约定好的格式发送出去,当另外一台机器收到这些信息后,也能够按照约定好的格式解析出来,从而准确、可靠地获得发送方想要表达的内容。这种约定好的格式就是网络协议(Networking Protocol)。

网络分层

以五层为例,应用层(http),传输层(tcp、udp),网络层(IP),数据链路层(MAC地址),物理层
应用层:http协议,Servlet处于这一层
传输层:TCP、UDP协议,丢包、乱序、重传、拥塞,处理这些问题。
网络层(IP层):由于网络环境非常复杂,例如一个ip地址:192.168.1.0/24,斜杠前面是IP地址,被点分为四个部分,每个部分8位,共32位,斜线后面24的意思是32位中,前24位是网络号,8位是主机号。
数据链路层(MAC层):每个网卡都有的唯一硬件地址(不是绝对唯一,相对大概率唯一)。
物理层: 电脑的网线。

为什么网络要分层呢?因为网络环境过于复杂,不是一个能够集中控制的体系。全球数以亿记的服务器和设备各有各的体系,但是都可以通过同一套网络协议栈通过切分成多个层次和组合,来满足不同服务器和设备的通信需求。

零拷贝

DMA 技术,也就是直接内存访问(Direct Memory Access 技术。
在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。

传统的拷贝是从内核态的磁盘到磁盘的缓冲区,再到用户态,再从用户态写到内核态网络缓冲区,再写到网卡,频繁的切换效率比较低。
计算机操作系统 - 图28
首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是read() ,一次是write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的

所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数

优化后的零拷贝,就是不再把缓冲区的内容拷贝到用户态,而是直接从用户态映射到缓冲区,再在内核态中从一个缓冲区拷贝到另一个缓冲区。
计算机操作系统 - 图29
从 Linux 内核2.4版本开始起,又进行了优化,直接从磁盘缓冲区拷贝到网卡中,少了一次缓冲区间的拷贝。
计算机操作系统 - 图30

PageCache

内核缓冲区,就是磁盘告诉缓存,为了提高读磁盘的速度,通过DMA把磁盘的内容保存到内存中,根据算法选择保存到内存的数据,也提供了预读功能。在传输大文件时,不会起作用。零拷贝时使用。缓存I/O。

大文件传输

采用异步I/O 和 直接I/O的方式,一个I/O请求发送完成,会立即返回,由内核向磁盘继续发送I/O请求,等读取完数据后,拷贝到用户缓冲区(这里大文件不会拷贝到内核缓冲区,也叫作直接I/O),对于CPU来说是异步的,对于磁盘来说是同步的。