操作系统

体系结构基础

冯 诺伊曼 体系结构

  1. 计算机处理的数据和指令一律用二进制表示
  2. 顺序执行程序

计算机运行过程中,把要执行的程序和处理的数据首先存入主存储器(内存),计算机执行程序时,将自动地并按顺序从主存储器中取出指令一条一条的执行,这以概念称作顺序执行程序。

  1. 计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。

数据的机内表示

二进制表示

  1. 机器数

由于计算机中符号和数字一样,都必须用二进制数串来表示,因此,正负号也必须用0、1来表示。
用最高位0表示正、1表示负,这种正负号数字化的机内表示形式就称为“机器数”,而响应的机器外部用正负号表示的数称为“真值”,将一个真值表示成二进制字符串的机器数的过程就称为编码。

  1. 源码

源码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。
源码是人脑最容易理解和计算的表示方式

  1. 反码

反码的表示方法是: 正数的反码是其本身;负数的反码是在其源码的基础上,符号位不变,其余各个位取反。
如果一个反码表示的是负数,人脑无法直观的看出来它的数值。通常要将其转换成源码再计算。

  1. 补码

补码的表示方法是:正数的补码就是其本身;负数的补码是在其源码的基础上,符号位不变,其余各位取反,最后+1。(即在反码的基础上+1)
对于负数,补码表示方式也是人脑无法直观看出其数值的。通常也需要转换成源码在计算其数值。

  1. 定点数与浮点数

定点数是小数点固定的数。在计算机中没有专门表示小数点的位,小数点的为止是约定默认的。一般固定在机器书的最低位之后,或是固定在符号位之后。前者称为定点纯正数,后者称为定点纯小叔。
定点数表示法简单直观,但是数值表示范围太小,运算时容易产生溢出。

浮点数是小数点的为止可以变动的数。为增大数值表示范围,防止溢出,采用浮点数表示法。浮点表示法类似于十进制中的科学计数法。
在计算机中通常把浮点数分成阶码和尾数两部分来表示,其中阶码一般用补码定点整数表示,尾数一般用补码或源码定点小数表示。为保证不损失有效数字,对尾数进行规格化处理,也就是平时所说的科学计数法,即保证尾数的最高位为1,实际数值通过阶码进行调整。
阶符表示指数的符号位、阶码表示幂次、数符表示尾数的符号位、尾数表示规格化后的小数值。

位(Bit)、字节(Byte)、字(Word)

  • 位: 是电子计算机中最小的数据单位。每一位的状态智能是0或1。
  • 字节:8个二进制位构成1个”字节(byte)“,它是存储空间的基本计量单位。1个字节可以存储1个英文字母或者半个汉字,换句话说,1个汉字占据2个字节的存储空间。
  • 字:有若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。字是计算机进行数据处理的运算单位。

字节序

字节顺序是指占内存多余一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。
小端字节序指低字节数据存放在内存低地址出,高字节数据存放在内存高地址出;大端字节序是高字节数据存放在低地址出,低字节数据存放在高地址处。
所有网络协议都是采用 big endian 的方式来传输数据的。会把 big endian 方式称之位网络字节序。

字节对齐

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的反问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

  • 为什么要进行字节对齐?
1. 某些平台智能在特定的地址处访问特定类型的数据
2. 最根本的原因是效率问题,字节对齐能提高存取数据的速度
  • 字节对齐的原则
1. 数据成员对齐原则:结构(struct)或联合(union)的数据成员,第一个数据成员放在 offset 为0的地方,以后每个数据成员存储的起始为止要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说数据,结构体等)的正数倍开始(比如int在32位机位4字节,则要从4的整数倍地址开始存储)。
2. 结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a 里存有 struct b, b 里面有 char,int,double 等元素,那b应该从8的整数倍开始存储)。
3. 收尾工作:结构体的总大小,也就是sizeof的结构,必须是其内部最大成员的整数倍,不足的要补齐。

操作系统提供的服务

