操作系统之进程和线程

操作系统也是软件,区别于应用软件的最大特点具有进程管理、内存管理等功能。

一 进程

1.1 什么是进程(process)

进程指的就是正在运行中的程序。进程也是有生命周期,当程序运行结束,则进程结束。如果程序没有运行呢就是代码。所以我们判断是不是进程的最主要区别就是看程序是否正在运行。

1.2 进程分类

1.2.1 按照运行在不同的态分为用户进程和系统进程

第一:运行在用户态的进程就属于用户进程,一般是没有权限操作系统资源的
第二:运行在内核态的进程就属于系统进程,一般是有权限操作系统资源的

1.2.2 按照对CPU的依赖程度

第一:对于CPU依赖较重,则属于计算型的进程,侧重于计算,比较消耗CPU
第二:对于CPU依赖较弱,则属于偏I/O型进程,侧重于IO读写,比较消耗I/O

1.4 进程的状态(5种)

新建态:刚刚创建的进程就处于新建状态
就绪态:处于就绪队列,等待被CPU执行的进程的状态就是就绪态。比如新创建的进程放入就绪队列或者时钟到期CPU将未执行完的进程放入就绪队列
运行态:如果被CPU调度执行,就是运行状态
阻塞态:运行中被阻塞,则处于阻塞态,比如I/O读,或者等待键盘输入等
终止态:进程被终止,则处于中止态

1.9 进程通信(进程之间信息交换或者传递)

我们知道进程是分配系统资源的单位(包括内存地址空间),因此各个进程拥有的内存地址空间相互独立。
为了保证安全,一个进程是不能直接访问另外一个地址空间。但是有的时候,进程之间信息交换又是必须的,为了保证进程间的通信安全,操作系统提供了一些方式。共享存储、消息传递、管道通信三种方式实现。

1.9.1 共享存储

操作系统为进程分配共享空间,两个进程之间可以通过共享空间来实现通信,但是需要注意的是,进程在访问共享空间的时候应该是互斥的,不然会出现并发问题,互斥是通过操作系统提供的工具实现的,比如P,V操作。
共享存可以基于数据结构或者存储共享。
第一:基于数据结构
比如提供一个16位长度的数组放在共享空间,然后每次进程只能通过这个数组进行通信,速度慢,限制多,是一种低级的通信方式
第二:基于存储区共享
在内存中获取一块位置,数据的形式,存放位置由进程控制,而不是操作系统,这种方式速度快更快,是一种高级的通信方式。

1.9.2 管道通信

管道是指用于连接读写进程的一个共享文件,其实就是在内存开辟的一个大小固定的缓冲区。

二 线程

2.1 线程出现的背景

第一:进程创建、切换资源消耗大
我们知道,进程是资源分配的基本单位,进程的切换需要保存进程状态,那么对资源的消耗很大。
第二:并发能力不强
我们知道,一个应用程序或者说一个进程,是从上往下顺序执行的,如果要执行某一个操作需要耗时很长,比如QQ中上传文件,我们不可能等待文件传输完了再继续视频聊天或者文字聊天吧,所以这种方式并发能力不强

所以,为了提高并发度,减小系统的开销,引入了线程,进程还是资源分配的基本单位,但是不再是CPU调度的基本单位了。线程现在才是调度的基本单位,共享进程的资源。

2.2 用户线程和内核线程

2.2.1 用户级线程(user-level thread)

#1 内核管理所有线程,并向应用程序提供API接口
#2 内核线程才是CPU调度的基本单位,而不是用户级线程。
#3 内核维护同时维护进程和线程的上下文
#4 线程的切换需要内核的支持
#5 一个线程发起系统调用,不会阻塞其他线程运行,时间片分配给线程,所以多线程的进程获取更多的CPU时间
比如WINDOWS实现了内核级线程

2.2.3 混合模型

线程的创建的在用户空间;但是线程的调度在内核空间,比如Solaris操作系统,即多个用户级线程通过多路复用技术,复用多个内核级线程。

