操作系统相关

165 阅读14分钟

进程和线程

进程:资源分配的基本单位,一个进程中可以有多个线程,它们共享进程资源,线程不拥有资源,线程可以访问隶属进程的资源

线程:独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换

  • 系统开销 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
  • 通信方面 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
  • 为什么有进程还要线程:减少程序在并发执行时所付出的时空开销,提高并发性

进程通信

  • 进程同步:控制多个进程按一定顺序执行
  • 进程通信:进程间传输信息

进程通信的五种方式:管道、FIFO、消息队列、信号量、共享存储、套接字

  • 管道:速度慢,容量有限,只有父子进程能通讯
  • FIFO:任何进程间都能通讯,但速度慢
  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
  • 信号量:不能传递复杂消息,只能用来同步
  • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

消息队列对比管道FIFO的优点:

  • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
  • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
  • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。

线程安全

线程安全性问题出现的三个必要条件:

  • 多线程环境下
  • 多个线程共享同一个资源
  • 对资源进行非原子性操作

iOS解决线程安全的方式:

  • 串行队列

一般说来,确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、过度优化

竞争与原子操作 多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。

因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。

同步与锁 为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。

可重入 一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

过度优化 在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。

我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作volatile变量的指令顺序。

在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。

锁,知道的锁和用法,程序锁,乐观锁,悲观锁,单线程锁,共享锁,非共享锁还有什么锁

进程的几种状态

img

运行态:该进程正在执行。 就绪态:进程已经做好了准备,只要有机会就开始执行。 阻塞态(等待态):进程在某些事情发生前不能执行,等待阻塞进程的事件完成。 新建态:刚刚创建的进程,操作系统还没有把它加入到可执行进程组中,通常是进程控制块已经创建但是还没有加载到内存中的进程。 退出态:操作系统从可执行进程组中释放出的进程,或由于自身或某种原因停止运行。

应该注意以下内容:

  • 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
  • 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。

悲观锁与乐观锁

悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。

死锁

必要条件:

  • **互斥:**每个资源要么已经分配给了一个进程,要么就是可用的。
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • **不可抢占:**已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • **环路等待:**有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

处理方法:

  • **鸵鸟策略:**忽略它,假装根本没发生问题。
  • **死锁检测与死锁恢复:**利用抢占恢复、利用回滚恢复、通过杀死进程恢复
  • 死锁预防:
    • 破坏互斥条件,例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
    • 破坏占有和等待条件,一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
    • 破坏不可抢占条件
    • 破坏环路等待,给资源统一编号,进程只能按编号顺序来请求资源。
  • **死锁避免:**在程序运行时避免发生死锁,银行家算法

虚拟内存

虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。*当程序引用到不在物理内存中的页时*(缺页中断),由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,即一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。

分页

进程中的块称为页,内存中的块称为页框,外存中的块就叫块。分页管理系统里块是相等的长度。

一个虚拟地址分成两个部分,一部分存储页号,一部分页内偏移量

QQ截图20201026165120

页表:系统为每个进程建立一张页表,页表由页表项组成,每一个页表项分为页号物理内存中的块号两部分,物理内存中的块号与页内偏移量共同组成物理地址

image.png

QQ截图20201026165425

image.png

分页管理系统中地址空间是一维的,页表不能太大,否则内存利用率会降低

分段

分段的做法是把每个表分成段,一个段构成一个独立的地址空间。**每个段的长度可以不同,并且可以动态增长。**段内地址空间要求连续,段间不要求连续。

QQ截图20201026165530

QQ截图20201026165737

image.png

image.png

段页式

**程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。**这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。系统为每个进程建立一张段表,每个分段都有一张页表。在一个进程中,段表只有一个,但页表可能有多个。

QQ截图20201026165827

QQ截图20201026165822

QQ截图20201026165923

分页与分段的比较

  • 对程序员的透明性:分页透明,分段需要程序员显式划分每个段。
  • 地址空间的维度:分页是一维地址空间,分段是二维的。
  • 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
  • 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

CPU资源调度

时间片分配(进程调度):先来先服务FCFS、短作业优先SJF进程调度、时间片轮转RR

内存分配管理(段式、页式,段页式),虚拟内存页面置换算法:先进先出FIFO、最佳OPI、最近最久未使用LRU、最近未使用NRU

页面置换算法

在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。

页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。

页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。

  1. 最佳(OPI, Optimal replacement algorithm)

    所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。

    举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列:

7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1

开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。

  1. 最近最久未使用(LRU, Least Recently Used)

    虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。

    为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。

    因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。

4,7,0,7,1,0,1,2,1,2,6

img

  1. 最近未使用(NRU, Not Recently Used)

    每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:

    • R=0,M=0
    • R=0,M=1
    • R=1,M=0
    • R=1,M=1

    当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。

    NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。

  2. 先进先出(FIFO, First In First Out)

    选择换出的页面是最先进入的页面。该算法会将那些经常被访问的页面换出,导致缺页率升高。

  3. 第二次机会算法

    FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:

    当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。

    img

  4. 时钟Clock

    第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。

    img