操作系统的五大功能,分别为:作业管理、文件管理、存储管理、输入输出设备管理、进程及处理机管理

中断与系统调用

中断

所谓的中断就是在计算机执行程序的过程中,由于出现了某些特殊事情,使得CPU暂停对程序的执行,转而去执行处理这一时间的程序。等这些特殊事情处理完之后再回去执行之前的程序。中断一般分为三类:

  1. 有计算机硬件异常或故障引起的中断,称为内部异常中断
  2. 由程序中执行引起中断的指令而造成的中断,称为软中断
  3. 由外部设备请求引起的中断,称为外部中断。

与中断紧密相连的一个概念就是中断处理程序。当中断发生的时候,系统需要去对中断进程处理,对这些中断的处理是由操作系统内核中的特定函数进行的,这些处理中断的特定的函数就是我们所说的中断处理程序。
另一个与中断紧密相连的概念就是中断的优先级。中断的优先级说明的是当一个中断正在被处理的时候,处理器能接受的中断的几杯。中断的优先级也表明了中断需要被处理的紧急程度。每个中断都有一个对应的优先级,当处理器在处理中断的时候,只有比这个中断优先级高的中断可以被处理器接受并且被处理。优先级比这个当前正在被处理的中断优先级要低的中断将会被忽略。
典型的中断优先级如下:

机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断

当发生软件中断时,其他所有的中断都可能发生并被处理;但当发生磁盘中断时,就只有时钟中断和机器错误中断能被处理。

系统调用

在讲系统调用之前,先说下进程的执行在系统上的两个级别:用户级和核心级,也称为用户态和系统态(user mode and kernel mode)。
程序的执行一般是在用户态下执行的,但当程序需要使用操作系统提供的服务时,比如说打开某一设备、创建文件、读写文件等,就需要向操作系统发出调用服务的请求,这就是系统调用。
Linux系统有专门的函数库来提供这些请求从A座系统服务的入口,这个函数库中包含了操作系统所提供的对外服务的借口。当进程发出系统调用之后,它所处的运行状态就会由用户态变成核心态。但这个时候,进程本身其实并没有做什么事情,这个时候是由内核在做响应的操作,去完成进程所提出的这些请求。
系统调用和中断的关系就在于,当进程发出系统调用申请的时候,会产生一个软件中断。产生这个软件中断以后,系统会去对这个软中断进程处理,这个时候进程就处于核心态了。

  • 用户态和核心态之间的区别是什么?
  1. 用户态的进程能存取他们自己的指令和数据,但不能存取内核指令和数据(或其他进程的指令和数据)。然而,核心态下的进程能够存取内核和用户地址
  2. 某些机器指令是特权指令,在用户态下执行特权指令会引起错误

对此要理解的一个是,在系统中内核并不是作为一个与用户进程平行的级别的进程的集合,内核是为用户进程运行的。

并发技术

多任务

多道程序(Multiprogramming):为了让CPU得到更好的利用,人们编写了一个监控程序,如果发现某个程序暂时无须使用CPU时,监控程序就把另外的正在等待CPU资源的程序启动起来,以充分利用CPU资源。这种方法被称为多道程序。

分时系统(Time-Sharing System):在多道程序之上进行改进,使得每个程序运行一段时间之后,都主动让出CPU资源,这样每个程序在一段时间内都有机会运行一小段时间。

多任务(Multi-tasking)系统:为了避免某个程序出现错误,导致死循环,导致系统死机。操作系统从最底层接管所有硬件资源,所有应用程序在操作系统之上以进程(Process)的方式运行,每个进程都有自己独立的地址空间,相互隔离。CPU由操作系统统一进行分配,每个进程都有机会得到CPU,同时在操作系统控制之下,如果一个进程运行超过了一定时间,就会被暂停掉,失去CPU资源。如果操作系统分配给各个进程的运行时间都很短,CPU可以在多个进程间快速切换,就像很多进程都同时运行的样子。

进程

进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区处处着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有声明的实体,只有处理器赋予程序生命时,它才能称为一个活动的实体,我们称其为进程。

