八股文-0907

知识列表

  1. 进程,线程,协程的区别
  2. 进程和线程在上下文切换时切换了什么,共享了什么
  3. 线程独占什么资源?在切换时内核内核执行了哪些操作?
  4. 进程的内存空间分布
  5. 一个可执行文件如何变成进程
  6. 什么时候该用多线程,什么时候该用多进程
  7. 僵尸进程,守护进程和孤儿进程的区别
  8. 什么情况适合用协程池,什么情况适合用线程池
  9. fork和system调用的区别
  10. CPU调度
  11. 进程如何实现访问隔离
  12. 进程的信号机制
  13. 用户级线程和内核级线程的区别
  14. 程序什么时候单线程效率高

1.进程,线程,协程的区别

进程是一个正在执行的程序的实例。进程是操作系统分配资源的基本单位,包含程序代码,当前活动的程序计数器,寄存器状态,堆栈和数据段

进程特性:

  1. 独立性:每个进程都有自己的地址空间,进程间相互独立,一个进程的崩溃不会影响其他进程
  2. 资源拥有:进程拥有自己的资源(如内存,文件描述符等等),这些资源在进程创建时分配
  3. 上下文切换:进程切换时需要保存和恢复大量的上下文信息,开销较大

优点:进程之间的隔离性强,安全性高。适合处理CPU密集型任务

缺点:创建和销毁进程的开销较大。进程间通信复杂,通常需要使用IPC(进程间通信)机制,如管道,消息队列,共享内存等

应用场景:多任务操作系统中的程序并发执行,需要高安全性和稳定性的应用,如服务器应用

线程是进程内的一个执行单元,是程序执行的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源。

线程特性:

  1. 共享资源:同一进程内的所有线程共享该进程的内存空间和资源
  2. 轻量级:线程的创建和销毁比进程更快,切换开销也较小
  3. 并发执行:多个线程可以并发执行,适合多核处理器

优点:资源开销小,创建和销毁速度快;线程间通信效率高(因为共享内存)

缺点:由于共享资源,线程间的同步和互斥问题就需要特别处理,容易引发死锁和竞争条件;线程安全问题需要额外的控制和管理

应用场景:I/O密集型任务,如网络请求,文件读写等;需要并发执行的应用,如图形用户界面(GUI)程序

协程是一种轻量级的用户级线程,允许在单个线程中运行多任务处理,协程通过非抢占式调度实现协作式多任务。

协程特性:

  1. 用户级调度:协程的调度由程序员控制,通常通过yield或await等关键字来暂停和恢复执行
  2. 共享上下文:协程通常在同一线程中运行,使用相同的内存空间
  3. 轻量级:协程的创建和切换开销非常小,适合高并发的场景

优点:资源开销小,创建和切换速度快;适合处理大量I/O密集型任务,提高资源利用率

缺点:协程的调度需要程序员手动管理,可能导致复杂性增加;由于协程共享同一线程的上下文,容易出现状态管理问题

应用场景:网络爬虫、异步 I/O 操作;游戏开发中的状态机实现;高并发服务器,如 Web 服务器,处理大量连接

  • 进程:适合需要高安全性和稳定性的应用,资源开销大,适合 CPU 密集型任务。
  • 线程:适合 I/O 密集型任务,资源开销小,但需要处理线程安全问题。
  • 协程:适合高并发 I/O 密集型任务,开销极小,但需要程序员手动管理调度。
#include <iostream>
#include <thread>
#include <coroutine> // C++20 开始支持协程
#include <future>
// 线程函数
void threadFunction() {
std::cout << "Hello from Thread" << std::endl;
}
// 协程函数
std::future<void> coroutineFunction() {
co_await std::suspend_always{};
std::cout << "Hello from Coroutine" << std::endl;
}
int main() {
// 创建进程的例子通常涉及创建完全独立的应用实例
// 创建并启动线程
std::thread t(threadFunction);
if (t.joinable()) t.join();
// 启动协程
auto future = coroutineFunction();
future.wait(); // 等待协程完成
return 0;
}

2.进程和线程在上下文切换时切换了什么,共享了什么

进程切换上下文时,操作系统需要保存当前进程的状态(包含程序计数器,寄存器集,内存管理信息等)并加载新进程状态。进程切换涉及到完整的硬件地址空间切换,包括页表,缓存清空等,因此开销较大。

