1. 冯诺依曼体系结构:现代计算机的硬件基础
冯诺依曼体系结构是现代计算机的硬件体系结构,它将计算机硬件划分为五大类:
- 输入设备:如键盘、鼠标、扫描仪等
- 输出设备:如显示器、打印机、音响等
- 存储器:内存(主存)和外部存储器
- 运算器:执行算术和逻辑运算
- 控制器:协调和控制计算机各部件工作
在计算机中,数据流是围绕内存工作的。这意味着CPU要处理任意数据,都需要先把数据放到内存中。因此,要运行一个程序:先把这个程序的指令+数据加载到内存中。
2. 操作系统的功能层次划分
2.1 操作系统是什么?
操作系统是一个软件,负责对计算机上的软硬件资源进行管理。
2.2 系统调用接口
操作系统内核提供给上层用于进行计算机操作的接口。
2.3 库函数
库函数是对系统调用接口进行的二次封装接口,如write、printf等。
库函数与系统调用接口的关系:库函数内部调用了系统调用接口。
3. 进程概念
3.1 不同角度的进程定义
- 站在用户的角度:进程就是运行中的程序
- 站在系统的角度:进程就是对于程序的动态运行中描述
这个描述在Linux下是一个结构体:struct task_struct{ }
3.2 进程描述了什么信息?
进程控制块(PCB)中包含了丰富的信息:
- 标识符:进程ID(pid)
- 进程调度信息:描述了如何调度一个程序的运行
- 进程状态:对进程不同运行状态进行描述,让进程在合适的时候做合适的事情
- 进程的内存管理:进程内部所有数据都会占据内存,了解进程内部内存管理方式
- 内存内打开的文件信息:通过了解这块信息,更加深入地了解系统调用I/O
- 进程间通信资源管理:学习进程间通信
- 进程信号信息:描述进程的软中断相关内容
4. 进程标识符与调度
4.1 标识符:进程ID
进程ID其实就是一个数字。在Linux下可以通过ps指令查看进程信息:
ps -ef4.2 进程的调度
并行:多个进程可以同时运行
串行:有多个进程,但是同一时间只能运行一个
并行与串行在计算机上取决于CPU核心数量:每个CPU核心上都有一套独立的寄存器(保存程序运行的上下文信息)来执行指令。
计算机上运行的程序非常多,但是CPU核心很少,如何让它们同时运行?
CPU分时机制:
- 一个程序运行的时候,只能在CPU上运行很短的一段时间(时间片)
- 时间片运行完毕,则切换下一个进程运行
- 分时机制实现了多个程序在宏观上的并行,但我们要理解微观上它们实际上是串行的
一旦程序是切换运行的,等切换回来的时候,CPU又怎么知道该从哪里开始执行?
为了解决这个问题,在struct task_struct(PCB-进程控制块)中就保存了一系列程序的运行上下文:
- pc寄存器内容:即将要执行的指令地址
- 一系列上下文数据:正在处理的数据,即将要处理的数据......
因此一旦CPU切换轮转回来了,就可以通过pid找到task_struct,从其中取出这些上下文数据,重新放到CPU寄存器中,CPU就可以从原来的位置继续向下执行,处理原来没有处理完的数据。
5. 进程状态
5.1 三态模型
就绪、运行、阻塞
5.2 Linux下的进程状态
- 运行态:就绪+运行,拿到时间片就能运行,以及正在运行的都属于运行态。
- 可中断休眠态:可以被打断的阻塞状态,阻塞态都有一个唤醒条件,只有唤醒条件达成的时候才会转为就绪态。
- 不可中断休眠态:只能被条件唤醒,不能被中断打断的阻塞状态。
- 停止态:让进程停止运行,只能通过特定方式唤醒。
- 追踪态:调试的时候涉及到的状态。
- 死亡态:进程退出后,资源全部释放完毕处于的一个短暂的状态。
- 僵尸态:一个进程退出后,但是资源没有完全被释放的状态。
5.3 僵尸态详解
产生原因:由于子进程退出,但是父进程没有关注子进程的退出状态(没有等待也没有忽略SIGCHLD信号)所导致的。
SIGCHLD信号就是子进程退出时,操作系统给父进程发送的信号。忽略这个信号就是告诉操作系统,子进程退出,直接释放资源,我不管他的返回值。
危害:会造成系统资源泄露。
解决:kill -9强制杀死。
避免:进程等待wait/waitpid:等待子进程退出,获取他的返回值。或者忽略SIGCHLD信号。
6. 进程的内存管理:程序地址空间
6.1 程序地址空间概念
程序地址空间:进程的虚拟地址空间。进程的地址空间是虚拟的地址空间,并不是给每个进程都分配了4G内存。
程序地址空间实际上是系统给我们进程所描述的一个虚拟的地址空间(并不是分配了这么多内存,而是虚拟的地址分配)。这个描述在pcb中是mm_struct结构体。
6.2 虚拟地址空间作用
实现数据在物理内存中的离散式存储,提高内存利用率,并进行内存访问控制,提高进程独立性。
6.3 虚拟地址的实现
虚拟地址的实现依赖了一个关键性技术:页表(内存的分页式管理)。
页表信息:虚拟地址和物理地址的映射关系,访问权限,缺页中断标志......
虚拟地址组成:页号(针对页面的编号)+ 页内偏移
32位系统下,地址大小4字节,32个比特位:
- 页号:占据地址中的高20位
- 页内偏移:占据地址中的低12位
6.4 进程的独立性
每个进程,操作系统都会为该进程创建task_struct(pcb),其中就包含虚拟地址空间描述和页表信息。因此每个进程内部,数据存储所分配的地址都是虚拟地址。这也是为什么进程间通信需要操作系统提供进程间通信资源的原因——让操作系统给多个不同的进程分一块大家都能访问的空间。
6.5 内存置换
当内存不够用了,操作系统如何处理?
内存置换:操作系统认为,内存中的很多数据并不是一直在访问(热数据)。因此当内存不够用的时候,操作系统就会根据一定的算法,将指定内存中的数据置换出去(存放到硬盘中)。存放的这块硬盘区域有个专业名称:交换分区。
常见置换算法:
- LRU:最久未使用
- LFU:最少未使用
- FIFO:先进先出
6.6 缺页中断
当访问内存数据的时候,通过页表将虚拟地址转换为物理地址,但是转换的时候,发现页表中置位了缺页中断位(当前这个虚拟地址对应的那块内存数据,没有在内存中)。
缺页中断的处理:从交换分区将数据重新置换到内存中,更新页表信息。
6.7 分段式与段页式内存管理
- 分段式内存管理:将代码分为代码段、数据段等,对于程序的内存管理比较友好。
- 段页式内存管理:对内存分段,在每个段内进行分页管理。
7. 进程控制:创建、退出、等待、程序替换
7.1 创建:pid_t fork()
功能:通过复制父进程的方式,创建一个子进程。
复制了什么:缓冲区;调度切换相关的上下文信息;虚拟地址空间+页表信息;文件描述符信息......
pid_t pid = fork(); if (pid < 0) { // 错误处理 } else if (pid == 0) { // 子进程运行的代码 } else { // 父进程运行的代码 }1. 复制了虚拟地址空间:复制处理的子进程,要执行的代码、要处理的数据跟父进程是一样的。
创建子进程这里使用了写时拷贝技术:创建子进程后,子进程与父进程指向同一块内存区,但是当任意一方对一块内存区域数据进行修改,则给当前进程重新开辟一块空间拷贝新数据进去。这大大提高了创建子进程的效率。
2. 复制了程序调度上下文信息:pc寄存器保存的是即将要执行的指令地址(程序运行到了哪里),以及其他切换信息:正在执行的指令,正在处理的数据...
7.2 退出:终止程序的运行
正常退出:
- 在main中return
- 在任意位置调用exit()函数——exit()函数是一个库函数
- 在任意位置调用_exit()函数——_exit()函数是一个系统调用函数
库函数是对系统调用函数在特定场景下的一些功能补充,库函数内部调用了系统调用函数。库函数在exit退出的时候,会进行用户态空间的一些资源清理操作,例如:缓冲区的数据刷新。
异常退出:程序在运行中的时候,遇到了某种异常(内存访问错误),导致程序崩溃。异常退出的情况下,进程的返回值是没有参考意义的。
7.3 等待:父进程创建子进程之后,等待子进程退出
为什么要等待:捕获子进程的退出状态,获取他的返回值,释放子进程所有资源(避免产生僵尸进程)。
pid_t wait(int *wstatus); pid_t waitpid(pid_t pid, int *wstatus, int options);wstatus:内部包含两个信息:1. 进程的退出原因;2. 进程的退出码
处理逻辑:先取低7位,判断是否为0,为0则表示正常退出:status & 0x7f;再取低16位中的高8位,当作退出码进行处理:(status >> 8) & 0xff。
7.4 程序替换
默认创建子进程之后,子进程与父进程的代码段是一样的,可以通过fork返回值进行分支控制,但是这样会导致两个不同的功能放在一个程序中实现,代码较为臃肿。
如何让子进程重新运行另一个程序呢?使用程序替换:
- 将新的程序(指令 + 数据)加载到内存中。
- 将子进程的页表映射信息初始化,并更新映射到新的内存区域(加载到内存的新程序)。
- 初始化调度上下文信息(让当前进程pc寄存器指向新程序指令的起始位置)。
Exec函数族:execl, execlp, execle, execv, execvp, execve
- l和v的差别:设置程序运行参数的差别(l是通过不定参逐个给予;v是组织成数组一次性给予)
- 有没有p的差别:是否会到系统path环境变量指定的路径下去找这个程序(execl(/bin/ls) vs execlp(ls))
- 有没有e的差别:子进程中是否自定义环境变量
8. 内存内打开的文件信息
8.1 进程的I/O:文件的输入输出
系统调用:open, read, write, lseek, close
文件描述符的本质:是一个数组的下标
- 一个进程内部能够打开的文件数量是有上限的
- 重定向就是修改描述符所对应的描述信息而实现的。重定向:修改数据的输出位置
每个进程运行起来后,默认会打开三个文件:
- 标准输入-键盘(描述符:0)
- 标准输出-显示器(描述符:1)
- 错误输出-显示器(描述符:2)
9. 进程间通信资源
进程间通信就是让操作系统为进程之间提供一个公共内存访问区。
原因:进程之间具有独立性。
根据不同的应用场景,操作系统提供了多种不同的方式:管道(数据传输)、共享内存(数据共享)、消息队列(数据传输)、信号量(同步控制)、套接字。
9.1 管道
1. 管道的本质:内核中的一块缓冲区
匿名管道:没有名字的管道,没有标识符。其他进程怎么找到这个管道跟我通信呢?
特性:只能用于具有亲缘进程关系的进程间通信(创建子进程,子进程会复制父进程的信息)。
操作:在创建子进程之前创建管道,然后创建子进程(子进程就复制进程内部打开的文件信息)。
int pipe(int fd[2]); // 接收两个描述符 // fd[0]用于读;fd[1]用于写命名管道:有名字的管道,可以被所有进程找到,可以用于任意进程间通信。
名字:一个管道文件(这个文件只是一个标识名字,本质上管道就是内核的缓冲区)。
操作:mkfifo(filename, flag);
9.2 公共特性
- 管道的特性是半双工通信:可以选择方向的单向通信
- 管道的数据写满了,write就会阻塞;管道中没有数据,read就阻塞
- 管道所有读端关闭,则write就会触发异常-SIGPIPE;所有写端关闭,取完所有数据后,继续read不再阻塞,而是返回0
- 数据传输是字节流传输:以字节为单位进行数据传输