操作系统-线程与进程

本文最后更新于:1 年前

[TOC]

进程与线程

进程

更好的描述和控制程序的并发执行。
PCB 进程控制块-进程存在的唯一标志。
程序的一次执行过程。
拥有资源的基本单位。
状态:运行,就绪,阻塞,创建,结束

线程

一个基本的 CPU 执行单元。
是系统独立调度派的基本单位。
一个进程中可以有多个线程。
线程切换只需要保存设置程序计数器,虚拟机栈少量内容,开销小。轻量级进程。

进程调度

  • ⼀旦操作系统把进程切换到运⾏状态,也就意味着该进程占⽤着 CPU 在执⾏,但是当操作系统把进程切换到其他状态时,那就不能在 CPU 中执⾏了,于是操作系统会选择下⼀个要运⾏的进程。
  • 选择⼀个进程运⾏这⼀功能是在操作系统中完成的,通常称为调度程序(scheduler)
  • 在进程的⽣命周期中,当进程从⼀个运⾏状态到另外⼀状态变化的时候,其实会触发⼀次调度。
  • ⾮抢占式调度算法挑选⼀个进程,然后让该进程运⾏直到被阻塞,或者直到该进程退出,才会调⽤另外⼀个进程,也就是说不会理时钟中断这个事情。
  • 抢占式调度算法挑选⼀个进程,然后让该进程只运⾏某段时间,如果在该时段结束时,该进程仍然在运⾏时,则会把它挂起,接着调度程序从就绪队列挑选另外⼀个进程。
  • 这种抢占式调度处理,需要在时间间隔的末端发⽣时钟中断,以便把 CPU 控制返回给调度程序进⾏调度,也就是常说的时间⽚机制 。

调度的算法评价:

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

进程通信

管道

  • 连接两个进程实现通信的一个共享文件。pipe
  • 字符流的形式写入接收。
  • 固定大小的缓冲区,4kb。
  • 同一时刻只能单向通信。
  • 不管是匿名管道还是命名管道,进程写⼊的数据都是缓存在内核中,另⼀个进程读取数据时候⾃然也是从内核中获取,同时通信数据都遵循先进先出原则
  • 缓冲区中有数据才能读,写数据会先写满,才会允许读。还有数据时,不允许写。

消息队列

  • 管道的通信⽅式是效率低的,因此管道不适合进程间频繁地交换数据

  • 消息队列的通信模式就可以解决。⽐如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

  • 消息队列是保存在内核中的消息链表,在发送数据时,会分成⼀个⼀个独⽴的数据单元,也就是消息体(数据块),消息体是⽤户⾃定义的数据类型,消息的发送⽅和接收⽅要约定好消息体的数据类型,所以每个消息体都是固定⼤⼩的存储块,不像管道是⽆格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

  • 消息队列⽣命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会⼀直存在,⽽前⾯提到的匿名管道的⽣命周期,是随进程的创建⽽建⽴,随进程的结束⽽销毁。

  • 消息队列不适合⽐较⼤数据的传输,因为在内核中每个消息体都有⼀个最⼤⻓度的限制,同时所有队列所包含的全部消息体的总⻓度也是有上限。

  • 消息队列通信过程中,存在⽤户态与内核态之间的数据拷⻉开销。

共享内存

  • 拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写⼊的东⻄,另外⼀个进程⻢上就能看到了,不需要拷⻉来拷⻉去,⼤⼤提⾼了进程间通信的速度。

信号量

  • 多个进程同时修改同⼀个共享内存,很有可能就冲突。信号量就实现了这⼀保护机制。

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

  • 信号量表示资源的数量,控制信号量的⽅式有两种原⼦操作:

  • ⼀个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 **< 0,**则表明资源已被占⽤,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使⽤,进程可正常继续执⾏。

  • 另⼀个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 **<= 0**,则表明当前有阻塞中的进程,于是会将该进程唤醒运⾏;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

  • P 操作是⽤在进⼊共享资源之前,V 操作是⽤在离开共享资源之后,这两个操作是必须成对出现的。

  • 信号量初始化为 1 ,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有⼀个进程在访问,这就很好的保护了共享内存。

  • 信号量来实现多进程同步的⽅式,我们可以初始化信号量为 0

image-20210801220932770

