操作系统复习

操作系统

概述

基本特征

  1. 并发:宏观上一段时间内能同时运行多个程序,并行指的是同一时刻能运行多个指令。并行需要硬件支持,比如多流水线、多核处理器等。常见的单板系统中,也有通过中断来模拟并发的。操作系统通过引入进程和线程,使得程序能够并发执行。
  2. 共享:系统中的资源能够被多个并发进程共同食用,包括互斥共享与同时共享。其中互斥共享的资源被称为临界资源,比如打印机,需要同步机制来实现互斥访问。
  3. 虚拟:把一个物理实体转化为多个逻辑实体。主要虚拟技术包括:时分复用,空分复用。多个进程能在同一个处理器上并发执行,就是用的时分复用,时间片轮转。虚拟地址使用了空分复用,将物理内存抽象为空间地址,每个进程都有各自的地址空间。地址空间的页被映射到物理地址,地址空间的页不需要全部在物理内存中,当使用到一个没有在物理内存中的页时,执行页面置换算法,将该页置换到内存中。
  4. 异步:进程并不是一次性执行完,而是走走停停。

基本功能

  1. 进程管理:进程控制、进程同步、进程通信、死锁处理、调度机调度
  2. 内存管理:内存分配、地址映射、内存保护与共享、虚拟内存
  3. 文件管理:文件存储空间的管理、目录管理、文件读写管理与保护
  4. 设备管理:完成用户I/O请求,方便用户使用各种设备,提高设备的利用率。包括缓冲管理、设备分配、设备处理、虚拟设备

