操作系统学习笔记

来源:互联网 发布:实战linux编程精髓 编辑:程序博客网 时间:2024/05/18 00:22

二进制

机器码:用最高位0表示正、1表示负, 这种正负号数字化的机内表示形式就称为“机器数”
真值:而相应的机器外部用正负号表示的数称为“真值”

将一个真值表示成二进制字串的机器数的过程就称为编码。

原码:原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。

[+1]原 = 0000 0001
[-1]原 = 1000 0001

由于第一位为符号为,因此8位原码可以表示的数的范围是[11111111]-[01111111],即-127~127。

反码:正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。

[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反

补码:正数的补码就是其本身
负数的补码是在其反码+1。

[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补

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

浮点数:小数点的位置可以变动的数。为增大数值表示范围,防止溢出,采用浮点数表示法。
在计算机中通常把浮点数分成阶码尾数两部分来表示,其中阶码一般用补码定点整数表示,尾数一般用补码或原码定点小数表示。

位、字节

位bit:是电子计算机中最小的数据单位。每一位的状态只能是0或1

字节byte:8个二进制位构成1个”字节(Byte)”,它是存储空间的基本计量单位。
1个字节可以储存1个英文字母或者半个汉字,换句话说,1个汉字占据2个字节的存储空间。

字word:”字”由若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。
例如一台8位机,它的1个字就等于1个字节,字长为8位。
如果是一台16位机,那么,它的1个字就由2个字节构成,字长为16位。字是计算机进行数据处理和运算的单位。

字节序

字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序。

  • 小端字节序:低字节数据存放在内存低地址处,高字节数据存放在内存高地址处
  • 大端字节序:高字节数据存放在低地址处,低字节数据存放在高地址处

所有网络协议也都是采用big endian的方式来传输数据的。所以有时我们也会把big endian方式称之为网络字节序。

比如数字0x12345678在两种不同字节序CPU中的存储顺序如下所示:

Big Endian低地址                                            高地址---------------------------------------------------->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|     12     |      34    |     56      |     78    |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+Little Endian低地址                                            高地址---------------------------------------------------->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|     78     |      56    |     34      |     12    |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

从上面两图可以看出,采用Big Endian方式存储数据是符合我们人类的思维习惯的。

字节对齐

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

为什么要进行字节对齐?

  • 某些平台只能在特定的地址处访问特定类型的数据;
  • 最根本的原因是效率问题,字节对齐能提高存取数据的速度。

字节对齐的原则:(有些笔试题有涉及到)

  • 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在 offset 为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始
    比如int在32位机为4字节,则要从4的整数倍地址开始存储。

  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
    (struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储。)

  • 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

例子:

typedef struct bb{ int id;             //[0]....[3] double weight;      //[8].....[15]      原则1 float height;      //[16]..[19] //总长要为8的整数倍,补齐[20]...[23] 原则3}BB;

这个结构体的长度为24。

typedef struct aa{ char name[2];     //[0],[1] int  id;         //[4]...[7]          原则1 double score;     //[8]....[15]     short grade;    //[16],[17]         BB b;           //[24]......[47]          原则2}AA;

这个结构体的长度为48。

操作系统

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

中断与系统调用

中断

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

中断一般分为三类:

  • 内部异常中断:计算机硬件异常或故障引起的中断
  • 软中断:程序中执行了引起中断的指令而造成的中断
  • 外部中断:由外部设备请求引起的中断

中断处理程序:当中断发生的时候,系统需要去对中断进行处理,对这些中断的处理是由操作系统内核中的特定函数进行的,这些处理中断的特定的函数就是我们所说的中断处理程序了。

中断优先级:中断的优先级表明了中断需要被处理的紧急程度。每个中断都有一个对应的优先级,当处理器在处理某一中断的时候,只有比这个中断优先级高的中断可以被处理器接受并且被处理。优先级比这个当前正在被处理的中断优先级要低的中断将会被忽略。

典型的中断优先级如下所示:

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

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

系统调用

进程的执行在系统上的两个级别:用户级和核心级,也称为用户态和系统态。

程序的执行一般是在用户态下执行的,但当程序需要使用操作系统提供的服务时,比如说打开某一设备、创建文件、读写文件等,就需要向操作系统发出调用服务的请求,这就是系统调用

当进程发出系统调用申请的时候,会产生一个软件中断。产生这个软件中断以后,系统会去对这个软中断进行处理,这个时候进程就处于核心态了。

那么用户态和核心态之间的区别是什么呢?

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

并发

多道程序:如果发现某个程序暂时无须使用 CPU 时,监控程序就把另外的正在等待 CPU 资源的程序启动起来,以充分利用 CPU 资源。

对于多道程序来说,最大的问题是程序之间不区分优先级。

分时系统:对多道程序进行改进,每个程序运行一段时间之后,都主动让出 CPU 资源,这样每个程序在一段时间内都有机会运行一小段时间。

多任务系统:在分时系统下,如果某个程序出错或者发生了死循环,整个系统都会死机。多任务系统使得操作系统从最底层接管了所有硬件资源。

  • 所有的应用程序在操作系统之上以 进程(Process) 的方式运行,每个进程都有自己独立的地址空间,相互隔离
  • CPU 由操作系统统一进行分配。每个进程都有机会得到 CPU,同时在操作系统控制之下,如果一个进程运行超过了一定时间,就会被暂停掉,失去 CPU 资源。这样就避免了一个程序的错误导致整个系统死机。
  • 如果操作系统分配给各个进程的运行时间都很短,CPU 可以在多个进程间快速切换,就像很多进程都同时在运行的样子。

几乎所有现代操作系统都是采用这样的方式支持多任务,例如 Unix,Linux,Windows 以及 macOS。

进程

进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。它可以申请和拥有系统资源,是一个活动的实体。

两个要点:

  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
  • 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。

基本状态

  • 等待态:等待某个事件的完成
  • 就绪态:等待系统分配处理器以便运行
  • 运行态:占有处理器正在运行

运行态→等待态 往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。

等待态→就绪态 则是等待的条件已满足,只需分配到处理器后就能运行。

运行态→就绪态 不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。

就绪态→运行态 系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态

进程调度

调度种类

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

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

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

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

响应时间: 从用户输入到产生反应的时间

周转时间: 从任务开始到任务结束的时间

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

调度算法

  • FIFO或First Come, First Served (FCFS)
    调度的顺序就是任务到达就绪队列的顺序。
    公平、简单(FIFO队列)、非抢占不适合交互式。未考虑任务特性,平均等待时间可以缩短

  • Shortest Job First (SJF)
    最短的作业(CPU区间长度最小)最先调度。
    可以证明,SJF可以保证最小的平均等待时间。

    • Shortest Remaining Job First (SRJF)
      SJF的可抢占版本,比SJF更有优势。

SJF(SRJF): 如何知道下一CPU区间大小?根据历史进行预测: 指数平均法。

  • 优先权调度
    每个任务关联一个优先权,调度优先权最高的任务。
    注意:优先权太低的任务一直就绪,得不到运行,出现“饥饿”现象。

  • Round-Robin(RR)
    设置一个时间片,按时间片来轮转调度(“轮叫”算法)
    优点: 定时有响应,等待时间较短;缺点: 上下文切换次数较多;
    如何确定时间片?
    时间片太大,响应时间太长;吞吐量变小,周转时间变长;当时间片过长时,退化为FCFS。

FCFS是RR的特例,SJF是优先权调度的特例。这些调度算法都不适合于交互式系统。

  • 多级队列调度
    按照一定的规则建立多个进程队列
    不同的队列有固定的优先级(高优先级有抢占权)
    不同的队列可以给不同的时间片和采用不同的调度方法
    存在问题1:没法区分I/O bound和CPU bound;
    存在问题2:也存在一定程度的“饥饿”现象;

  • 多级反馈队列
    在多级队列的基础上,任务可以在队列之间移动,更细致的区分任务。
    可以根据“享用”CPU时间多少来移动队列,阻止“饥饿”。
    最通用的调度算法,多数OS都使用该方法或其变形,如UNIX、Windows等。

进程同步

在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。

临界资源:一次只能被一个进程所占用的资源

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

临界区:进程中访问临界资源的代码为临界区。

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

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

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

  • 一般软件方法
  • 关中断方法
  • 硬件原子指令方法
  • 信号量方法

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

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

除信号量的初值外,信号量的值仅能由P操作和V操作更改,操作系统利用它的状态对进程和资源进行管理。

P操作:P操作记为P(s),其中s为一信号量,它执行时主要完成以下动作:

s.value = s.value - 1/*可理解为占用1个资源,若原来就没有则记帐“欠”1个*/

若s.value ≥ 0,则进程继续执行,否则(即s.value < 0),则进程被阻塞,并将该进程插入到信号量s的等待队列s.queue中

V操作:V操作记为V(s),其中s为一信号量,它执行时,主要完成以下动作:

s.value = s.value + 1/*可理解为归还1个资源,若原来就没有则意义是用此资源还1个欠帐*/

若s.value > 0,则进程继续执行,否则(即s.value ≤ 0),则从信号量s的等待队s.queue中移出第一个进程,使其变为就绪状态,然后返回原进程继续执行。

死锁

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

死锁产生的4个必要条件:

  • 互斥使用(Mutual exclusion)
    指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

  • 不可抢占(No preemption)
    指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  • 请求和保持(Hold and wait)
    指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  • 循环等待(Circular wait)
    指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

死锁避免——银行家算法
思想: 判断此次请求是否造成死锁,若会造成死锁,则拒绝该请求

进程通信

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

线程

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。

线程具有以下属性:

  • 轻型实体 线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。

  • 独立调度和分派的基本单位。

  • 在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

  • 可并发执行。 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

  • 共享进程资源。 在同一进程中的各个线程,都可以共享该进程所拥有的资源。

    • 所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;
    • 还可以访问进程所拥有的已打开文件、定时器、信号量等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
    • 线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID

协程

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

IO多路复用

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

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

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

常见的IO复用实现

  • select(Linux/Windows/BSD)
  • epoll(Linux)
  • kqueue(BSD/Mac OS X)

内存管理

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

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

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

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

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

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

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

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

页面置换算法

  • FIFO算法
    先入先出,即淘汰最早调入的页面。

  • OPT(MIN)算法
    选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。
    可惜,MIN需要知道将来发生的事,只能在理论中存在,实际不可应用。

  • LRU(Least-Recently-Used)算法
    用过去的历史预测将来,选最近最长时间没有使用的页淘汰(也称最近最少使用)。
    LRU准确实现:计数器法,页码栈法。
    由于代价较高,通常不使用准确实现,而是采用近似实现,例如Clock算法。

Linux

Linux文件权限

Linux文件采用10个标志位来表示文件权限:

-rw-r--r--  drwxr-xr-x

第一个字符一般用来区分文件和目录,其中:

  • d:表示是一个目录。
  • -:表示这是一个普通的文件。
  • l : 表示这是一个符号链接文件,实际上它指向另一个文件。
  • b、c:分别表示区块设备和其他的外围设备,是特殊类型的文件。
  • s、p:这些文件关系到系统的数据结构和管道,通常很少见到。

第2~10个字符当中的每3个为一组,左边三个字符表示所有者权限,中间3个字符表示与所有者同一组的用户的权限,右边3个字符是其他用户的权限。

原创粉丝点击