操作系统八股文

一.进程和线程的区别

1.本质区别和所属关系是什么?

进程是资源调度以及分配的基本单位。

线程是CPU调度的基本单位。
一个线程属于一个进程,一个进程可以拥有多个线程。

2.地址空间和内存

进程拥有独立的虚拟地址空间。

线程没有独立的地址空间;线程有栈,程序计数器(PC),本地存储(LS) 等少量独立空间。
系统会为每个进程分配不同的内存空间。

系统不会为线程分配内存,线程所使用的资源来自所属的进程资源。

3.并发性和健壮性

进程的并发性较低。

线程的并发性较高。

对于单个CPU,系统会把CPU运行时间划分为多个时间段,再将时间段分配给各个线程执行。

在切换效率上,进程切换效率低,线程切换效率高,都会涉及到上下文的切换。
一个进程崩溃不会影响其他进程。

一个线程崩溃可能会导致整个进程崩溃 。

进程隔离性更强一些。

二.操作系统中进程和线程的切换过程

1.进程由哪几个部分构成?

task_struct

进程的地址空间 = 代码段___数据段___栈区____堆区

2.上下文由哪几个部分组成?

用户级上下文

系统级上下文 = __进程标识符信息__进程现场信息__进程控制信息__系统内核栈

寄存器上下文(硬件上下文) = __CPU各寄存器的内容___进程的现场信息

3.何时发生切换?

主动 = 系统调用,产生软中断。

被动 = 时间中断

靠中断来完成切换

4.现场信息存储在哪里?

程序计数器(PC),寄存器,堆栈,进程/线程控制块(PCB)

5.进程/线程切换过程

保存当前进程的硬件上下文,修改当前进程的PCB,状态由运行态变为就绪态或者阻塞态,加入相关队列,调度另外一个进程,修改被调度进程的PCB,状态变为运行态,把当前进程的存储管理数据改为被调度进程的存储管理信息(页表,cache,TLB)__(这一小段线程没有__线程不具备虚拟地址空间__共享进程的虚拟地址空间),恢复被调度进程的硬件上下文,让PC指向新的进程代码。

三.系统调用的整个流程

1.系统调用是什么?干嘛的?

系统调用是内核给用户程序提供的编程接口,内核具有最高的权限,可以直接访问所有资源,用户只能访问受限制的资源,不能直接访问内存,网络,磁盘等硬件资源。
大致的流程 = 应用程序--->函数库---->系统调用--->内核
靠中断使程序从用户态切换到内核态或者从内核态切换到用户态

2.系统调用是否引起进程或者线程切换?

不会必然切换 :很多系统调用(如getpid()gettimeofday()等)只是简单地获取信息,执行很快,不会导致进程或线程切换。

可能导致切换 :如果系统调用涉及阻塞操作(如read()write()accept()sleep()等),当前进程或线程因为等待I/O或资源而被阻塞,操作系统会把它挂起,调度其他进程或线程运行,这时就会发生切换。

主动让出CPU :有些系统调用(如sched_yield()sleep())会主动让出CPU,也会导致切换。

3.系统调用引起中断上下文切换

系统调用一定会引起用户态到内核态的中断上下文切换,但是否会引起进程或线程的切换,要看系统调用是否导致阻塞或主动让出CPU。

4.系统调用的流程

用户态发起系统调用

应用程序通过调用库函数(如C语言的read()write()等)发起系统调用请求。

陷入内核态

库函数内部会使用特定的指令(如x86上的syscallint 0x80)触发软中断或陷入,CPU从用户态切换到内核态。

保存现场

CPU会保存当前用户态的寄存器等现场信息,确保系统调用返回后能恢复原来的执行状态。

内核处理系统调用

操作系统根据系统调用号找到对应的内核服务例程,执行相应的内核代码,完成所需的操作(如文件读写、进程管理等)。

返回用户态

系统调用执行完毕后,内核将结果返回给用户程序,并通过特定的指令(如sysret)切换回用户态,恢复之前保存的现场。

用户程序继续执行

用户程序获得系统调用的返回值,继续后续的执行。

四.后台进程有什么特点?

1.前台进程是什么?有什么用?