3.2.1.2双标志先检查法

设置两个变量pturn = false; qturn = fasle; 表示p q 进程想进入临界区的意愿,比如pturn = true; 表示p进程想进入临界区。
每一个进程进入临界区之前,都会先检查当前有没有其他进程想进入临界区,如果没有则自己进入,然后自己的turn变量设置为true,开始访问临界区,访问结束。自己的turn置为false,表示自己不想进入了。
但是存在的问题就是,如果当p进程先检查q不想进入临界区,然后自己进入,但是还没有来得及将pturn置为true,则时钟中断,然后CPU运行q进程,q进程判断p进程没有意愿进入临界区,则q进程自己进入,则现在两个进程或者线程都进入了临界区。

3.2.2.2 TAS(Test and Set)

单处理器可以使用关闭中断的方式,但是多处理器是不行的,比如两个进程一个存钱一个取钱,同时操作金额,取钱的那个进程关闭中断了,但是另外一个没有,则还是可能给临界变量金额带来错误。
测试并设置(TAS),是为了防止多处理器并发引入的一种锁,也叫作自旋锁,设计的初衷是忙则等待。有的地方也叫作TSL即Test and Set Lock,其实表达的东西是一样的。
用Java语言来描述:

第一:创建全局的共享变量lock ,初始值是lock = false
第二:进程1调用原子指令TestAndSet(lock),由于lock = false, 所以更新全局的lock = true,并且返回旧的值false,对于自己来说

优点:实现简单,把上锁和检查用用硬件的方式封装成了原子操作,适合于多处理器机器
缺点:如果要求进程有限等待,比如等待M毫秒,就超时不等待,TLS无法实现;而且如果大量进程或者线程自旋,浪费CPU资源

3.2.2.3 Swap(XCHG)指令

Swap用Java语言描述:

优点:实现简单,把上锁和检查用用硬件的方式封装成了原子操作,适合于多处理器机器
缺点:如果要求进程有限等待,比如等待M毫秒,就超时不等待,没有实现;而且如果大量进程或者线程自旋,浪费CPU资源

3.3 同步机制

3.3.1 信 量机制

信 量(Semaphore)和PV操作
信 量是一种特殊的变量,用于进程之间传递信息的一个整数值,他可以解决同步和互斥问题,比如生产者和消费者问题、读写问题
对信 量可以实施的操作:
init:
P: 给信 量的值减1,比如count–,如果信 量的值小于0,则处于阻塞等待状态;然后将该进程插入到等待队列末尾

V:给信 量的值加1,比如count++,如果信 量小于或者等于0,则唤醒相应等待队列中的一个进程,改变其状态为就绪态,插入到就绪队列
注意:P V 操作是原子操作,执行过程中不允许被中断

缺点:程序编写技巧要求高,易出错

3.3.2 管程(monitor)

管程是一种高级同步机制,由关于共享资源的数据机构及在其上操作的一组过程组成。即它是管理共享资源的,在管理过程中提供了各种的各样操作。
进程只能够调用管程中的操作来间接访问管程中的共享数据结构。

管程要解决两个问题:
第一:互斥
管程是互斥进入的,只能有一个进程调用管程操作,编译器负责保证管程互斥性
第二:同步
管程中设置条件变量以及等待/唤醒操作

可以让一个进程或者线程在条件变量上等待,也可以发送信 将等待在条件变量上的进程或者线程唤醒

遇到的问题:
第一:进程A进入管程,但是需要阻塞等待其他进程的数据
第二:进程A进入等待队列,释放CPU
第三:进程B进入等待队列,但是进程B需要唤醒A,唤醒之后那就是两个进程同时存在
一般多线程是没有这个问题,因为临界区的代码都一样

3.3.3 PThread同步机制

PThread_mutex_lock 有则获取锁,没有阻塞等待
PThread_mutex_tryLock 要么获取锁,要么获取锁失败
PThread_mutex_unlock 释放一个锁

