chapter 2
parent
277fcb1b8b
commit
ecf76d91fc
|
@ -260,4 +260,429 @@ date: 2023-07-01T15:59:00+08:00
|
||||||
|
|
||||||
### 线程组成和控制
|
### 线程组成和控制
|
||||||
|
|
||||||
*
|
* 每个线程配置一个线程控制块TCP
|
||||||
|
* 功能
|
||||||
|
* 记录控制和管理线程的信息
|
||||||
|
* 组成
|
||||||
|
* 线程标识符
|
||||||
|
* 一组寄存器
|
||||||
|
* 线程运行状态
|
||||||
|
* 优先级
|
||||||
|
* 线程专有存储区
|
||||||
|
* 堆栈指针
|
||||||
|
* 控制线程
|
||||||
|
* 创建线程
|
||||||
|
* 终止线程
|
||||||
|
|
||||||
|
|
||||||
|
### 线程的分类
|
||||||
|
|
||||||
|
||ULT|KLT|组合模式|
|
||||||
|
|---|---|---|---|
|
||||||
|
|定义|由用户级线程库来完成整个线程的管理和调度【库函数】负责|线程对应的TCB放在OS里,线程的管理和调度由OS负责【OS负责】|内核支持的用户线程|
|
||||||
|
|模型|多对一模型(多个用户对应一个线程)|一对一模型(一个用户对应一个线程)|多对多模型|
|
||||||
|
|优点|TCB由用户级线程库函数维护,可用于不支持线程技术的OS<br>无需用户态和内核态的切换,速度特别快|某个内核线程发起系统调用被阻塞,不会影响到其他内核线程的运行<br>内核能同时调度同一进程中的多个线程并行执行||
|
||||||
|
|缺点|一个线程发起了系统调用二阻塞,那进程所包含的用户线程都不能执行了<br>在多线程执行时,每个线程得到的时间片少,执行慢<br>线程与线程之间不用内核切换,但是跨进程的话就需要内核参与|同一进程的线程切换,需要从用户态转到核心态,系统开销大||
|
||||||
|
|
||||||
|
### 多线程模型
|
||||||
|
|
||||||
|
* 多对一模型
|
||||||
|
* 定义
|
||||||
|
* 多个ULT映射到一个KLT
|
||||||
|
* 优点
|
||||||
|
* 线程管理在用户空间进行,效率高
|
||||||
|
* 缺点
|
||||||
|
* 如果一个线程阻塞,其他进程都会被阻塞
|
||||||
|
* 任何时刻,只有一个线程能访问内核
|
||||||
|
* 多个线程不能同时在多个处理机上运行
|
||||||
|
* 一对一模型
|
||||||
|
* 定义
|
||||||
|
* 每个ULT映射到一个KLT
|
||||||
|
* 优点
|
||||||
|
* 一个线程被阻塞,运行调度另一个线程运行,并发能力强
|
||||||
|
* 缺点
|
||||||
|
* 每创建一个用户线程,就要创建一个对应的内核线程,开销大
|
||||||
|
* 多对多模型
|
||||||
|
* n个ULT映射到m个KLT,n$geq$m
|
||||||
|
* 优点
|
||||||
|
* 克服了多对一模型的并发度不高的缺点
|
||||||
|
* 克服了一对一模型的一个用户进程占用太多内核线程而开销打的缺点
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 处理机调度
|
||||||
|
|
||||||
|
### 常考知识点
|
||||||
|
|
||||||
|
* 作业是用户提交的,以用户任务为单位
|
||||||
|
* 进程是系统自动生成的,以操作系统控制为单位
|
||||||
|
|
||||||
|
### 基本概念
|
||||||
|
|
||||||
|
* 调度是处理机进行分配,即从就绪队列中按一定算法(公平,高效的原则)选择一个进程并将处理机分配给他运行,以实现进程并发地执行
|
||||||
|
* 调度是多道程序OS的基础:调度是OS设计的核心问题
|
||||||
|
* 层次分类模型图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* 高级调度、作业调度
|
||||||
|
* 是内存与辅存的调度,从后备队列中调度作业
|
||||||
|
* 每个作业只调入调出一次
|
||||||
|
* 通常存在于多道批处理系统中
|
||||||
|
* 中级调度、内存调度
|
||||||
|
* 提高内存利用率和系统吞吐量
|
||||||
|
* 将暂时不能运行的进程调度到外存等待,设为挂起态,最后修改状态为就绪态,挂在就绪队列上
|
||||||
|
* 是存储器管理中的对换功能
|
||||||
|
* 低级调度、进程调度
|
||||||
|
* 从就绪队列中选取一个进程,调度频率很高
|
||||||
|
* 各种OS都必须配置这种调度
|
||||||
|
* 三种调度的联系
|
||||||
|
* 作业调度为进程活动做准备,进程调度使进程正常活动
|
||||||
|
* 中级调度将暂时不能运行的进程挂起,中级调度处于另外两个调度之间
|
||||||
|
* 调用频率:作业调度<内存调度<进程调度
|
||||||
|
* 进程调度使最基本的,不可或缺
|
||||||
|
|
||||||
|
### 调度的实现
|
||||||
|
|
||||||
|
* 调度程序结构图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* 调度器的组成
|
||||||
|
* 用于调度和分派CPU的组件
|
||||||
|
* 排队器
|
||||||
|
* 按策略给就绪进程排出一个或多个队列
|
||||||
|
* 分派器
|
||||||
|
* 从就绪队列中取出进程,并分配CPU
|
||||||
|
* 上下文切换器
|
||||||
|
* 在对处理机进行切换时,会发生两对上下文的切换操作
|
||||||
|
* 调度的时机
|
||||||
|
* 先调度再切换
|
||||||
|
* 进程从一个状态到另一个状态变化时,就会触发一次调度
|
||||||
|
* 调度时机举例
|
||||||
|
* 运行的进程运行完毕
|
||||||
|
* 运行的进程时间片用完
|
||||||
|
* 运行的进程所需资源未准备好
|
||||||
|
* 运行的进程自我阻塞
|
||||||
|
* 运行的进程出现错误
|
||||||
|
* 进程的切换
|
||||||
|
* 不能进行调度与切换的情况
|
||||||
|
* 在处理中断的过程中
|
||||||
|
* 进程在OS内核临界区中
|
||||||
|
* 其他需要完全屏蔽中断的原子操作过程中
|
||||||
|
* 可以进行调度与切换的情况
|
||||||
|
* 发生引起调度条件且当前进程无法继续进行下去时(非剥夺调度)
|
||||||
|
* 中断处理结束或自陷处理结束后,被置上请求调度标志(剥夺方式的调度)
|
||||||
|
* 在进程结束时能进行处理机调度
|
||||||
|
* 创建新进程后能进行处理机调度
|
||||||
|
* 在系统调用完成并返回用户态时能进行处理机调度
|
||||||
|
* 进程处于临界区时,只要不破坏临界资源的使用规则,就不影响处理机的调度
|
||||||
|
* 切换的过程
|
||||||
|
* 将原进程的信息推入当前进程的内核堆栈中,并更新堆栈指针
|
||||||
|
* 内核从新进程的内核栈中装入新进程的信息
|
||||||
|
* 内核更新当前运行的进程空间指针,重设PC寄存器后开始运行新的进程
|
||||||
|
* 调度方式
|
||||||
|
* 非抢占式调度
|
||||||
|
* 优点
|
||||||
|
* 实现简单,系统开销小,适合批处理系统
|
||||||
|
* 缺点
|
||||||
|
* 不适合分时和大多数的实时系统
|
||||||
|
* 抢占式调度
|
||||||
|
* 优点
|
||||||
|
* 有利于提高系统吞吐率和响应效率
|
||||||
|
* 缺点
|
||||||
|
* 必须遵循一定的准则(如优先级,短进程优先,时间片原则)
|
||||||
|
|
||||||
|
### 调度相关指标
|
||||||
|
|
||||||
|
* 调度最终考虑的元素:特定用户的要求 + 系统整体效率 + 调度算法的开销
|
||||||
|
* 常用指标
|
||||||
|
* CPU利用率:$\frac{CPU有效工作时间}{CPU有效工作时间 + CPU空闲等待时间}$
|
||||||
|
* 等待时间:等待CPU的时间
|
||||||
|
* $周转时间(t_i)= 作业完成时间 - 作业提交时间$
|
||||||
|
* $带权周转时间(w_i) = \frac{周转时间}{作业实际运行时间}$
|
||||||
|
* $平均周转时间 = \frac{t_1+t_2+\cdots+t_i}{i}$
|
||||||
|
* $平均带权周转时间 = \frac{w_1+w_2+\cdots+w_i}{i}$
|
||||||
|
|
||||||
|
|
||||||
|
### 调度算法
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|作业号|提交时间|运行时间|开始时间|等待时间|完成时间|周转时间|带权周转时间|
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
|1|8|2|8|0|10|2|1|
|
||||||
|
|2|8.4|1|10|1.6|11|2.6|2.6|
|
||||||
|
|3|8.8|0.5|11|2.2|11.5|2.7|5.4|
|
||||||
|
|4|9|0.2|11.5|2.5|11.7|2.7|13.5|
|
||||||
|
|
||||||
|
* FCFS先来先服务
|
||||||
|
* 平均等待时间 = 1.575
|
||||||
|
* 平均周转T = 2.5
|
||||||
|
* 平均带权周转时间 = 5.625
|
||||||
|
|
||||||
|
|
||||||
|
|作业号|提交时间|运行时间|开始时间|等待时间|完成时间|周转时间|带权周转时间|
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
|1|8|2|8|0|10|2|1
|
||||||
|
|2|8.4|1|10.7|2.3|11.7|3.3|3.3|
|
||||||
|
|3|8.8|0.5|10.2|1.4|10.7|1.9|3.8|
|
||||||
|
|4|9|0.2|10|1|10.2|1.2|6|
|
||||||
|
|
||||||
|
* 短作业优先算法
|
||||||
|
* 平均等待时间 = 1.175
|
||||||
|
* 平均周转时间 = 2.1
|
||||||
|
* 平均带权周转时间 = 3.525
|
||||||
|
|
||||||
|
|
||||||
|
* 高响应比优先调度算法
|
||||||
|
* 响应比$R_p = \frac{等待时间+要求服务时间}{要求服务时间}$(值越大,调用优先级越高)
|
||||||
|
* 其余算法
|
||||||
|
* 优先级调度算法
|
||||||
|
* I/O繁忙型作业优于计算繁忙作业【因为I/O操作要及时完成,无法长时间保存数据】
|
||||||
|
* 系统进程优于用户进程
|
||||||
|
* 时间片轮转算法
|
||||||
|
* 若时间片过大,退化为FCFS
|
||||||
|
* 若时间片过小,切换频繁,处理机开销增大
|
||||||
|
* 多级反馈队列算法
|
||||||
|
* 综合考虑
|
||||||
|
* 优先级数量,优先级之间的转换规则
|
||||||
|
* 就绪队列数量,就绪队列的调度算法
|
||||||
|
* 进程在就绪队列间的迁移条件
|
||||||
|
|
||||||
|
|
||||||
|
### 调度算法对比
|
||||||
|
|
||||||
|
| | FCFS |SJFA|高响应比|时间片轮转|多级反馈队列|
|
||||||
|
|---| --- |---| --- | --- | --- |
|
||||||
|
|可抢占|F|T|T|T|队列内算法不一定|
|
||||||
|
|不可抢占|T|T|T|F|队列内算法不一定|
|
||||||
|
|特点|公平<br>实现简单<br>有利于长作业<br>不利于短作业<br>有利于CPU繁忙型作业<br>不利于I/O繁忙作业|平均等待时间最少<br>效率最高|兼顾长短作业<br>满足短作业优先且不会发生饥饿现象|兼顾长短作业<br>为了多个用户能够及时干预系统<br>绝对可抢占的|兼顾长短作业<br>有较好的响应时间<br>可行性强|
|
||||||
|
|缺点|不利于短作业|长作业会饥饿<br>估计时间不易确定|计算响应比的开销大|平均等待时间最长<br>上下文切换浪费时间|无|
|
||||||
|
|适用于|无|作业调度<br>批处理系统|无|分时系统<br>适用于人机交互系统|相当通用<br>大家都满意的算法|
|
||||||
|
|默认决策模式|非抢占|非抢占|非抢占|抢占|抢占|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 同步与互斥
|
||||||
|
|
||||||
|
### 常见考点
|
||||||
|
|
||||||
|
* 可重入代码/纯代码:一种允许多个进程同时访问的代码,如进程映像中的共享程序段
|
||||||
|
* PV操作实现的同步的S的初值由用户确定,如果期望的信息还没发送,则对应的初值为-,若信息已存在,则初值为非0的正数,PV操作实现的互斥的S的初值为1
|
||||||
|
* 执行P操作的进程处于运行态;若P操作后S<0,则表示没有可用资源,该进程进入阻塞状态
|
||||||
|
* 判断代码中的语句是否要互斥执行
|
||||||
|
* 不同范围的变量不需要互斥
|
||||||
|
* 对变量赋值前,都有声明语句的话,不需要互斥
|
||||||
|
* 信箱通信是一种间接通信
|
||||||
|
|
||||||
|
|
||||||
|
### 基本概念
|
||||||
|
|
||||||
|
* 引入同步互斥原因
|
||||||
|
* 因为并发进程是异步的,为了协调进程之间的相互制约关系,所以引入同步互斥
|
||||||
|
* 进程异步性
|
||||||
|
* 由于系统的资源有限,进程的执行不是一贯到底的,而是走走停停,以不可预知的速度向前推进
|
||||||
|
* 并发过程执行产生的两种相互制约关系
|
||||||
|
* 同步:进程A应在进程B之前执行
|
||||||
|
* 互斥:进程A和进程B不能再同一时刻执行
|
||||||
|
* 临界资源
|
||||||
|
* 一次仅允许一个进程使用的资源(如物理设备,共享变量,共享数据,共享缓冲区,公用队列)
|
||||||
|
* 共享资源
|
||||||
|
* 可被多个进程同时使用的资源;如可重入代码/纯代码、共享程序段、磁盘、非共享数据
|
||||||
|
* 临界区
|
||||||
|
* 访问临界资源的那段代码
|
||||||
|
* 如果n个进程涉及到了同一个变量A,则A的相关临界区 = 访问临界资源A的那段代码 = n个代码段
|
||||||
|
* 同步机制遵循的准则
|
||||||
|
* 空闲让进:临界区空闲,允许一个进程进入【运行进程访问空闲的临界资源】
|
||||||
|
* 忙则等待:有进程进入临界区时,其他进程需要等待【两个进程不能同时进入临界资源】
|
||||||
|
* 有限等待:请求访问的进程应保证在有限时间内进入临界区【进程等待进入临界区的时间是有限的】
|
||||||
|
* 让权等待:进程不能进入临界区时,应该立即释放处理器,防止进程忙等待【不能进入临界区的执行态进程立即放弃CPU】
|
||||||
|
|
||||||
|
### 进程同步与互斥机制:硬件和软件方法
|
||||||
|
|
||||||
|
* 硬件方法【此方法实现同步时不能实现让权等待】
|
||||||
|
* 中断屏蔽法
|
||||||
|
* 硬件指令方法
|
||||||
|
* swap指令
|
||||||
|
* TestAndSet指令
|
||||||
|
* 软件方法
|
||||||
|
* 单标志法
|
||||||
|
* 双标志法
|
||||||
|
* Peterson's Algorithm【满足有限等待但不满足让权等待】
|
||||||
|
* 记录型信号量引入阻塞机制,可以让权等待
|
||||||
|
|
||||||
|
|
||||||
|
### 进程同步与互斥机制:管程
|
||||||
|
|
||||||
|
* 定义
|
||||||
|
* 管程定义了共享数据结构和各种进程在该数据结构上的全部操作
|
||||||
|
* 结构类似于Class,把对共享资源的操作封装起来
|
||||||
|
* 管程支持进程互斥;任何时候只有一个进程在管程中执行
|
||||||
|
* 管程不仅能实现进程间互斥。还能实现进程间同步
|
||||||
|
* java采用管程机制
|
||||||
|
* Java中管程相关代码:sunchronized关键字,wait(),notify(),notifyAll()
|
||||||
|
* 组成
|
||||||
|
* 管程的名字
|
||||||
|
* 局部于管程内部的共享数据结构或者共享变量说明
|
||||||
|
* 对管程内的数据结构进行操作的一组过程
|
||||||
|
* 对局部于管程内部的共享数据设置初始值的语句
|
||||||
|
* 管程的组成都是基于管程内部的
|
||||||
|
* 管程中设置的条件变量
|
||||||
|
* 定义
|
||||||
|
* 阻塞原因定义为条件变量condition
|
||||||
|
* 操作
|
||||||
|
* x.wait:阻塞进程,将其插入到阻塞队列中
|
||||||
|
* x.signal:唤醒进程,将其插入到就绪队列中
|
||||||
|
* 与信号量相似点
|
||||||
|
* wait/signal类似于信号量的P/V操作,实现进程的阻塞/唤醒,但不能说和PV操作相同
|
||||||
|
* 与信号量不同点
|
||||||
|
* 条件变量没有值,仅实现排队等待功能
|
||||||
|
* 信号量有值,这个值反映了剩余资源数
|
||||||
|
|
||||||
|
|
||||||
|
### 进程同步与互斥机制:互斥锁
|
||||||
|
|
||||||
|
* 定义
|
||||||
|
* 解决临界区最简单的工具
|
||||||
|
* 特点
|
||||||
|
* 通常采用硬件机制实现
|
||||||
|
* 常用于多处理器系统
|
||||||
|
* 缺点
|
||||||
|
* 忙等待
|
||||||
|
* 代码
|
||||||
|
* accquire() 获取锁
|
||||||
|
* release() 释放锁
|
||||||
|
|
||||||
|
|
||||||
|
### 进程同步与互斥机制:信号量和PC操作
|
||||||
|
|
||||||
|
* PV操作定义
|
||||||
|
* P操作
|
||||||
|
* 将信号量值S减一,表示申请占用一个资源
|
||||||
|
* 如果S<0,表示已经没有可用资源,执行P操作的进程被阻塞
|
||||||
|
* 如果S$\leq$0,表示现有资源足够使用,执行P操作的进程继续执行
|
||||||
|
* 举例:当信号量的值为2时,表示有2个资源可以使用,当信号量的值为-2时,表示有两个进程正在等待使用这个资源
|
||||||
|
* V操作
|
||||||
|
* 将信号量S加1,表示释放一个资源,即使用完资源后归还资源
|
||||||
|
* 如果S$\leq$0,表示有某些进程正在等待该资源
|
||||||
|
* 由于我们已经释放出一个资源了,因此需要唤醒一个等待使用该资源的进程,使之运行下去
|
||||||
|
* 常见问题
|
||||||
|
* S>0表示有临界资源可使用,这时候为什么不需要唤醒进程
|
||||||
|
* 所谓唤醒进程是从就绪队列中唤醒进程,信号量的值大于0表示有临界资源可供使用
|
||||||
|
* 即此时没有进程被阻塞在这个资源上,因而不需要唤醒,正常运行即可
|
||||||
|
* S=0表示没有临界资源可供使用,为什么要唤醒进程
|
||||||
|
* V操作时先执行S+1,也就是把信号量的值加1后才变成了0
|
||||||
|
* 在之前信号量的值为-1,即有一个进程正在等待这个临界资源
|
||||||
|
* 其他概念
|
||||||
|
* 原子操作就是要么全部执行,要么都不执行,不能出现执行到一半的中间状态的操作
|
||||||
|
* PV操作时低级进程通信语言
|
||||||
|
* 信号量分类
|
||||||
|
* 整型信号量
|
||||||
|
* 该信号量被定义为一个用于表示资源数目的整型量S
|
||||||
|
* 该机制不遵循让权等待的准则
|
||||||
|
* 记录型信号量
|
||||||
|
* 一种不存在忙等现象的进程同步机制
|
||||||
|
* 需要一个用于代表资源数目的变量Value
|
||||||
|
* 需要一个进程链表L,用于链接所有等待该资源的进程
|
||||||
|
* wait操作 = P操作 = 请求一个资源
|
||||||
|
* signal操作 = V操作 = 释放一个资源
|
||||||
|
* 信号量应用
|
||||||
|
* 信号量实现同步
|
||||||
|
* 用资源P操作,释放资源V操作,此时信号量表示资源量
|
||||||
|
* 同步信号量初始值不确定,可以设置
|
||||||
|
* 信号量最大值 = 最多可以请求的资源数
|
||||||
|
* 信号量最小值 = 最大值/初始值-最大请求值
|
||||||
|
* 信号量实现互斥
|
||||||
|
* PV操作夹着互斥资源,此时信号量表示互斥量
|
||||||
|
* 互斥信号量初始值 = 1,表示临界区只运行一个进程进入,从而实现互斥
|
||||||
|
* 互斥信号量 = 0,表示临界区已经有一个进程进入,临界区外还没有进程等待
|
||||||
|
* 互斥信号量<0,表示临界区中有一个进程
|
||||||
|
* 互斥信号量:表示临界区外等待进入的进程数
|
||||||
|
* 利用信号量实现前驱关系
|
||||||
|
|
||||||
|
|
||||||
|
### 信号量和PV操作的代码定义
|
||||||
|
|
||||||
|
```C
|
||||||
|
//信号量定义
|
||||||
|
typedef struct semaphore{
|
||||||
|
int value;
|
||||||
|
struct pcb * list;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//P操作
|
||||||
|
void P(semaphore s){
|
||||||
|
s.value --;
|
||||||
|
if(s.value < 0)
|
||||||
|
asleep(s.list);
|
||||||
|
}
|
||||||
|
|
||||||
|
//V操作
|
||||||
|
void V(semaphore s){
|
||||||
|
s.value ++;
|
||||||
|
if(s.value <=0)
|
||||||
|
wakeup(s.list);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 同步与互斥的代码实现
|
||||||
|
|
||||||
|
* 进程同步
|
||||||
|
* 定义一个同步信号量,并初始话为当前可用资源的数量
|
||||||
|
* 在优先级较高的操作后面执行V操作,释放资源
|
||||||
|
* 在优先级较低的操作的前面执行P操作,申请占用资源
|
||||||
|
|
||||||
|
```C
|
||||||
|
|
||||||
|
semaphore S =0;//初始化同步信号量,表示当前可用资源为0
|
||||||
|
|
||||||
|
P1(){
|
||||||
|
代码 1;
|
||||||
|
代码 2;
|
||||||
|
V(S);//代码2运行完后,释放资源
|
||||||
|
代码 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
P2(){
|
||||||
|
P(S);//代码4运行前先申请占用资源,保证代码4一定在代码2之后运行
|
||||||
|
代码4;
|
||||||
|
代码5;
|
||||||
|
代码6;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
* 进程互斥
|
||||||
|
* 定义一个互斥信号量,并初始化为1
|
||||||
|
* 把对于临界资源的访问置于P操作和V操作之间
|
||||||
|
* P操作和V操作必须成对出现
|
||||||
|
* 缺少P操作就不能保证对临界资源的互斥访问
|
||||||
|
* 缺少V操作就会导致临界资源永远得不到释放,处于等待态的进程永远得不到唤醒
|
||||||
|
|
||||||
|
|
||||||
|
```C
|
||||||
|
|
||||||
|
semaphore mutex = 1;//初始化互斥信号量,初始化为1
|
||||||
|
|
||||||
|
//进程P1
|
||||||
|
P1(){
|
||||||
|
···
|
||||||
|
P(mutex);//申请占用资源
|
||||||
|
临界区
|
||||||
|
V(mutex);//释放资源
|
||||||
|
}
|
||||||
|
//进程P2
|
||||||
|
P2(){
|
||||||
|
···
|
||||||
|
P(mutex);//申请占用资源
|
||||||
|
临界区
|
||||||
|
V(mutex);//释放资源
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
After Width: | Height: | Size: 149 KiB |
Binary file not shown.
After Width: | Height: | Size: 121 KiB |
Loading…
Reference in New Issue