基础八股文:操作系统
操作系统
操作系统是什么?
- 是一个系统软件,介于应用程序和底层硬件资源之间;
- 能够控制和管理整个计算机系统的硬件和软件资源,调度计算机的工作和资源分配;
- 是计算机系统中最基本的系统软件;
什么是并发?什么是并行?二者的区别是什么?
并发:并发指的是两个或多个事件在同一时间间隔内发送;(不是同时发生,而是交替执行)
并行:并行指的是两个或多个事件在同一时刻发送;
并发是同一时间间隔,并行是同一时刻;
在实际的操作系统中:
- 单个处理核在很短时间内分别执行多个进程,称为并发;
- 多个处理核在同一时刻同时执行多个进程,称为并行;
- 宏观上,并发很像并行,但是并发只是并行的模拟,受限于一个CPU,一个CPU同一时刻只能执行一个进程,所以并发在任何时刻都只有一个进程在运行,只是切换速度够快,所以看上去向并发(切换速度不可能无限快,受限于上下文切换的资源消耗)
操作系统有什么特征?
- 并发:并发指的是两个或多个事件在同一时间间隔内发送;
- 共享:系统中的资源可以供内存中多个并发执行的进程使用;
- 虚拟:把一个物理上的实体变成多个逻辑上的对应物;
- 异步:进程的执行并不是一贯到底,而是以不可预知的速度(调度不可预知,可能先启动的进程反而很后面才完成)向前推进;
操作系统的功能是什么?
操作系统介于应用程序和硬件资源之间,为应用程序提供服务,同时管理应用程序;
- 资源分配,资源回收:为应用程序分配内存,以及考虑内存回收和内存回收之后的合并;
- 为应用程序提供服务:提供系统调用给应用程序使用,使得应用程序可以不考虑底层硬件逻辑可以直接安全地访问硬件资源;
- 管理应用程序:控制进程的生命周期,管理进程、线程,决定哪个进程、线程占用CPU资源;
什么是进程?什么是线程?二者有什么区别?
进程:就是运行中的程序,是资源分配的基本单位;(程序是一个静态的二进制代码,在编译之后变成二进制可执行文件,运行这个可执行文件,可执行文件就会被装载到内存中,CPU执行程序中每一条指令,这个运行中的程序,就是进程)
线程:线程是进程的子执行单元;是CPU调度的基本单位;
区别:
- 进程有自己独立的内存空间;线程只有自己的栈,但共享进程的地址空间和资源;
- 进程通信要复杂的机制,线程通信可以直接用共享变量;
- 线程切换速度快,无需切换内存空间(虚拟地址映射不变);进程切换需要切换内存空间;
- 一个进程崩溃不会影响其他进程,一个线程崩溃可能影响其他线程;
什么是进程控制块PCB?
每一个进程在运行时都有自己的内存空间来保存运行时产生的局部变量,如果进程切换时,必须保存自己内存空间(即虚拟地址空间要切换);此外,进程运行到了哪一条指令,在切换时也要保存;
进程控制块PCB包括以下信息:
- 进程描述信息:进程标识符(唯一标识进程,全局唯一),用户标识符;
- 进程控制和管理清单:进程当前状态(就绪、阻塞等),进程优先级;
- 进程资源分配清单:进程占用的内存空间和虚拟地址空间的信息(页表要保存切换),所打开的文件列表和所使用的I/O设备信息;
- CPU相关信息:CPU寄存器的值要先拷贝到PCB中,进程切换回来时,再将PCB中值恢复到CPU中;
核心:保证上下文切换可以顺利进行;
进程有几种状态?状态切换的过程是什么?发生什么事件时会发生状态切换?
进程状态:
- 创建态:进程正在被创建;
- 就绪态:进程可以运行,但是由于CPU被其他进程占据,所以不能运行;
- 运行态:进程正在占据CPU;
- 阻塞态:进程正在等待某一事件发生而处于暂时停止运行(可以是某个I/O调用,也可以是等待某个资源被其他进程释放等);
- 结束态:进程正在从系统中消失;
进程状态转换:(注意发生状态转换的条件)
- 进程被创建,从创建态到达就绪态;
- 进程等待CPU调度,当CPU调度进程时,从就绪态转换到运行态;
- 进程没有运行完成,但是时间片耗尽,从运行态回到就绪态;
- 进程运行完毕,从运行态转换到结束态;
- 进程在运行时,发生等待事件(比如I/O操作要等待I/O操作结果),从运行态转换到阻塞态;
- 当等待事件完成之后,进程从阻塞态回到就绪态(不会直接到运行态,因为到运行态必须CPU从就绪队列里调度);
挂起状态:阻塞状态的进程可能占据大量物理内存空间,所以操作系统通常会把阻塞状态的进程从物理内存空间换出到硬盘(外存)中,再次运行时从硬盘换入到物理内存空间中;挂起状态就是用来描述一个进程实际没有占据物理内存空间的状态;
- 阻塞挂起状态:进程在外存,并且还要等待某个事件发生;
- 就绪挂起状态:进程在外存,但是随时可以换入到内存中执行;(换入即可执行,不用换入之后还去就绪队列,直接去运行队列)

