操作系统 多线程 OS-Dev

操作系统
基本概念
现代操作系统的主要基本特征是 并发和共享。
操作系统的特征
并行性
并行(parallelism)和并发(concurrency)是:
  1. 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  2. 并行就是同时执行多个任务,并发则是轮流执行,同一时间只执行一个任务

共享性
共享的前提是并发,可以让多个用户共同使用系统中的资源。资源共享方式可分为:
  1. 互斥共享。打印机、队列、lock等一段时间内仅供一个作业使用的、需轮替使用的资源。
  2. 同时访问。系统中的另一类资源如磁盘、可重入代码等,可供多个作业同时访问的,此指宏观上的"同时",微观上可能是作业交替访问资源,但访问顺序不会影响结果。

操作系统的运行环境
  • 控制计算机的硬件资源,并提供上层应用程序运行的环境。
  • 是操作系统管理程序执行时机器所处的状态,具有较高的特权,能执行一切指令、访问所有寄存器和存储区。
  • 当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(内核态)。

用户态
  • 上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。
  • 是用户程序执行时机器所处的状态,低特权执行状态,访问和执行受限制。

系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口。用于访问内核函数。
特权指令:只能由操作系统内核部分使用,不允许用户直接使用的指令。如,I/O指令、置终端屏蔽指令、清内存、建存储保护、设置时钟指令。
非特权指令:所有程序均可直接使用。

Unix图例

进程,是操作系统进行资源分配和调度的基本单位。
进程之间,有一些资源是可以共享的,如进程代码段data section、进程公共数据等系统资源。多个进程可以调用同一段程序代码(多开进程原理)

线程,是操作系统能够进行运算调度的最小单位,是CPU执行的基本单位
一个进程可以有多个线程,多个线程也可以并发执行
同一进程的各线程间,可以共享进程的:
堆内存、静态、常量区、代码区(而线程的栈区、寄存器与程序计数器是独享的)