信号

  • 对于异常情况下的⼯作模式,就需要⽤「信号」的⽅式来通知进程。
  • 信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。
  • 信号是进程间通信机制中唯⼀的异步通信机制,因为可以在任何时候发送信号给某⼀进程,⼀旦有信号产⽣,我们就有下⾯这⼏种,⽤户进程对信号的处理⽅式。
  • 默认操作,捕捉信号,忽略信号。

socket

Socket 通信不仅可以跨⽹络与不同主机的进程间通信,还可以在同主机上进程间通信。
根据创建 socket 类型的不同,通信的⽅式也就不同:

  • 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
  • 实现 UDP 数据报通信: socket 类型是 AF_INET 和 SOCK_DGRAM;
  • 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket。
TCP
  • 服务端和客户端初始化 socket ,得到⽂件描述符;
  • 服务端调⽤ bind ,将绑定在 IP 地址和端⼝;
  • 服务端调⽤ listen ,进⾏监听;
  • 服务端调⽤ accept ,等待客户端连接;
  • 客户端调⽤ connect ,向服务器端的地址和端⼝发起连接请求;
  • 服务端 accept 返回⽤于传输的 socket 的⽂件描述符;
  • 客户端调⽤ write 写⼊数据;服务端调⽤ read 读取数据;
  • 客户端断开连接时,会调⽤ close ,那么服务端 read 读取数据的时候,就会读取到了 EOF ,待处理完数据后,服务端调⽤ close ,表示连接关闭。
UDP
  • 每⼀个 UDP 的 socket 都需要 bind 。

进程同步

哲学家就餐

⽤⼀个数组 state 来记录每⼀位哲学家在进程、思考还是饥饿状态(正在试图拿叉⼦)。那么,⼀个哲学家只有在两个邻居都没有进餐时,才可以进⼊进餐状态。

生产者消费者

互斥信号量 mutex :⽤于互斥访问缓冲区,初始化值为 1;
资源信号量 fullBuffers :⽤于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0
(表明缓冲区⼀开始为空);
资源信号量 emptyBuffers :⽤于⽣产者询问缓冲区是否有空位,有空位则⽣成数据,初始化值为 n
(缓冲区⼤⼩);

死锁

多个进程因为竞争资源造成僵局,互相等待。

条件

  • 互斥-资源仅为一个线程占有
  • 不剥夺
  • 请求与保持-保持了至少一个资源又在请求其他的资源
  • 循环等待-进程资源的循环等待链

其他的锁

互斥锁加锁失败后,线程会释放 CPU ,给其他线程;(两次线程上下文切换)
⾃旋锁加锁失败后,线程会忙等待,直到它拿到锁
⾃旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「⽤户态」完成加锁和解锁操作,不会主动产⽣线程上下⽂切换,所以相⽐互斥锁来说,会快⼀些,开销也⼩⼀些。

互斥锁、⾃旋锁、读写锁,都是属于悲观锁
悲观锁做事⽐较悲观,它认为多线程同时修改共享资源的概率⽐较⾼,于是很容易出现冲突,所以访问共享资源前,先要上锁。
那相反的,如果多线程同时修改共享资源的概率⽐较低,就可以采⽤乐观锁。

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

放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很⾼,但是冲突的概率⾜够低的话,还是可以接受的。

场景例⼦:在线⽂档。
我们都知道在线⽂档可以同时多⼈编辑的,如果使⽤了悲观锁,那么只要有⼀个⽤户正在编辑⽂档,此时其他⽤户就⽆法打开相同的⽂档了,这⽤户体验当然不好了。
那实现多⼈同时编辑,实际上是⽤了乐观锁,它允许多个⽤户打开同⼀个⽂档进⾏编辑,编辑完提交之后才验证修改的内容是否有冲突。
怎么样才算发⽣冲突?这⾥举个例⼦,⽐如⽤户 A 先在浏览器编辑⽂档,之后⽤户 B 在浏览器也打开了相同的⽂档进⾏编辑,但是⽤户 B ⽐⽤户 A 提交改动,这⼀过程⽤户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并⾏修改的地⽅就会发⽣冲突。

服务端要怎么验证是否冲突了呢?
当⽤户提交修改时,发给服务端的请求会带上原始⽂档版本号,服务器收到后将它与当前版本号进⾏⽐较,如果版本号⼀致则修改成功,否则提交失败