操作系统(二)进程管理
进程与线程
为什么创建进程比创建线程慢?
Linux 中创建一个进程自然会创建一个线程,也就是主线程。创建进程需要为进程划分出一块完整的内存空间,包含代码区、堆区、栈区、数据区等内存资源
创建线程则简单得多,只需要确定 PC 指针和寄存器的值,并且给线程分配一个栈用于执行程序,同一个进程的多个线程间可以复用进程的虚拟内存空间,减少了创建虚拟内存的开销。
因此,创建进程比创建线程慢,而且进程的内存开销更大。
为什么进程的切换比线程开销大?
进程间的切换的时候,除了需要切换 CPU 上下文,还需要切换页表,切换了页表会影响TLB的命中率,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换只需要切换 CPU 上下文,不会改变虚拟地址空间,不会影响TLB命中率。
线程的上下文切换过程
线程发生上下文切换的时候,正在运行的线程会将寄存器的状态保存到内核中的 TCB(线程控制块) 里,然后恢复另一个线程的上下文。和进程的区别是,线程只需要切换CPU的上下文,不会改变地址空间.
进程有哪些状态?
操作系统理论中的进程状态
主要有 5 种,分别是创建状态、就绪状态、运行状态、阻塞状态、结束状态。
一个进程刚开始创建的时候,是处于创建状态,随后就会进入就绪状态,等待被操作系统调度,当进程被调度器选中后,就会进入运行状态,此时进程就持有 CPU 执行权,当进程发生 I/O 事件的时候,就会进入到阻塞状态,等待 I/O 事件完成,I/O 事件完后进程就会恢复为就绪状态,当进程退出后,就会变为结束状态。
Linux系统的进程状态
- 可运行状态:该状态表示进程正在运行或者等待被调度(就绪和运行);
睡眠状态:在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态;
- 可中断的睡眠状态:等待 I/O 完毕期间,如果收到了其他信号,还是可以被唤醒;
- 不可中断的睡眠状态:只有等待 I/O 完毕才有可能返回运行状态,任何信号都无法打断它。如果这种状态的进程出错,无法杀死,只能重启。
- 暂停状态:当进程接收到SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态;
- 跟踪状态:当对进程进行 gdb 调试的时候,进程会进入跟踪状态;
- 僵尸状态:当子进程先于父进程退出后,父进程没有执行 wait 或者 waitpid 函数来回收子进程的时候,子进程的状态就会变为僵尸状态,表示已经死亡的进程,但是进程ID还保留着;
- 结束状态:当进程正确退出后,就进入结束状态,是进程的最终状态。
僵尸进程,孤儿进程,守护进程的区别?
僵尸进程是指子进程已经终止,但其父进程尚未调用wait()或waitpid()函数来获取子进程的终止状态,导致子进程的进程描述符仍然保留在系统进程表中,成为僵死进程,僵死进程不占用系统资源,但会占用一个进程ID。
孤儿进程是指父进程先于子进程退出或异常终止,导致子进程成为孤儿进程。孤儿进程会被init进程(进程ID为1)接管,init进程会成为孤儿进程的新的父进程。
守护进程是在后台运行的一种特殊进程,不与任何终端关联,关闭终端并不会影响守护进程的生命周期,守护进程的生命周期通常是伴随系统的启动和关闭,会一直在后台运行。
怎么杀死僵尸进程?
僵尸进程是已经死了的,不能直接使用 kill 命令杀掉僵尸进程,只能通过 ps 命令找到僵尸进程的父进程的 pid 号,然后通过 kill 命令杀掉父进程的方式来达到僵尸进程的效果,因为当父进程被杀掉后,操作系统会将僵尸进程的父进程会变为 1 号 init 进程,接着 init 进程会自动接管僵尸进程的回收工作。
多进程和多线程的区别?
多线程由于可以共享进程资源,而多进程不共享地址空间和资源,多进程需要通过进程间通信技术来实现数据传输,开发起来会比较麻烦。
但多进程安全性较好,在某一个进程出问题时,其他进程一般不受影响;而在多线程的情况下,一个线程执行了非法操作会导致整个进程退出。
一个进程fork出一个子进程,那么他们占用的内存是之前的2倍吗?
不是的。
fork 的时候,创建的子进程是复父进程的虚拟内存,并不是物理内存,这时候父子的虚拟内存指向的是同一个物理内存空间,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。
不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,新页面的访问权限设置为可读写。此时,写入操作的进程将拥有该页面的独立副本,而另一个进程仍然共享原始的只读页面,这个过程被称为「写时复制」,发生了这个过程,内存占用才会增多。
进程间通信
进程间有哪些通信方式?
进程间通信方式主要有管道、消息队列、共享内存、信号、信号量和 socket 通信。
管道通信的数据是无格式的字节流,并且通信方向是单向的,只能在一个方向上流动,管道分为匿名管道和有名管道;
- 匿名管道是没有文件实体,只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
- 有名管道有文件实体,可以用于任何进程间的通信,并且数据可以持久化保存,即使创建它的进程退出,其他进程仍然可以使用该管道。
- 消息队列在内核中是通过链表来组织消息的,克服了管道通信的数据是无格式的问题;
管道和消息队列在读写数据的时候,都需要经过用户态与内核态之间的拷贝过程,共享内存就解决了这个问题,多个进程可以将共享内存映射到各自的虚拟地址空间,实现共享数据的读写,不会涉及用户态和内核态之间的数据拷贝,所以共享内存方式是进程间通信方式里最高效的,不过共享内存带来新的问题,多进程竞争同个共享资源会造成数据的错乱,因为需要同步机制来保证多进程下的读写数据的安全。
信号量可以实现进程间的同步和互斥访问共享资源,信号量其实是一个计数器,表示的是资源个数,当一个进程想要访问资源时,它会尝试递减信号量(称为P操作)。如果信号量的值大于零,这个操作会成功,否则进程就会阻塞,直到信号量变为正数。当进程完成对资源的访问后,它会递增信号量(称为V操作),允许其他阻塞的进程访问资源。
信号是一种异步的通知机制,用于通知进程某个事件已经发生。当一个信号发送给一个进程时,操作系统会中断进程的正常流程来处理信号,只适用于简单的通知和事件处理。
前面提到的这些通信方式都只能在本地上进行进程间通信,如果要实现跨主机的进程间通信,就需要通过 socket 通信了,可以实现基于 TCP 或者 UDP 协议的通信方式。
哪个进程间通信效率最高的
共享内存的通信效率最高,因为共享内存不涉及内核态和用户态之间的数据拷贝。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝、传递,大大提高了进程间通信的速度。
信号和信号量的区别
信号用于异步处理异常或特殊事件,而信号量用于同步进程以安全地访问共享资源。
- 信号是一种异步的通知机制,用于通知进程某个事件已经发生。当一个信号发送给一个进程时,操作系统会中断进程的正常流程来处理信号。
- 信号量是一种用于解决多进程同步和互斥问题的工具,主要用于保护共享资源,防止多个进程同时访问。信号量有一个整数值,该值的含义是可以同时访问某个资源的进程数量。当一个进程想要访问资源时,它会尝试递减信号量(称为P操作)。如果信号量的值大于零,这个操作会成功,否则进程就会阻塞,直到信号量变为正数。当进程完成对资源的访问后,它会递增信号量(称为V操作),允许其他阻塞的进程访问资源。
调度
进程的调度算法有哪些?
先来先服务:按照进程到达的顺序进行调度,先到达的进程先执行。优势是简单、公平,但可能导致长作业等待时间过长,无法适应实时性要求高的场景。
最短作业优先:选择估计运行时间最短的进程优先执行。优势是能够最大程度地减少平均等待时间,但需要准确预测进程的运行时间,这个实现起来会比较困难。
时间片轮转(RR):将CPU时间分成多个时间片,每个进程轮流占用一个时间片,如果一个进程在该时间片结束时还没有完成,则将其移到队列的末尾,等待下一次调度。优势是公平,但对于长作业和实时性要求高的场景可能不够高效。
优先级调度:为每个进程分配一个优先级,优先级高的进程先执行。可根据不同的调度策略确定优先级,如静态优先级、动态优先级等。优势是能够根据进程的重要性和紧急程度进行调度,但可能导致优先级低的进程长时间等待。
多级反馈队列调度(MFQS):「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列。优点:动态优先级调整、短作业优先处理、提高系统吞吐量和利用率、支持实时任务。多级反馈队列调度存在的问题,优先级较低的队列可能会被优先级较高的队列长时间占用,导致优先级较低的进程无法得到执行,从而产生饥饿现象。
设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
锁
线程间同步方式有哪些?
Linux 系统提供了五种用于线程间同步的方式:互斥量、读写锁、信号量、自旋锁、条件变量。
- 互斥锁:用于保护共享资源,确保同一时间只有一个线程可以访问该资源。只有获得互斥锁的线程才能进入临界区,其他线程需要等待锁的释放。
- 读写锁:也称为共享-独占锁,允许多个线程同时读取共享资源,但在写操作时需要独占访问。读写锁在读多写少的场景中可以提供更好的并发性能。
- 信号量:用于控制对一组资源的访问。信号量可以允许多个线程同时访问资源,但是需要在访问前进行P操作(申请资源)和在访问结束后进行V操作(释放资源),以确保资源的正确使用。
- 自旋锁:是一种忙等待锁,在获取锁之前,线程会一直尝试获取锁,而不会进入睡眠状态。自旋锁适用于保护临界区较小、锁占用时间短暂的情况。
- 条件变量:用于在线程之间进行条件同步。一个线程可以等待某个条件满足,而另一个线程在满足条件时可以通知等待的线程继续执行。(wait-notify)
信号量和互斥锁应用场景有什么区别?
- 信号量一般以同步的方式对共享资源进行控制,而互斥锁通过互斥的方式对共享资源对其进行控制;
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
自旋锁和互斥锁有什么区别?分别适合哪些应用场景?
- 互斥锁加锁失败的时候,线程会放弃CPU,陷入到内核态,执行线程切换和CPU上下文切换的过程,切换到其他线程;
- 而自旋锁加锁失败后,线程不会放弃 CPU,而是选择忙等待,直到它拿到锁。
自旋锁适用于并发竞争时间短暂的情况,可以减少线程切换和CPU上下文切换的开销。互斥锁适用于并发竞争时间较长或资源争用较激烈的情况,可以避免线程忙等待,但会带来线程切换和上下文切换的开销。
悲观锁和乐观锁有什么区别?
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,很容易出现冲突,造成数据错乱,所以会每次读写共享资源之前,先要加锁,确保任意时刻只有一个线程才能对数据进行读写操作。
乐观锁做事比较乐观,它假定冲突的概率很低,先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作,并选择报错、重试等策略。
悲观锁适合并发写入多和竞争激烈的场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。乐观锁适合读多写少和并发不激烈的场景,在这些场景下乐观锁不加锁的特点能让性能大幅提高。