什么是操作系统?
- 操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石
- 操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源
- 操作系统存在屏蔽了硬件层的复杂性。
- 操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性
系统调用
用户态(user mode):运行用户程序
系统态(kernel mode):运行操作系统程序,操作硬件
特权级:R0、R1、R2和R3。
R0相当于内核态,R3相当于用户态;不同级别能够运行不同的指令集合;
用户态—->内核态:唯一途径是通过中断、异常、陷入机制(访管指令)
内核态—->用户态:设置程序状态字PSW
系统调用:如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。
系统调用按功能大致可分为如下几类:
- 文件管理。文件的读、写、创建及删除等。
- 进程控制。进程控制、进程同步、进程通信、死锁处理、处理机调度等。
- 进程通信。进程之间的消息传递或信号传递等。
- 内存管理。内存分配、内存回收、地址映射、内存保护与共享、虚拟内存等。
- 设备管理。完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。主要包括缓冲管理、设备分配、设备处理、虛拟设备等
Linux 的系统调用主要有以下这些:
Task | Commands |
---|---|
进程控制 | fork(); exit(); wait(); |
进程通信 | pipe(); shmget(); mmap(); |
文件操作 | open(); read(); write(); |
设备操作 | ioctl(); read(); write(); |
信息维护 | getpid(); alarm(); sleep(); |
安全 | chmod(); umask(); chown(); |
宏内核和微内核
宏内核
宏内核是将操作系统功能作为一个紧密结合的整体放到内核。
由于各模块共享信息,因此有很高的性能。
微内核
将一部分操作系统功能移出内核,划分成若干服务,相互独立,降低内核的复杂性
只有微内核这一个模块运行在内核态,其余模块运行在用户态
系统调用,需要频繁地在用户态和核心态之间进行切换,会有一定的性能损失
- 为何要区分用户态和内核态
限制代码行为
- 什么时候会陷入内核态?
系统调用(trap)、中断(interrupt)和异常(exception)。
系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。 set_system_gate(0x80,&system_call); 其中system_call是系统调用入口函数
寄存器
CPU内部有很多用于存放数据的寄存器,32位架构(IA-32,即X86体系)为:
CPU相关的:8个通用寄存器;1个指令指针寄存器;1个状态寄存器;6个段寄存器
系统寄存器:5个控制寄存器;3个系统地址寄存器;1个任务寄存器;8个调试寄存器
主要分类,控制和状态寄存器(用户不可见),和运算寄存器(用户可见)
- 控制和状态寄存器
数据寄存器(Data Register,DR/MDR),又称数据/指令缓冲寄存器
地址寄存器(Address Register,AR/MAR),用来保存CPU当前要所访问的主存单元的地址
指令寄存器(Instruction Register,IR)用来保存当前正在执行的一条指令
指令包括操作码和地址码两个字段,使用指令译码器(Instruction Decoder,ID),识别并执行指令
程序计数器(Program Counter,PC),用来指出下一条指令在主存储器中的地址
累加寄存器(Accumulator,AC),是一个通用寄存器
程序状态字寄存器(Program Status Word,PSW),用来表征当前运算的状态及程序的工作方式
- 用户可见寄存器
通用寄存器(Extended * register): 8个32位,可用于存放操作数,也可作为满足某种寻址方式
- 数据/一般寄存器:4个X,做运算时,所借助的媒介
EAX(Accumulator):被称为累加寄存器,用以进行算数运算和返回函数结果等
EBX(Base):被称为基址寄存器,在内存寻址时(比如数组运算)用以存放基地址
ECX(Counter):被称为记数寄存器,用以在循环过程中记数
EDX(Data) : 被称为数据寄存器,常配合 eax 一起存放运算结果等数据 - 索引寄存器:2个I(index),也叫做变址和指针寄存器
ESI(Source Index): 指向要处理的数据地址
EDI(Destination Index): 指向存放处理结果的数据地址 - 指针寄存器: 2个P(Pointer)
ESP(Stack Pointer): 栈指针寄存器,其内存放着一个指针,永远指向当前栈的栈顶
EBP(Base Pointer):基址指针寄存器,指向当前栈的栈底。
指令指针寄存器:1个P(Pointer)32位
EIP(Instruction Pointer):处理器使用EIP来跟踪下一条要执行的指令,也称为程序计数寄存器
状态寄存器:1个P,,32位,EFLAGS(Flags): 1个Flags
段寄存器(Segment):6个16位,存放的是当前运行的那个进程的段地址
EIP寄存器,,用于存储下一条要执行的指令 ESP寄存器,用于保存程序的栈顶位置 指令寄存器,程序计数器 通用寄存器EAX、EDX,用来存放需要进⾏运算的数据 段寄存器CS、DS、SS、ES,分别用于存放可执行代码的代码段、数据段、堆栈段和其他段的基地址
中断
中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保存现场后自动去执行相应的处理程序,处理完该事件后再返回中断处继续执行原来的程序。
中断请求,会打断正在执⾏的进程,然后调⽤内核中的中断处理程序来响应请求
分类
外中断
由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
异常
由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。
陷入
在用户程序中使用系统调用。
中断的处理方式
1. 屏蔽(禁止)中断,当处理机正在处理一个中断时,处理机对任何新到的中断请求,都暂时不予理睬,而让他们等待,直到处理机已完成本次中断的处理后,处理机再去检查是否有中断发生。
2. 嵌套中断,设置了中断优先级,当同时有多个不同优先级的中断请求时,CPU优先响应最高优先级的中断请求;高优先级的中断请求可以抢占正在运行的低优先级中断的处理机
进程管理
进程和线程和协程
- 进程是资源分配的基本单位。
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
线程是进程划分成的更小的运行单位,不拥有资源,可以访问隶属进程的资源,是独立调度的基本单位。
- 一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行和切换开销小,但不利于资源的管理和保护;而进程正相反。
- 线程没有地址空间,线程包含在进程的地址空间中。
同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。 - 线程间通信可以通过直接读写同一进程中的数据进行,但是进程通信需要借助 IPC。
- 子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。
内核线程和普通线程最大的区别在于内核线程从来不会访问属于用户态虚拟地址空间,这种线程只在内核虚拟地址空间范围活动。
- 为什么在linux中线程和进程的概念很模糊?
因为它们基本的数据结构是一样的,task_struct 结构所表示的一个调度实体
- 内核线程的task_struct实例中 mm_struct实例配合内核进行内存管理的成员是 NULL
因为在内核线程获得cpu执行权之前,内核把之前被调度出去的进程的mm赋值给内核线程。
- 协程
什么是协程? - 知乎
协程C语言实现 - 知乎
协程(coroutine)简介 - DoubleLi - 博客园
多线程面临的问题,系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程
①系统线程会占用非常多的内存空间,②过多的线程切换会占用大量的系统时间
协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。
协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,
而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多
协程只有在等待IO的过程中才能重复利用线程,不能调用导致线程阻塞的操作
协程对计算密集型的任务也没有太大的好处,计算密集型的任务本身不需要大量的线程切换,因此协程的作用也十分有限,反而还增加了协程切换的开销
java没有引入协程。 协程可以理解成用户态的线程,状态机保留
- 实现
协程切换一般是通过调用一个 swapcontext()
的C函数来进行,在栈中保存多个栈帧
作用:保存旧协程上下文和替换新协程上下文来进行协程切换,而新旧协程上下文就是通过C函数的参数来传递的
入栈时,C语言是从右到左开始入栈的 swapcontext(old, new)这个函数时,会先把new参数入栈,然后再把old参数入栈
进程相关数据结构
tack_struct:进程用户空间
mm_struct结构对进程整个用户空间进行描述
vm_area_structs结构对用户空间中各个区间(简称虚存区)进行描述
进程的状态
创建状态(new) :进程正在被创建,尚未到就绪状态。
就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
僵尸/孤儿进程
- 僵尸进程
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。
孤儿进程是父进程先退出,子进程被init进程接管,子进程推出后init会回收其占用的相关资源。如果太多僵尸进程的话,会导致系统的进程号不够用,不再产生进程。
解决方法:
(1) 让僵尸进程成为孤儿进程,由init进程(1号进程)回收;(手动杀死父进程)。
(2)父进程用wait或waitpid去回收资源(方案不好)
父进程通过wait或waitpid等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。
进程的调度算法
批处理系统
- 先到先服务(FCFS) : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 短作业优先(SJF) : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 最短剩余时间优先 shortest remaining time next(SRTN):按剩余运行时间的顺序进行调度
交互式系统
- 时间片轮转 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。先进先出算法(FIFO),调度就绪队列排在队首的进程运行;若在一个时间片内进程没有运行完,就将它送到就绪队列的末尾,等待下一个时间片再执行。
- 优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
- 多级反馈队列 :既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。多级反馈队列调度算法-讲解
- 临界区
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
线程同步是两个或多个共享关键资源的线程的并发执行。
- 同步与互斥
同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
互斥:多个进程在同一时刻只有一个进程能进入临界区。
- 信号量
信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
- down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
- up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
使用信号量实现生产者-消费者问题
typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 临界区
up(&mutex);
}
void P2() {
down(&mutex);
// 临界区
up(&mutex);
}
事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操
①临界区不是内核对象,只能用于进程内部的线程同步,是用户方式的同步。互斥、信号量是内核对象可以用于不同进程之间的线程同步(跨进程同步)。 ②互斥其实是信号量的一种特殊形式。互斥可以保证在某一时刻只有一个线程可以拥有临界资源。信号量可以保证在某一时刻有指定数目的线程可以拥有临界资源。
进程间的通信IPC⭐
- 进程同步:控制多个进程按一定顺序执行;
- 进程通信IPC:进程间传输信息。
管道/匿名管道(Pipes) :具有亲缘关系的父子进程间或者兄弟进程之间的通信,只存在于内存中的文件
管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。#include <unistd.h>
int pipe(int fd[2]);
有名管道(Names Pipes) :严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。存在于实际的磁盘介质或者文件系统
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
- 信号(Signal) :通知接收进程某个事件已经发生,异步 | SIGABRT | 程序的异常终止,如调用 abort。 | | —- | —- | | SIGFPE | 错误的算术运算,比如除以零或导致溢出的操作。 | | SIGILL | 检测非法指令。 | | SIGINT | 程序终止(interrupt)信号。按下Ctrl+C 产生中断 | | SIGSEGV | 非法访问内存。 | | SIGTERM | 发送到程序的终止请求。 |
// 注册信号 SIGINT 和信号处理程序
signal(registered signal, signal handler);
参数[signal]:信号的编号
参数[handler]:一个指向信号处理函数的指针
// 生成信号,一个整数信号编号作为参数
// 信号发送给进程自身,同时raise函数支持多线程模型
int raise (signal sig);
// 把信号sig发送给,进程号为pid的进程
int kill(pid_t pid, int sig);
pid == 0,信号发送给当前进程所属进程组
pid > 0,信号发送给指定的进程(用户权限满足情况下)
pid < 0,信号发送给指定进程组:进程组ID ==|pid|(用户权限满足情况下)
pid == -1,信号发送给所有有权限发送信号的进程
// 信号捕获等待函数
// 进程休眠,并且只在捕获一个信号并从信号处理程序返回时,pause才会返回
int pause(void);
- 信号产生的方式
- 用户按键:用户使用某种特殊字符递送给终端,产生一个信号(事件)递送给进程
- 硬件故障:进程执行错误,比如访问一个无效内存地址,这时候先由硬件报告给内核,再由内核把事件递送给进程
- kill函数或者命令:通过函数把需要的事件直接递送给进程
信号量(Semaphores) :
#include<semaphore.h>
下的semaphore
和sem_t
是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步并避免竞争条件。(互斥锁mutex
)typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 临界区
up(&mutex);
}
void P2() {
down(&mutex);
// 临界区
up(&mutex);
}
消息队列(Message Queuing) :是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,遵循先进先出(first in first out),但也可以实现随机查询。存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺,可以独立地接收含有不同类型的数据结构。每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有一个上限(MSGMNI)相比于 FIFO,消息队列具有以下优点:
- 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
- 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
// 用来创建和访问一个消息队列
int msgid = msgget(key_t key,int msgflg);
key:某个消息队列的名字,唯一的身份标识。
msgflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。
返回值:成功返回一个非负整数,即该消息队列的标志码;失败返回-1。
//消息队列的控制函数
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
msqid:由msgget函数返回的消息队列标志码。
cmd:是将要采取的动作,有三个可取值。// IPC_RMID删除,
buf:消息队列结构
返回值 :成功返回0,失败返回-1。
// 消息结构体
struct msgbuf{
long mtype; //消息的类型,且mtype > 0
char mtext[1];
}
// 把一条消息添加到消息队列中
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
// 从一个消息队列接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
特点
- (1)双向通信(2)用于随意进程(3)面向数据块(一个个节点)(4)自带同步互斥(5)生命周期随内核
(6)消息队列是队列,但是区分类型的(在同类型中支持先进先出原则) 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。最有用,最快的一种IPC
操作系统linux共享内存-csdn//生成一个IPC对象指定的外部访问键值key
key_t key = ftok(PATHNAME,PROJ_ID);
// 创建共享内存
int shmget(key_t key, size_t size, int shmflg);
[参数key]:由ftok生成的key标识,标识系统的唯一IPC资源。
[参数size]:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,一般申请的内存大小为页的整数倍。
[参数shmflg]:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0。
[返回值]:成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码。
// 关联共享内存
int shmdt(const void *shmaddr);
[参数*shmaddr]:连接以后返回的地址。
[返回值]:成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;出错返回-1。
- 套接字(Sockets) : 支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点
死锁⭐
- 什么是死锁?
多个进程(线程)互相同时阻塞等待某个资源被释放,导致线程被无限期地阻塞,程序不可能正常终止。
四个必要条件
互斥:进程(线程)申请的资源在一段时间中只能被一个进程(线程)使用
请求和等待:进程(线程)已经拥有了一个资源,但是又申请新的资源,拥有的资源保持不变
不可抢占:在一个进程(线程)没有用完,主动释放资源的时候,不能被抢占。
循环等待:多个进程(线程)之间存在资源循环链。
只有四个条件同时成立时,死锁才会出现
死锁预防
破坏死锁产生的四个条件至少一个,预防四个条件同时成立,即可预防死锁
- 请求和等待,可以一次性申请所有的资源,这样就不存在请求。
- 不可抢占,占用部分资源的线程进一步申请其他资源时,如果申请不到,主动释放占有的资源
- 循环等待,系统给每类资源赋予⼀个编号,每⼀个进程按编号递增的顺序请求资源,释放则相反
互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。但这种场景很少,因为资源共享,如果是只读就可以,但涉及到修改很可能会导致异常,而加锁就是为了互斥。
检测与恢复
资源分配图检测
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
死锁恢复
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
- 银行家算法
安全状态:指系统能按某种进程推进顺序( P1, P2, …, Pn),为每个进程Pi分配其所需资源,直至满足每个进程对资源的最大需求,使每个进程都可顺序地完成。
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
内存管理
负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存)
将逻辑地址转换成相应的物理地址等功能
内存管理机制⭐
- 块式管理:将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。在每个块中未被利用的空间,称之为碎片。
- 页式管理:把整个虚拟和物理内存空间切成一段段固定尺寸的大小,在 Linux 下,每一页的大小为4KB,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页实际并无任何实际意义,通过页表对应逻辑地址和物理地址。
- 段式管理:把主存分为有实际意义的一段段,每个段定义了一组逻辑信息,通过段表对应逻辑地址和物理地址以 32 位系统为例,每个进程是4GB
- 程序文件段,包括二进制可执行代码;
- 已初始化数据段,包括静态常量;
- 未初始化数据段,包括未初始化的静态变量;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关)
- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是
8 MB
。当然系统也提供了参数,以便自定义大小
- 段页式管理机制:段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址,Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下)
分页和分段的同异处
共同点 :
- 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。
- 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
区别 :
- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
内存管理单元MMU
页表实际上存储在 CPU 的内存管理单元 (MMU) 中,于是 CPU 就可以直接通过 MMU,找出要实际要访问的物理内存地址。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)多级页表
多级页表如何节约内存
在 32 位和页大小 4KB
的环境下,每个页表项是占用 4 字节大小的,于是相当于每个页表需占用 4MB
大小的空间
每个进程都有 4GB
的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到 4GB,会存在部分对应的页表项都是空的,对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,不会占用物理内存。
页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项
如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表
属于时间换空间的典型场景
快表
在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,把最常访问的几个页表项存储到访问速度更快的硬件,就是 TLB(Translation Lookaside Buffer),通常称为页表缓存、转址旁路缓存、快表
在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它用来完成地址转换和 TLB 的访问与交互。
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
- 根据虚拟地址中的页号查快表;
- 如果该页在快表中,直接从快表中读取相应的物理地址;
- 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
缺页中断异常
进程运行时,CPU访问的是用户空间的虚地址
Linux仅把当前要使用的用户空间中的少量页面装入内存,需要时再通过请页机制将特定的页面调入内存
当要访问的虚页不在内存时,产生一个页故障并报告故障原因
内存碎片
内存碎⽚的问题共有两处地方:
外部内存碎片,产⽣了多个不连续的小物理内存,导致新的程序⽆法被装载;
- 内部内存碎片,程序所有的内存都被装载到物理内存,但是这个程序有部分的内存可能并不是很常
使用,这也会导致内存的浪费;
解决方法:
Linux 系统⾥常看到的 Swap 空间,这块空间是从硬盘划分出来的,⽤于内存与硬盘的空间交换。
把程序内存写到硬盘上,然后再从硬盘上读回来到内存里
再读回的时候,不能装载回原来的位置,而是整理内存空间,紧紧跟着已经被占用了的内存后面
会产⽣性能瓶颈,因为硬盘的访问速度要比内存慢太多
如果内存交换的时候,交换的是⼀个占内存空间很大的程序,这样整个机器都会显得卡顿
虚拟内存
如果直接使用物理地址,用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃;同时运行多个程序容易造成对内存的赋值就会覆盖微信之前所赋的值
通过虚拟地址访问内存有以下优势:
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间),实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
- 局部性原理
局部性原理是虚拟内存技术的基础
在某个较短的时间段内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域。
时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
虚拟地址-物理地址
早期 Intel 的处理器从 80286 开始使用的是段式内存管理
在不久以后的 80386 中,除了完成并完善从 80286 开始的段式内存管理的同时还实现了页式内存管理
段式内存管理先将程序所使用的逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址
- 内核空间到物理地址映射
进程的用户空间中存放的是用户程序的代码和数据
内核空间映射到物理内存总是从最低地址(0x00000000)开始,使之在内核空间与物理内存之间建立简单的线性映射关系。
page.h头文件中对内核空间中地址映射的说明及定义:
给定一个虚地址x,其物理地址为x-PAGE_OFFSET
给定一个物理地址x,其虚地址为x+PAGE_OFFSET
#define __PAGE_OFFSET (0xC0000000)
……
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
虚拟内存-驻留内存-共享内存
top命令,可以在shell窗口中看到VIRT,RES,SHR三个字段
- 虚拟内存(VIRT)
– 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
– 假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量
- 驻留内存(res)
– 进程当前使用的内存大小,但不包括swap out
– 包含其他进程的共享
– 如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反
– 关于库占用内存的情况,它只统计加载的库文件所占内存大小
- 共享内存(shr)
– 除了自身进程的共享内存,也包括其他进程的共享内存
– 虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小
– 计算某个进程所占的物理内存大小公式:RES – SHR
– swap out后,它将会降下来
虚拟内存⭐
分为内核空间和用户空间两部分,地址空间的范围也不同
32
位系统的内核空间占用1G
,位于最高处,剩下的3G
是用户空间;64
位系统的内核空间和用户空间都是128T
,分别占据整个内存- 存空间的最高和最低处,剩下的中间部分是未定义的。
Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。
于是 Linux 就把所有段的基地址设为 0
,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。
虚拟内存标准段⭐
用户进程部分分段存储内容如下表所示(按地址递减顺序)
Linux进程在虚拟内存中的标准内存段布局
栈 | 局部变量、函数参数、返回地址等 |
---|---|
堆 | 动态分配的内存 |
BSS段 | 未初始化或初值为0的全局变量和静态局部变量 |
数据段Data | 已初始化且初值非0的全局变量和静态局部变量 |
代码段text | 可执行代码、字符串字面值、只读变量 |
保留区 | 位于虚拟地址空间的最低部分,用于捕捉使用空指针和小整型值指针引用内存的异常情况。 |
- 程序默认栈空间大小
linux 32位下线程的默认栈大小是8M
$ ulimit -s
8192 # 8 * 1024KB
windows 32 下程序的栈大小是链接时决定的,默认单个线程的栈大小为1M
$ link /stack 102400 ....
win 32下,进程的高位2G留给内核,低位2G留给用户,所以进程堆的大小小于2G
windows32用户态空间大小是2GB,如果fork出线程数过多,导致进程的栈大小超过2GB,程序会崩溃
Linux 32位下,进程的高位1G留给内核,低位3G留给用户,所以进程堆大小小于3G。
虚拟存储器
基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行。
运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统所需要的部分调入内存;将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息,然后继续执行程序。
计算机好像为用户提供了一个比实际内存大的多的存储器——虚拟存储器,是一种时间换空间的策略,用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的空间来支持程序的运行。
- 虚拟内存技术实现
请求分页存储管理:建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能
请求分段存储管理 :建立在分段存储管理之上,增加了请求调段功能、分段置换功能
请求段页式存储管理:
请求分页存储管理≠分页存储管理
请求分页存储管理不要求将作业全部地址空间同时装入主存,可以提供虚存
分页存储管理全部地址空间都装入主存,不能提供虚存
都需要:
- 一定容量的内存和外存:在载入程序的时,只需将程序的一部分装入内存,其他部分留在外存,程序就可执行;
- 缺页中断:如果需执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,后继续执行程序;
- 虚拟地址空间 :逻辑地址到物理地址的变换。
页面置换算法
只有与用户空间建立了映射关系的物理页面才会被换出,内核空间中内核所占的页面则常驻内存 进程映像所占的页面 ,其代码段、数据段可被换入换出,但堆栈段一般不换出 通过系统调用
mmap()
把文件内容映射到用户空间时,页面所使用的交换区就是被映射的文件本身
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法
OPT 页面置换算法(最佳页面置换算法)
最佳(Optimal, OPT)置换算法,选择淘汰将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。
但由于法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。
一般作为衡量其他置换算法的方法。
FIFO(First In First Out) 页面置换算法(先进先出页面置换算法)
总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法)
LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法)
该置换算法选择在之前时期使用最少的页面作为淘汰页。
物理内存分配和回收
在Linux中,CPU所访问的地址是虚拟地址空间的虚地址;
管理内存页面时,先在虚存空间中分配一个虚存区间,然后才根据需要为此区间分配相应的物理页面并建立起映射
内核用struct page结构表示系统中的每个物理页,也叫页描述符,该结构位于
伙伴(Buddy)算法
Linux的伙伴算法把所有的空闲页面分为10个块链表,
每个链表中的一个块含有2的幂次个页面(叫做“页块”或简称“块”)
大小相同、物理地址连续的两个页块被称为“伙伴”
- 工作原理
首先在大小满足要求的块链表中查找是否有空闲块,若有则直接分配,否则在更大的块中查找。其逆过程就是块的释放,此时会把满足伙伴关系的块合并
伙伴算法为什么能减少碎片?
(1)在分配时,根据需要,分配与所需内存大小最相近稍大的块,这样碎片最少;
(2)在回收时,检查相邻的伙伴块,实时对释放的伙伴块进行合并
-
slab分配器
在释放内存时,slab分配器将释放的内存块保存在一个列表中,而不是返回给伙伴系统
在下一次内核申请同样类型的对象时,会使用该列表中的内存
内核经常反复使用某一内存区,减少对伙伴算法的调用次数,适合硬件高速缓存设备管理
磁盘结构
盘面(Platter):一个磁盘有多个盘面;
- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
- 制动手臂(Actuator arm):用于在磁道之间移动磁头;
- 主轴(Spindle):使整个盘面转动。
- 磁盘调度算法
读写一个磁盘块的时间的影响因素有:
- 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
- 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
- 实际的数据传输时间
其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短
先来先服务(FCFS, First Come First Served)
按照磁盘请求顺序进行调度。
优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
最短寻道时间优先(SSTF, Shortest Seek Time First)
优先调度与当前磁头所在磁道距离最近的磁道。
虽然平均寻道时间比较低,但是不公平,两端的磁道请求更容易出现饥饿现象。
电梯算法(SCAN)
电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。
因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。
文件系统
- 主存储器空间
在 Linux 操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件,一个重要的概念:一切都是文件
文件结构
树型结构(文件结构是文件存放在磁盘等存贮设备上的组织方法)
- 目录树
Linux 文件系统的结构层次
/bin: 存放二进制可执行文件(ls、cat、mkdir 等),常用命令一般都在这里;
/etc: 存放系统管理和配置文件;
/home: 存放所有用户文件的根目录,是用户主目录的基点,比如用户 user 的主目录就是/home/user,可以用~user 表示;
/usr : 用于存放系统应用程序;
/opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把 tomcat 等都安装到这里;
/proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;
/root: 超级用户(系统管理员)的主目录(特权阶级o);
/sbin: 存放二进制可执行文件,只有 root 才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如 ifconfig 等;
/dev: 用于存放设备文件;
/mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统;
/boot: 存放用于系统引导时使用的各种文件;
/lib : 存放着和系统运行相关的库文件 ;
/tmp: 用于存放各种临时文件,是公用的临时文件存储点;
/var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等;
/lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows 下叫什么.chk)就在这里。
文件类型
Linux 支持很多文件类型
普通文件(-) : 用于存储信息和数据, Linux 用户可以根据访问权限对普通文件进行查看、更改和删除。比如:图片、声音、PDF、text、视频、源代码等等。
目录文件(d,directory file) :目录也是文件的一种,用于表示和管理系统中的文件,目录文件中包含一些文件名和子目录名。打开目录事实上就是打开目录文件。
符号链接文件(l,symbolic link) :保留了指向文件的地址而不是文件本身。
字符设备(c,char) :用来访问字符设备比如键盘。
设备文件(b,block) : 用来访问块设备比如硬盘、软盘。
管道文件(p,pipe) : 一种特殊类型的文件,用于进程之间的通信。
套接字(s,socket) :用于进程间的网络通信,也可以用于本机之间的非网络通信。
软/硬链接
- 硬链接(hard link)
让一个文件对应一个或多个文件名,或者说把使用的文件名和文件系统使用的节点号链接起来,这些文件名可以在同一目录或不同目录
1.硬链接,以文件副本的形式存在。但不占用实际空间。 2.不允许给目录创建硬链接 3.硬链接只有在同一个文件系统中才能创建
- 软链接(符号链接)
是一种特殊的文件,这种文件包含了另一个文件的任意一个路径名。
这个路径名指向位于任意一个文件系统的任意文件,甚至可以指向一个不存在的文件
1.软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式 2.软链接可以 跨文件系统 ,硬链接不可以 3.软链接可以对一个不存在的文件名进行链接 4.软链接可以对目录进行链接
文件描述符
fd_set是int数据,是files_struct文件描述表的下标
Linux 文件描述符 fd 究竟是什么? - 51博客
如果通过文件描述符找到对应的数据
- 从姿势上,用户
open
文件得到一个非负数句柄fd
,之后针对该文件的 IO 操作都是基于这个 fd ; - 文件描述符 fd 本质上来讲就是数组索引,fd 等于 5 ,那对应数组的第 5 个元素而已,该数组是进程打开的所有文件的数组,数组元素类型为
struct file
; - 结构体
task_struct
对应一个抽象的进程,files_struct
(文件描述符表) 是这个进程管理该进程打开的文件数组管理器。fd 则对应了这个数组的编号,每一个打开的文件用 file 结构体表示,内含当前偏移等信息; file
结构体可以为进程间共享,属于系统级资源,同一个文件可能对应多个 file 结构体,file 内部有个inode
指针,指向文件系统的 inode;inode
是文件系统级别的概念,只由文件系统管理维护,不因进程改变( file 是进程出发创建的,进程 open 同一个文件会导致多个 file ,指向同一个 inode )虚拟文件系统(VFS)
为了支持其他各种不同的文件系统,Linux提供了一种统一的框架
超级块(superblock)对象: 存放系统中已安装文件系统的有关信息
索引节点(inode)对象: 存放关于具体文件的一般信息
目录项(dentry)对象: 存放目录项与对应文件进行链接的信息
文件(file)对象: 存放打开文件与进程之间进行交互的有关信息
inode 和 block
block:是实际文件的内容,用于实际的数据存储,如果一个文件大于一个块时候,那么将占用多个 block,但是一个块只能存放一个文件。大小可以是 1KB、2KB、4KB,默认为4KB。inode 中是不记录文件名的,那么因为文件名记录在文件所在目录的 block 中
inode :是 linux/unix 文件系统的基础,记录文件的属性信息
inode 默认大小为 128 byte,用来记录文件的权限、文件的所有者和属组、文件的大小、文件的状态改变时间(ctime)、文件的最近一次读取时间(atime)、文件的最后一次修改时间(mtime)、文件的数据真正保存的 block 编号。每个文件需要占用一个 inode。
硬盘的最小存储单位是扇区(Sector),块(block)由多个扇区组成。文件数据存储在块中。块的最常见的大小是 4kb,约为 8 个连续的扇区组成(每个扇区存储 512 字节)。
一个文件可能会占用多个 block,但是一个块只能存放一个文件
虽然,我们将文件存储在了块(block)中,但是我们还需要一个空间来存储文件的 元信息 metadata :如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。这种 存储文件元信息的区域就叫 inode,译为索引节点:i(index)+node。 每个文件都有一个 inode,存储文件的元信息。
页缓冲区
页缓冲区 != 快表
Linux页缓冲区使用address_space结构描述页缓冲区中的页面
为了避免每次读写文件时,都需要对硬盘进行读写操作,Linux 内核使用页缓存(Page Cache)机制来对文件中的数据进行缓存
读的角度:从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户;
写的角度:对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。
- 分析数据结构
在 Linux 内核中,使用 file 对象来描述一个被打开的文件,其中有个名为 f_mapping 的字段,定义如下:
struct file {
...
struct address_space *f_mapping;
};
从上面这段代码可以看出,f_mapping 字段的类型为 address_space 的结构,其定义如下:
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
rwlock_t tree_lock; /* and rwlock protecting it */
...
};
address_space 结构其中的一个作用就是用于存储文件的 页缓存,下面介绍一下各个字段的作用:
- host:指向当前 address_space 对象所属的文件 inode 对象(每个文件都使用一个 inode 对象表示)。
- page_tree:用来存储当前文件的 页缓存。
- tree_lock:用于防止并发访问 page_tree 导致的资源竞争问题。
从 address_space 对象的定义可以看出,文件的 页缓存 使用了 radix树 来存储。
radix树:又名基数树,它使用键值(key-value)对的形式来保存数据,并且可以通过键值对快速查找到其对应的值。内核以文件读写操作中的数据偏移量作为键,以数据偏移量所在的页缓存作为值,存储在address_space结构的page_tree字段中。
补充一个比较常用的东西:
假如我们装了一个 zookeeper,我们每次开机到要求其自动启动该怎么办?
- 新建一个脚本 zookeeper
- 为新建的脚本 zookeeper 添加可执行权限,命令是:
chmod +x zookeeper
- 把 zookeeper 这个脚本添加到开机启动项里面,命令是:
chkconfig --add zookeeper
- 如果想看看是否添加成功,命令是:
chkconfig --list
- 压缩文件
打包并压缩文件
tar -zcvf 打包压缩后的文件名 要打包压缩的文件
解压压缩包
tar [-xvf] 压缩文件
- 权限命令
文件的类型:
- d: 代表目录
- -: 代表文件
- l: 代表软链接(可以认为是 window 中的快捷方式)
Linux 中权限分为以下几种:
- r:代表权限是可读,r 也可以用数字 4 表示
- w:代表权限是可写,w 也可以用数字 2 表示
- x:代表权限是可执行,x 也可以用数字 1 表示
超级用户可以无视普通用户的权限
修改文件/目录的权限的命令:**chmod**
例如
chmod u=rwx,g=rw,o=r aaa.txt 或者 chmod 764 aaa.txt
补充一个比较常用的东西:
假如我们装了一个 zookeeper,我们每次开机到要求其自动启动该怎么办?
- 新建一个脚本 zookeeper
- 为新建的脚本 zookeeper 添加可执行权限,命令是:
chmod +x zookeeper
- 把 zookeeper 这个脚本添加到开机启动项里面,命令是:
chkconfig --add zookeeper
- 如果想看看是否添加成功,命令是:
chkconfig --list
设备驱动
Linux操作系统把设备纳入文件系统的范畴来管理
从概念上可以把一个系统划分为应用、文件系统和设备驱动三个层次
Linux将设备分成三大类。
一类是像磁盘那样以块或扇区为单位,成块进行输入/输出的设备,称为块设备;
另一类像键盘那样以字符(字节)为单位,逐个字符进行输入/输出的设备,称为字符设备,
还有一类是网络设备。文件系统通常都建立在块设备上
设备驱动程序:处理和管理硬件控制器的软件
- 层层隔离,屏蔽
当用户进程发出输入输出时,系统把请求处理的权限放在文件系统,
文件系统通过驱动程序提供的接口将任务下放到驱动程序,
驱动程序根据需要对设备控制器进行操作,
设备控制器再去控制设备本身