线程共享进程的内存空间(代码段,数据段和打开的文件等),但各自都有独立的栈(用于存储执行历史)、寄存器状态和程序计数器。独立内容:线程栈,局部变量,寄存器和程序计数器。

线程上下文切换:主要用于保存和恢复寄存器状态,程序计数器和栈的指针。开销相较于进程来说不需要切换内存地址空间

3.线程独占什么资源?在切换时内核内核执行了哪些操作?

线程作为进程的执行单元,虽然与其他线程共享大部分资源,但他们仍然拥有一些独占资源:

  1. 线程栈(Thread Stack):每个线程拥有自己的调用栈,用于存储函数调用的局部变量,返回地址等,这些是必须的,因为每个线程可能在执行不同任务或函数调用序列
  2. 程序计数器(Program Counter,PC):程序计数器指向线程下一条执行指令的位置。每个线程必须独立维护自己的程序计数器,以保证线程执行的正确性
  3. 线程特定数据(Thread-Local Storage,TLS):线程可以拥有特定的数据,即时在相同的进程中,这些数据也不被其他线程共享
  4. 寄存器集(Registers):每个线程都有自己的寄存器集,这包括通用寄存器,索引寄存器,栈指针和状态寄存器。这些寄存器在执行过程中保存线程特有的状态信息

在切换时内核内核执行了哪些操作:

线程切换通常由操作系统内核自动管理的,当发生线程切换时,内核需要执行:(来保证系统的稳定和线程的正确执行)

  1. 保存线程状态:内核首先保存当前线程的寄存器状态,程序计数器和其他关键信息到线程的控制块中(如线程上下文)。这确保了线程可以在之后的某个时刻从相同的位置继续执行。
  2. 加载新线程的状态:内核从即将运行的线程的控制块加载寄存器状态,程序计数器等信息,准备这个线程的执行
  3. 更新调度结构:内核可能需要更新内部的调度数据结构,例如优先级队列或事件尿等待列表,以反映当前活跃或等待的线程变化
  4. 栈切换:线程的栈指针需要被更新,以指向即将执行的线程的栈顶
  5. 处理器缓存:为了效率,有时候还需要清理或更新处理器缓存,尤其是多核处理器上,以避免缓存一致性问题。

线程切换比进程切换开销小,因为线程切换不涉及地址空间的变化,但仍然涉及到显著的处理

开销。因此,在设计多线程程序时,应尽量减少不必要的线程切换,以优化性能。

4.进程的内存空间分布

  1. 程序计数器或其他寄存器:这部分通常不直接出现在内存布局图中,但对于理解程序如何控制流和管理状态是至关重要的
  2. 栈:位于内存布局中较高部分(从高地址向低地址增长),存放函数的局部变量,返回地址等
  3. 堆:位于栈和全局/静态变量之间,用于动态分配内存,其大小和位置在运行时可变
  4. 未初始化数据(BSS):存放未初始化或初始化为零的全局变量和静态变量,位于初始化数据段之下
  5. 初始化数据:包含已初始化的全局变量和静态变量
  6. 代码段:位于最底部,存放程序的机器指令,通常为只读以防止修改

5.一个可执行文件如何变成进程

  1. 加载可执行文件
    1. 启动过程:点击程序图标或在命令行输入程序名称时,操作系统的shell或其他页面将用户的请求转化为启动程序的指令
    2. 读取文件:操作系统读取可执行文件的元数据(如ELF在Linux,PE在WIndows),这些元数据包括程序的入口点,所需资源,依赖库信息
  2. 创建进程控制块(PCB) 分配PCB:操作系统为新进程创建一个进程控制块(PCB),PCB是操作系统用来维护进程状态,程序计数器,CPU寄存器信息,内存管理信息和其他关键信息的数据结构
  3. 分配内存
    1. 分配地址空间:操作系统为进程分配独立的虚拟地址空间,这些包括程序的代码段,数据段,堆,栈分配内存
    2. 设置内存保护:操作系统设置内存访问权限,例如代码段设置为只读
  4. 初始化CPU寄存器
  5. 加载依赖库如果程序依赖于共享库(.dll或.so),操作系统将这些库加载到内存中。如果这些库已经被加载,操作系统会重用已加载的库
  6. 初始化程序运行环境
    1. 环境设置:操作系统设置程序运行的环境变量和输入输出设备
    2. 传递参数:命令行参数和环境变量被传递给进程
  7. 开始执行操作系统将程序计数器(PC)设置到可执行文件指定的入口点,开始执行程序的第一条指令
  8. 调度和执行操作系统的调度器将进程加入到调度队列,根据调度策略(如轮询,优先级调度等),进程将被CPU执行

