知识列表
- 进程,线程,协程的区别
- 进程和线程在上下文切换时切换了什么,共享了什么
- 线程独占什么资源?在切换时内核内核执行了哪些操作?
- 进程的内存空间分布
- 一个可执行文件如何变成进程
- 什么时候该用多线程,什么时候该用多进程
- 僵尸进程,守护进程和孤儿进程的区别
- 什么情况适合用协程池,什么情况适合用线程池
- fork和system调用的区别
- CPU调度
- 进程如何实现访问隔离
- 进程的信号机制
- 用户级线程和内核级线程的区别
- 程序什么时候单线程效率高
1.进程,线程,协程的区别
进程是一个正在执行的程序的实例。进程是操作系统分配资源的基本单位,包含程序代码,当前活动的程序计数器,寄存器状态,堆栈和数据段
进程特性:
- 独立性:每个进程都有自己的地址空间,进程间相互独立,一个进程的崩溃不会影响其他进程
- 资源拥有:进程拥有自己的资源(如内存,文件描述符等等),这些资源在进程创建时分配
- 上下文切换:进程切换时需要保存和恢复大量的上下文信息,开销较大
优点:进程之间的隔离性强,安全性高。适合处理CPU密集型任务
缺点:创建和销毁进程的开销较大。进程间通信复杂,通常需要使用IPC(进程间通信)机制,如管道,消息队列,共享内存等
应用场景:多任务操作系统中的程序并发执行,需要高安全性和稳定性的应用,如服务器应用
线程是进程内的一个执行单元,是程序执行的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源。
线程特性:
- 共享资源:同一进程内的所有线程共享该进程的内存空间和资源
- 轻量级:线程的创建和销毁比进程更快,切换开销也较小
- 并发执行:多个线程可以并发执行,适合多核处理器
优点:资源开销小,创建和销毁速度快;线程间通信效率高(因为共享内存)
缺点:由于共享资源,线程间的同步和互斥问题就需要特别处理,容易引发死锁和竞争条件;线程安全问题需要额外的控制和管理
应用场景:I/O密集型任务,如网络请求,文件读写等;需要并发执行的应用,如图形用户界面(GUI)程序
协程是一种轻量级的用户级线程,允许在单个线程中运行多任务处理,协程通过非抢占式调度实现协作式多任务。
协程特性:
- 用户级调度:协程的调度由程序员控制,通常通过yield或await等关键字来暂停和恢复执行
- 共享上下文:协程通常在同一线程中运行,使用相同的内存空间
- 轻量级:协程的创建和切换开销非常小,适合高并发的场景
优点:资源开销小,创建和切换速度快;适合处理大量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.线程独占什么资源?在切换时内核内核执行了哪些操作?
线程作为进程的执行单元,虽然与其他线程共享大部分资源,但他们仍然拥有一些独占资源:
- 线程栈(Thread Stack):每个线程拥有自己的调用栈,用于存储函数调用的局部变量,返回地址等,这些是必须的,因为每个线程可能在执行不同任务或函数调用序列
- 程序计数器(Program Counter,PC):程序计数器指向线程下一条执行指令的位置。每个线程必须独立维护自己的程序计数器,以保证线程执行的正确性
- 线程特定数据(Thread-Local Storage,TLS):线程可以拥有特定的数据,即时在相同的进程中,这些数据也不被其他线程共享
- 寄存器集(Registers):每个线程都有自己的寄存器集,这包括通用寄存器,索引寄存器,栈指针和状态寄存器。这些寄存器在执行过程中保存线程特有的状态信息
在切换时内核内核执行了哪些操作:
线程切换通常由操作系统内核自动管理的,当发生线程切换时,内核需要执行:(来保证系统的稳定和线程的正确执行)
- 保存线程状态:内核首先保存当前线程的寄存器状态,程序计数器和其他关键信息到线程的控制块中(如线程上下文)。这确保了线程可以在之后的某个时刻从相同的位置继续执行。
- 加载新线程的状态:内核从即将运行的线程的控制块加载寄存器状态,程序计数器等信息,准备这个线程的执行
- 更新调度结构:内核可能需要更新内部的调度数据结构,例如优先级队列或事件尿等待列表,以反映当前活跃或等待的线程变化
- 栈切换:线程的栈指针需要被更新,以指向即将执行的线程的栈顶
- 处理器缓存:为了效率,有时候还需要清理或更新处理器缓存,尤其是多核处理器上,以避免缓存一致性问题。
线程切换比进程切换开销小,因为线程切换不涉及地址空间的变化,但仍然涉及到显著的处理
开销。因此,在设计多线程程序时,应尽量减少不必要的线程切换,以优化性能。

4.进程的内存空间分布

