这里写目录标题
进程
在操作系统中,进程(Process)是指正在运行的程序的实例。它是操作系统分配资源、调度和执行任务的基本单位。
特点
- 独立性:每个进程都是独立的,拥有自己的地址空间、内存、文件描述符和其他系统资源。进程之间彼此隔离,互不干扰。
- 并发执行:操作系统可以同时运行多个进程,通过分配 CPU 时间片给各个进程,实现并发执行。
- 互相通信:进程之间可以通过进程间通信(IPC)机制进行数据交换和共享资源,例如管道、信号、共享内存等。
- 生命周期:进程的生命周期包括创建、执行、等待、终止等阶段。进程可以被其他进程创建、终止或等待。
- 状态:进程可以处于运行态、就绪态、阻塞态等不同的状态,根据操作系统的调度算法进行状态切换。
每个进程都由操作系统分配一个唯一的进程标识符(PID),可以通过 PID 来唯一标识和管理进程。
操作系统通过进程调度器来分配 CPU 时间片给不同的进程,根据调度算法决定哪个进程获得执行。进程可以通过系统调用或中断请求操作系统提供的服务和资源。
进程是操作系统中的核心概念,它使得操作系统能够同时运行多个任务,并提供了资源管理和任务调度的基础。
并行和并发
在操作系统中,并行(Parallel)和并发(Concurrent)是两个重要的概念,它们描述了多个任务同时执行的方式。
并行是指两个或多个任务在同一时刻同时执行,每个任务都在自己的处理器核心上独立运行。在并行执行中,多个任务可以在不同的处理器上同时进行,从而实现更高的处理能力和效率。并行处理通常用于并行计算、多核处理器等场景。
并发是指两个或多个任务在一段时间内交替执行,虽然任务可能不是同时发生的,但给人的感觉是同时执行的。在并发执行中,任务之间通过操作系统的调度算法分时共享处理器,并以快速的时间片轮转方式进行切换。并发处理通常用于提高系统的响应性、资源利用率和任务间的交互。
总结一下并行和并发的区别:
- 并行:多个任务在同一时刻同时执行,每个任务独立运行在自己的处理器核心上,实现高效的同时处理能力。
- 并发:多个任务在一段时间内交替执行,通过时间片轮转方式进行切换,实现快速的任务切换和资源共享。
需要根据具体的场景和需求来选择并行还是并发方式,以充分利用系统资源、提高处理能力和响应性。
进程的状态
一个进程的活动期间应该具备五种基本状态,即创建状态、运行状态、就绪状态、阻塞状态、结束状态
- NULL -> 创建状态:一个新进程被创建时的第一个状态;
- 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
- 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
- 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
- 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
- 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
- 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
进程的控制结构
在操作系统中,是用进程控制块(PCB)数据结构来描述进程的。PCB是进程存在的唯一标识
PCB包含什么信息
-
进程描述信息
-
进程控制和管理信息
-
资源分配清单
-
CPU相关信息
每个PCB通常通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。
进程上下文切换
在操作系统中,进程上下文切换是指从一个正在运行的进程切换到另一个就绪态的进程,使得另一个进程可以继续执行。上下文切换发生的时候,操作系统会保存当前进程的状态(上下文),包括寄存器的值、程序计数器、堆栈指针和其他相关信息,并加载下一个进程的状态,以便让其继续执行。
进程上下文切换的过程包括以下步骤:
-
保存当前进程的上下文:操作系统会保存当前进程的寄存器状态、程序计数器和其他相关寄存器的值,以及进程的堆栈指针等信息。
-
切换到目标进程:操作系统根据调度算法选择下一个要执行的进程,并加载该进程的上下文。这包括恢复目标进程的寄存器状态、程序计数器和堆栈指针等。
-
更新内核数据结构:操作系统会更新内核的进程控制块(PCB)等数据结构,记录进程切换的相关信息。
-
执行目标进程:操作系统将控制权转移到目标进程,使其开始执行。
进程上下文切换是操作系统实现进程调度和并发执行的重要机制。它允许多个进程在一个处理器上交替执行,实现并发性和共享资源。然而,上下文切换也会带来一定的开销,包括保存和恢复进程的上下文、更新数据结构等,因此需要在性能和响应性之间进行权衡。优化上下文切换的方式包括减少切换次数、减少上下文保存的数据量等。
进程上下文切换发生的场景
进程上下文切换在以下几种情况下会发生:
-
抢占式调度:当操作系统采用抢占式调度算法时,可能会在一个进程被抢占(被更高优先级的进程取代)时发生上下文切换。这可以是由于时间片用完、高优先级进程就绪或中断事件发生等原因。
-
阻塞和唤醒:当进程请求某个事件(如等待 I/O 完成)而无法继续执行时,操作系统会将该进程阻塞,并将 CPU 分配给另一个就绪态的进程。当等待的事件完成后,操作系统会将进程唤醒,并进行上下文切换,使其继续执行。
-
多任务环境切换:当操作系统运行多个任务时,通过时间片轮转或优先级调度等方式,会在进程之间进行切换,以实现并发执行。这种情况下,操作系统会周期性地进行上下文切换,以平衡各个任务的执行和资源利用。
-
中断处理:当发生硬件中断或软件中断(如系统调用)时,操作系统会暂停当前进程的执行,保存其上下文,并切换到中断处理程序。中断处理程序执行完后,操作系统会恢复先前被中断的进程的上下文,使其继续执行。
需要注意的是,进程上下文切换是一种开销较高的操作,因为它涉及到保存和恢复大量的寄存器状态和内核数据结构。因此,在设计和优化系统时,需要合理选择调度算法、减少上下文切换次数,以提高系统的性能和响应性。
进程的通信方式
在操作系统中,进程之间可以通过多种方式进行通信,以实现数据交换、共享资源和协作完成任务。常见的进程通信方式包括:
-
管道(Pipe):管道是一种半双工的通信方式,适用于有亲缘关系的进程间通信。它可以在父子进程或兄弟进程之间传递数据,通常用于单向通信。
-
命名管道(Named Pipe):命名管道是一种有名字的管道,可以在无亲缘关系的进程之间进行通信。不同于管道,命名管道可以在不同的进程间共享数据。
-
信号(Signal):信号是一种异步通信机制,用于通知进程发生了某个事件。进程可以通过发送信号给其他进程,来请求某种操作或通知其发生的事件。
-
共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,允许多个进程直接访问同一块内存区域。进程可以通过读写共享内存来进行数据交换。
-
消息队列(Message Queue):消息队列是一种通过消息传递进行进程间通信的方式。进程可以将消息发送到消息队列中,其他进程可以从队列中获取消息。
-
信号量(Semaphore):信号量是一种用于进程同步和互斥的机制。通过对信号量的操作,进程可以控制对共享资源的访问。
-
套接字(Socket):套接字是一种网络编程中常用的通信方式,允许不同主机上的进程进行通信。它提供了一种标准化的接口,支持进程间的数据传输和网络通信。
这些进程通信方式各有特点,适用于不同的场景和需求。在选择进程通信方式时,需要考虑进程间的关系、数据量、性能要求和安全性等因素。
线程
什么是线程
线程是进程当中的一条执行流程,是CPU调度的最小单位。
线程的优缺点
线程的优点:
- 一个进程中可以同时存在多个线程;
- 各个线程之间可以并发执行;
- 各个线程之间可以共享地址空间和文件等资源;
线程的缺点:
- 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言)
进程线程的区别
进程和线程是操作系统中的两个重要概念,它们有以下区别:
-
定义:进程是正在执行的程序的实例,是资源分配和调度的基本单位。线程是进程内部的执行单元,是进程的一个独立执行流。
-
资源和隔离:每个进程都有独立的地址空间、文件描述符、堆栈等资源,进程之间相互隔离。而线程是共享进程的资源,包括内存空间、文件描述符等。线程之间可以直接访问和共享数据,需要注意同步和互斥。
-
创建和切换开销:创建进程需要分配独立的资源和建立相应的数据结构,开销较大。而创建线程的开销较小,因为线程共享进程的资源,只需要分配线程控制块。线程的切换开销也较小,因为它们共享进程的地址空间和其他资源。
-
并发性:进程是操作系统分配资源和调度任务的基本单位,可以并发执行多个进程。而线程是进程内部的执行单元,多个线程可以在同一进程中并发执行。
-
通信和同步:进程之间通信需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。线程之间可以通过共享内存、信号量、互斥锁等机制进行通信和同步。
-
失败影响:一个进程的失败通常不会影响其他进程。而一个线程的失败会导致整个进程的失败。
需要根据具体的应用需求和系统特点来选择使用进程还是线程。进程适合于独立的任务和资源隔离,线程适合于并发执行和共享的任务。多线程编程可以提高系统的性能和响应性,但同时也需要注意线程间的同步和互斥,避免竞态条件和数据不一致的问题。
线程的上下文切换
- 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
- 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
在操作系统中,线程上下文切换是指从一个正在执行的线程切换到另一个就绪态的线程,以便让另一个线程继续执行。线程上下文切换涉及保存当前线程的上下文,并加载下一个线程的上下文。
线程上下文切换的过程包括以下步骤:
-
保存当前线程的上下文:操作系统会保存当前线程的寄存器状态、程序计数器和其他寄存器的值,以及线程的堆栈指针等信息。
-
切换到目标线程:操作系统根据调度算法选择下一个要执行的线程,并加载该线程的上下文。这包括恢复目标线程的寄存器状态、程序计数器和堆栈指针等。
-
更新内核数据结构:操作系统会更新内核的线程控制块(TCB)等数据结构,记录线程切换的相关信息。
-
执行目标线程:操作系统将控制权转移到目标线程,使其开始执行。
线程上下文切换是操作系统实现多线程并发执行的重要机制。它允许多个线程在同一进程中交替执行,实现并发性和共享资源。然而,上下文切换也会带来一定的开销,包括保存和恢复线程的上下文、更新数据结构等,因此需要在性能和响应性之间进行权衡。优化上下文切换的方式包括减少切换次数、减少上下文保存的数据量等。
需要注意的是,线程上下文切换与进程上下文切换相比,开销较小,因为线程共享进程的资源和地址空间。因此,多线程编程可以更高效地利用系统资源,提高程序的性能和响应性。
线程的实现
-
用户线程:在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理
-
内核线程:在内核中实现的线程,是由内核管理的线程
-
轻量级线程:在内核中来支持用户线程
用户线程和内核线程的对应关系
- 一对一
- 多对一
- 多对多
用户线程如何理解、优缺点
用户线程是基于用户态的线程管理库来实现的,那么线程控制块(*Thread Control Block, TCB*) 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
用户线程的优点:
- 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
- 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;
用户线程的缺点:
- 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。
- 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;
内核线程如何理解、优缺点
内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。
内核线程的优点:
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 分配给线程,多线程的进程获得更多的 CPU 运行时间;
内核线程的缺点:
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如 PCB 和 TCB;
- 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;
轻量级线程如何理解
轻量级进程(*Light-weight process,LWP*)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持,而且 LWP 是由内核管理并像普通进程一样被调度。在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。
LWP 与用户线程的对应关系就有三种:
1 : 1
,即一个 LWP 对应 一个用户线程;N : 1
,即一个 LWP 对应多个用户线程;M : N
,即多个 LWP 对应多个用户线程;
调度
调度时机
以下状态的变化都会触发操作系统的调度:
- 从就绪态 -> 运行态:当进程被创建时,会进入到就绪队列,操作系统会从就绪队列选择一个进程运行;
- 从运行态 -> 阻塞态:当进程发生 I/O 事件而阻塞时,操作系统必须选择另外一个进程运行;
- 从运行态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外一个进程运行;
调度算法
- 非抢占式调度算法挑选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,也就是说不会理时钟中断这个事情。
- 抢占式调度算法挑选一个进程,然后让该进程只运行某段时间,如果在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程。这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制。
调度原则
- CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
- 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
- 周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
- 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
- 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
调度算法
调度算法是操作系统中用于决定哪个进程或线程获得 CPU 时间片并执行的策略。调度算法的选择对系统的性能、响应时间和资源利用率等方面有重要影响。
常见的调度算法包括:
-
先来先服务(FCFS,First-Come, First-Served):按照进程到达的顺序进行调度,先到达的进程先执行。这是一种非抢占式调度算法,适用于长作业时间的进程。然而,FCFS 可能导致短进程等待时间过长,产生“饥饿”现象。
-
最短作业优先(SJF,Shortest Job First):选择估计执行时间最短的进程优先执行。SJF 是一种非抢占式调度算法,理论上可以实现最优的平均等待时间。但是,对于无法准确估计执行时间的情况,可能导致长作业的等待时间过长。
-
优先级调度:为每个进程分配一个优先级,并按照优先级高低进行调度。优先级可以是静态的,由进程属性决定,也可以是动态的,根据进程的行为和状态进行调整。优先级调度可以是抢占式或非抢占式的。
-
时间片轮转(RR,Round Robin):按照时间片的大小,为每个进程分配固定的执行时间,轮流执行。当时间片用完时,进程被放回就绪队列,等待下一次调度。时间片轮转是一种公平的调度算法,可以避免长作业等待时间过长,但可能导致上下文切换的开销增加。
-
多级反馈队列(MFQ,Multi-Level Feedback Queue):将进程划分为多个队列,每个队列有不同的优先级和时间片大小。进程先进入高优先级队列,如果用完时间片还未执行完,则进入下一级低优先级队列。这种调度算法可以在保证短作业优先的同时,也允许长作业得到一定的执行时间。
选择合适的调度算法需要根据系统的需求和性能要求。不同的调度算法可能适用于不同的场景,需要权衡任务的响应时间、资源利用率和公平性等因素。另外,一些高级调度算法,如实时调度算法和动态优先级调度算法,还可以根据任务的特殊需求和时间约束进行调度。
多线程冲突怎么办
当多个线程同时访问共享数据或共享资源时,可能会出现冲突和竞态条件。这种情况下,需要采取适当的措施来处理多线程冲突,以确保数据的一致性和避免意外的错误。
以下是一些常用的处理多线程冲突的方法:
-
互斥锁:使用互斥锁(Mutex)来保护共享资源。在访问共享资源之前,线程会尝试获取互斥锁。如果互斥锁被其他线程持有,则线程会被阻塞,直到锁被释放。一次只能有一个线程获得互斥锁,从而确保对共享资源的独占访问。
-
读写锁:对于共享资源的读操作,可以使用读写锁(ReadWrite Lock)。读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这样可以提高并发性,同时确保对共享资源的写操作是互斥的。
-
原子操作:对于简单的操作,可以使用原子操作来确保操作的原子性。原子操作是不可中断的,可以在一个步骤中完成,不会被其他线程打断。
-
条件变量:使用条件变量(Condition Variable)来实现线程之间的等待和通知机制。当线程需要等待某个条件满足时,可以进入等待状态,并释放锁。当条件满足时,其他线程可以通过条件变量来通知等待的线程恢复执行。
-
同步工具:使用其他同步工具,如信号量(Semaphore)、屏障(Barrier)等,来控制多线程间的同步和互斥。
-
避免竞态条件:在设计和编写代码时,尽量避免竞态条件的产生。可以通过合理的数据结构设计、避免共享数据、使用不可变对象等方式来减少竞态条件的发生。
以上方法都是为了避免多线程冲突和提供线程间的协调机制。选择适当的方法需要根据实际情况和需求来确定,以确保程序的正确性和性能。同时,需要注意避免死锁、活锁和饥饿等并发编程中的常见问题。
死锁
什么死锁,死锁的条件
死锁(Deadlock)是指在并发系统中,两个或多个进程(或线程)无限期地等待彼此持有的资源,造成系统无法继续运行的一种状态。
当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件:至少有一个资源被一个进程独占,其他进程无法同时使用
- 持有并等待条件:一个进程在持有至少一个资源的同时,又请求其他进程持有的资源
- 不可剥夺条件:资源只能由占有它的进程主动释放,其他进程无法强行剥夺
- 循环等待条件:两个线程获取资源的顺序构成了环形链
避免死锁发生方法
避免死锁发生是在设计和编写程序时采取一些策略和方法,以预防和减少死锁的发生。下面是一些常见的避免死锁的方法:
-
破坏死锁的四个必要条件:互斥条件、请求与保持条件、不可剥夺条件和循环等待条件。通过适当的设计和约束,破坏其中一个或多个条件,可以避免死锁的发生。
-
资源有序分配策略:对系统中的资源进行规定的分配顺序,以避免进程之间形成循环等待。例如,按照资源的编号、优先级等进行顺序分配。
-
预防策略:在进程请求资源之前,先检查是否会导致死锁的发生。如果预测到可能会发生死锁,可以选择不进行资源请求,或者撤销已经分配的资源。
-
资源剥夺策略:当一个进程请求资源时,发现所需资源已被其他进程持有时,可以考虑剥夺已经持有资源的进程,以满足当前进程的请求。
-
按序申请资源:进程在申请资源时,按照固定的顺序申请,避免同时申请多个资源,从而降低死锁的发生概率。
-
资源回收策略:当一个进程不再需要某个资源时,及时释放该资源,以供其他进程使用。
-
死锁检测与恢复:通过周期性地进行系统资源的状态检测,发现死锁的发生后,采取相应的恢复措施,如资源剥夺或进程终止。
需要根据具体的应用场景和系统需求来选择合适的死锁避免策略。同时,编写高质量的并发程序,合理规划资源的使用和释放,避免不必要的资源竞争,也有助于减少死锁的发生。
线程崩溃,进程一定崩溃吗?
一般来说如果线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃,为什么系统要让进程崩溃呢,这主要是因为在进程中,各个线程的地址空间是共享的,既然是共享,那么某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃
- 针对只读内存写入数据
- 访问了进程没有权限访问的地址空间(比如内核空间)
- 访问了不存在的内存
在操作系统中,线程和进程是不同的概念。一个进程可以包含多个线程,线程是进程内的执行单元。当一个线程崩溃并引发异常时,并不一定会导致整个进程崩溃。
在典型情况下,当一个线程崩溃时,操作系统会捕获异常并尝试处理它。操作系统可以选择终止崩溃的线程,然后允许其他线程继续执行。这种情况下,崩溃的线程会被“终止”,而不是整个进程被终止。
然而,有一些情况下线程的崩溃可能会影响整个进程的稳定性:
-
共享资源问题: 如果线程崩溃导致共享资源状态不一致或资源泄漏,这可能会影响其他线程的正常执行。
-
线程间通信问题: 如果线程之间通过消息传递、信号量、锁等机制进行通信,崩溃的线程可能会破坏这些通信机制,影响其他线程之间的协作。
-
内存问题: 一些崩溃可能会导致内存损坏或泄漏,这可能会影响整个进程的稳定性。
-
异常处理问题: 如果崩溃的线程的异常处理不当,可能会导致进程崩溃。例如,未捕获的异常可能会传播到进程级别。
总的来说,虽然线程的崩溃不一定会导致整个进程的崩溃,但线程崩溃可能会对进程的正常运行产生负面影响,特别是当线程之间有强烈的依赖关系、共享资源或通信机制时。为了确保系统的稳定性,线程的异常处理是一个重要的设计考虑因素。