运行在前台的进程,终端是该进程的控制终端,终端关闭(SIGHUP),进程退出。

可接受终端输入,并可以在终端输出。

2.后台进程是什么?有什么用?

运行在后台的进程,若在终端运行,终端关闭,进程可能退出。

不可以接受终端输入,可以在终端输出。

3.前后台程序切换

Ctrl + Z:可以将当前前台运行的程序挂起,转为后台暂停状态。

fg:用于将后台的程序切换回前台继续运行。

bg:让已经挂起的后台程序在后台继续运行。

&:在命令末尾加上&,可以让程序直接在后台运行。

nohup:用于忽略SIGHUP信号,通常配合&一起使用,保证程序在终端关闭后依然运行。

Ctrl + D:用于断开当前终端的session,相当于退出登录。

4.守护进程

后台进程的延申,脱离终端的后台进程。

5.如何成为守护进程?

fork子进程令父进程退出,让子进程被init进程接管,成为孤儿进程。

setsid()建立新的进程会话,使守护进程成为会话首进程,从而脱离与终端的关联。

打开/dev/null,把0,1,2重新定向到/dev/null。

五.进程间通信有哪几种方式

1.管道

单根管道是半双工的,通常用于父子进程之间,通过pipe文件速度慢,容量有限。

2.FIFO (有名/命名管道)

FIFO(First In First Out)有名管道是一种用于进程间通信(IPC)的机制。与匿名管道不同,有名管道有一个路径名,存在于文件系统中,通常通过mkfifo命令或mkfifo()系统调用创建。

3.消息队列

消息的连接表存储在内核当中,独立于进程,容量有限。
消息队列的流程主要包括通过msgget创建队列,使用msgsnd发送消息,msgrcv接收消息,最后用msgctl管理或删除队列。

4.共享内存

多个进程共享的一块存储区,最快的一种IPC方式,需要和信号量配合使用。
共享内存的接口使用流程主要包括通过shmget创建共享内存,shmat挂载到进程地址空间,进程间读写数据,最后用shmdt分离和shmctl删除共享内存。

5.信号

信号是一种软中断,处理方式是忽略/捕获/默认动作。

6.信号量

信号量是一个计数器,主要用于进程/线程之间的同步和互斥。

7.socket套接字

不局限于是否在同一个机器上。

六.操作系统中进程调度策略

1.什么是进程调度?

调度对象

线程和进程相对于操作系统而言都是任务(task_struct)。

线程又称为共享用户虚拟地址空间的进程。

包含多个线程的进程称之为线程组。

只有一个线程的进程称为进程。

没有用户虚拟地址空间的进程称之为内核线程。
用于绝对由谁(哪个或者哪几个)获得处理器的执行权
进程状态

就绪 -->|进程调度| 运行
运行 -->|时间片用完| 就绪

运行 -->|io请求| 阻塞
阻塞 -->|io就绪| 就绪

调度时机

主动调度(系统调用等待某个资源)。

周期调度(系统进程不主动让出,内核依靠周期时钟来抢占调度)。

唤醒抢占,创建新进程抢占,内核抢占。

2.进程调度有哪些算法?

先来先服务(FCFS)

从就绪队列中存在时间最长的进程执行调度。
短作业优先(SJF)

从就绪队列中选择估计运行时间最短的任务执行调度。
高响应比优先

FCFS和SJF的综合;综合考虑等待时间和估计运行时间来选择执行调度。
时间片轮转调度

适用分时系统,为任务分配时间片,执行完时间片后放入就绪队列。
优先级调度

从就绪队列中选择优先级最高的若干任务执行调度。

优先级用来描述运行的紧迫程度。
多级反馈队列

时间片和优先级的综合。

动态调整任务的优先级和时间片的大小,从而兼顾多方面的系统目标。

七.线程同步的方式

1.什么是线程同步?

让线程对临界资源的访问按照规定的次序执行。

主要解决线程之间的协同问题。

2.为什么需要线程同步 ?

在一个进程当中的所有线程都共享该进程的临界资源。

如果不干预线程对临界资源的访问可能造成意想不到的错误,甚至导致进程崩溃。

3.线程同步有哪些方式?

互斥锁