- 程序计数器或其他寄存器:这部分通常不直接出现在内存布局图中,但对于理解程序如何控制流和管理状态是至关重要的
- 栈:位于内存布局中较高部分(从高地址向低地址增长),存放函数的局部变量,返回地址等
- 堆:位于栈和全局/静态变量之间,用于动态分配内存,其大小和位置在运行时可变
- 未初始化数据(BSS):存放未初始化或初始化为零的全局变量和静态变量,位于初始化数据段之下
- 初始化数据:包含已初始化的全局变量和静态变量
- 代码段:位于最底部,存放程序的机器指令,通常为只读以防止修改
5.一个可执行文件如何变成进程
- 加载可执行文件
- 启动过程:点击程序图标或在命令行输入程序名称时,操作系统的shell或其他页面将用户的请求转化为启动程序的指令
- 读取文件:操作系统读取可执行文件的元数据(如ELF在Linux,PE在WIndows),这些元数据包括程序的入口点,所需资源,依赖库信息
- 创建进程控制块(PCB) 分配PCB:操作系统为新进程创建一个进程控制块(PCB),PCB是操作系统用来维护进程状态,程序计数器,CPU寄存器信息,内存管理信息和其他关键信息的数据结构
- 分配内存
- 分配地址空间:操作系统为进程分配独立的虚拟地址空间,这些包括程序的代码段,数据段,堆,栈分配内存
- 设置内存保护:操作系统设置内存访问权限,例如代码段设置为只读
- 初始化CPU寄存器
- 加载依赖库如果程序依赖于共享库(.dll或.so),操作系统将这些库加载到内存中。如果这些库已经被加载,操作系统会重用已加载的库
- 初始化程序运行环境
- 环境设置:操作系统设置程序运行的环境变量和输入输出设备
- 传递参数:命令行参数和环境变量被传递给进程
- 开始执行操作系统将程序计数器(PC)设置到可执行文件指定的入口点,开始执行程序的第一条指令
- 调度和执行操作系统的调度器将进程加入到调度队列,根据调度策略(如轮询,优先级调度等),进程将被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
- 虚拟内存管理:Linux使用硬件支持的虚拟内存,通过页表将每个进程的虚拟地址空间到物理内存,每个进程都有自己的页表,确保其不能访问到其他进程的内存空间
- 内核模式和用户模式:进程在用户模式下运行其代码,而系统资源的管理和访问控制则通过内核模式下进行。系统调用是进程从用户模式切换到内核模式的桥梁
- Cgroups和命名空间:Linux提供了控制组(Cgroups)和命名空间技术,可以进一步隔离进程的资源调用(如CPU,内存)和运行环境(如网络,文件系统)
Windows
- 虚拟内存管理:与Linux类似,WIndows为每个进程分配一个独立的虚拟地址空间,通过硬件支持的页表来隔离不同的进程的内存访问
- 访问令牌和安全描述符:Windows使用访问令牌(Acess Tokens)和安全描述符(Security Descriptors)来实施安全策略。每个进程或线程都有一个与之关联的访问令牌,其中包含了用户身份和权限信息,用于访问控制策略
- 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.用户级线程和内核级线程的区别

用户级线程
- 线程管理:用户级线程的创建,管理和销毁都在用户空间进行,不需要内核的参与,这些线程通常由线程库(如POSIX线程库)管理
- 上下文切换:由于上下文切换在用户空间完成,不涉及内核态的切换,因此速度小,开销小
- 系统调用:用户级线程的操作无需系统调用,这使得线程操作更加高效,但在进行I/O操作时,整个进程会被阻塞
- 多线程调度:用户级线程由用户级库调度,无法利用多核处理器的优势,因为内核只看到单个进程,而不是识别内部的多个线程
内核级线程
- 线程管理:内核级线程的创建,管理和销毁由操作系统内核负责,需要通过系统调用进行
- 上下文切换:上下文切换需要从用户态切换到内核态,这使得切换速度较慢,开销较大
- 系统调用:内核级线程的操作需要系统调用,尽管开销较大,但可以利用操作系统的各种功能,如进程调度和I/O调度
- 多线程调度:内核级线程由操作系统调度,可以充分利用多核处理器的优势,因为内核能够识别和管理每个线程
14.程序什么时候单线程效率高
(1)任务不需要并行处理
如果任务本质上是顺序的,没有并行执行的需求或潜力,使用单线程可能更简单且高效。这类
任务包括对数据进行线性处理,如读取文件然后处理数据,再写回文件。
(2)避免上下文切换的开销
多线程程序涉及线程之间的上下文切换,尤其是在单核处理器上运行时,这会带来显著的性能
开销。单线程程序没有这种额外负担,因此在资源受限的环境中可能表现得更好。
(3)避免同步机制的开销
多线程程序需要同步机制(如互斥锁、信号量等)来管理对共享资源的访问,这不仅增加了编
程的复杂性,还可能引入死锁和竞态条件等问题。这些同步操作本身就是一种开销,可能会降低程
序的运行效率。单线程程序则无需考虑这些问题。
(4)I/O 密集型应用
在 I/O 密集型应用中,程序的性能瓶颈通常是磁盘 I/O 或网络 I/O,而不是 CPU。在这种情况
下,使用多线程可能不会带来明显的性能提升,因为大部分时间都花在了等待 I/O 操作完成上。单
线程模型在这种情况下简化了设计,且效率足够高。
(5)内存使用优化
多线程程序每个线程都可能需要自己的堆栈等资源,这在内存使用上可能不如单线程程序高效。
在内存资源受限的环境中,单线程程序能更好地利用有限的内存资源。
(6)非共享资源的操作如果程序中的任务完全独立,不需要共享任何资源,单线程执行可能更为直接和高效。这样可以避免多线程程序中资源锁定和同步的复杂性。