6.什么时候该用多线程,什么时候该用多进程

(1)使用多线程的情况:

1)共享内存和资源:当需要多个执行单元共享大量数据或状态时,线程是更好的选择。线程

共享其所属进程的内存空间和资源,如文件句柄和数据结构等,这使得线程间的数据交换和通信更

加高效。

2)轻量级并发任务:线程的创建和销毁比进程更快,资源开销较小。如果应用程序需要频繁

地创建和销毁执行单元,或者需要大量轻量级的并发操作,线程通常是更合适的选择。

3)响应性要求高的应用程序:在需要快速响应用户输入或其他事件的应用程序中使用线程,

可以通过并行执行提高应用程序的响应性。

(2)使用多进程的情况:

1)隔离和安全性:进程提供了更好的隔离级别,每个进程拥有自己的内存空间和系统资源。

这种隔离可以防止进程间的干扰,并提高系统的稳定性。在安全性和稳定性要求较高的应用中,如

网银系统,多进程是更佳的选择。

2)利用多核处理器:多进程可以更好地利用多核处理器的能力。操作系统可以将不同的进程

调度到不同的 CPU 核心上,从而提高程序的执行效率和系统的整体性能。

3)资源密集型应用:对于大量计算和资源需求很高的应用程序,使用多进程可以避免一个进

程中的错误影响到其他进程,提高程序的健壮性。

(3)具体应用场景示例:

1)多线程:网络服务器和客户端应用程序,如 Web 服务器和现代 Web 浏览器。多媒体应

用,如视频播放软件,需要在后台加载数据的同时保持用户界面的响应。

2)多进程:数据科学和大数据处理应用,每个进程执行不同的数据集分析。大型计算应用,

如渲染软件,科学计算软件,它们需要大量计算资源而且稳定性要求极高

7.僵尸进程,守护进程和孤儿进程的区别

僵尸进程:通常发生在父进程没有正确处理子进程终止状态的情况。僵尸进程就是已终止但仍在进程表中占位置的进程,不占用系统资源(内存和CPU),只保留在进程表中的一个位置,僵尸进程会耗尽进程表空间,可能阻止新进程的创建。需要父进程调用wait来处理

守护进程:crond,syslogd,在Linux系统启动时初始化并持续运行;也就是说守护进程是在后台运行的长生命周期进程,用于处理系统级任务,提供系统服务如日志记录,系统监控等,不直接与用户交互;特点是没有控制终端,独立于用户交互的操作,父进程通常是init

孤儿进程:如果一个服务进程在启动子任务后异常终止,子任务会成为孤儿进程;所以孤儿进程就是父进程终止后仍在运行的子进程,孤儿进程会被Init进程或其他系统级进程接管,继续运行知道完成或被显式终止,孤儿进程由系统自动管理,通常不会导致系统资源泄漏或性能问题

(1)僵尸进程:僵尸进程通常是程序编写不当造成的,特别是在父进程未能正确管理子进程

的终止。这些进程已经完成执行但未被父进程回收,导致在系统的进程表中占据条目。系统管理员

通常需要定期检查并清理僵尸进程,以保持系统资源的有效利用。

(2)守护进程:守护进程通常在系统引导时启动,以 root 权限或其他特定用户身份运行。它

们的设计目的是使其在没有用户登录时后台运行,执行如电子邮件服务、打印服务等任务。守护进程的创建通常需要特定的编程技巧,如在 C 语言中通过 fork()产生子进程然后结束父进程,使子进

程成为 init 的子进程,这样就没有控制终端,可以在后台运行。

(3)孤儿进程:孤儿进程不会对系统性能造成负面影响,因为操作系统(如 Linux 的 init 进

程)会自动接管这些进程并在适当的时候进行清理。操作系统的 init 进程会周期性地调用 wait()来

