操作系统——进程&线程
进程和线程的定义
进程是资源分配的基本单位,他是程序执行是的一个实例,在程序运行时创建
线程是程序执行的最小单位,是进程的一个执行流,一个进程里包含多个线程
进程和线程的区别
定义: 进程:进程是独立的执行单位,每个进程都有自己独立的内存空间和系统资源。进程之间通常是相互隔离的。 线程:线程是进程内的执行单元,多个线程可以共享同一个进程的内存空间和系统资源。
资源占用: 进程:每个进程都有独立的内存空间,因此占用较多的系统资源。 线程:线程共享同一个进程的内存空间,因此占用的系统资源较少。
通信和同步: 进程:不同进程之间的通信较复杂,需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。进程之间的同步也需要额外的手段。 线程:线程之间可以直接共享内存,因此通信较为简单。线程之间的同步可以使用线程同步原语,如互斥锁、信号量等。
创建和销毁开销: 进程:创建和销毁进程通常需要较大的开销,包括分配和释放内存、建立和销毁进程控制块等。 线程:创建和销毁线程的开销较小,因为它们共享了大部分进程的资源。
独立性: 进程:进程是相对独立的,一个进程的崩溃通常不会影响其他进程。 线程:线程是共享同一个进程的资源,因此一个线程的错误可能会影响整个进程。
适用场景: 进程:适用于需要高度隔离和独立性的任务,如独立的应用程序。 线程:适用于需要轻量级并发、共享数据、协同工作的任务,如多线程编程和并行计算。
进程和线程的状态
- 创建状态(New):当一个新的进程被创建,但操作系统尚未为其分配足够的资源或初始化必要的数据结构时,进程处于创建状态。在这个阶段,进程正在初始化,并等待操作系统分配资源。
- 就绪状态(Ready):一旦进程获得了必要的资源,它进入就绪状态。在就绪状态下,进程已准备好运行,只等待CPU的分配。多个就绪状态的进程可以等待CPU时间片,等待操作系统的调度。
- 运行状态(Running):进程进入运行状态时,它正在执行指令并占用CPU。在任何给定时间,只有一个进程可以处于运行状态。其他进程则处于就绪或阻塞状态。
- 阻塞状态(Blocked):当进程在执行中遇到需要等待某些事件发生的情况(如等待I/O操作完成或等待其他进程的信号)时,它会从运行状态切换到阻塞状态。在阻塞状态下,进程暂停执行,直到所需事件发生。
- 终止状态(Terminated):当进程完成其任务或被操作系统终止时,它进入终止状态。在终止状态下,操作系统会回收进程的资源,包括内存空间、文件描述符等。
进程间通信方式
- 管道(Pipes):管道是一种单向通信机制,通常用于父子进程之间或具有亲缘关系的进程之间。有命名管道和匿名管道两种。匿名管道在内存中创建,而命名管道则存在于文件系统中,允许无亲缘关系的进程之间通信。
- 消息队列(Message Queues):消息队列是一种通过消息进行通信的IPC方式。进程可以将消息发送到队列中,而其他进程可以从队列中接收消息。这种方式适用于异步通信,不要求发送者和接收者同时在线。
- 共享内存(Shared Memory):共享内存允许多个进程共享同一块内存区域,从而可以非常高效地进行数据交换。但需要额外的同步机制来避免数据竞争。
- 信号(Signals):信号是一种用于通知进程发生特定事件的机制,如中断或错误。进程可以发送信号给其他进程,以请求它们采取某种动作。例如,
SIGKILL
信号用于终止进程。- 套接字(Sockets):套接字通常用于网络编程,但它们也可以在本地进程之间进行通信。本地套接字(Unix域套接字)可以实现进程之间的双向通信。
- 信号量(Semaphores):信号量是一种用于控制并发访问共享资源的IPC方式。它们可以用于同步多个进程,以确保只有一个进程可以访问临界区。
线程间通信方式
- 共享内存:多个线程可以通过访问共享内存区域来进行通信。这是一种高效的通信方式,但需要额外的同步机制来避免数据竞争。
- 互斥锁(Mutex):互斥锁用于保护共享资源,确保在任何时候只有一个线程可以访问它。当一个线程锁住互斥锁时,其他线程将被阻塞,直到该锁被释放。
- 信号量(Semaphores):信号量是一种计数器,可以用于控制多个线程对共享资源的访问。它可以用于限制同时访问资源的线程数量。
- 消息队列:与进程间通信类似,线程间也可以使用消息队列进行通信。这是一种异步通信的方式。
- 管道:类似于进程间通信中的管道,线程间也可以使用管道进行通信。然而,它们通常用于父子线程之间或在同一进程中。
进程和线程通信区别
- 共享资源:
- 线程间通信:线程共享同一进程的内存空间,它们可以直接访问共享变量和数据结构,因此通信相对容易。但同时需要特别小心数据竞争和同步问题。
- 进程间通信:进程有各自独立的内存空间,彼此之间不能直接访问,因此进程间通信需要特殊的机制来交换数据,如管道、消息队列或共享内存。
- 资源开销:
- 线程间通信:由于线程共享进程的内存空间,通信的开销较小。
- 进程间通信:进程间通信需要操作系统提供额外的资源和机制,通常开销较大,例如在不同地址空间之间传递数据需要复制数据。
- 并发性:
- 线程间通信:由于线程共享相同的内存空间,它们可以更容易地实现并发操作,但也更容易出现竞争条件。
- 进程间通信:进程之间相对更独立,通信和同步需要更多的开销,但可以提供更好的隔离性和稳定性。
- 编程难度:
- 线程间通信:由于线程共享内存,开发者需要小心处理同步和竞争条件,以避免数据不一致或死锁等问题。
- 进程间通信:进程间通信需要使用特定的IPC机制,编写代码可能相对复杂,但它提供了更好的隔离性,降低了程序的耦合度。
- 安全性:
- 线程间通信:由于线程共享内存,必须小心处理同步,以避免数据竞争和不一致性。
- 进程间通信:进程间通信通常更安全,因为进程具有独立的地址空间,相互之间不会直接影响,降低了错误传播的风险。
僵尸/孤儿/守护进程
- 僵尸进程(Zombie Process):
- 僵尸进程是已经完成执行的子进程,但其父进程尚未调用
wait()
或waitpid()
系统调用来获取子进程的退出状态。- 僵尸进程的进程控制块仍然存在,但它不再执行任何操作。僵尸进程占用系统资源,但不能执行任何操作。
- 通常,父进程会在子进程退出后获取其退出状态,以释放子进程的资源,并告诉操作系统该子进程不再需要保留。如果父进程没有处理僵尸进程,它们可能会累积并浪费系统资源。
- 孤儿进程(Orphan Process):
- 孤儿进程是指其父进程已经终止或意外终止,但孤儿进程仍在运行。在这种情况下,孤儿进程会被操作系统的
init
进程(通常具有进程ID 1)接管。init
进程成为孤儿进程的新父进程,负责回收孤儿进程的资源并管理它们的终止。
- 守护进程(Daemon Process):
- 守护进程是在后台运行的系统进程,通常在系统启动时启动,以执行某种系统任务或服务,如日志记录、定时任务等。
- 守护进程通常以超级用户权限(root)运行,独立于用户会话,没有终端关联。
- 守护进程的目的是在后台执行任务,不与用户直接交互。它们通常会在系统启动时启动,并在系统关闭时停止。
内核线程和用户线程区别
- 特权级别:
- 内核线程在内核级别运行,具有更高的特权级别,可以执行特权指令和访问受限资源。
- 用户线程在用户级别运行,其执行受到限制,无法直接访问系统资源。
- 调度和管理:
- 内核线程由操作系统内核直接管理,包括调度、撤销等。
- 用户线程的创建和调度由用户级线程库完成,操作系统对其无直接感知。
- 开销和效率:
- 用户线程的切换和管理开销相对较小,因为它们不涉及内核的直接参与。
- 内核线程的管理开销相对较大,���它们可以更灵活地利用操作系统的各种功能。
进程调度算法即策略
- 先来先服务调度(First-Come, First-Served,FCFS):
- 进程按照它们到达调度队列的顺序执行,先到达的进程先执行。
- 简单且容易实现,但可能导致"等待时间过长"(Convoy Effect)问题。
- 短作业优先调度(Shortest Job Next,SJN):
- 选择估计执行时间最短的进程,以最小化平均等待时间。
- 也称为最短作业优先(Shortest Job First,SJF)。
- 优先级调度:
- 每个进程被分配一个优先级,优先级高的进程先执行。
- 静态优先级:由系统分配。
- 动态优先级:根据进程的行为和等待时间进行调整。
- 轮转调度(Round Robin,RR):
- 每个进程在调度队列中轮流执行一个时间片,当时间片用尽时,被移到队列末尾等待。
- 确保每个进程都有机会执行,适用于时间共享系统。
- 多级反馈队列调度:
- 将就绪队列划分为多个队列,每个队列具有不同的优先级。
- 进程首先从最高优先级队列开始执行,如果时间片用尽而未完成,移至较低优先级队列。
- 最高响应比优先调度(Highest Response Ratio Next,HRRN):
- 选择响应比最高的进程执行,响应比定义为(等待时间 + 服务时间)/ 服务时间。
- 优先选择等待时间较长且服务时间较短的进程。
并发和并行
并发是对于单个cpu来说,在一个是个只能一个进程运行,但是线程的切换时间则是减少到纳秒数量级,多个任务不停的来回切换;
并行是对于多个CPU来说,多个进程同时运行;
区别;并行的"同时"是同一时刻可以多个任务在运行(处于running),并发的"同时"是经过不同线程快速切换;
死锁
死锁是指多个进程在执行过程中,因争夺资源而造成互相等待,此时系统产生了死锁;
产生条件:
- 互斥条件:进程对所分配的资源不允许其他进程访问,若其他进程需要访问,只能等待,知道该进程使用完毕后释放资源
- 请求保持条件:进程获得一定资源后,有对其他资源发出请求,但该资源被其他进程占用,此时请求阻塞,而且这个进程不会释放自己已经占有的资源
- 不可剥夺条件:进程获得资源,只能自己释放,不可剥夺
- 环路等待条件:若干进程之间形成一种头尾相接等待资源关系
解决:
- 资源一次性分配,从而解决请求保持的问题
- 可剥夺资源:当进程新的资源未得到满足时,释放已有的资源;
- 资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反
多线程程序考虑加锁
因为线程锁只要是用来实现线程的同步和通信,在抢占是操作系统中,通常为每个县城分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突
线程的中断切换
- 中断发生:与进程中断一样,硬件或软件引发中断。
- 内核响应:操作系统内核捕获到中断并选择相应的中断服务例程。
- 当前线程上下文保存:操作系统保存当前运行线程的上下文信息,如寄存器状态、程序计数器、栈指针等。
- 中断处理程序执行:执行与中断相关的处理程序,可能包括设备驱动程序、系统调用等。
- 线程调度:操作系统可能决定切换到另一个线程。这可能会涉及线程调度算法。
- 新线程上下文加载:如果发生线程切换,操作系统加载新线程的上下文信息。
- 中断处理完成:中断处理程序执行完毕,控制返回到新的或原始的线程。
多线程和单线程
多线程 vs. 单线程:
- 并行执行:
- 多线程允许多个线程同时执行,以提高系统性能,尤其在多核处理器上。
- 单线程一次只能执行一个任务,无法充分利用多核处理器。
- 资源共享:
- 多线程可以共享相同的内存空间,这使得线程之间的通信更容易。
- 单线程通常无法轻松地实现资源共享,因为它们运行在相对隔离的环境中。
- 复杂性:
- 多线程编程通常更复杂,因为需要处理竞争条件和并发问题。
- 单线程编程通常更简单,因为不需要担心并发。
多线程编程注意事项:
- 线程同步:确保多个线程之间的安全访问共享资源。使用互斥锁、信号量等机制来控制线程访问。
- 避免竞争条件:竞争条件会导致不可预测的结果。通过合理的同步和锁定来避免竞争条件。
- 线程安全数据结构:使用线程安全的数据结构,如线程安全的队列,以减少自己实现同步逻辑的复杂性。
- 避免死锁:死锁是多线程编程中的常见问题。确保良好的锁定管理和避免相互等待。
多线程加锁注意事项:
- 锁粒度:选择适当的锁粒度。锁的范围应该足够小,以减少竞争,但不应过小,以避免锁争用带来的开销。
- 锁顺序:在多个锁的情况下,确保按照相同的顺序获取锁,以减少死锁的风险。
- 锁超时:在尝试获取锁时,考虑设置锁超时机制,以避免长时间等待锁导致的性能问题。
- 锁性能:了解锁的性能特性,以确保不会因锁开销而降低程序性能。
- 避免锁滥用:不是每个代码段都需要锁,避免过度使用锁,以避免不必要的同步开销。
线程池
线程池是为了有效管理和复用线程资源而创建的一种机制。它的存在有以下主要原因和优势:
- 降低线程创建和销毁开销:线程的创建和销毁代价较高。线程池维护一组可重用的线程,避免频繁地创建和销毁线程,从而降低开销。
- 控制并发度:线程池允许您限制同时执行的线程数量,以控制系统的并发度。这有助于防止系统过载和资源饱和。
- 提高响应时间:线程池使任务能够迅速排队并执行,而不必等待新线程的创建。这有助于提高系统的响应时间。
- 资源管理:线程池可以帮助有效管理系统的资源,防止不受控制的线程数量导致资源耗尽。
线程池的设计思路通常包括以下要点:
- 线程池大小:决定线程池可以容纳多少线程。线程池大小的选择通常取决于系统资源、性能需求和可用硬件。如果线程池太小,可能无法满足并发需求;如果太大,可能导致资源浪费和竞争条件。
- 任务队列:线程池通常维护一个任务队列,其中等待执行的任务排队。新任务被提交到队列中,并由线程池中的线程异步执行。
- 线程复用:线程池中的线程是可重用的,它们在完成一个任务后会从队列中获取下一个任务而不被销毁。这减少了线程创建和销毁的开销。
- 线程调度:线程池负责调度线程以执行任务,通常使用调度算法(如先进先出、最短作业优先等)来选择下一个任务。
线程池中线程的数量由以下因素确定:
- 系统资源:线程池的大小应受限于可用的物理内存和处理器核心数量。不宜创建过多线程,以避免资源耗尽和上下文切换开销。
- 性能需求:线程池的大小应该根据应用程序的性能需求来确定。如果需要高并发性能,可以增加线程池的大小。
- 任务性质:不同类型的任务可能需要不同数量的线程来获得最佳性能。某些任务可能需要更多线程来并行执行。
- 硬件特性:线程池的大小还取决于硬件特性,如多核处理器的数量,以充分利用硬件资源。
自旋锁和信号量区别
自旋锁:
- 自旋锁是一种轻量级的锁,通常用于保护临界区,确保同时只有一个线程可以进入临界区执行。
- 当一个线程尝试获取自旋锁时,如果锁已被其他线程占用,它会不断自旋(循环检查锁状态),而不是立即被阻塞挂起。
- 自旋锁适用于短时间内的临界区保护,因为它减少了线程的上下文切换开销。但如果锁被占用时间较长,自旋可能会浪费CPU资源。
信号量:
- 信号量是一种更通用的同步机制,可以用于控制并发线程的访问或控制资源的分配。
- 信号量有两种类型:二进制信号量(只有0和1)和计数信号量(可以大于1)。
- 二进制信号量通常用于互斥,即只有一个线程可以访问临界区。计数信号量用于限制同时访问某些资源的线程数量。
区别:
- 等待机制:
- 自旋锁通过忙等待来获取锁,不会阻塞线程,不会进入睡眠
- 信号量使用阻塞和唤醒机制,当线程无法获得信号量时,它将被阻塞,直到信号量可用,会进入睡眠
- 用途:
- 自旋锁通常用于短时间内的临界区保护,以减少上下文切换开销。
- 信号量更通用,可以用于控制线程数量、资源分配和同步多个线程之间的操作。
- 适用性:
- 自旋锁适合用于低竞争和低争用的情况,例如多核CPU上的短期临界区。
- 信号量更适合于需要阻塞等待的场景,例如生产者-消费者问题或限制同时访问资源的情况。
线程的同步和互斥的区别
同步:
- 同步是一种协调多个线程之间的操作顺序或执行时机的机制,以确保它们按照一定的顺序或条件来执行。
- 同步通常涉及在多个线程之间建立依赖关系,以便它们协同工作或按照特定的规则执行。
- 同步机制可以包括等待其他线程完成、通知其他线程继续执行、等待条件变为真等。
互斥:
- 互斥是一种同步机制,用于确保在同一时间只有一个线程可以访问共享资源或进入临界区。
- 互斥锁、信号量、互斥量等是用于实现互斥的工具,它们用于防止多个线程同时访问或修改共享资源,从而避免数据竞争和不一致性。
区别:
- 同步是更广泛的概念,它可以包括协调线程的任何方式,而不仅限于互斥。同步可以用于等待事件、通信、任务协作等。
- 互斥是同步的一种特定形式,它关注的是线程在访问共享资源或执行临界区时的互斥性。
联系:
- 互斥通常用于同步的实现中,以确保在某个时间点只有一个线程可以执行共享资源的操作。
- 同步机制可以包括互斥,但也可以包括其他形式的同步,如条件变量、信号量、屏障等,用于线程之间的协调和通信。