图解系统 - 进程管理

本文详细讲解了进程和线程的基础知识,包括它们的状态、控制结构、上下文切换,以及线程的优点和与进程的区别。讨论了进程间通信的各种方式,如管道、消息队列、共享内存和信号量,并介绍了如何处理多线程冲突和避免死锁。还涵盖了乐观锁与悲观锁的区别以及进程创建线程的数量限制。
摘要由CSDN通过智能技术生成

文章提纲

5.1 进程、线程基础知识

进程

进程的状态

进程的控制结构

进程控制块(PCB):存储进程的描述信息、进程的控制和管理信息、资源分配清单、CPU相关信息

PCB是由链表方式组织的

进程的上下文切换

一个进程切换到另一个进程运行,称为进程的上下文切换

进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源

线程

什么是线程?

线程是进程当中的一条执行流程

线程的优点:

  • 一个进程中可以同时存在多个线程;
  • 各个线程之间可以并发执行;
  • 各个线程之间可以共享地址空间和文件等资源;

线程与进程的比较

  • 进程是资源分配单位,线程是CPU调度单位
  • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源
  • 线程与进程同样用用就绪、阻塞、执行三种基本状态
  • 线程能减少并发执行的时间和空间开销

线程的实现

  • 用户线程
  • 内核线程
  • 轻量级进程

调度

针对上面的五种调度原则,总结成如下:

  • CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
  • 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
  • 周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
  • 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
  • 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。

说白了,这么多调度原则,目的就是要使得进程要「快」。

5.2 进程间有哪些通信方式?

管道

命名管道

匿名管道

缺点:管道这种通信方式效率低,不适合进程间频繁地交换数据

消息队列

缺点:一是通信不及时,二是附件也有大小限制

共享内存

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中

信号量

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。

具体过程:

  • 如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
  • 接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
  • 最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。

可以发现,信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。

信号

对于异常情况下的工作模式,就需要用「信号」的方式来通知进程

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如

  • Ctrl+C 产生 SIGINT 信号,表示终止该进程;
  • Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;

Socket

跨网络与不同主机上的进程之间通信,就需要 Socket 通信了

5.3 多线程冲突了怎么办?

互斥和同步的概念

互斥:一个线程在运行时,其他线程只能等待

同步:一个线程必须等待业务执行到某个节点并通知可以执行了,另外一个线程才能执行

互斥与同步的实现

自旋锁:用while循环一直获取锁,处于自旋的状态,知道获取锁才继续执行

无需等待锁:把未获取到锁的线程放到等待队列中,等待其他线程释放锁

信号量

P操作:将sem -1,如果sem<0,则进程/线程进入阻塞等待,否则继续,P操作会阻塞

V操作:将sem +1,如果sem<=0,唤醒一个等待中的进程/线程,V操作不会阻塞

生产者-消费者问题

生产者-消费者问题描述:

  • 生产者在生成数据后,放在一个缓冲区中;
  • 消费者从缓冲区取出数据处理;
  • 任何时刻,只能有一个生产者或消费者可以访问缓冲区;

5.4 怎么避免死锁

  • 死锁的概念;
  • 模拟死锁问题的产生;
  • 利用工具排查死锁问题;
  • 避免死锁问题的发生;

死锁的概念

两个线程使用了互斥锁,都在等待对方释放锁时就会发生死锁

  • 死锁的概念;
  • 模拟死锁问题的产生;
  • 利用工具排查死锁问题;
  • 避免死锁问题的发生;

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;
  • 持有并等待条件;
  • 不可剥夺条件;
  • 环路等待条件;

互斥条件

多个线程不能同时使用同一个资源。

5.5 什么是悲观锁、乐观锁?

  • 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的

如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。

读写锁

当写锁没有被线程持有的时候,读锁可以被多个线程并发持有

一旦写锁被线程持有,读锁获取锁的操作会被阻塞,其他的写锁也会被阻塞

读写锁在读多写少的场景,能发挥出优势

 读优先锁

当A线程获取到了读锁,B线程获取写锁时会阻塞,C线程能顺利获取读锁,知道A、C线程释放读锁,B线程才能获取读锁

  

 写优先锁

 当A线程获取读锁,B线程获取读锁会阻塞,C线程获取读锁也会阻塞,当A线程释放读锁的时候,会优先让B线程先获取读锁,C线程继续阻塞

  

饥饿现象

  1. 在读优先锁的场景下,如果一直有读线程获取读锁,那么写线程永远获取不到写锁,这就造成了线程的【饥饿】现象
  2. 在写优先锁的场景下,如果一直有写线程获取写锁,读线程也会被【饿死】

公平读写锁

公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象

 乐观锁与悲观锁

前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。

悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁

乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作

乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

总结

开发过程中,最常见的就是互斥锁的了,互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。

如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。

如果能区分读操作和写操作的场景,那读写锁就更合适了,它允许多个读线程可以同时持有读锁,提高了读的并发性。根据偏袒读方还是写方,可以分为读优先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,而写优先锁会优先服务写线程,读线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先入先出的原则来对线程加锁,这样便保证了某种线程不会被饿死,通用性也更好点。

互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。

另外,互斥锁、自旋锁、读写锁都属于悲观锁,悲观锁认为并发访问共享资源时,冲突概率可能非常高,所以在访问共享资源前,都需要先加锁。

相反的,如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

但是,一旦冲突概率上升,就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。

不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。再来,使用上了合适的锁,就会快上加快了。

5.6 一个进程最多可以创建多少个线程?

这个问题跟两个东西有关系:

  • 进程的虚拟内存空间上限
  • 系统参数限制

简单总结下:

  • 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
  • 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值