回收任何已终止子进程的资源,防止它们变成僵尸进程。

8.什么情况适合用协程池,什么情况适合用线程池

(1)线程池

线程池是一组预先分配的线程,用于执行多个任务。它们在以下情况中非常适合使用:

1)CPU 密集型任务:线程池适合执行需要大量计算的任务。由于操作系统可以将线程映射到

多个处理器上,线程池可以有效地使用多核处理器的能力,提高计算密集型任务的处理速度。

2)适度的 I/O 操作:对于涉及适度 I/O 操作的任务(例如,I/O 和计算任务交错执行),线程

池可以在等待 I/O 完成时切换到其他任务,从而提高效率。

3)任务执行时间较长:线程池适用于执行时间较长的任务,因为线程的切换和管理开销相对较

高,使用线程池可以减少这种开销。

4)语言或环境支持有限:在某些编程环境中,如 Java 或 C#,线程是这些环境提供的主要并发

原语,而协程的支持可能有限或不够成熟。

(2)协程池

协程是轻量级的线程,它们在用户空间内进行调度,不需要操作系统介入。协程池在以下情况中非

常适合使用:

1)高 I/O 密集型应用:对于大量 I/O 操作,如网络请求或数据库操作,协程池可以在等待 I/O

操作完成时释放 CPU,切换到其他协程执行,极大地提高了资源利用率和吞吐量。

2)大量短暂任务:协程的切换开销非常小,非常适合处理大量的短暂任务,如处理大量的小网

络请求或消息。

3)需要高并发的应用:协程池可以轻松创建和管理成千上万的协程,因为它们的资源需求比线

程小得多,更适合构建高并发应用程序。(3)总结

1)使用线程池:适合计算密集型任务,需要利用多核优势,或者执行的任务中计算与 I/O 操作

相对均衡。

2)使用协程池:适合 I/O 密集型应用,需要处理大量并发的小任务,或者当编程环境对协程有

很好的支持时

9.fork和system调用的区别

fork和system是两个常用的系统调用,用于进程的创建和管理

fork:创建一个新的子进程,该子进程是调用进程的副本,子进程继承了父进程的地址空间,文件描述符等资源,但有独立的进程ID,适用于多进程编程,需要手动管理进程间的通信和同步

system:执行一个命令字符串,通常是外部程序,system调用会创建一个子进程来运行指定的命令,父进程等待子进程完成,适用于需要程序中执行外部命令或脚本的场景,简化了执行外部命令的过程,但缺乏对新进程的细粒度控制

10.CPU调度

CPU 调度是操作系统根据某种策略选择下一个要运行的进程的过程。调度的基本单位可以是进

程或线程,具体取决于操作系统的设计。CPU 调度的主要目标是提高系统的效率和资源利用率,提

供公平的资源分配,并满足系统和用户的性能要求。

11.进程如何实现访问隔离

Linux
  1. 虚拟内存管理:Linux使用硬件支持的虚拟内存,通过页表将每个进程的虚拟地址空间到物理内存,每个进程都有自己的页表,确保其不能访问到其他进程的内存空间
  2. 内核模式和用户模式:进程在用户模式下运行其代码,而系统资源的管理和访问控制则通过内核模式下进行。系统调用是进程从用户模式切换到内核模式的桥梁
  3. Cgroups和命名空间:Linux提供了控制组(Cgroups)和命名空间技术,可以进一步隔离进程的资源调用(如CPU,内存)和运行环境(如网络,文件系统)
Windows
  1. 虚拟内存管理:与Linux类似,WIndows为每个进程分配一个独立的虚拟地址空间,通过硬件支持的页表来隔离不同的进程的内存访问
  2. 访问令牌和安全描述符:Windows使用访问令牌(Acess Tokens)和安全描述符(Security Descriptors)来实施安全策略。每个进程或线程都有一个与之关联的访问令牌,其中包含了用户身份和权限信息,用于访问控制策略
  3. Intergrity Levels:Windows引入了完整性级别(Integrity Levels),这是一个用于确定进程和其他对象之间交互权限的机制。例如,一个低完整性级别的进程不能写入一个高完整性级别的对象。
实际应用

