文章目录
- 线程和进程
- 线程和进程的区别?
- 有了进程为什么还需要线程?
- 为什么使用多线程?
- 线程间的同步方式
- 进程控制块(PCB)
- 进程有哪几种状态?
- 进程间的通讯方式有哪些?
- 进程常见的调度算法有哪些?
- 什么是僵尸进程和孤儿进程?
- 死锁
- 什么是死锁?
- 产生死锁的四个必要条件?
- 解决死锁的方法?
线程和进程
进程和线程是操作系统中的重要概念,常用于描述程序运行时的并发执行。进程是程序运行时的实例,它拥有独立的内存空间和系统资源,是计算机分时操作系统中进行资源分配的最小单位。而线程是进程内的执行单元,一个进程可以包含多个线程,它们共享进程的内存空间和系统资源,每个线程拥有自己的栈空间和寄存器状态。
线程和进程的区别?
进程与线程之间的主要区别在于资源的分配和使用方式。进程是分配资源的基本单位,它需要为每个进程分配独立的资源,包括独立的内存空间、文件描述符、环境变量以及其他需要的系统资源。而线程则是在进程内共享资源的,一个线程存储在进程的虚拟地址空间中,它们共享内存区域、全局变量以及其他资源。
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
有了进程为什么还需要线程?
因为线程共享进程的内存空间和系统资源,所以它们比进程更轻量级,可以更快速地创建和退出。另外,多线程编程可以提高程序的并发性和效率,因为多个线程可以同时执行任务,而不需要等待其他线程完成。
为什么使用多线程?
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
再深入到计算机底层来探讨:
单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
线程间的同步方式
- 互斥锁(Mutex):通过互斥锁来保护共享资源,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种Lock
都是这种机制。- 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
- 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量,通过信号量的数量来限制线程的访问。
- 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的
CyclicBarrier
是这种机制。- 事件(Event):事件是一种双状态同步机制,在事件被触发之前,等待事件的线程会一直阻塞,当事件被触发后,所有等待的线程都可以继续执行。Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
进程控制块(PCB)
PCB(Process Control Block) 即进程控制块,是操作系统维护进程信息的数据结构,也是操作系统进行进程调度的基本单位。
当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。
PCB 主要包含下面几部分的内容:
- 进程的描述信息,包括进程的名称、标识符等等;
- 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等;
- 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
- 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
- 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
进程有哪几种状态?
我们一般把进程大致分为 5 种状态,这一点和线程很像
- 创建状态(new):进程正在被创建,尚未到就绪状态。
- 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running):进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
进程间的通讯方式有哪些?
- 管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
- 有名管道(Named Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 先进先出(First In First Out) 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 信号(Signal):进程之间能够发送强制中断(异常)信号,并且可以注册一个信号处理函数进行响应。信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
- 消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间通信和同步。
- 共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新,实现进程间数据共享。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
- 套接字(Sockets) :一种通用标准的进程间通信方法,可应用于网络编程或本机进程间通讯。 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
进程常见的调度算法有哪些?
- 先到先服务调度算法(FCFS,First Come, First Served) : 按照进程到达的先后顺序,依次执行所有进程,直到执行完毕或阻塞。优点是简单易实现,缺点是可能导致短进程等待长进程,进程响应时间长。
- 短作业优先的调度算法(SJF,Shortest Job First) : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 时间片轮转调度算法(RR,Round-Robin) : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 多级反馈队列调度算法(MFQ,Multi-level Feedback Queue):前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。针对不同类别的进程设置不同的时间片,若某个进程在一个队列等待了一个时间片仍未被执行,则将其移动到下一个队列并增加该进程的时间片长度。可以适应各种情况,但调度算法比较复杂
- 优先级调度算法(Priority):为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
什么是僵尸进程和孤儿进程?
- 僵尸进程: 是一种已经结束但没有被完全释放的进程,一个进程已经终止,但是其父进程仍在运行,且父进程并未主动回收该进程的僵尸进程,导致子进程的PCB仍然存在于系统中,占据系统资源,这是的子进程就被称为“僵尸进程”。为了避免僵尸进程的产生,父进程需要及时调用
wait()或waitpid()
系统调用来回收子进程。- 孤儿进程: 指一个进程的父进程已经终止或不存在,但是该进程仍在运行。这种情况下该进程就是孤儿进程。孤儿进程通常是由于父进程意外终止或未及时调用
wait()或waitpid()
等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为init
进程,由init
进程来回收孤儿进程的资源。
死锁
什么是死锁?
多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁的四个必要条件?
- 互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
- 占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
- 非抢占:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
- 循环等待: 系统中若干进程形成环路,环路中每个进程正在等待下一个进程所占用的资源。
这四个条件是产生死锁的 必要条件 ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
解决死锁的方法?
解决死锁一般由四种: 预防、避免、检测和解除
- 预防死锁: 该方法主要是从资源分配和进程调度两方面进行处理,例如破坏死锁的四个必要条件之一,如破坏互斥条件、满足资源预先分配等。
- 避免死锁: 在进程运行时,动态地检测当前状态,根据当前状态进行优化调度,避免死锁的产生。例如银行家算法是一种著名的避免死锁算法。
- 检测死锁: 系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
- 解除死锁: 该方法是用户手工干预,通过撤销进程、资源剥夺和进程回退等方法将系统从死锁状态中解脱出来。