进程的基本状态

  1. 等待态:等待某个时间的完成
  2. 就绪态:等待系统分配处理器以便运行
  3. 运行态:占有处理器正在运行

运行态->等待态 往往是由于等待外设,等待主存等资源分配或等待人工敢于而引起的。
等待态->就绪态 则是等待的条件已满足,只需分配到处理器后就能运行。
运行态->就绪态 不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或由更高优先级的进程来抢占处理器等。
就绪态->运行态 系统按某种策略选中就绪队列种的一个进程占用处理器,此时就变成了运行态。

进程调度

调度种类

高级、中级和低级调度作业从提交开始直到完成,往往要经历下述三级调度:

  • 高级调度(High-Level Scheduling):又称为作业调度,它决定把后备作业调入内存运行
  • 中级调度(Intermediate-Level Scheduling):又称为在虚拟存储器种引入,在内、外存对换区进程进程对换。
  • 低级调度(Low-Level Scheduling):又称为进程调度,它决定把就绪队列的某进程获得CPU。

非抢占式调度与抢占式调度

  • 非抢占式

分派程序一旦把处理机分配给某进程后便让它一致运行下去,直到进程完成或发生进程调度进程调度某事件而阻塞时,才把处理机分配给另一个进程。

  • 抢占式

操作系统将正在运行的进程强行暂停,由调度程序将CPU分配给其他就绪进程的调度方式

调度策略的设计

响应时间:从用户输入到产生反应的时间
周转时间:从任务开始到任务结束的时间
CPU任务可以分为交互式任务和批处理任务,调度最终的目标是合理的使用CPU,使得交互式任务的响应时间尽可能短,用户不至于感到延迟,同时使得批处理任务的周转时间尽可能短,减少用户等待的时间。

调度算法

  1. FIFO或First Come,First Served(FCFS)
  • 调度的数据就是任务到达就绪队列的顺序。
  • 公平、简单(FIFO队列)、非抢占、不适合交互式。未考虑任务特性,平均等待时间可以缩短
  1. Shortest Job First(SJF)
  • 最短的作业(CPU区间长度最小)最先调度
  • 可以证明,SJF可以保证最小的平均等待时间
    PS:Shortest Remaining Job First(SRJF),SJF的可抢占版本,比SJF更有优势
    SJF(SRJF):如何直到下一CPU区间大小?根据历史进行预测:指数平均法。
  1. 优先权调度
    每个任务关联一个优先权,调度优先权最高的任务
    注意:优先权太低的任务一直就绪,得不到运行,出现“饥饿”现象。
    FCFS是RR的特例,SJF是优先权调度的特例。这些调度算法都不适合于交互式系统。
  2. Round-Robin(RR)
    设置一个时间片,按时间片来轮转调度(“轮叫”算法)
    优点:定时有响应,等待时间较短;缺点:上下文切换次数较多
    如何确定时间片?
    时间片太大,响应时间太长;吞吐量变小,周转时间变长;当时间片过长时,退化为FCFS
  3. 多级队列调度
  • 按照一定的规则建立多个进程队列
  • 不同的队列有固定的优先级(高优先级有抢占权)
  • 不同的队列可以给不同的时间片和采用不同的调度方法
    存在问题1: 没法区分I/O bound 和 CPU bound
    存在问题2: 也存在一定程度的“饥饿”现象
  1. 多级反馈队列
    在多级队列的基础上,任务可以在队列之间移动,更细致的区分任务
    可以根据“享用”CPU时间多少来移动队列,阻止“饥饿”
    最通用的调度算法,多数OS都使用该方法或其变形。

进程同步

临界资源与临界区

在操作系统种,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间智能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如无力上的打印机,或是存在硬盘或内存种被多个进程所共享的一些变量和数据等(如果这类资源不被堪称临界资源加以保护,那么很有可能造成丢数据的问题)。

对于临界资源的访问,必须是互斥进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被称为临界区。