系统调用

  1. POSIX协议规定了操作系统暴露出的统一的系统调用接口。
  2. 如果一个进程在用户态需要使用到内核态的功能,就会进行系统调用从而转换到内核态,由操作系统完成。以下这些方法都是系统调用函数。
    [外链图片转存失败(img-pmAohb6r-1563194127003)(https://github.com/CyC2018/CS-Notes/blob/master/notes/pics/tGPV0.png)]
    |Task|Commands|
    |----|--------|
    |进程控制|fork() exit() wait()|
    |进程通信|pipe() shmget() mmap()|
    |文件设备|open() read() write()|
    |设备操作|ioctl() read() write()|
    |信息维护|getpid() alarm() sleep()|
    |安全|chmod() umask() chown()|
  3. 之所以要分系统调用的原因是为了安全。通过处理器的硬件设计区分了内核态和用户态
    [外链图片转存失败(img-oIx4NXKO-1563194127004)(http://img.sonihr.com/2127f68b-33b8-4e85-a40d-30fdb4606888.jpg)]
    DPL表示描述符特权级(代码的特权级,是固定的),CPL为当前特权级,RPL是请求特权级,一定是小于等于CPL的,是程序员尝试以低于CPL的方式请求,比如我明明有内核的权限,但是我以用户态请求,更急安全,CPL<=DPL或者RPL<=DPL当前指令才允许,核心态为0,用户态为3。
  4. 系统调用的原理,对于Intel X86而言,中断指令int,将CS中的CPL改为0,进入内核,然后根据调用方法不同调用相应系统调用方法的代码。

大内核与微内核

  1. 大内核:将操作系统功能作为一个紧密结合的整体放入内核中,各模块共享信息,因此有很高的性能
  2. 微内核:将一部分操作系统功能移出内核,从而降低内核复杂性。只有微内核本体在内核态,其他模块都在用户态,因此需要频繁的用户态与核心态之间的切换,因此有一定性能损失。
    [外链图片转存失败(img-cLJJ6b4L-1563194127012)(https://github.com/CyC2018/CS-Notes/raw/master/notes/pics/2_14_microkernelArchitecture.jpg)]

中断分类

  1. 外中断 由CPU执行指令以外的事件引发,比如IO完成中断,时钟中断
  2. 异常 由CPU内部产生的,比如地址越界,算术溢出
  3. 陷入 用户程序使用系统调用

进程管理

进程与线程

[外链图片转存失败(img-TlVlEO4K-1563194127014)(http://img.sonihr.com/69bfb72a-cf04-47d3-b241-a2012390589f.jpg)]

  1. 进程是操作系统分配资源的最小单位。进程控制块(Process Control Block,PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。4个程序创建4个进程,4个进程可以并发的执行。
    [外链图片转存失败(img-LKqCLi4X-1563194127016)(https://github.com/CyC2018/CS-Notes/raw/master/notes/pics/a6ac2b08-3861-4e85-baa8-382287bfee9f.png)]
    多进程通过不同的队列存放,比如就绪队列,磁盘等待队列,方便操作系统调用和管理。
  2. 线程是操作系统调度的最小单位。一个进程可以有多个线程。因为每个进程具有自己独立的虚拟内存地址,而线程之间共享同一个进程的虚拟地址,因此做上下文切换很快。进程=资源+指令执行序列,现在分开,让线程承担指令执行序列的功能,进程负责资源分配。线程即保留了并发的优点,也减轻了进程切换的开销。
  3. 区别:
    • 进程是资源分配单位,线程不拥有资源,但是可以访问隶属进程的资源。
    • 线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从不同进程的线程切换会引起进程切换。
    • 创建或撤销进程是,系统都要为之分配或回收资源,如内存空间、IO设备,所付出的开销远大于创建或撤销线程时的开销。类似的,在进行进程切换时,要切换进程的上下文,而线程切换只要保存和设置少量寄存器内容,开销很小。
    • 线程间通信的核心是同步,进程间通信需要采用IPC。

进程状态的切换

  1. 为什么要有进程的切换?因为CPU是从内存中取指令然后顺序执行,先指定PC(程序计数器),然后CPU从内存中取指令(指令要先加载到内存里),指令通过总线到CPU,CPU执行。如果遇到了一条IO指令,或者其他阻塞要求,CPU就会卡在这里,效率很低,因此此时CPU应该切换到别的程序多进程可以提高CPU效率。这种提高CPU效率的方式,叫做并发。那如何切换到别的程序呢?修改PC就行了么?不行,除了PC还要记录切出去之前的进程的上下文信息,还要装载切出去之后的进程的上下文信息(类似于中断开始前的保护现场)。上下文信息存放在哪里呢?PCB中,进程控制块。
  2. 进程有三种状态 a.就绪ready 等待被调度 b.运行running c.阻塞 waiting 等待资源。ready和running是双向转化关系,看时间片轮转,获得时间片就running,失去时间片就ready。阻塞状态是因为缺少必要的资源(IO、锁等),由运行态转换为阻塞态,资源不包括CPU时间,缺少CPU时间。 补充:java线程包括runnable(running,ready),wait,timed_wait,block。其中等待和阻塞的区别是wait都是程序员手动让线程等待,比如wait(),sleep(),但是block不是程序员能控制的。本质来说block在entrylist而wait在waitlist。wait和timed_wait区别在是否有超时唤醒的机制。 补充x2:java线程池的状态为运行,stop,shutdown,tidying和terminated
  3. 多进程如何交替(队列操作+调度+切换)
    [外链图片转存失败(img-e2dV1FCW-1563194127020)(http://img.sonihr.com/fde305a3-6c32-486e-af02-6bd2b7902267.jpg)]
    重点是schedule方法,getNext方法从就绪队列中获取一个进程,这个获取的过程就是调度器要做的,switch_to方法就是执行现场的切换。切换的过程是把CPU中的数据保存在PCB中,然后CPU加载新进程的PCB数据。

用户级别线程的切换

  1. 核心是create和yield。create创造出第一次切换时应该的样子,yield用于切换。
    [外链图片转存失败(img-SQ9yw6X4-1563194127023)(http://img.sonihr.com/2768d91b-7870-48c5-b401-e3d87a79cde2.jpg)]
    上图表述了为什么每个线程都要有一个内部栈,因为每个线程应该由自己独立的指令序列。
  2. 同进程之间的切换,首先进行栈切换(TCB存储esp->栈指针寄存器,该寄存器存放一个地址,这个地址指向栈顶。TCB存放指向栈的指针,esp指向正在使用的栈地址,栈内存的是PC
    [外链图片转存失败(img-jN4ZMsui-1563194127025)(http://img.sonihr.com/4595382a-3724-4706-93d7-dca73438e012.jpg)]
  3. yield中不需要跳转PC,只需要改变栈指针即可。因为从A线程yield到B线程(此时,A线程yield方法的下一个PC已经被压栈了,记做a),B线程又yield到A的时候,要运行的PC一定在A的栈帧中的栈顶a。
  4. create的作用是创建TCB,创建栈,将esp指针指向栈顶。
    [外链图片转存失败(img-dT4c6BIk-1563194127026)(http://img.sonihr.com/1ba89a46-0810-4875-a76d-30d54f1029d4.jpg)]
  5. 问题是:用户级别线程一旦阻塞,整个进程全部阻塞,因为操作系统只能看到进程而看不到用户态线程。
  6. 小结:切换TCB(内部存放esp)->(esp指向不同栈)切换用户栈(栈内保存PC)->栈顶PC指向应该执行的指令行

内核级别线程的切换

  1. 多核CPU才能发挥内核级线程的优势,多个核心共用一个MMU,即共用一套内存映射的规则。
    [外链图片转存失败(img-0RmaaGuW-1563194127028)(http://img.sonihr.com/ad4fe4b0-b25c-4fa8-8601-053f94f3e3ed.jpg)]
    [外链图片转存失败(img-HEnQ8e5u-1563194127029)(http://img.sonihr.com/69a3fba3-8144-41ee-9403-d237db8b04a5.jpg)]
  2. 和用户级线程相比,ThreadCreate是系统调用,内核管理TCB,内核负责切换线程。用户态线程是用两个栈来实现两个线程的切换,而内核级用的是两套栈,既要有用户栈,也要有内核栈。
  3. 用户栈和内核栈的关联:SS是存放栈的首地址,SP堆栈寄存器(stack pointer)存放栈的偏移地址,CS是代码段寄存器。因此核心态线程做栈切换的时候,可以找到用户态的栈,然后让用户态的栈进行切换。
    [外链图片转存失败(img-0X42C8l3-1563194127031)(http://img.sonihr.com/e9b68be7-fe47-4c92-8938-bbd418549e71.jpg)]
  4. 切换过程:中断进入内核->内核中切换TCB(存放esp)->(esp存放内核栈)切换内核栈->iret返回用户态(返回时,内核栈中弹出包括cs,pc,ss,sp)->(根据上一步返回的值)切换用户栈->用户栈栈顶PC指向应该执行的命令行
  5. ThreadCreate方法,创建内核态栈空间及指针,传入用户栈地址,向tcb的esp中传入内核栈指针。
  6. 小结:[外链图片转存失败(img-3XAKQBPU-1563194127032)(http://img.sonihr.com/98bdfb0b-3257-48b4-a87e-a6914dae533f.jpg)]

进程调度算法

  1. 批处理系统:没有用户操作,保证吞吐量(系统内耗时间少、完成任务数量大)、周转时间(从任务进入到任务结束),代表切换次数少,因为切换时间多导致系统内耗时间多,吞吐量下降
  • 先到先来服务:非抢占式,有利于长作业,但是平均周转时间可能比较长。
    [外链图片转存失败(img-dcbaHdPz-1563194127034)(http://img.sonihr.com/2258af8f-8f7b-4115-bb5d-de2bf9645189.jpg)]
  • 最短作业优先:非抢占式,长作业饥饿,周转时间最小。
  • 最短剩余时间优先:最短作业优先的抢占式版本。当新作业达到后,将新作业的完整运行时间与当前作业的剩余时间比较,如果新的时间更少,则新作业抢占。
  1. 交互式系统:目标是快速响应(从操作发生到响应),快速响应代表切换次数多
  • 时间片轮转:所有就绪进程按照先来先服务的准则排成一个队列,每次调度时把时间片交给队首进程,当时间片用完后,定时器发出中断,调度器将队首元素送往就绪队列的末尾,同时继续将CPU时间分配给队首的进程。时间片过短导致CPU上下文切换次数过多,浪费CPU性能,时间片过长导致吞吐量下降,响应变慢。
  • 优先级调度:每个进程分配一个优先级,根据优先级调度,为防止饥饿,可以随着等待时间的增加而增加等待线程的优先级
  • 多级反馈队列:有多个队列,每个队列中的时间片长度不同,队列与队列之间有优先级关系,上面的优先级更高。因此一个需要执行100时间片的进程,如果单队列,时间片为1,就要执行100次。如果是多队列,分别是1.2,4,8.。。这样只要满足1+2+4+。。。>100即可,减少了上下文切换次数。
  1. 批处理系统和交互式系统在操作系统中同时存在,怎么办?Linux0.11中,在就绪队列中获得持有最长时间片的counter,并让其运行。以counter的数字作为优先级,每次经过时间片都会自减,当所有就绪态进程的counter都等于0的时候,遍历所有就绪进程,并将counter变为现在的counter/2+初始值。因此,IO阻塞的进程counter不是因为时间片轮转而阻塞,因此counter/2+priority一定大于那些时间片用光的。从而做到了IO型任务优先级动态提高,IO型任务是交互式系统的特征,因此做到了交互式系统的优先级高于批处理系统。

进程同步

多进程之间互相的影响
  1. [外链图片转存失败(img-bcdVelWY-1563194127036)(http://img.sonihr.com/ef8aeb1c-1125-41ad-b354-c5293466d580.jpg)]
    多个进程可能操作同一块内存地址。解决方法:限制对地址100的读写。多进程的地址空间其实是分离的,因为有虚拟地址的存在,因此进程1的100和进程2的100其实对应的物理地址是不同的。
  2. [外链图片转存失败(img-rXUi2nyv-1563194127037)(http://img.sonihr.com/c95d5aae-e756-4b64-bebb-05951702ddee.jpg)]
    多个进程可能需要合作。如上图,因为进程1、2交替执行,可能他们同时放在队列中的同一位置。这边就需要加锁,即进行进程同步,规定进程顺序或者说切换的时间点。
同步与互斥
  1. 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后关系执行
  2. 互斥:多个进程在同一时刻只有一个进程能进入临界区
信号量
  1. 为什么不能只用信号来解决同步问题?
    [外链图片转存失败(img-vxSGYVTi-1563194127039)(http://img.sonihr.com/c1cb42af-a656-4038-8783-595971d12b7a.jpg)]
    根据上图,可以通过counter的数量是否为SIZE-1来判断是否要发信号,但是问题是,如果有两个生产者同时等待,一个消费者只会唤醒一个生产者,另一个将永远不被唤醒。信号量不是0/1,不只是等待信号、发信号,而是能记录更多的信息
    [外链图片转存失败(img-SM2PHVbm-1563194127041)(http://img.sonihr.com/eb1f9450-02cd-41da-b9fa-ea08b1b99327.jpg)]
  2. 信号量是一个整型变量,可以进行down和up操作,当信号量只能取0/1时,就是互斥量。为正数表示有n个空闲资源可以用,为负数表示有n个进程在等待资源。如上图,P就是消费资源,V就是产生资源,P方法中信号量自减(消耗资源),若小于0则sleep,V方法中信号量自增(生产资源),若小于0则wakeup。
    [外链图片转存失败(img-thJ6xH9p-1563194127042)(http://img.sonihr.com/14a2aff6-948d-4f2e-b014-009805c08ff5.jpg)]
    由上图,在P方法中,每次消耗资源则s.value–,然后如果资源已经<0,则消费线程sleep,在V方法中,s.value++,如果生产完后仍然有s.value<=0,说明此时有线程在sleep,因此要wakeup唤醒。
    [外链图片转存失败(img-l4zZXEax-1563194127045)(http://img.sonihr.com/48a602cf-281f-4f80-a9bb-8320fea6708e.jpg)]
    上图中,Producer的P(empty)表示消费空闲缓冲区,如果没有空闲则sleep,Consumer的P(full)表示消费非空闲缓冲区,如果没有非空闲,即全为空闲则sleep。Producer的V(full)表示生产非空闲缓冲区,如果有多个消费者在P(full)的时候将full变成-2然后sleep,此时Producer一定可以P(empty),且empty=10,full=-2,然后Producer用v(full)把full变成-1,发现full<=0,所以唤醒生产者线程。注意,此刻虽然full为负,但是已经有资源生产出来了,full=-1表示有一个进程在等待资源,empty=9表示生产出了1个资源。
  3. java中的Semaphore用的是AQS的shareacquire。
  4. 小结:通过信号量的访问和修改,可以使各个进程按照一定的先后关系执行,即同步。如果信号量只能取0/1可以达成互斥。
临界区
  1. 如果多个线程对同一信号量进行并发修改,可能产生问题。

  2. 对临界资源进行访问的代码成为临界区,进入临界区之前要先进行检查
    [外链图片转存失败(img-ea8Q5YCA-1563194127047)(http://img.sonihr.com/e35c1a8a-d003-4ac9-8b42-c9015c431b1d.jpg)]

  3. 临界区原则是:

    • 基本原则:互斥进入
    • 有空让进(多个进程要求进入临界区时,应尽快使一个进程进入临界区)
    • 有限等待:从进程发出请求到允许进入,不能无限等待
  4. 临界区算法:结合标记+轮转的思想,Peterson算法。
    [外链图片转存失败(img-6Q0UQjwh-1563194127048)(http://img.sonihr.com/5d0e7f29-f336-4750-840a-8f6692efa059.jpg)]
    上图是两个线程,多个线程时每个线程获得一个序号,比如说有10个线程同时请求进入临界区,那么就会分配给10个进程序号,序号小的先执行,其他等待。如果第11个进程进来,则会分配当前序号中最大的那个+1的序号给他。序号最小的执行完成后,序号为改为0表示运行完毕。由此实现了后到临界区的线程一定后执行,大小保证了互斥。

  5. 结合硬件的临界区保护解决方案:阻止时间中断,即时间片不会轮转,自然就不会产生进程调度,从而时间临界区保护,cli关中断,sli开中断。但是多核/多CPU时不行,因为关中断只能关一个CPU/核心的中断寄存器标志位。

    [外链图片转存失败(img-rFnSBQFw-1563194127050)(http://img.sonihr.com/6b611340-07f7-475e-ae18-1dea333585a3.jpg)]

    硬件原子指令法,比如用硬件原子性的对flag进行置真操作,确保只有一个进程可以修改这个标志位,也可以实现临界区保护。

    [外链图片转存失败(img-blmmsnP1-1563194127051)(http://img.sonihr.com/a8b4ca61-8b38-4afc-95f9-b5163da9ab9d.jpg)]

  6. 小结:用临界区保证信号量的语义正确,用信号量实现同步和互斥。

管程
  1. 封装了信号量的同步操作,一个时刻只能由一个进程使用管程。管程引用了条件变量的概念,使用wait和singal来实现同步操作,对条件变量执行wait操作会调用进程阻塞,把管程让给另一个进程持有,signal用于唤醒被阻塞的线程。

Java与操作系统中的进程/线程

  1. 我们常接触的线程其实是JVM中的线程,也就是用户态下的线程,但是用户态下的线程是和内核中的线程一一对应的。Thread类中的start0等方法都是native方法,当你调用new Thread().start的时候,都会在JVM层面用C语言新建一个线程,在linux操作系统中相当于fork了一个线程。

  2. 可以说一下常见的线程模型。

    • 线程在用户空间下实现,即操作系统只能看到进程,因此程序员需要掌管线程的生命周期和调度,缺点是如果一个线程阻塞,那么整个进程都阻塞了,优点是不要进行内核态和用户态的转换,效率很高。
    • 线程和内核线程1:1。但是一般情况下会不会将内核线程直接暴露出来,会有一个叫做LWP的轻量级线程作为借口暴露出来,程序员操作用户态的线程,但是用户态的线程会调用操作系统中的内核线程,因此调度还是由操作系统实现的。缺点是操作系统的内核线程数量有限,一一对应导致数量受限。
    • 线程和内核线程n:m。n到m之间有一个映射器,Golang的协程使用了这种模型,java使用了上一个模型。
  3. 操作系统的线程状态包括:ready、running、waiting。java的线程状态包括:runnable(ready+running)、timed-waiting、waiting、blocked。其中ready和running对应runnable,timed-waiting和waiting和blocked都是waiting。JVM中线程状态,不反应任何操作系统线程状态。

  4. https://blog.csdn.net/ixidof/article/details/24579879#commentBox

    补充:在linux2.6后,NPTL(Native POSIX Thread Libray)实现了POSIX标准规定的线程标准。在之前,采用的是LinuxThread,模型是1个用户态线程对应一个内核态进程(LWP):本质上是创建了两个拥有相同内存空间的进程,但是getpid的结果不同,因为毕竟是不同的进程。所以用户每创建一个新线程,内核态就创建一个新进程。在NPLT中,仍然是1:1模型,但是内核管理结构不再是LWP,而是提出了进程组的概念。每个进程有一个线程组组长,只有组长配有PID,其他的组员只有TGID字段并且指向组长的PID,因此同组线程全部具有一样的PID,互斥同步原语通过futex实现,这个锁保存在用户空间,不必切换到内核态。

死锁

必要条件

  1. 互斥:每个资源要么已经分配给了一个进程,要么就是可用的
  2. 请求和保持(占有后又请求):一个进程占有了一个资源,却有请求了另一个已经被他人占有的资源,因此等待
  3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有他的进程显示地释放
  4. 环路等待:有两个或两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源

处理方法

死锁忽略
  1. 当做无事发生过,大多数操作系统,包括Unix,Linux,Windows,都是忽略死锁
死锁预防
  1. 概念:在程序运行之前预防发生死锁,破坏死锁发生的请求。
  2. 方法:
    • 破坏互斥:资源可共享,非互斥
    • 破坏占有和等待:规定所有进程在执行前获取全部资源,这样就不会有先获得资源、再申请别的资源这种情况发生。但是需要预知未来,编程困难。资源如果分配后很长时间后才使用,导致资源利用率低。
    • 破坏不可抢占
    • 破坏环路等待:给资源统一编号,进程只能按照编号顺序请求资源。也就是说资源是线性且不成环的,因此无法形成环路。缺点是可能造成资源浪费。
死锁避免
  1. 概念:在程序运行时避免发生死锁,检测每个资源请求,如果可能造成死锁就拒绝
  2. 方法
    • 安全状态:
      定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。
      如上图所示:has表示已经拥有的资源数量,max表示共需要的资源数,free表示还有可以使用的资源数。对于B而言,max-has=2,free=3>2,因此让B执行,然后释放B,让C执行,最后让A执行。必然不会发生死锁。

    • 单个资源的银行家算法:
      c为不安全状态,因此算法拒绝之前的请求,这就叫做死锁避免。根据死锁避免的定义,每次请求的时候都要假装分配资源,然后看看是否会造成死锁,时间复杂度为O(mn^2),m为资源剩余数量,n为n个进程,算法时间开销比较大

    • 多个资源的银行家算法:
      从一维的数字变成了多维的向量

死锁检测与死锁恢复
  1. 概念:不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
  2. 每种类型一个资源的死锁检测

    上图方框代表资源,圆圈代表进程。mysql采用这种环路死锁检测方式,通过检测有向图是否存在环来实现。从一个节点出发进行DFS,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环。
  3. 每种类型多个资源的死锁检测

    三个进程,四个资源。E:资源总量、A:资源剩余量、C:每个进程拥有的资源数量、R:每个进程请求的资源数量。有图可知,根据R的第三行,只有p3进程得以满足,因此先分配给p3,p3执行完毕后,回收资源,此时p2也可以继续,最后p1。
  4. 死锁恢复包括:抢占恢复、回滚恢复、通过杀死进程恢复

内存管理

虚拟内存

  1. 重定位。指令中存入的内存地址是逻辑地址,需要在运行时进行重定位(嵌入式系统中的静态程序可以编译时重定位)。所谓运行时重定位指的是,运行时call 40这个命令的40不是内存中物理地址为40的,而是相对地址为40的,哪怕经过了交换操作(进程1放在物理内存区域1,但是置换进磁盘,后来又置换到了物理内存区域2),此时仍然可以执行重定位。重定位的相对指的是相对IP,即指令指针。从逻辑地址算出物理地址的行为叫做:地址翻译。每个进程各自的基地址放在PCB中,执行指令时的第一步先从PCB中取出基地址,然后之后所有的PC都要加上这个基地址。
  2. LDT,GDT。这是两种表,G表示Global,L表示Local,LDT表存放各进程的CS,DS等段基地址,然后PCB持有LDT表的指针。
  3. 为什么要有虚拟内存?你的电脑内存有4G,但是一个应用8G,这个应用不需要全部载入物理内存,但是要全部载入虚拟内存。因为虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存。
  4. 虚拟内存的目的就是为了让物理内存扩充成为更大的逻辑内存,从而让程序获得更多的可用内存。为了更好地管理内存,操作系统将内存抽象成地址空间,每个程序拥有自己的地址空间,这个地址空间被分割成很多块,每一块被称为一页。 这些页映射到物理内存,但不需要连续,也不需要所有的页都在物理内存中。当应用程序引用到不在物理内存中的页时,由硬件执行必要的映射,将需求的部分装入物理内存,并重新执行失败的指令。 应用与应用之间可以进行隔离,更加安全,防止别的应用访问你应用的内存空间。 还有,现在的内存一般是段页式,如果没有虚拟内存而段页全部在物理内存上,那段页的设计就没有意义了。具体解释一下,比如都在物理内存上,你有20 xx 10的内存,此时数据段需要扩充30,假如只有段,就要进行内存紧缩,如果用段页,你想把30分成若干个页放进20和10中,那段想通过偏移量查找的时候又很难处理。所以段在虚拟内存上连续,在物理内存上分散
分段与分区
  1. 操作系统是将整个程序一起载入内存的么?一个程序分成代码段,数据段,堆栈段等,段是比页粒度更大的存在。好处是根据不同的段可以有不同的操作方式,比如代码段一般是只读的,而变量段是可写的,栈可以单向增长,单独管理,因地制宜
    [外链图片转存失败(img-vGakHAwi-1563194127062)(http://img.sonihr.com/6442219f-8f09-43ea-8998-c18ff5f1cf0d.jpg)]

比如上图,3是栈,可以向下生长。

  1. 虚拟内存采用的是分页技术,也就是将地址空间划分成固定的页,每一页再与内存进行映射。但是试想以下情况,比如编译器在编译过程中建立多个表,而表所占用的内存是动态增长的,如果分页系统采用一维地址空间,动态增长就有可能覆盖其他的页。
  2. 进程中各个段的基地址存在LDT中,PCB持有LDT的指针。切换进程时,切换的是不同的PCB,oldPCB保存当前CPU中信息,CPU加载newPCB中新信息,根据newPCB的LDT中的基地址和代码段中的偏移地址等执行命令。
  3. 内存中用的是可变分区,分区有大有小,段请求多大就从空闲内存中分配多少给他。但是分区总会造成内存碎片,然后通过内存紧缩来移动内存,将空闲区域合并,但是复制内存需要花费大量四溅,因此引入了分页的概念。
分页系统地址映射
  1. 分页的思想是把内存的分配粒度分小,此时不需要内存紧缩,因为分配单位够小,仅4k,且最大内存浪费只有4k。

  2. 内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表Page table,存储着页(程序地址空间)和页框(物理内存空间)的映射表。每个进程都有自己的页表

  3. 一个虚拟地址分成两个部分,一部分存储页面号,另一部分存储偏移量。

    由上图,虚拟地址为0010[4:15],物理地址为1100[4:15],两个地址中后面的部分都是一样的。0010中[0:2]代表页表索引,根据索引查找相对应的值,为110,然后[3:3]表示是否在内存中,1表示存在。因此物理地址组合后位1100[4:15]。

  4. [外链图片转存失败(img-bIiUI3q1-1563194127065)(http://img.sonihr.com/0e8268c0-39ee-445a-a834-29fec0ede0ac.jpg)]

    解释一下这个图,相对地址即虚拟地址是0x2240,假设一页为4k,即0x2240在第几页呢?用0x2240/4k即0x2240>>12,为2。查页表,发现页面号2的页框号为3,3<<12+240=0x3240即为物理地址。

多级页表和快表
  1. 为了提高内存的利用率,页应该小(粒度更小),但是页小了,相对页表(就是存储页号和页框号对应关系的表)就大了。而且每个进程都有一个页面,占用的空间就更大。

  2. 优化思路1:假如页表不连续呢?比如页表号为0,1,4,5,6,9,10,我们要找5,可以用二分法找到,时间复杂度为O(logN)。页表连续的时候,我们可以直接用偏移量找到,比如要找4,直接用首地址+4就能找到页表对应的记录,时间复杂度为O(1),可见页表不连续的话,时间开销会变大。

  3. 优化思路2:多级页表=页目录号+页号+offset(当然,前面可以有很多级别)
    [外链图片转存失败(img-RinWCluC-1563194127066)(http://img.sonihr.com/436bf02b-6c42-4413-b0d9-107b85e631ed.jpg)]

    页目录是连续的,页目录存放不同的页表指针,根据页表指针找到页表然后根据页号找到页号对应的物理地址。优点是提高了空间效率,缺点是牺牲了时间利用率。

  4. 优化思路3:TLB是一组相联快速存储,是寄存器。其实就是一个缓存。
    [外链图片转存失败(img-iD5mMmQw-1563194127067)(http://img.sonihr.com/50d6c040-b7a7-49a0-a322-70dd2d1630cf.jpg)]

段页结合的实际内存
  1. 程序员希望用段、物理内存希望用页,段的本质是连续,页的本质是分散,所以——程序地址空间划分成多个拥有独立地址空间的段,每个段又划分成大小相同的页,这样既有分段系统的共享和保护,又有分页系统的虚拟内存功能。
  2. 段页同时存在的重定位过程:段号+偏移->段地址(虚拟地址)->页表->物理地址
    [外链图片转存失败(img-Q43R5pGB-1563194127068)(http://img.sonihr.com/23d419ca-880f-4316-9020-1c749bb8fe93.jpg)]
  3. 具体来说:
    • 现在虚拟内存中为程序的用户栈段、用户代码段、用户数据段等分配空间,利用分区算法,想要多少就划分多少给你。
    • 分配页,建页表
  4. fork方法(每一个进程都有自己独立的段表和页表,但是Linux0.11中是所有进程都共用页表)
内存的换入换出-页面置换算法
  1. 当一个进程发生缺页中断后,进程会陷入内核态,执行以下操作:
    1. 查找要访问的虚拟地址,判断是否合法
    2. 分配/查找一个物理页
    3. 填充物理页内容(读取磁盘,或者置0,或者啥也不干)
    4. 建立虚拟地址到物理地址的映射关系
    5. 重新执行却也中断对的指令
  2. 在程序运行过程中,如果要访问的页不在内存中,就会产生缺页中断,将磁盘中找到这一页,然后从物理内存中找到空闲页/要被替换的页,然后调入该页中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘兑换区来腾出空间。
  3. 页面置换算法和缓存淘汰算法类似,可以把内存看做是硬盘的缓存。页面置换算法的主要目标是使页面置换频率最低(缺页率最低)
  4. 方法
    • 最佳替换算法:所选择被置换的页面是最长时间不再访问的页面。但是无法预知。
    • 最近最久未使用(LRU): 在内存中维护一个所有页面的链表,当一个页面被访问,这个页面移到链表表头,这样来保证链表表位的页面是最近最久未被访问的。
    • 最近未使用(NRU):每个页面有两个状态为,R和M,当页面被访问时R置1,被修改时M置1,R会被定时清0。(R,M)=(0,1)表被修改的脏页面,(1,0)表示频繁使用的干净页面。干净页面表示持久、长期,脏页面表示变化快速。操作系统第一次扫描00,若第一次失败则第二次扫描01,并将被扫描的页M置0,如果有01则淘汰,如果没有则重复1、2次扫描,必然能找到00的。
    • 先进先出:置换最先进入的,会导致经常访问的页面也被置换出,缺页率升高
    • 第二次机会:对FIFO的优化,为避免把经常使用的页面置换出去,对FIFO进行修改。当页面被访问时,对标志位R置1,需要置换时从头向尾遍历直到R为0的才置换,如果在途中遇到R为1的,就把这些节点放到末尾。
      [外链图片转存失败(img-Gw0JiPXd-1563194127074)(https://github.com/CyC2018/CS-Notes/raw/master/notes/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png)]
    • 时钟:第二次机会算法中,移动页面导致降低了效率。时钟算法用环形链表将页面链接起来,再额外使用一个指针指向最老的页面。访问的时候,标志位置1,当发生缺页中断时,转动指针,遇到1则置0,遇到0则淘汰,然后停下。为0说明在指针上一次停下到这一次转动的时间内未被使用过,所以可以被淘汰。
      [外链图片转存失败(img-zOQOACGc-1563194127075)(http://img.sonihr.com/13631cf0-d581-4655-a47c-425e360734a6.jpg)]
      但是假如缺页比较少呢?会导致R全部为1,当发生缺页中断时,就要遍历一圈然后全部置0,开销比较大。原因是:记录了太长的历史信息,没有体现出最近。解决方法:可以用一个定时的扫描指针,比如设计5s,那就是只有5s内用过的才是1,否则为0,那么淘汰指针指向的0一定是5s内未用过的,如果没有定时的扫描指针,那就是自上次缺页到本次缺页这段时间内未被用过的,显然时间太长,用的概率太大,导致置1的过多。
  5. 系统颠簸:系统进程变多,按道理CPU利用率应该提高,但是因为进程太多,导致每个进程的缺页率都会增加,从而导致进程总在等待调页完成,所以CPU利用率下降,
    [外链图片转存失败(img-ADn2cynj-1563194127076)(http://img.sonihr.com/6dc3b70b-e995-4e4c-adb3-87104dc65b04.jpg)]
小结
  1. 串联:为了实现段页,必须要有虚拟内存,为了实现虚拟内存,必须有内存的换入换出。实现段页就必须有页表,段表,LDT,为了解决页带来的问题,就要有快表和多级表。
  2. 分段和分页都是一种对内存的管理方式,段的粒度更大,页的粒度更小,段是为了数据可程序可以被划分为逻辑上独立的地址空间有利于共享和保护,也是为了实现虚拟内存,获得更大的地址空间和内存利用效率。
  3. 段是程序员划分的,页是操作系统做的。
  4. 分页是一维的,分段是二维的。因为分页是固定大小,只要有虚拟内存地址就能找到物理内存地址,分段要先找到段,再找到偏移量。回忆一下虚拟地址到物理地址的映射,将虚拟地址的前面几位替换成页表中存储的物理地址,然后后面保留,就完成了映射,所以是一维的。但是因为段地址分配的时候不知道分配在了虚拟地址的哪里,因此要先找到段地址的起始地址,然后再找到偏移量。

操作系统内存分配-brk、mmap

brk与mmap
  1. brk是将数据段(.data)的最高指针_edata向高地址推
  2. mmap是在进程的虚拟地址空间中(堆和栈之间,称为文件映射区)找一块空闲的虚拟内存。
  3. 两者分配的都是虚拟内存,当进程第一次访问这些虚拟内存的时候会发生缺页中断,然后操作系统负责分配物理内存,进一步建立虚拟内存与物理内存之间的映射关系。
举例说明
  1. 若malloc小于128k的内存,使用brk分配内存。
    [外链图片转存失败(img-J9g6S9py-1563194127076)(http://abcdxyzk.github.io/images/kernel/2015-08-05-1.jpg)]
  2. 若malloc大于128k,则使用mmap
    [外链图片转存失败(img-JlVIatoZ-1563194127077)(http://abcdxyzk.github.io/images/kernel/2015-08-05-2.jpg)]
  3. 为什么要这么做呢?因为brk分配的内存只有当高地址内存释放后才能释放低地址,即释放B后才能释放A,否则会产生内存碎片,而mmap分配的内存可以单独释放。
  4. 当调用free(B)后,不是说不能释放么?确实,你看B的虚拟内存虽然不放B了,但_edata却没有向后退,B的那个虚拟内存区域可以被其他小于等于40K的请求复用。
    [外链图片转存失败(img-1w4Qp5FB-1563194127078)(http://abcdxyzk.github.io/images/kernel/2015-08-05-3.jpg)]
  5. 调用trim时会将_edata指针指会最低的空闲地址。

设备管理

磁盘结构

  1. 柱面:所有盘面的同一磁道构成一个柱面,即每个柱面都是一个空心圆柱体,多个空心圆柱体合在一起就是整个磁盘。
  2. 盘面Platter,一个磁盘有多个盘面
    [外链图片转存失败(img-7S5MOR9a-1563194127079)(http://img.sonihr.com/70724c0d-81d1-4ab2-b436-8087866f0561.jpg)]
  3. 磁道Track,盘面上的圆形带状区域,一个盘面可以有多个磁道,一次磁道有多个扇区,扇区是磁道中的一个扇形区域,磁道是盘面中的一个圆环区域,多个同心圆环一起构成了一个柱面
  4. 扇区Track Sector,磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理存储单位,目前有512字节和4k两种。
    [外链图片转存失败(img-20efjxJ9-1563194127080)(http://img.sonihr.com/a1342e0f-debc-4308-8a25-1ec866a912c8.jpg)]
  5. 磁头Head,与盘面非常接近,能够将盘面的磁场和电信号之间转化
    [外链图片转存失败(img-tf9bYvpy-1563194127081)(http://img.sonihr.com/1a6cd9b9-c2a0-4169-ad3a-6a7a42457776.jpg)]
    有多个盘面,每个盘面上都有多个扇区,磁头是纵向排列的若干个头,都平行放置于手臂上,所以比如有10个盘面,就有10个头。如上图,4个盘面,四个竖直排列的磁头。
  6. 制动手臂Actuator arm,用于在磁道之间移动磁头
  7. 主轴Spindle,使整个盘面转动

IO相关

  1. 磁盘块/簇是虚拟出来的,可以包括2、4、8。。。2^n个扇区。目的是为了读取方便,将相邻的扇区合成一个快,再对快进行整体操作。操作系统认为块是IO的最小单位。从物理上说,磁头的旋转和寻道时间是大头,因为块中的扇道都比较近,因此寻道时间比较短。
  2. 扇区,基本单位,我的服务器中位512字节
  3. 页。页不是磁盘的概念,而是内存的概念。innodb可以设定索引页大小,本质上就是规定了每次用多少内存页去装载磁盘中的值。

磁盘读写原理

  1. 基础版本:一个空磁盘,第一次IO是在第一盘面的第一磁道上,按照扇区顺序写入,如果这个磁道写满了,不是向圆心方向切换磁道,而是用下面那个盘面的第一磁道。第一盘面和下面那个盘面的第一磁道,其实就是柱面,也就是说,会从外向内以空心圆柱的形式一层一层的向俯视图圆心的内部写。
  2. 优化点:磁盘访问时间 = 寻道时间 + 旋转时间 + 传输时间。因为传输时间和前两者相比很短,因此主要是前两者,大约10ms。即一次IO无论是读取1k还是100k(具体看磁盘传输速率了,如果传输内容比较大,那时间还是比较多),基本都是寻道时间+旋转时间。那我们干脆哪怕要读1k,但是我们还是按照100k去读,这样虽然会有99k不需要的出来,但是读写的速度上升了。这个技术就叫做block。
  3. 操作系统的神奇之处: 如果我们想指定读取磁盘某一个扇区的内容,那么我们首先要确定盘面,然后确定对应的磁头,然后在寻道到对应的删除,我们需要三个参数,对用户很不友好。于是,**操作系统将三维参数映射到一个一维参数,block,且相邻的block可以快速的读出。**即从block(块)可以转化成柱面,磁头和扇区。
  4. 多个进程通过队列使用磁盘,要通过调度算法,就是下一张的内容。
    [外链图片转存失败(img-i6Ye8Nyp-1563194127083)(http://img.sonihr.com/8a0416f7-5531-471d-8057-475df653b843.jpg)]

磁盘调度算法

  1. 影响一个磁盘块的时间的影响因素有(寻道,旋转,传输)
    • 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区)
    • 寻道时间(制动手臂移动,使得磁头移动到是适当的磁道)
    • 实际传输数据的时间
    • 其中寻道时间最长,因此磁盘调度的主要目标是让磁盘的平均寻道时间最短
  2. 算法
    • 先来先服务
    • 最短寻道时间优先
      磁头始终调度向离自己最近的磁道,磁道两端更容易出现饥饿
    • 电梯算法

      单方向运行,直到该方向没有请求为止,然后改变运行方向。

链接

编译系统

  1. 对于一个hello.c程序,
    #include <stdio.h>
    
    int main()
    {
        printf("hello, world\n");
        return 0;
    }
    
    在Unix系统上,编译器把源文件转化为目标文件gcc -o hello.c,过程大致如下:
    [外链图片转存失败(img-bMrMFMGm-1563194127085)(https://github.com/CyC2018/CS-Notes/raw/master/notes/pics/b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg)]
    • 预处理阶段:处理以#开头的预处理命令
    • 编译阶段:将文本编译成汇编文件
    • 汇编阶段:将汇编文件翻译成可重定位目标程序,已经是二进制了
    • 连接阶段:将需要的额外目标文件和本题合并,合成最终的可执行文件

静态链接

  1. 静态链接是以.o作为输入,最终生成一个完整连接的可执行目标文件,主要任务包括

目标文件

  1. 可执行目标文件:可以直接在内存中执行
  2. 可重定位目标文件:可与其他可重定向目标文件在链接节点合并 .o
  3. 共享目录文件:特殊的可重定位目标文件,可以再运行时被动态加载进内存并链接

动态链接

  1. 解决什么问题:当静态链接库更新时,整个程序都要重新进行链接。对于printf这种标准函数库里的函数,如果每个程序都要链接,并且要代码替换,就很浪费资源。
  2. 解决方案:共享库,windows中是dll,linux中是so。
  3. 特点:给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,不会被复制到引用它的可执行文件中。而静态链接时会直接复制到代码中 在内存中,一个共享库的已编译的机器码,可以被多个进程共享。
    [外链图片转存失败(img-BBmOQDkm-1563194127088)(https://github.com/CyC2018/CS-Notes/raw/master/notes/pics/76dc7769-1aac-4888-9bea-064f1caa8e77.jpg)]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值