什么时候发生进程上下文切换?进程上下文切换要保存什么信息?
进程从运行态脱离时一般要继续上下文切换:
- 从运行态到阻塞态:等待某个事件,发生中断后进行中断处理、阻塞等待;
- 从运行态到就绪态:CPU时间片耗尽,CPU调度,高优先级队列抢占CPU运行;
- 从运行态到挂起态:通过sleep函数等主动挂起;
进程上下文切换要保存的信息:虚拟内存、栈、全局变量等用户资源,内核堆栈、寄存器等内核空间资源;
进程的创建、终止、阻塞、唤醒的简单实现过程。
进程创建:
- 一个进程可以通过
fork()创建另一个进程,创建者为父进程,被创建者为子进程; - 为新进程分配进程控制块PCB;
- 为新进程分配资源,比如内存、CPU时间等;
- 初始化进程控制块PCB各字段;
- 将状态设置为就绪态,加入就绪队列;
进程终止:
- 根据进程标识符PID查找要终止的进程的PCB;
- 终止进程的活动;
- 如果进程有子进程,将子进程交给1号进程init进程接管(子进程并不终止,变为孤儿进程)
- 将进程所有资源归还给操作系统;
- 将进程PCB从所在队列中删除;
进程的阻塞:
- 根据PID找到PCB;
- 如果进程为运行态,则保护现场,进行上下文切换,将进程转换为阻塞态,停止运行,将该PCB加入到阻塞队列;
进程的唤醒:
- 在该事件的阻塞队列(注意是事件的阻塞队列不是进程的)中找到相应进程的PCB;
- 将其从阻塞队列中移出,并设置为就绪态;
- 将该进程的PCB插入的就绪队列中,等待CPU调度;
什么是线程?
线程是进程中的一个实体,是程序执行的最小单元,也是被系统独立调度和分配的基本单位;线程是进程中的一条执行流程,同一个进程内的多个线程可以共享代码段、数据段、打开的文件等资源;但是每个线程又有自己独立的一套寄存器和栈,可以保证线程的控制流相对独立;
特点:
- 一个进程中可以有多个线程,线程共享进程空间,但是也有自己独立的栈和寄存器;
- 各个线程之间可以并发执行;(并发执行也要频繁上下文切换实现)
- 线程也有自己的控制块PCB(称为TCB),创建线程使用的底层函数和创建进程使用的底层函数一样(都是**
clone函数**); - 进程可以蜕变为线程;
Linux内核不区分进程和线程:
- 如果在创建时复制对方的地址空间,就是创建进程(进程有独立的内存空间)
- 如果在创建时共享对方的地址空间,就是创建线程(线程共享进程内存空间)
- 创建进程的
fork函数和创建线程的pthread_create函数底层都是clone函数; - 线程所有操作函数都是在用户态,即所有函数都是库函数而不是系统调用;所以Linux内核不区分进程和线程,只在用户层面上区分;
进程和线程的差异是什么?
进程时资源分配和调度的基本单位;线程是操作系统能够进行运算调度的最小单位;(线程不涉及资源分配)
线程是进程的子任务,是进程的执行单元;一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存;
资源开销:
- 进程有自己独立的内存空间,创建和删除的资源开销都比较大;进程切换要保存和恢复整个进程空间,上下文开销高;
- 线程共享进程的内存空间,创建和删除的资源开销都比较小,线程切换只需要保存和恢复少量线程上下文,上下文开销低;
通信和同步:
- 进程间相互隔离,通信要特殊机制:管道、消息队列、共享内存等;
- 线程共享进程空间,通信只需要直接访问内存共享数据即可;
安全性:
- 进程间相互隔离,一个进程奔溃不会影响其他进程;
- 线程共享进程空间,所以一个线程错误可能导致整个进程的稳定性,从而导致其他线程也可能奔溃;
什么是中断和异常,二者差异是什么,发生情况是什么?
中断和异常都会暂停当前CPU的执行,然后转向一个特定的处理程序;处理完成后回到之前暂停的地方继续执行;
中断:
- 软中断:
- 中断来源:CPU内部或软件发起的中断,通常由操作系统或应用程序主动触发;
- 处理方式:并不破坏当前任务执行流程,CPU在执行完指令之后,会主动检查是否有软中断请求,并立即响应处理;软中断是在内核态中完成的,可以不破坏当前任务的执行流程情况下进行;
- 举例:文件操作(程序里如果有write函数,只显示就会调用系统调用函数,就会触发软中断,可是程序在用户态并没有被打断,还是正常执行的)、信号(信号可以导致一个运行中的进程被另一个正在运行的异步进程终端,转而处理某一个突发中断);
- 硬中断:
- 中断来源:外部硬件设备或外部信号触发的中断;
- 处理方式:CPU立刻暂停当前程序的执行,跳转到中断服务程序进行处理;
- 举例:网卡中断、键盘中断、定时器中断;
异常:一般是由于计算机系统内部事件触发的,通常与正在执行的程序或指令有关;比如程序的非法操作,如地址溢出、运算溢出等,异常不能被屏蔽,发生异常时,计算机系统会暂停正常的执行流程,转到异常处理程序处理异常;
用户态和核心态的区别是什么?什么时候会发生切换?
区别:控制进程或程序对计算机硬件资源的访问权限和操作范围不同;
用户态:只能访问受限的资源和执行受限的指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源;
核心态:允许进程或程序执行特权指令和访问操作系统的核心部分。在核心态下,进程可以直接访问硬件资源,执行系统调用,管理内存、文件系统等资源;
切换场景:
- 系统调用:用户态通过系统调用进入内核态;
- 异常:程序发生异常时,CPU自动进入内核态来方便操作系统处理异常;
- 中断:中断信号会导致CPU从用户态切换到内核态,操作系统处理完中断后会从内核态返回到用户态;
什么是内部碎片,什么是外部碎片?
内部碎片:分页式存储中,每页的大小固定,栈的顶部和堆的底部之间有部分空间没被进程使用,也无法分给其他进程;
外部碎片:分段式存储中,段的大小不固定,段和段之间存在一些小的空间即无法被分配也无法被使用;
内部碎片是已经分配的空间中浪费的部分;外部碎片是没有被分配空间中浪费的部分;
什么是僵尸进程和孤儿进程?
孤儿进程:
在一个进程终止时,它可能还存在一个或多个子进程,这些子进程并不会也终止,那么这些子进程就会成为孤儿进程;这些孤儿进程会被1号进程(init进程)收养,并由init进程对它们完成状态收集工作;
僵尸进程:
一个进程用fork()创建子进程,子进程终止,但是父进程没有用调用wait或者waitpid获取子进程的状态信息,那么子进程的状态描述符仍然保存在系统中,这种子进程被称为僵尸进程(明明已经终止,却还是可以访问到);
多进程程序,父进程⼀般需要跟踪子进程的退出状态,当子进程退出,父进程在运行,子进程必须等到父进程捕获到了子进程的退出状态才真正结束。在子进程结束后,父进程读取状态前,此时子进程为僵尸进程。
信号和信号量有什么区别?
信号:一种处理异步事件的方式;用于通知接收进程有某种事件发生,或者发送信号到进程本身;比如阻塞队列中有一个进程正在等待某个事件发生,则该事件发生之后会向该进程发送一个信号唤醒该进程进入就绪队列;
常见信号:
- SIGKILL(信号编号为9):用于强制终止进程。该信号发送给进程后,进程将被立即终止,无法被忽略、阻塞或捕获。
- SIGTERM(信号编号为15):用于请求进程正常终止。通常由系统管理员或进程管理工具发送给进程,进程收到该信号后可以进行清理工作后终止。
- SIGINT(信号编号为2):用于终止前台进程。通常由用户在终端上按下
Ctrl+C发送给前台进程,要求进程终止。 - SIGALRM(信号编号为14):用于定时器超时通知。当设置了定时器,并且定时器时间到达时,系统会向进程发送这个信号。
信号量:进程间通信处理同步互斥的机制;(本质就是资源的计数器)负责协调各个线程保证它们以合理、正确的顺序使用公共资源;信号量就是一个变量,可以初始化设定不同的值,来实现不同的功能;
局部性原理是什么?
在同一段时间内,程序倾向于多次访问相同的数据或者接近的数据,而不是随机地访问内存中地各个位置;
时间局部性原理:刚才访问的,之后更可能被访问;
空间局部性原理:一个数据被访问,它周围的数据更可能被访问;
进程之间的通信方式有哪些?
管道、命名管道、信号量、消息队列、信号、共享内存、Socket套接字;
- 管道:半双工的通信方式,只能单向流动;并且只能在父子进程通信中使用;(本质就是读写一个共享文件,但是不是普通的文件,不属于任何文件系统,值存在于内存之中,文件描述符只有父子进程知道,所以只能在父子进程通信中使用,传入数据是无格式的,并且遵循先入先出,从管道中读数据是一次性的,一旦被读取,数据就会从管道中抛弃,类似队列,通信效率低)
- 命名管道:可以在没有亲缘关系的进程中使用的管道;(因为命名管道的文件描述符可以让两个没有亲缘关系的进程知道)
- 信号量:本质是一个计数器,控制多个进程对共享资源的访问,作为一种锁机制;不能传递大量信息,只能作为一种不同进程间同步的手段;(使用PV操作来操作信号量,P操作信号量减1,V操作信号量加1)
- 消息队列:消息的链表,存放在内核中;发送消息时将消息挂在接收进程的消息缓冲队列上;(不适合较大数据的传输)
- 信号:用于通知接收进程某个事件已经发生;(是唯一的异步通信机制,可以在一个进程中通知另一个进程发生了某种事件从而实现进程通信)
- 共享内存:最快的进程通信方式;由一个进程创建一片共享内存,多个进程都可以访问该空间;(进程空间一般都是独立的,但是内核空间是共享的,所以进程之间通信一定要通过内核)
- Socket套接字:主要用于不同端的进程之间的通信;(可以是不同主机上的端口,也可以是相同主机上的不同端口)
// 管道就是读写一个共享文件,通过文件描述符读写文件;
#include <unistd.h>
/**
* 创建⽆名管道.
* @param pipefd 为int型数组的⾸地址,其存放了管道的⽂件描述符
* pipefd[0]、 pipefd[1].
* @return 创建成功返回0,创建失败返回-1.
*/
int pipe(int pipefd[2]);
/**
* 当⼀个管道建⽴时,它会创建两个⽂件描述符 fd[0] 和 fd[1]。其中
* fd[0] 固定⽤于读管道,⽽ fd[1] 固定⽤于写管道。
* ⼀般⽂件 I/O的函数都可以⽤来操作管道(lseek() 除外。)
*/
// 命名管道提供了一个路径名和管道相关联,即使不存在亲缘关系的进程,只要知道路径名可以访问该路径,即可通过管道通信;
#include <sys/types.h>
#include <sys/stat.h>
/**
* 命名管道的创建.
* @param pathname 普通的路径名,也就是创建后 FIFO 的名
* @param mode ⽂件的权限,
* 与打开普通⽂件的 open() 函数中的 mode 参数相同。 (066
* @return 成功: 0 状态码;
* 失败: 如果⽂件已经存在,则会出错且返回 -1.
*/
int mkfifo(const char *pathname, mode_t mode);
匿名管道和命名管道的不同之处:
- 命名管道在文件系统中作为一个特殊文件存在,但是管道的内容却存放在内存中;(匿名管道不属于任何文件系统,只是在内存中保存)
- 当使用命名管道的进程退出之后,命名管道文件依旧在文件系统中保存;
- 命名管道有名字,不相关进程可以通过名字打开命名管道通信;(匿名管道只能在父子进程中通信)
信号量实现互斥锁是如何实现的?信号量实现多进程同步是如何实现的?信号量实现条件变量是如何实现的?
互斥锁:是一种同步原语,保证同一时刻只有一个进程/线程可以访问临界区资源;
核心思想:
- 初始化信号量为1,表示资源可以使用;
- 进程/线程在访问临界区前,先使用
P()操作获取资源,如果信号量值为1,则可以获取到资源,信号量值减1;如果信号量值为0,代表资源不可用,需要等待直到信号量变成1然后才能获取; - 进程/线程在退出临界区后,立刻使用
V()操作释放资源,,即信号量加1;
(自旋锁思想也是一样的,线程在获取锁失败时会不断重试(自旋),直到获得锁。)
示例:
#include <iostream>
#include <thread>
#include <semaphore.h>
sem_t mutex; // 定义信号量
void critical_section(int id) {
// 进入临界区前,先获取信号量
sem_wait(&mutex);
// 临界区代码
std::cout << "Thread " << id << " is in critical section." << std::endl;
// 退出临界区,释放信号量
sem_post(&mutex);
}
int main() {
// 初始化信号量为1,表示资源可用
sem_init(&mutex, 0, 1);
// 创建两个线程访问临界区
std::thread t1(critical_section, 1);
std::thread t2(critical_section, 2);
t1.join(); // 非分离状态
t2.join();
// 销毁信号量
sem_destroy(&mutex);
return 0;
}
多进程同步的实现:
- 使用有名信号量,进程之间共享;
- 在需要同步的地方,对信号量进行
P()和V()操作,控制进程的执行顺序;
示例:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main() {
// 创建一个有名信号量
int semid = semget(ftok(".", 'a'), 1, IPC_CREAT | 0666);
// 初始化信号量值为0
union semun {
int val;
struct semid_ds *buf;
ushort *array;
} arg;
arg.val = 0;
semctl(semid, 0, SETVAL, arg);
// 创建子进程
pid_t pid = fork();
if (pid == 0) { // 子进程
// 子进程等待信号量
struct sembuf op = {0, 1, 0};
semop(semid, &op, 1);
std::cout << "Child process" << std::endl;
} else { // 父进程
sleep(2); // 模拟其他工作
// 父进程释放信号量,唤醒子进程
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1);
std::cout << "Parent process" << std::endl;
}
// 删除信号量
semctl(semid, 0, IPC_RMID, 0);
return 0;
}
读写锁:是一种特殊的互斥锁,读写锁**允许多个线程/进程同时读取共享资源,但只允许一个线程/进程写入共享资源。**读写锁可以使用两个信号量来实现:一个用于控制读取操作,一个用于控制写入操作;
核心思想:
- 定义两个信号量:
read_lock读锁,write_lock写锁;都初始化为1; - 定义一个
int型变量reader_count记录读者计数器; - 读线程获取锁时,先
P(read_lock),然后reader_count++,之后判断是否是第一个读者,如果是第一个读者,则P(write_lock),防止读的时候写; - 读线程读数据时,并不需要加锁;(已经将写锁阻塞了,所以读的时候不用加锁)
- 读线程读取数据接收后,先
P(read_lock),然后reader_count--,判断该读线程是否是最后一个读线程,如果是,则需要释放写锁(没人读了,可以写),即V(write_lock),然后V(read_lock); read_lock读锁并不是用来保护读数据,而是保护读线程对写锁操作的过程;- 写线程获取锁时,先
P(write_lock),然后P(write_lock);
示例:

最低0.47元/天 解锁文章
5609

被折叠的 条评论
为什么被折叠?