对于临界区的访问过程分为四个部分:

  1. 进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞
  2. 临界区:在临界区做操作
  3. 退出区:清楚临界区被占用的标志
  4. 剩余区:进程与临界区不相关部分的代码

解决临界区问题可能的方法:

  1. 一般软件方法
  2. 关中断方法
  3. 硬件原子指令方法
  4. 信号量方法

信号量

信号量是一个确定的二元组(s,q),其中s是一个具有非负初值的整形变量,q是一个初试状态为空的队列,整形变量s表示系统种某类资源的数据:

  • 当其值 >= 0 时, 表示系统中当前可用资源的数目
  • 当其值 < 0 时,其绝对值表示系统中因请求该类资源而被阻塞的进程数目

除信号量的初值外,信号量的值仅能由P操作和V操作更改,操作系统利用它的状态对进程和资源进行管理
P操作:记为P(s),其中s为一信号量,它执行时主要完成资源占用,当前可用资源 - 1,当前可用资源<0,阻塞,否则继续执行
V操作:记为V(s),其中s为一信号量,它执行是主要完成资源归还,当前可用资源 +1
实际上,V操作可以理解为归还资源的计数器,或是唤醒进程使其处于就绪状态的控制指令

死锁

死锁:多个进程因循环等待资源而造成无法执行的现象
死锁会造成进程无法执行,同时会造成系统资源的极大浪费(资源无法释放)
死锁产生的4个必要条件:

  • 互斥使用(Mutual exclusion)指进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,则请求者只能等待,直到占有资源的进程用毕释放。
  • 不可抢占(No preemption)指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
  • 请求和保持(Hold and wait)指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放
  • 循环等待(Circular wait)指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,...,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,....,Pn正在等待已被P0占用的资源。

死锁避免——银行家算法
思想:判断此次请求是否造成死锁,若会造成死锁,则拒绝该请求
当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。


进程间通信

本地进程间通信的方式有很多,可以总结为下面四类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(Solaris门和Sun RPC)

PS:

管道(pipeline),本源是使用消息传递的进程间通信机制,它构成自连接起来的处理元素(进程、线程、协程和函数等),它们被安置为每个元素的输出都是下一个元素的输入。在连贯的元素之间,通常会提供一定数量的缓冲区。在管道中流动的信息,经常是记录、字节或位的流(stream),而管道的元素可以叫做过滤器。将元素连接成管道类似于函数复合。

FIFO(先进先出算法),是一种计算机科学的排序算法。