确保同一时间只有一个线程访问临界资源。

当锁被占用的时候,其他试图获取锁的线程阻都进入阻塞状态。

当锁被释放的时候,会通过信号通知其他被锁阻塞的线程,被阻塞的线程可能修改为就绪态,也可能直接调度执行。
自旋锁

当一个线程尝试获取自旋锁的时候,如果该锁被其他线程占用,它不会进入阻塞状态,而是会在循环中不断地检查锁是否可用,这个过程被称为自旋。

通常适用于锁持有时间很短的场景,避免线程切换的开销,但会持续占用CPU资源。
互斥锁和自旋锁

相同点

都用于多线程同步,保护临界区,防止数据竞争。

同一时刻只允许一个线程进入临界区。

不同点

获取锁失败,互斥锁阻塞,自旋锁轮询检测。
读写锁

当加写锁而处于写状态的时候,任何试图加读锁或写锁的线程都会阻塞。

当加读锁而处于读状态的时候,其他试图加读锁的线程不会阻塞,而试图加写锁的线程会阻塞。

通常适用于读远大于写的场景中。
条件变量

当特定条件为真的时候解除阻塞。

对条件的判断需要在互斥锁的保护下进行。

互斥锁和条件变量需要一起适用。
信号量

非负整数的计数器,用来实现对资源的控制。

适用于多份临界资源的访问。

只有当信号量大于0的时候,才能访问资源。
一个线程加锁了另外一个线程可以解锁吗?

只有条件变量可以通过另外一个线程解锁

八.CAS是怎样的一种同步机制

1.cas是什么?

cas属于原子操作

2.什么是原子操作?

底层级别的同步操作:cpu指令

不会被调度机制所打断的操作

原子性:该原子操作是不可分割的,要么都做完了,要么还没开始,总之不会被其他线程看到只完成了一部分。

3.什么时候使用原子操作?

锁竞争或性能要求高

当多线程对共享资源的竞争激烈,或者对性能有较高要求时,原子操作可以减少锁带来的性能损耗。

操作简单

当需要保护的操作非常简单(如自增、自减、赋值等),使用原子操作可以避免加锁的复杂性和开销。

冲突较少

当线程间对共享资源的访问冲突较少时,原子操作能高效地完成同步,避免不必要的阻塞。

4.C++当中的cas操作(compare and swap)

cas操作组成

内存位置V,期待值Expect,替换值New value
cas操作

比较内存中的值是否与期望值Expect是否相等。

若相等,写入替换值,并返回true。

若不等,返回false。
c++ atomic

compare_exchange_weak

compare_exchange_strong

5.用C++的aotomic实现一个自旋锁
cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>

// 自旋锁实现
class SpinLock {
public:
    SpinLock() : flag_(false) {}

    void lock() {
        bool expected = false;
        while (!flag_.compare_exchange_weak(expected, true, std::memory_order_acquire)) {
            expected = false;
        }
    }

    void unlock() {
        flag_.store(false, std::memory_order_release);
    }

private:
    std::atomic<bool> flag_;
};

// 全局计数器和自旋锁
int counter = 0;
SpinLock spinlock;