PThread_mutex_wait 阻塞等待,直到被唤醒
PThread_mutex_signal 被其他先程唤醒
PThread_mutex_broadcast 同时唤醒其他所有的线程

3.4 死锁和饥饿

3.4.1 什么是死锁

并发环境下,各进程或者线程因为竞争资源而导致都在等待获取对方手里的资源,导致各进程或者线程阻塞,都没办法向前推进,所以这就是死锁。造成这种死锁的情况,比如因为抛出异常,导致锁或者资源没有被释放,又或者是因为获取资源的顺序。举个例子:
例子1:锁嵌套导致的死锁

当用户A给用户B转账的时候,首先对自己账 from加锁,然后对转入账 加锁,一般情况下没什么问题,但是如果在A给B转账的同时 B也在给A转账,比如:
第一:A转账,对A账户加锁,此时正准备给B账户加锁的时候,时钟中断,CPU切换到了B给A转账的线程
第二:B开始给自己账户加锁,然后给A账户加锁,发现A账户已经被别的线程持有锁,所以就等待
第三:时钟中断,CPU切换到A,A获取B账户的锁,发现B账户被别的线程加了锁,然后就等待
第四:这样也是互相等待释放锁,从而产生了死锁

3.4.2 进程死锁、饥饿和死循环有什么区别

饥饿:指的是长期得不到想要资源,某进程或者线程无法向前推进,比如调度算法中的优先级调度,优先级低的可能很久都不会被调度,从而发生进程或者线程饥饿
死循环:某进程或者线程执行过程中,一直跳不出某个循环的现象,这个和并发没有关系,更多的是逻辑上的错误;而死锁更多是因为并发带来问题

3.4.3 死锁和活锁有啥区别

进程或者线程执行的时候,没有被阻塞,但是无法满足某种条件,导致一直重复的进行操作或者尝试,但是程序就是无法前进。这种就是活锁。

活锁和死锁的区别:
#1 死锁是必须阻塞等待对方线程释放锁资源;活锁则不是阻塞等待,而是重复运行或者重试
#2 死锁是进程或者线程必须持有一个资源,然后去获取另外一个资源;但是活锁没有这个限制

比如ZK中,在阶段1的时候,提案者1提出了M1的方案,然后提交给接收者,返回过半票数;然后提案者2提出了M2的方案,也提交了,因为M2>M1,所以也返回过半票数
当在第二阶段的时候,提案者1提交的时候,发现自己的提案不是最新的,则重新发起新的一阶段请求,提交提案M3,,M3大于M2,同样M3批准
然后提案者在二阶段的时候,发现自己的提案也不是最新的,则重新发起新的一阶段请求,提交提案M4,M4大于M3
如此这样,一直反复。这就是活锁。

3.4.4 死锁产生的必要条件

#1 进程必须是互斥的,即存在竞争临界资源
#2 进程获取资源之后,不能在未结束之前,强行被别的进程夺走,只可以主动释放
#3 进程持有多个资源。即 这个进程在持有某个资源不放的同时,还希望持有别的资源,这时候才会被阻塞
#4 存在资源循环等待

3.4.5 死锁的处理策略

3.4.5.1预防死锁(静态策略)

第一:破坏互斥条件
将互斥资源改造成可以共享的资源,比如使用SPOOLing技术

第二:破坏不剥夺条件
方案1: 当某个进程请求新的资源得不到满足的时候,他必须理科释放掉所保持的资源,待以后重新申请,也就是说先申请的资源即使没有使用,也需要主动释放
方案2:可以借助操作系统强行剥夺,比如优先级调度

第三:破坏请求和保持条件
进程一次性申请完他所需要的全部资源,在资源没有满足的时候,不让其运行;一旦获取资源这个进程,该进程就不会再请求别的任何资源

第四:破坏循环等待条件

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年3月15日
下一篇 2021年3月15日

相关推荐