消息队列:在计算机科学中,消息队列(Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的伫列用来处理一些列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个伫列中的记录包含详细说明的资料,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。

互斥锁(Mutual exclusion, Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(cirtical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。

读写锁:是计算机程序的并发控制的一种同步机制,也成“共享-互斥锁”、多读者-单写者锁。用于解决读写问题,读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。

条件变量(Monitors):是一种程序结构,结构内的多个子程序(对>象或模块)形成的多个工作线程互斥访问共享资源。一个条件变量就是一个线程队列(queue),其中的线程正等待某个条件变为真。

共享内存:是指在多处理器的计算机系统中,可以被不同中央处理器访问的大容量内存。由于多个CPU需要快速访问存储器,这样就要对存储器进行缓存。由于其他处理器可能也要存取,任一缓存数据更新后,共享内存就需要立即更新,否则不同处理器可能用到不同的数据。

线程

多进程解决了多任务问题。然而很多时候不同的程序需要共享同样的资源(文件、信号量等),如果全部使用进程的话会导致切换成本很高,造成CPU资源的浪费。于是就出现了线程的概念
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
线程具有以下属性:

  1. 轻型实体,线程中的实体基本不拥有系统资源,只有一点必不可少的、能保证独立运行的资源。线程的实体包括程序、数据和TCB(线程控制模块Thread Control Block)。线程是动态概念,它的动态特性由线程控制快TCB描述。
  2. 独立调度和分派的基本单位。
    在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
  3. 可并发执行。在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
  4. 共享继承资源。在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间爱你的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。线程共享的环境包括:进程代码段、进程的共有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组的ID。

此处主要是多线程编程中需要使用的锁。
锁要解决的是线程之间争夺资源的我难题,这个问题大概有下面几个角度:

  • 资源是否独占(独占锁 - 共享锁)
  • 抢占不到资源怎么办(互斥锁 - 自旋锁)
  • 自己能不能重复抢(重入锁 - 不可重入锁)
  • 竞争读的情况比较多,读可不可以不加锁(读写锁)

上面这几个角度不是相互独立的,在实际场景中往往要结合起来,才能构造除一个合适的锁。

独占锁 - 共享锁

当一个共享资源只有一份的时候,通常我们使用独占所,常见的即各个语言中的Mutex。当共享资源有多份时,可以使用技术信号量(Semaphore,同步机制)。

互斥锁 - 自旋锁

对于互斥锁来说,如果一个线程已经锁定了一个互斥锁,第二个线程又试图去获取这个互斥锁,则第二个线程将被挂起(即休眠,不占用CPU资源)。
在计算机系统中,频繁的挂起和切换线程,也是有成本的。自旋锁就是解决这个问题的。
自旋锁,指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该新城将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
容易看出,当资源等待的时间较长,用互斥锁让线程休眠,会消耗更好的资源。当资源等待的时间较短时,使用自旋锁将减少线程的切换,获得更高的性能。

重入锁 - 不可重入锁

可重入锁(Reentry Lock),也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获得到的锁。
使用可重入锁时,在同一线程中可以获取锁,不会导致死锁。使用不可重入锁,则会导致死锁发生。

读写锁

有些情况下,对于共享资源读竞争的情况远远多余写竞争,这种情况下,对读操作每次都进行加锁,是得不偿失的。读写锁就是为了解决这个问题。
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。简单可以总结为,读读不互斥,读写互斥,写写互斥。
对读写锁来说,有一个升级和降级的概念,即当前获得了读锁,想把当前的锁变成写锁,称为升级,反之称为降级。锁的升降级本身也是为了提升性能,通过改变当前锁的性质,避免重复获取锁。

协程

协程,又称微线程,纤程(Coroutine)。
协程可以理解为用户级线程,协程和线程的区别是:线程是抢占式的调度,而协程是协同式的调度,协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。

IO多路复用

基本概念

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场景:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器既要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要用I/O复用。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

内存管理

内存管理基础

程序可执行文件的结构

一个程序的可执行文件在内存中的结果,从大的角度可以分为两个部分:只读部分和可读部分。只读部分包括程序代码(.text)和程序中的常量(.rodata)。可读写部分(也就是变量)大致可以分成下面几个部分:

  • .data : 初始化了的全局变量和静态变量
  • .bss : 即 Block Started by Symbol,为初始化的全局变量和静态变量
  • heap : 堆,使用 malloc, realloc 和 free 函数控制的变量,堆在所有的线程,共享库和动态家在的模块中被共享使用
  • stack : 栈,函数调用时使用栈来保存函数现场,自动变量(即生命周期限制在某个 scope 的变量)也存放在栈中。

data 和 bss 区

这两个区经常放在一起说,因为他们都是用来存储全局变量和静态变量的,区别在于 data 区存放的是初始化过的, bss 区存放的是没有初始化过的。
变量的值会一开始被存储在 .text 中(因为值是写在代码里面的),在程序启动使会拷贝到 .data 区,未初试话的会被放到 bss 区。

静态变量和全局变量

全局变量:在一个代码文件中,一个变量要么定义在函数中,要么定义在函数外。当定义在函数外时,这个变量就有了全局作用域,成了全局变量。全局变量不光意味着这个变量可以在整个文件中使用,也意味着这个变量可以在其他文件中使用。

静态变量:指使用 static 关键字修饰的变量, static 关键字对变量的作用域进行了限制,具体的限制如下:
在函数外定义:全局变量,但是只在当前文件中可见(叫做 internal linkage)
在函数内定义:全局变量,但是只在此函数内可见(同时,在多次函数调用中,变量的值不会丢失)
(C++)在类中定义:全局变量,但是只在此类中可见

注意:static 跟不可改变没有关系,不可改变的变量使用 const 关键字修饰。

程序在内存和硬盘上不同的存在形式
这里我们提到的几个区,是指程序在内存中的存在形式。和程序在硬盘上存储的格式不是完全对应的。程序在硬盘上存储的格式更加复杂,而且是和操作系统有关的。

栈是用于存放本地变量,内部临时变量以及有关上下文的内存区域。程序在调用函数时,操作系统会自动通过压站和弹栈完成保存函数现场等操作,不需要程序员手动干预。

栈是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先预定好的。能从栈获得的空间较小。如果申请的空间超过栈的剩余空间时,例如递归深度过深,将提示 stackoverflow。

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。

堆是用于存放除了栈里的东西之外所有其他东西的内存区域,当使用malloc 和 free 时就是在操作堆中的内存。对于堆来说,释放工作由程序员控制,容易产生 memory leak(内存泄漏)。

堆是想高地址扩展的数据结构,是不连续的内存区域。这事由于系统是用链表来存储的空闲内存地址,依然是不连续的,而链表的遍历方向是由低地址想高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

对于堆来讲,频繁的new/delete势必会造成内存空间爱你的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出。

堆都是动态分配的,没有静态分配的堆。栈有2中分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是有编译器进行释放,无需我们手动实现。

计算机底层并没有对堆的支持,堆则是C/C++函数库提供的,同时由于碎片问题,都会导致堆的效率比栈要低。

内存分配

  • 虚拟地址:用户变成时将代码(或数据)分成若干个段,每条代码或每个数据的地址由段名称+段内相对地址构成,这样的程序地址称为虚拟地址。
  • 逻辑地址:虚拟地址中,段内相对地址部分称为逻辑地址
  • 物理地址:实际无力内存中所看到的存储地址称为物理地址
  • 逻辑地址空间:在实际应用中,将虚拟地址和逻辑地址经常不加区分,通称为逻辑地址。逻辑地址的集合称为逻辑地址空间
  • 线性地址空间:CPU地址总线可以访问的所有地址集合称为线性地址空间
  • 物理地址空间:实际存在的可访问的物理内存地址集合称为物理地址空间
  • MMU(Memery Management Unit 内存管理单元):实现将用户程序的虚拟地址(逻辑地址)->物理地址映射的CPU中的硬件电路
  • 基地址:在进行地址映射时,经常以段或页为单位并以其最小地址(即起始地址)为基值来进行计算
  • 偏移量:在以段或页为单位进行地址映射时,相对于基地址的地址值

虚拟地址先经过分段机制映射到线性地址,然后线性地址通过分页机制映射到物理地址。

虚拟内存

计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如[RAM 随机存取存储器])的使用也更有效率。

请求调页,也称按需调页,即对不在内存中的“页”,当进程执行时要用时才调入,否则可能到程序结束时也不会调入。

页面置换算法

  • FIFO算法
    先进先出,即淘汰最早调用的页面
  • OPT(MIN)算法
    选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。MIN需要直到将来发生的事,只能在理论上存在,实际不可应用。
  • LRU(Least-Recently-Userd)算法
    用过去的历史预测将来,选最近最长时间没有使用的页淘汰(也称最近最少使用)。
    LRU准确实现:计数器法,页码栈法
    由于代价较高,通常不实用准确实现,而是采用金丝实现,例如Clock算法

内存抖动现象:页面的频繁更换,导致整个系统效率几句下降,这个现象称为内存抖动(或颠簸)。抖动一般是内存分配算法不好,内存太小或者程序的算法不佳引起的。
Belady现象:在分页式虚拟存储器管理中,发生缺页时的置换算法采用FIFO(先进先出)算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。

FIFO会长生Belady异常。
栈式算法无Belady异常,LRU,LFU(最不经常使用),OPT都属于栈式算法。

磁盘调度

磁盘调度

你可能感兴趣的:(操作系统)