进程通信
方式
  1. 进程互斥与同步(PV
  2. 信号signal 与 信号量semaphore
  3. 共享存储器系统(共享存储区
  4. 消息传递系统(直接发送/信箱通信)
  5. 管道通信系统(共享文件)
  6. socket,文件锁
> 管道 是半双工的,是一种固定大小的缓冲区。当管道满时,进程在写管道会被阻塞,而当管道空时,进程读管道会被阻塞

线程通信
方式
  1. 共享内存(如全局变量
  2. 消息队列
  3. 事件
  4. 管道

进程调度 / 线程调度
CPU是进程、线程间竞争的首要资源,因为资源往往不够用,所以需要调度算法来进行资源分配,保证大家都满足
调度方式
非剥夺方式: 分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到完成或阻塞
剥夺方式: 当一个进程正在运行时,系统可以剥夺已分配给它的处理机
常见算法:
  • 先来先服务 FCFS
  • 短作业优先调度 每次选最短的一个或几个作业调入内存
  • 高优先权优先 有高响应比优先调度算法
  • 时间片轮转 有多级反馈队列调度算法,队列越靠前时间片越短

进程同步
一个进程因需要数据在某一点等待另一个进程提供信息,实现访问者对资源的有序访问,这叫同步。与进程通信类似,区别在于一个是主动告知,一个是等待响应
  • 互斥量机制:mutex(进程) / lock(线程),用一把锁只允许一个访问者对其进行访问,保证同类进程互斥
  • 信号量同步:即PV操作等,详见生产者消费者,
  • 原子操作:由于是不可分割的指令,所以不会被调度机制打断
  • 自旋锁: 调用者申请的资源如果被占用,即自旋锁被已经被别的执行单元保持,则调用者一直循环在那里看是否该自旋锁的保持着已经释放了锁(比较低级)
  • 管程: 将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。模块之间联系清晰,便于维护和修改,易于保证正确性
  • 会合进程间直接进行相互作用, 一个进程可以调用另一个进程的入口。先到达会合处的等待后到达者 +FCFS。分布式系统可用。
  • 分布式系统 网络上的一组计算机形成的系统。参数全为值参, 而且不可为指针。

信号量S(用于同步)
当它的值大于0时,表示当前可用资源的数量;
当它的值小于0时,其绝对值表示等待使用该资源的进程个数。
信号量机制-PV操作
--wait ++signal 操作

线程同步
  • 临界区Critical Section): 在任意时刻只允许一个线程对共享资源进行访问,通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。不能跨进程,只能实现线程互斥
  • 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  • 信号量:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  • 事件信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
以上Event、Semaphore、Mutex 是内核对象,能够跨进程使用

临界资源 :一次仅允许一个进程使用的资源。
临界区 :每个进程中访问临界资源的那段程序代码

什么是缓冲区溢出
缓冲区存放 程序数据或用户的输入数据
溢出指超出缓冲区容量,(原因)计算机对接收的输入数据没有进行有效的检测(往程序缓冲区写超出其长度的内容),从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它指令,以达到攻击的目的
有什么危害?
可以导致程序运行失败、系统宕机、重新启动等后果。 可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。
要实现依序调用方法,最常见的思路是用synchronization(互斥与信号量)机制
Lock限定printSecond必须在printFirst之后调用,printFirst调用后锁才释放,在此之前thread2需等待,这个机制由语言本身实现

要求一定要调用一次A后调用一次B,然后下次再是调用A,这样交替

两线程分别开一个锁,然后每个线程运行前,先获取另一个线程的锁,执行结束后再释放自己的锁。
最后一开始给开始线程解锁即可

设计一个算法,让这5个人用5个筷子吃饭还不会死锁

可以让一部分哲学家优先去获取其左边的叉子,再去获取其右边的叉子;再让剩余哲学家优先去获取其右边的叉子,再去获取其左边的叉子。

其他
死锁与银行家算法
异步与线程的区别是异步是CPU发完指令后、由硬件完成工作,如IO、内存加载等;而线程是在跑代码段,是需要CPU来执行的。异步不占用CPU,只需要一个完成回调即可

主要学过C#的多线程开发,线程池,任务依赖等

生产者-消费者问题(先P(full)/P(empty)再P(mutex)以保证多个进程并发时互斥,避免死锁)
private Semaphore full = new Semaphore(0); // 满缓冲区,表示目前消费者可消费的数目
private Semaphore empty = new Semaphore(10); // 空缓冲区,表示可容纳生产者生产物的数目
private int mutex = 1; // 互斥量,保证多个生产者消费者互斥访问缓冲池

void Producer()
{
while (true)
{
empty.Wait();
mutex = 0;
// 生产中
mutex = 1;
full.Signal();
}
}
void Consumer()
{
while (true)
{
full.Wait();
mutex = 0;
// 生产中
mutex = 1;
empty.Signal();
}
}

读者-写者问题(读者优先/公平竞争/写者优先)

内存管理
内存分配算法
  • 首次适应算法 FF(第一个)
  • 最佳适应算法 BF(找最小)
  • 最坏适应算法 WF(找最大)
  • 快速适应算法(内存回收分区合并)

分页存储-划分为均等大小的内存块,用页表记录(逻辑页到物理地址对应关系)
分段存储-用户程序地址空间,根据需求自己划分(可分为多段程序段)

请求分页存储

页面置换算法
  • FIFO算法:先进先出
  • LRU:最近最久未用算法,选择最近一段时间最久没有使用过的页面予以淘汰
  • LFU:最近一段时间使用次数最小
  • OPT:最佳置换算法。最少缺页率,可以用于对其他算法的性能进行衡量
  • Clock置换算法:链接成循环队列,访问置1,检查为0的换出,为1的置0
缺页 就是块中没有这个页,需要置换

文件管理
磁盘调度算法

设备管理
I/O控制方式
缓冲区
(缓冲区溢出,会破坏堆栈...)

虚拟内存是一种实现在计算机软硬件之间的内存管理技术,它将程序使用到的内存地址(虚拟地址)映射到计算机内存中的物理地址,虚拟内存使得应用程序从繁琐的管理内存空间任务中解放出来,提高了内存隔离带来的安全性,虚拟内存地址通常是连续的地址空间,由操作系统的内存管理模块控制,在触发缺页中断时利用分页技术将实际的物理内存分配给虚拟内存,而且64位机器虚拟内存的空间大小远超出实际物理内存的大小,使得进程可以使用比物理内存大小更多的内存空间。

虚拟内存
32位机中,每个进程最大2^32地址(因为指针4字节寻址),即4GB虚拟内存
将那些当前要运行的少数页面或段先装入内存便可运行,其余部分暂留在盘上。
内存耗尽时,电脑会自动调用硬盘来充当内存,以缓解内存的紧张。 若计算机运行程序或操作所需的随机存储器(RAM)不足时,则Windows 会用虚拟存储器进行补偿。 它将计算机的RAM和硬盘上的临时空间组合。 当RAM运行速率缓慢时,它便将数据从RAM移动到称为"分页文件"的空间中。
虚拟存储器是由硬件和操作系统自动实现存储信息调度和管理的。它的工作过程包括6个步骤:
①中央处理器访问主存的逻辑地址分解成组号a和组内地址b,并对组号a进行地址变换,即将逻辑组号a作为索引,查地址变换表,以确定该组信息是否存放在主存内。
②如该组号已在主存内,则转而执行④;如果该组号不在主存内,则检查主存中是否有空闲区,如果没有,便将某个暂时不用的组调出送往辅存,以便将这组信息调入主存。
③从辅存读出所要的组,并送到主存空闲区,然后将那个空闲的物理组号a和逻辑组号a登录在地址变换表中。
④从地址变换表读出与逻辑组号a对应的物理组号a。
⑤从物理组号a和组内字节地址b得到物理地址。
⑥根据物理地址从主存中存取必要的信息。
如果程序要访问的页/段尚未调入,那么请求调入
如果内存已满,则利用置换功能调出到盘上

我们常说的32位系统为每个进程分配4G虚拟内存空间(而MMU负责把这些个4G虚拟内存映射到实际内存条的物理内存),其实只有0~3G才是真正完全属于进程本身,是我们所说的用户区;3~4G这1G是所有进程间共享的,是我们所说的内核区,我们的程序是无法直接访问内核区的
教材上常说:"全局变量在编译时分配空间,局部变量在运行时分配空间",这在像vs这种集成式开发工具中,我们写个简单的程序,直接点个"运行",编译、运行一步到位,很容易理解,但如果在linux上用vim写代码,用gcc编译生成a.out执行文件,再./a.out运行时,其实只剩下了"运行"这一步,那编译时分配又怎么解释呢?

其实编译时就只是确定了全局变量(静态局部变量也算)的虚拟地址,并且相关信息存在了可执行文件a.out中。我们再说一个进程(即正在执行的可执行文件)大概有4种状态:就绪(等待cpu)—>运行(占用cpu)—>挂起(等待cpu之外的资源,主动放弃cpu)—>终止 另外:初始态为进程准备阶段,常与就绪合并 ,就是在这个初始态阶段,做了很多准备工作,其中一项就是根据全局变量的虚拟地址映射物理地址为其分配空间,并初始化好。所以全局变量在程序进入main()函数之前,就已经在内存中存在了,而局部变量和堆区变量就只有程序执行到声明(或定义)它的那一条语句,才开始为其申请分配空间。



comments powered by Disqus