void add() {
    for (int i = 0; i < 10000; ++i) {
        spinlock.lock();
        ++counter;
        spinlock.unlock();
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(add);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Final counter: " << counter << std::endl;
    return 0;
}
6.ABA问题

线程1:取出A,改为B,重新改为A。

线程2:取出A,由于时间差没有感知到A值的变化。
解决方案:加上版本号 。

把ABA问题转变为1A2B3A的问题。

九.CPU是怎么执行指令的

1.指令是什么?怎么产生的?

指令是二进制的机器码。

程序编译运行的时候产生的,通过以下几个环节:

编译:源代码->汇编代码 方式:词法句法分析。

汇编:汇编代码->二进制机器码 生成.o目标文件。

链接:目标文件+库文件 -> 可执行程序。

载入:可执行程序加载到内存中,给进程分配地址空间,创建页表,加载代码段,数据段,加载器把入口指令地址写入到指令寄存器中。

2.怎么执行的?涉及到哪些流程?

采用流水线的方式执行指令,指令执行的过程被称之为指令周期,cpu周而复始的执行指令周期。
涉及到CPU,内存,总线。

CPU有寄存器,控制单元,逻辑运算单元。

内存中有代码段,数据段,BSS段,堆区,栈区。

总线分为地址总线,数据总线,控制总线。
指令周期

取指令(Fetch)

控制单元从程序计数器中取得指令地址通过地址总线通知内存准备数据。

内存数据准备好,控制单元通过数据总线获取指令内容,然后写入指令寄存器。

指令译码(Decode)

CPU对指令解析成不同的操作信息。

执行(Execute)

CPU将指令放入逻辑运算单元进行运行。

结果写回(Write Back)

如果需要,运算结果会通过数据总线写回内存,或存入相关寄存器。

十.用户态和内核态

1.什么是用户态和内核态?

用户程序运行时的状态称为用户态,操作系统运行时的状态称为内核态。

2.为什么要区分用户态和内核态?

内核态具备特权操作外部资源,为了系统的安全和稳定。

3.如何实现用户态和内核态?

用户态和内核态的实现依赖于CPU的特权机制、内存空间的隔离以及对硬件资源(如磁盘)的受控访问。这样可以保证系统安全和稳定,防止用户程序对系统核心资源的非法访问。

4.什么时候需要切换?

用户态程序在需要操作系统完成特权操作的时候会切换到内核态。

5.什么条件引起用户态和内核态的切换?

系统调用

当用户程序需要操作硬件资源(如文件、网络、进程管理等)时,会通过系统调用请求操作系统服务。此时CPU会从用户态切换到内核态,进入内核执行相应的服务代码。

异常

当用户程序运行过程中发生异常(如除零错误、非法访问内存等),CPU会自动切换到内核态,由操作系统内核进行异常处理。

中断

当外部设备(如定时器、网卡、磁盘等)产生中断信号时,无论CPU当前处于用户态还是内核态,都会响应中断,切换到内核态执行中断服务程序。

6.用户态和内核态的区别?

运行实体

用户态:运行的是普通应用程序。

内核态:运行的是操作系统内核代码。

特权

用户态:CPU处于低特权级,不能直接访问硬件和受保护资源。

内核态:CPU处于高特权级,可以执行所有指令,访问所有资源。

安全

用户态:受限于操作系统,防止对系统和其他进程的非法操作,安全性高。

内核态:拥有全部控制权,若出错可能导致整个系统崩溃。

实现机制

用户态:通过特权级别、内存隔离等机制限制访问,只能通过系统调用进入内核态。

内核态:通过CPU特权指令、内存管理单元(MMU)等机制实现对资源的完全控制。

十一.内存管理方式

1.为什么要为进程分配虚拟地址空间

为了多进程之间的内存地址访问不受影响并且相互隔离,操作系统会为每个进程独立分配虚拟地址空间。

每个进程都要虚拟地址空间,而物理内存只有一个,当启动程序过多时,实际使用的内存超过物理内存,则发生内存交换。

内存交换:把不常用的内存交换到磁盘中,需要的时候再加载回内存。

虚拟地址需要映射到物理地址。

cpp 复制代码
+-------------------+         +-------------------+         +-------------------+
|  进程虚拟地址空间   |  ---->  |   页表(Page Table) |  ---->  |   物理内存(RAM)   |
+-------------------+         +-------------------+         +-------------------+
| 虚拟页号 | 页内偏移 |         | 虚拟页号 | 物理页框号 |         | 物理页框 | 数据 ...  |
+----------+--------+         +---------+----------+         +---------+---------+
      |                        |                             |
      |  虚拟页号查页表         |  页表查到物理页框号           |  物理页框号+偏移访问物理内存
      +----------------------->+--------------------------->+
2.分段内存管理
cpp 复制代码
+-------------------+
| 段0 | 段1 | 段2 ... |
+-------------------+
  |     |     |
  v     v     v
+-----+-----+-----+-------------------+
| 段0 | 段1 | 段2 | ...物理内存布局... |
+-----+-----+-----+-------------------+

逻辑地址 = 段号 + 段内偏移
通过段表找到段的起始地址,加上偏移访问物理内存

原理:将进程的地址空间划分为若干逻辑段(如代码段、数据段、堆栈段等),每个段长度不等,具有独立的起始地址和长度。

地址结构:逻辑地址由段号和段内偏移组成。

优点:支持程序的模块化和动态增长,便于保护和共享。

缺点:容易产生外部碎片,需要段表进行地址转换。

3.分页内存管理
cpp 复制代码
+--------------------------+
| 页0 | 页1 | 页2 | ...    |  进程虚拟地址空间
+--------------------------+
  |     |     |
  v     v     v
+-----+-----+-----+-------------------+
| 页框3 | 页框0 | 页框7 | ...物理内存... |
+-----+-----+-----+-------------------+

逻辑地址 = 页号 + 页内偏移
通过页表找到物理页框号,加上偏移访问物理内存

原理:将进程的虚拟地址空间和物理内存都划分为固定大小的页(Page)和页框(Frame),页与页框大小相同。

地址结构:逻辑地址由页号和页内偏移组成。

优点:消除了外部碎片,内存利用率高,便于离散分配。

缺点:可能产生内部碎片,需要页表进行地址转换。

4.段页式内存管理
cpp 复制代码
+-------------------+
| 段0 | 段1 | 段2 ... |  进程虚拟地址空间
+-------------------+
  |     |     |
  v     v     v
+-------------------+    每个段再分页
| 页0 | 页1 | ...   |  段0
+-------------------+
  |     |
  v     v
+-----+-----+-------------------+
| 页框2 | 页框5 | ...物理内存... |
+-----+-----+-------------------+

逻辑地址 = 段号 + 页号 + 页内偏移
先查段表,再查页表,最终定位物理内存

原理:结合分段和分页的优点。先将进程地址空间分段,每个段再分页。

地址结构:逻辑地址由段号、页号和页内偏移组成。

优点:既支持逻辑结构的分段,又支持物理内存的分页分配,减少碎片,灵活高效。

缺点:地址转换过程更复杂,需要段表和页表配合。

十二 .malloc是如何分配内存的

当我们调用malloc时,操作系统并不会每次都直接分配物理内存,而是由C运行时库维护一个内存池(堆),malloc会在这个内存池中查找一块足够大的空闲内存块,然后返回这块内存的首地址。如果没有足够的空间,malloc会向操作系统申请更大的内存区域(比如通过sbrk或mmap等系统调用),然后再分配给用户。

十三.写文件时进程宕机数据会丢失吗

缓冲机制:大多数操作系统和标准库在写文件时会先将数据写入内存缓冲区,而不是立即写入磁盘。如果进程在数据还未刷新到磁盘前宕机,这部分数据会丢失。

未调用flush或close :如果没有调用fflush()fclose()(C语言),或者文件句柄还未关闭,缓冲区的数据不会被强制写入磁盘。

操作系统缓存 :即使应用层已经flush,操作系统层面也可能还有缓存,只有在调用fsync()等系统调用后,数据才会真正落盘。

相关推荐
虾球xz8 小时前
CppCon 2016 学习:GAME ENGINE USING C++11
大数据·开发语言·c++·学习
虾球xz8 小时前
CppCon 2016 学习:fixed_point Library
开发语言·c++·学习
HaiQinyanAN8 小时前
【学习笔记】nlohmannjson&&cjson
c++·笔记·学习·json
C语言小火车8 小时前
【C语言】银行账户管理系统丨源码+解析
c语言·c++·算法·课程设计
东方芷兰9 小时前
Leetcode 刷题记录 17 —— 堆
java·c++·b树·算法·leetcode·职场和发展
lzb_kkk9 小时前
【MFC】编辑框、下拉框、列表控件
c语言·开发语言·c++·mfc·1024程序员节
漫步企鹅9 小时前
【PDF】Qt生成PDF文件,占用存储小
c++·qt·pdf
蚂蚁取经9 小时前
MFC动态链接库相关知识
c++·mfc
楼台的春风9 小时前
【Linux驱动开发 ---- 2.1_深入理解 Linux 内核架构】
linux·c++·人工智能·驱动开发·嵌入式硬件·ubuntu·架构
恒者走天下10 小时前
秋招是开发算法一起准备,还是只准备一个
c++