文章目录
-
- [01 进程、线程、协程分别是什么?](#01 进程、线程、协程分别是什么?)
- [02 描述一下进程的空间模型?](#02 描述一下进程的空间模型?)
- [03 多进程多线程多协程的优缺点](#03 多进程多线程多协程的优缺点)
- [04 多进程、多线程同步通信方式](#04 多进程、多线程同步通信方式)
-
- [4.1 多进程](#4.1 多进程)
- [4.2 多线程](#4.2 多线程)
- [05 进程线程的状态转换?](#05 进程线程的状态转换?)
- [06 进程上下文是什么?](#06 进程上下文是什么?)
- [07 中断上下文是什么?](#07 中断上下文是什么?)
- [08 父进程、子进程及其关系和区别](#08 父进程、子进程及其关系和区别)
01 进程、线程、协程分别是什么?
**进程是程序在计算机中执行的实例,是操作系统进行资源分配和调度的基本单位。**每个进程都有自己的虚拟地址空间、代码段、数据段以及系统资源(如文件描述符、IO资源等)。
**线程是进程中的一个执行单元,线程在进程内共享资源(如内存、文件描述符等),但每个线程有独立的执行栈和程序计数器。**线程是 CPU 调度的最小单位,多个线程可以并发地执行。
协程是一种用户级的线程,它是轻量级的、由程序员控制调度的并发单元。协程通常由程序员主动切换,而不像线程那样由操作系统进行调度。
进程、线程、协程的区别如下:
调度单位 | 操作系统调度 | 操作系统调度 | 用户调度(程序员控制) |
---|---|---|---|
内存隔离 | 每个进程有独立的内存空间 | 线程共享进程的内存空间 | 协程共享进程的内存空间 |
资源消耗 | 开销大(包括创建和上下文切换) | 较小(比进程小,但比协程大) | 开销最小(无操作系统介入) |
创建销毁开销 | 较大 | 较小 | 非常小(仅为函数调用) |
并发性能 | 并发能力受限于操作系统调度 | 多线程能提高并发能力 | 高并发,适合I/O密集型任务 |
通信方式 | 进程间通信(IPC) | 线程间通信(共享内存、锁等) | 协程间直接通过共享内存、变量等 |
使用场景 | 适用于需要高度隔离的任务 | 适用于需要高并发的CPU密集型任务 | 适用于大量I/O密集型操作 |
注意:
- 一个进程可以包含多个线程,一个线程从属于一个进程。一个线程可以有多个协程。
- 一个进程的消失不会影响其他进程,但是一个线程的消失会使相对应的进程消失。
- 协程不需要多线程的锁机机制,因为多个协程从属于一个线程,不存在同时写变量冲突。并且效率上比线程高。
02 描述一下进程的空间模型?
通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间具体如图,内核空间是受保护的,用户不能对该空间进行读写操作。
![](https://i-blog.csdnimg.cn/direct/5a309719273e4ec192789b946e602520.jpeg)
栈的空间有限,堆是很大的自由存储区,程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也是在栈上进行。
程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。
初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。
未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。
栈 (Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。
堆 (Heap):存储[动态内存分配,需要程序员手工分配,手工释放。注意它与数据结构中的堆是两回事,分配方式类似于链表。
注意:栈空间有向下的箭头,代表数据地址增加的空间是往下的,新的数据的地址的值反而更小,堆空间则是往上。
03 多进程多线程多协程的优缺点
内存开销 | 高,每个进程有独立内存空间 | 较小,线程共享进程的内存 | 非常小,多个协程共享线程内存 |
---|---|---|---|
创建/销毁开销 | 高,进程创建和销毁较慢 | 较低,线程创建和销毁较快 | 极低,协程创建和销毁非常快 |
上下文切换效率 | 低,进程切换涉及更多资源 | 较高,线程切换相对较轻 | 非常高,由用户级调度控制 |
并行性 | 高,支持多核并行处理 | 高,支持多核并行处理 | 仅限于单线程,不支持多核并行 |
稳定性 | 高,进程独立,一个崩溃不会影响其他进程 | 较低,线程共享内存,一个线程崩溃可能影响进程 | 较低,协程在同一线程中,崩溃会影响整个进程 |
线程/进程间通信 | 复杂,需要使用IPC(如管道、消息队列等) | 简单,线程共享内存,易于通信 | 极其简单,通过共享变量或异步IO等 |
适用场景 | 适用于CPU密集型任务,需要高独立性和并行性 | 适用于I/O密集型任务,尤其需要多任务并发时 | 适用于I/O密集型任务,极高并发且资源使用少 |
开发难度 | 较高,进程间通信和同步复杂 | 中等,需要处理线程同步和死锁等问题 | 低,协程的开发较简单,但调度和控制需要注意 |
调试难度 | 中等,进程间隔离相对容易调试 | 较高,线程安全问题导致调试较困难 | 高,调试协程中的并发和状态控制较复杂 |
04 多进程、多线程同步通信方式
4.1 多进程
(1)管道
- 匿名管道:用于父子进程之间的通信,通常为单向通信。
- 命名管道:允许不同的进程通过文件系统中的命名管道进行通信,支持双向通信。
- 优点:简单、轻量。
- 缺点:只能用于父子进程或具有相关关系的进程。
(2)消息队列
- 允许进程通过消息队列发送和接收消息,消息按顺序排队。
- 优点:提供了进程间异步通信,支持多个生产者和消费者。
- 缺点:性能受限,特别是消息队列长度较长时。
(3)共享内存
- 允许多个进程访问同一块内存区域,速度较快,但需要显式的同步机制(如信号量或互斥锁)。
- 优点:速度快,适合大数据量的共享。
- 缺点:需要额外的同步机制来保证数据一致性。
(4)信号量
- 信号量用于控制对共享资源的访问,通过计数来实现资源的限制,防止多个进程同时访问共享资源。
- 优点:可以有效地防止竞争条件,控制并发。
- 缺点:实现复杂,容易产生死锁。
(5)互斥锁
- 互斥锁是一种同步原语,确保同一时间只有一个进程能够访问某个资源。
- 优点:确保数据的一致性,防止竞争条件。
- 缺点:可能会产生死锁,需要小心设计。
4.2 多线程
(1)互斥锁/锁
- 互斥锁是最常用的同步工具,确保只有一个线程能访问共享资源,其他线程必须等待锁被释放。
- 优点:简单直接,保证了线程安全。
- 缺点:容易产生死锁,锁粒度过大会影响性能。
(2)条件变量
- 条件变量用于线程间的通知机制。当某个条件满足时,线程可以通过条件变量通知其他线程继续执行。
- 适合的场景是,线程在某个条件不满足时,需要等待一段时间或直到其他线程改变状态。
(3)读写锁
- 读写锁允许多个线程同时读取共享资源,但在写操作时会排它性地独占资源。适用于读操作频繁、写操作较少的场景。
- 优点:提高了读操作的并发性。
- 缺点:写操作时必须获得独占锁,可能会阻塞读操作。
(4)信号量
- 信号量是一种用于限制同时访问共享资源的线程数量的同步机制。它维护一个计数器,线程在进入临界区之前需要获取信号量,计数器为0时线程会被阻塞。
- 适用于有固定数量资源的情况。
(5)队列
- 使用队列进行线程间的通信
- 适用于生产者-消费者模式,一个线程向队列放入数据,另一个线程从队列中取出数据。
- 优点:队列是线程安全的,不需要显式的锁。
- 缺点:可能会影响性能,尤其是高并发的情况下。
(6)事件
- 事件对象用于线程间同步,当一个线程设置事件,其他线程可以等待该事件的发生来执行某些操作。
- 适用于需要线程等待某个特定事件发生的场景。
(7)共享变量
- 可以直接通过共享全局变量来进行通信,但这种方法需要加锁保证线程安全。
- 适用于少量数据交换的场景。
05 进程线程的状态转换?
创建态:一个进程正在被创建,还没到转到就绪状态之前的状态。操作系统为新进程分配资源、创建PCB
运行态:占有CPU,并在CPU上运行;
就绪态:已经具备运行条件,但是由于没有空闲的CPU,而暂时不能运行;一旦得到CPU时间片调度时即可运行。
阻塞态:一个进程正在等待某一事件而暂停运行时,已经具备运行条件,但是由于没有空闲的CPU,而暂时不能运行;
终止态:操作系统回收进程的资源,撤销PCB。
转换:
就绪 → 运行:当操作系统的调度器选择一个就绪状态的进程并将 CPU 分配给它时,进程进入运行状态。
运行 → 阻塞:进程执行过程中,如果需要等待某些资源(如 I/O 操作、网络响应等),它会进入阻塞状态。
运行 → 就绪:如果进程的时间片用完,或者调度器决定中断当前进程(如时间片轮转),进程会进入就绪状态,等待下一次调度。
阻塞 → 就绪:当进程等待的事件发生(如 I/O 完成、资源释放等),进程会从阻塞状态转变为就绪状态,准备重新调度执行。
运行 → 终止:进程执行完毕,或发生异常终止,进程进入终止状态。
阻塞 → 终止:如果进程在阻塞状态时发生了崩溃或终止,也会直接进入终止状态。
注意:
不能由阻塞态直接转换运行态,也不能由就绪态直接到阻塞态。(原因:因为进入阻塞态是进程主动请求的,必然需要进程在运行态时才能发出这种请求。)
![](https://i-blog.csdnimg.cn/direct/ad93a9a041a443b199f15c95bfebb14e.png)
06 进程上下文是什么?
**进程上下文是指在进程执行过程中,操作系统需要保存和恢复的关于该进程的状态信息。**当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。
**进程切换:**当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。
进程上下文包括:
- 程序计数器:指示进程下一条指令的地址。
- 寄存器的值:包括通用寄存器、浮点寄存器等,保存了进程在执行时的数据。
- 堆栈指针:指向当前进程的栈帧。
- 内存管理信息:如页表、段表等,用于管理该进程的虚拟内存。
- 进程状态:如进程是否处于运行、就绪、阻塞等状态。
- 文件描述符:进程当前打开的文件、套接字等的标识符。
- 信号状态:进程的信号掩码,表示哪些信号处于屏蔽状态。
**当发生进程调度时,进行进程切换就是上下文切换。**操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。
- 保存当前进程的上下文,包括所有寄存器、程序计数器、堆栈指针等。
- 恢复目标进程的上下文,让目标进程从上次中断的地方继续执行。
进程上下文切换的场景:
- 时间片耗尽:调度器决定切换到另一个进程。
- 阻塞事件:当前进程因为等待 I/O 操作或其他资源而被阻塞,操作系统切换到其他进程。
- 进程退出:进程执行完毕,操作系统切换到其他就绪进程。
07 中断上下文是什么?
**中断上下文是指当中断发生时,操作系统需要保存和恢复的执行状态。**当硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
中断上下文包括:
- 程序计数器:指示中断发生前的指令位置。
- 中断触发前的寄存器值:中断发生前 CPU 寄存器的内容,包括状态寄存器、通用寄存器等。
- 栈信息:中断发生时当前栈的状态。
- 标志寄存器:表示中断状态和 CPU 特权级(如中断屏蔽等)。
**中断上下文切换:**在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程 的执行。
中断上下文的场景:
- 定时器中断:操作系统的时钟中断用于实现多任务调度。
- 外部设备中断:例如磁盘 I/O 完成、网络包到达等。
- 系统调用:当进程发出系统调用时,通常会触发一个软件中断。
注意:
-
不能-睡眠或者放弃CPU。因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。牢记:中断服务子程序一定不能睡眠(或者阻塞)。
-
不能-尝试获得信号量。如果获得不到信号量,代码就会睡眠,导致(1)中的结果。
-
不能-执行耗时的任务。中断处理应该尽可能快,因为如果一个处理程序是IRQF_DISABLED类型,他执行的时候会禁止所有本地中断线,而内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。中断处理程序的任务尽可能放在中断下半部执行。
-
不能-访问用户空间的虚拟地址,因为中断运行在内核空间。
进程上下文与中断上下文的区别
触发方式 | 进程调度、阻塞、I/O 等系统调度引起的 | 外部中断源(硬件中断、定时器中断等) |
---|---|---|
发生时机 | 进程调度、时间片耗尽、进程终止、阻塞等 | 由硬件或软件触发(如 I/O 完成、时钟中断) |
内容保存 | 保存当前进程的程序计数器、寄存器、堆栈指针、内存管理等信息 | 保存当前 CPU 寄存器、程序计数器、中断状态等 |
上下文切换的目标 | 切换到其他进程 | 切换到中断处理程序(ISR) |
状态恢复 | 恢复另一个进程的执行状态 | 执行中断处理程序后,恢复中断发生前的状态 |
上下文切换的开销 | 较大,涉及进程的状态、资源等 | 较小,主要涉及保存和恢复寄存器和标志位等 |
08 父进程、子进程及其关系和区别
(1)父进程创建子进程
- 在许多操作系统中,进程是通过系统调用如
fork()
来创建的。 - 父进程调用
fork()
或类似系统调用时,操作系统会复制父进程的地址空间,并在此基础上创建一个新的进程,这个新进程就是子进程。 - 父进程和子进程拥有独立的内存空间,它们的进程 ID (PID) 是不同的,但它们共享一些资源(如打开的文件描述符、信号等),除非特定地关闭或重新设置。
在操作系统中父进程和子进程是进程间的关系,其中父进程是创建子进程的进程,而子进程是由父进程创建并继承其部分资源的进程。
(2)PID 和 PPID概念
- PID(Process ID):每个进程在操作系统中都有一个唯一的进程标识符(PID),用于标识进程。
- PPID(Parent Process ID):每个进程也会有一个父进程 ID(PPID),表示该进程的父进程。
- 父进程和子进程通过 PPID 和 PID 相互联系。父进程的 PID 会作为子进程的 PPID。
(3)父进程等待子进程结束
父进程可以通过系统调用(如 wait()
或 waitpid()
)等待子进程的终止,并获取子进程的退出状态。这是为了保证父进程能够处理子进程的退出,防止僵尸进程的产生。
- 僵尸进程 :当子进程终止时,它会保留在系统中,直到父进程通过
wait()
系统调用回收其退出状态。若父进程没有调用wait()
,子进程的退出状态会保留在系统中,变成僵尸进程。
(4)孤儿进程
如果父进程在子进程结束之前终止,那么该子进程会变成 孤儿进程。在 Unix/Linux 系统中,孤儿进程会被 init 进程(PID 1) 作为新的父进程收养,确保系统对这些进程进行管理,避免孤儿进程成为僵尸进程。
注意:父进程、子进程它们共享代码段,但并不共享数据段、堆、栈等,而是子进程拥有父进程数据段、堆、栈等副本,所以对于同一个局部变量,它们打印出来的值是不相同的。