1)多用户环境:在多用户操作系统中,例如在一个共享的服务器上,访问隔离确保了一个用户的活动不会影响到其他用户的进程,无论是数据安全还是系统稳定性。

2)云计算平台:在云计算环境中,虚拟化技术(如 KVM、Xen、VMware)和容器化技术(如

Docker、Kubernetes)依赖于操作系统的隔离机制来确保不同客户的应用相互独立,保障数据隐私

和应用安全。

12.进程的信号机制

(1)忽视信号的机制

进程可以通过设置信号处理函数来决定如何响应特定的信号。对于大多数信号,进程可以选择

以下几种方式之一进行响应:

1)默认行为:执行信号默认的操作,这通常包括终止进程、停止(暂停)进程、忽略信号等。

2)捕获信号:指定一个函数来处理信号。当信号发生时,系统会调用这个函数。

3)忽略信号:明确指示系统忽视该信号。对于可以被忽视的信号,这意味着当信号被递送时,

系统不会对进程采取任何行动。

(2)可以忽视的信号

在 Unix 和类 Unix 系统中,大多数信号都可以被忽视,但有两个例外:

1)SIGKILL:此信号用于强制终止进程。进程不能捕获、忽视或更改此信号的行为。

2)SIGSTOP:此信号用于强制停止(暂停)进程的执行。与 SIGKILL 一样,它不能被捕获、

忽视或更改。

(3)如何设置忽视信号

在 C 语言中,你可以使用 signal()函数或更为复杂但功能更全面的 sigaction()函数来设置信号

处理方式。signal(SIGINT, SIG_IGN);

13.用户级线程和内核级线程的区别

用户级线程
  1. 线程管理:用户级线程的创建,管理和销毁都在用户空间进行,不需要内核的参与,这些线程通常由线程库(如POSIX线程库)管理
  2. 上下文切换:由于上下文切换在用户空间完成,不涉及内核态的切换,因此速度小,开销小
  3. 系统调用:用户级线程的操作无需系统调用,这使得线程操作更加高效,但在进行I/O操作时,整个进程会被阻塞
  4. 多线程调度:用户级线程由用户级库调度,无法利用多核处理器的优势,因为内核只看到单个进程,而不是识别内部的多个线程
内核级线程
  1. 线程管理:内核级线程的创建,管理和销毁由操作系统内核负责,需要通过系统调用进行
  2. 上下文切换:上下文切换需要从用户态切换到内核态,这使得切换速度较慢,开销较大
  3. 系统调用:内核级线程的操作需要系统调用,尽管开销较大,但可以利用操作系统的各种功能,如进程调度和I/O调度
  4. 多线程调度:内核级线程由操作系统调度,可以充分利用多核处理器的优势,因为内核能够识别和管理每个线程

14.程序什么时候单线程效率高

(1)任务不需要并行处理

如果任务本质上是顺序的,没有并行执行的需求或潜力,使用单线程可能更简单且高效。这类

任务包括对数据进行线性处理,如读取文件然后处理数据,再写回文件。

(2)避免上下文切换的开销

多线程程序涉及线程之间的上下文切换,尤其是在单核处理器上运行时,这会带来显著的性能

开销。单线程程序没有这种额外负担,因此在资源受限的环境中可能表现得更好。

(3)避免同步机制的开销

多线程程序需要同步机制(如互斥锁、信号量等)来管理对共享资源的访问,这不仅增加了编

程的复杂性,还可能引入死锁和竞态条件等问题。这些同步操作本身就是一种开销,可能会降低程

序的运行效率。单线程程序则无需考虑这些问题。

(4)I/O 密集型应用

在 I/O 密集型应用中,程序的性能瓶颈通常是磁盘 I/O 或网络 I/O,而不是 CPU。在这种情况

下,使用多线程可能不会带来明显的性能提升,因为大部分时间都花在了等待 I/O 操作完成上。单

线程模型在这种情况下简化了设计,且效率足够高。

(5)内存使用优化

多线程程序每个线程都可能需要自己的堆栈等资源,这在内存使用上可能不如单线程程序高效。

在内存资源受限的环境中,单线程程序能更好地利用有限的内存资源。

(6)非共享资源的操作如果程序中的任务完全独立,不需要共享任何资源,单线程执行可能更为直接和高效。这样可以避免多线程程序中资源锁定和同步的复杂性。