操作系统学习笔记

本文详细介绍了操作系统的历史发展,从早期的批处理系统到现代的多任务操作系统,强调了多进程结构的重要性。操作系统通过系统调用连接应用程序,通过中断处理(如int0x80)进入内核。内存分为内核态和用户态,通过切换PCB实现进程的交替执行。CPU调度策略包括FCFS、SJF、RR等,以及死锁的预防和避免。内存管理涉及分页和分段,以及虚拟内存的使用。此外,还讲解了文件系统、磁盘管理和I/O操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

操作系统

笔记来源于b站上哈工大李治军老师的课程:https://www.bilibili.com/video/BV1d4411v7u7

操作系统简史

  1. 1955-1965: 上古神机IBM7094,造价在250w美元以上。批处理,只运算,无停止
  2. 1965-1980:
    • IBM OS/360(360表示全方位服务),此时计算机需要做多种操作(multiprogramming),用批处理是不合适的(必须要先完成I/O操作后才能做计算操作)。切换和调度应运而生,例如,IO任务执行比较慢,就可以跳转到计算任务。这是多进程结构和进程管理概念萌芽。
    • MULTICS,由于计算机的使用人数增加,如果每个人启动一个任务,任务之间需要快速切换,分时系统(timesharing)。核心仍然是任务切换,但是资源复用的思想对操作系统影响很大,虚拟内存就是一种复用。
  3. 1980-1990: 小型计算机出现,个人可以使用。在PDP-7上开发出了UNIX,是简化的MULTICS,核心概念差不多。IBM推出了IBM PC,个人计算机开始普及,很多人可以使用并接触UNIX。
  4. 1990-2000: 小Linux于1991年发布,1994年Linux 1.0发布并采用GPL协议,1998年以后互联网世界里展开了一场历史性的Linux产业化运动。

总结:多进程结构是操作系统的基本图谱。

  1. 1975年Digital Research开发了操作系统CP/M,单任务执行。1980年出现了鹅80806 16位芯片,从CP/M基础上开发出了QDOS(Quick and Dirty OS)。
  2. 1978年,Paul Allen 和 Bill Gates开发乐BASIC解释器,并开创了微软。1977年Bill Gates开发了FAT管理磁盘。1981年微软买下QDOS改名为MS-DOS和IBM PC打包出售。
  3. 1989年,MS-DOS 4.0出现,支持鼠标和键盘。后来出现Windows 3.0,XP,Win 7 …
  4. 1984年,苹果推出Mac机,处理器使用IBM、Intel和AMD,核心在于屏幕、能耗等。2007年发布IOS,核心仍是MAC OS。MAC OS核心是UNIX,专注于界面、文件、媒体等和用户有关的内容。

总结:对用户的使用感倍加重视:各种文件、编程环境、图形界面。

因此课程任务就是:(1)掌握、实现操作系统的多进程图谱;(2)掌握、实现操作系统的文件操作视图。

操作系统接口

连接应用程序和操作系统,表现为函数调用,又称为系统调用(System Call)。

POSIX (Portable Operating System Interface of Unix) IEEE制定的标准族。

系统调用的实现

问题:为什么应用程序不能直接调用操作系统里的数据?

回答:因为如果可以直接随意访问会造成不安全的问题,那么就可以看到root的密码可以修改它,可以通过显存看到别人Word里的内容……

系统调用:提供一种能进入内核的手段

内存

分为内核态和用户态,将内核程序和用户程序隔离。内核态可以访问任何数据而用户态不能访问内核数据。

要知道当前程序执行在哪一态,要检查CS:IP(当前指令)的最低两位:0是内核态,3是用户态。CS寄存器中存储的是CPL。

  • DPL(Descriptor/Destination Privilege Level)目标特权级:要访问的目标段的特权级,默认为0。
  • CPL(Curren Privilege Level)当前特权级。
  • 只有当DPL大于等于CPL(CPL数值更小)时才合法。
int 0x80中断

用户可以主动进入内核调用内核代码的唯一方法:中断指令int 0x80,将CS中的CPL改为0。

  1. 用户程序中包含int指令代码
  2. 操作系统写中断处理,获取想调程序的编号
  3. 操作系统根据编号执行相应代码

int 0x80中断处理过程:

  1. 初始化时,80号中断的DPL设为3,使得CPL=3的用户代码可以进入
  2. CPL根据段选择符处的8(1000的最后两位为00)被设置成了0,即进入内核

在这里插入图片描述

CPU的工作原理

CPU根据PC指向的地址取出内存中的指令,从总线传回CPU。CPU对指令进行译码,然后执行。然后如此往复。

并发(多道程序在一个CPU上交替执行):面对IO任务执行所需时间过长的问题,可以在等待磁盘完成读写操作之前切换到另一个程序执行。为了记录切出去之前的程序执行的状况和信息(例如PC、AX、BX等),要将这些信息存入PCB(Process Control Block)

进程:运行的程序

  • 进程有开始、结束,程序没有
  • 进程有走、停,程序没有
  • 进程需要记录ax、bx,程序不用

多进程图像

从启动开机到关机结束
  1. main中的fork()创建了第一个进程

    if(!fork()) {init();}  //init一号进程执行了shell(Windows桌面)
    
  2. Shell再启动其他进程在这里插入图片描述

多进程如何组织

PCB+状态+队列:用PCB放入不同队列中,用状态转移推进
在这里插入图片描述

多进程如何交替

队列操作+调度+切换:

  1. 一个进程启动磁盘读写

  2. 将自身状态设为“阻塞态”:pCur.state = ‘W’

  3. 将pCur放入DiskWaitQueue

  4. 运行schedule(),完成切换

    schedule()
    {
    	pNew = getNext(ReadyQueue);//调度,找到就绪队列中下一要执行进程的PCB
    	switch_to(pCur,pNew);//切换当前进程PCB与即将执行进程的PCB
    }
    
    swich_to(pCur, pNew) {
    	pCur.ax = CPU.ax;
    	pCur.bx = CPU.bx;
    	  ……
    	pCur.cs = CPU.cs;
    	pCur,retpc = CPU.pc;
    	
    	CPU.ax = pNew.ax;
    	CPU.bx = pNew.bx;
    	  ……
    }
    
多进程之间的影响

不同的进程可能对同一地址有不同操作,这样交替进行会使得处理内容有误,所以需要通过内存管理(映射表)对多进程的地址空间分离

多进程之间的合作

多个有合作的进程需要对同一个值进行修改后并同步,如果在未对该值的修改操作完成前就切换到其他进程,会导致出错。所以需要对该值上锁,在对该值进行写操作时阻断其他进程访问。

用户级线程

进程 = 资源 + 指令执行序列

线程是程序执行的最小单位,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间,所以各个线程之间的切换只需要修改PC指令,不需要映射表上的资源切换,切换速度比较快。

线程:指令之间的切换,保留了并发的优点,避免了进程切换的代价。

每个线程都有自己的栈,用于保存返回时的地址PC。栈的地址保存在TCB中。Yield()操作切换线程时要先切换TCB,切换栈。
在这里插入图片描述

内核级线程

在这里插入图片描述

进程的实质就是内核级线程。

核心级 VS 用户级
  • 用户级:多个栈;核心级:多套栈

    在这里插入图片描述

  • 在这里插入图片描述

内核中的切换:switch_to

switch_to通过TCB找到内核栈指针,然后通过ret切到某个内核程序,最后用内核栈中的CS:PC切到用户程序,根据IRET切换到用户栈。

在这里插入图片描述

CPU调度策略

需要考虑的问题
  • 吞吐量和响应时间之间的矛盾:响应时间小=>切换次输多=>系统内耗大=>吞吐量小
  • 前台任务和后台任务的关注点不同:前台任务关注响应时间(切换需要快速,才能够及时响应当前进程),后台任务关注周转时间(切换如果过于频繁,在切换进程时的时间损耗会变大)
  • 有的任务I/O操作频率高(IO约束型),有的任务是计算等CPU任务频率高(CPU约束型任务)。IO约束型任务的优先级会比较高,因为CPU约束型任务很少会需要切换进程,这样就会较长时间内在完成当前进程,而不能很好地利用系统资源。
调度算法
  1. First Come, First Served(FCFS)

    (1)按照作业提交,或进程变为就绪状态的先后次序分派CPU;
    (2)新作业只有当当前作业或进程执行完或阻塞才获得CPU运行
    (3)被唤醒的作业或进程不立即恢复执行,通常等到当前作业或进程出让CPU。(所以,默认即是非抢占方式)
    (4)有利于CPU繁忙型的作业,而不利于I/O繁忙的作业(进程)。

  2. 短作业优先(SJF):有效缩短周转时间

    (1)平均周转时间、平均带权周转时间都有明显改善。SJF/SPF调度算法能有效的降低作业的平均等待时间,提高系统吞吐量。

    平均周转时间公式:
    p 1 + p 1 + p 2 + p 1 + p 2 + p 3 + … … = ∑ i = 0 n ( n + 1 − i ) p i p_1 + p_1 + p_2 + p_1 + p_2 + p_3 + …… = \sum_{i=0}^n (n+1-i)p_i p1+p1+p2+p1+p2+p3+=i=0n(n+1i)pi
    可以看到长作业越靠后被加的次数越少,总和也就越少

    (2)未考虑作业的紧迫程度,因而不能保证紧迫性作业(进程)的及时处理、对长作业的不利、作业(进程)的长短含主观因素,不一定能真正做到短作业优先。

  3. Run Robin(RR) 按时间片来轮转调度

    (1)时间片大:响应时间会太长;时间片小:吞吐量小

    (2)时间片轮转算法过程:

    1、排成一个队列。2、每次调度时将CPU分派给队首进程。3、时间片结束时,发生时钟中断。4、暂停当前进程的执行,将其送到就绪队列的末尾,并通过上下文切换执行当前就绪的队首进程。
    说明:1、进程阻塞情况发生时,未用完时间片也要出让CPU。2、能够及时响应,但没有考虑作业长短等问题。3、系统的处理能力和系统的负载状态影响时间片长度。

schedule()调度函数
while (1) {
         c = -1;
         next = 0;
         i = NR_TASKS; //任务结构数组长度,在0.11版本内核中为64
         p = &task[NR_TASKS];//任务结构数组指针
         while (--i) {   
             if (!*--p)
                 continue;//跳过任务结构数组中条项为零的不做处理
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                c = (*p)->counter, next = i;   //找到进程表(数据结构表)中counter值最大的进程
        }
         if (c) break;找到则退出循环,switch到该进程
         for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)  //程序运行到这里则所有任务的时间片均用完
// 根据每个任务的优先权值,更新每一个任务的counter值
// counter 值的计算方式为counter = counter /2 + priority。
             if (*p)
                (*p)->counter = ((*p)->counter >> 1) +
                       (*p)->priority;
    }
  • 过程:找到就绪态任务中的最大counter,大于0则切换过去,否则更新所有任务的counter(右移一位,即除以2,再加上priority)。如此往复循环
  • 说明:未在就绪态的counter会一直更新,等待时间越长,counter值越大,优先级越高。

counter的作用:

  • 保证了响应时间的界
  • 经过IO后,counter会变大,优先级就会提高。照顾到了前台进程
  • 后台进程一直按照counter轮转,近似于SJF调度
  • 每个进程只用维护一个counter变量

进程同步与信号量

为什么需要信号量

用生产者和消费者的例子:

在这里插入图片描述

上例中,如果只用一个信号counter来记录的话,会造成如下问题

在这里插入图片描述

所以还需要一个信号量来记录:sem为负,表示需要消费者,为正代表需要生产者

在这里插入图片描述

信号量临界区保护

临界区:一次只允许一个进程进入的本进程的代码段。读写信号量的代码一定是临界区。

保护原则互斥进入,如果一个进程在临界区中执行,则其他进程不允许进入;有空让进,若干进程要求进入空闲临界区时,应尽快使一进程进入临界区;有限等待,从晋城发出进入请求到允许进入,不能无限等待。

Peterson算法

轮换法:一个turn值,P0进程的临界区在turn=0时才执行,执行完将其置位1,而P1则在turn=1时执行,执行完置位0。这种方法满足互斥进入要求,但是P0完成后不能再次进入,尽管P1并不在临界区,不满足有空让进原则。

标记法:通过两个布尔值,P0进程在进入临界区前,置flag0 = true, 在flag1为真时进入等待,为假时进入临界区,执行完后将flag0置位false。P1进程在进入临界区前,置flag1 = true, 在flag0为真时进入等待,为假时进入临界区,执行完后将flag1置位false。如果在P0进程执行完flag0 = true后进入P1进程执行flag1=true,再切回P0进程会进入等待,切到P1进程依然会等待,接下来将会无限等待。

Peterson算法通过标记轮转并用的方法,以两个进程为例:

在这里插入图片描述

对于多进程依然是标记和轮转的结合,可以通过面包店算法,使每个进程都获得一个序号,离开时序号为0,不为0的序号即标记。但是这种算法比较复杂,所以人们想到了下面用硬件的方法来保护临界区。

关中断法

问题:系统是如何调度各个进程的?

回答:通过中断来调度。

所以可以通过阻止在临界区执行的进程调用中断来阻断被调用。但是该方法在多核时不管用。

硬件原子指令法

对于临界区的保护说白了就是要在进入临界区前上锁,出临界区后开锁。只不过,对于锁的操作如果是多条语句的话,在各个进程之间来回切换时会导致锁变量被修改。因此有了硬件原子指令法,使操作在一条语句中完成就不会造成上述问题。

boolean TestAndSet(boolean &x) {
	boolean rv = x;
	x = true;
	return rv;
}

在进入临界区前调用 while(TestAndSet(&lock)),出临界区时lock = false。由于临界区前后对于锁的操作都是只有一条语句,不会被打断。

该方法依然是在多核时不管用。

死锁处理

死锁:互相等待对方持有的资源而造成谁都无法执行的情况。

造成死锁的四个必要条件

  • 互斥使用:资源固有特性
  • 不可抢占:资源只能自愿放弃
  • 请求和保持:进程必须占有资源再去申请
  • 循环等待:在资源分配图中存在一个环路
死锁预防

破坏死锁出现的条件

  • 在进程执行前,一次性申请所有需要的资源,不会占有资源再去申请其他资源
    • 缺点1:需要预知未来,编程困难
    • 缺点2:许多资源分配后很长时间后才使用,资源利用率低
  • 对资源类型进行排序,资源申请按照顺序进行,不会出现环路等待
    • 缺点:造成资源浪费
死锁避免

检测每个资源请求,如果造成死锁就拒绝

  • 用银行家算法。如果系统中所有进程存在一个可完成的执行序列P1,……,Pn,则称系统处于安全状态。

    以下表为例,初始状态可以用的ABC资源分别为230,所以得出安全序列为P1,P3,P2,P4,P0。

    AllocationNeed
    A B CA B C
    P00 1 07 4 3
    P13 0 20 2 0
    P23 0 26 0 0
    P32 1 10 1 0
    P40 0 24 3 1

    银行家算法的核心是:

    int Available[1..m]; //每种资源剩余数量
    int Allocation[1..n,1..m]; //已分配资源数量
    int Need[1..n,1..m]; //进程还需的各种资源数量
    int Work[1..m]; //工作向量
    bool Finish[1..n]; //进程是否结束
    while(true){
        for(i = 1; i <= n; i++) {
            if(Finish[i] == false && Need[i] <= Work) {
                Work = Work + Allocation[i];
                Finish[i] = true;
                break;
            else {
                goto end;
            }
            }
        }
    }
    End: for(i = 1; i <= n; i++)
             if(Finish[i] == false) return "deadlock";
    
    • 时间复杂度为O(mn2),资源消耗太大
死锁检测+恢复

检测到死锁出现时,让一些进程回滚,让出资源。一般是定时检测或是发现资源利用率低时再检测。

算法和银行家算法差不多,只是在最后End处有修改。

int Available[1..m]; //每种资源剩余数量
int Allocation[1..n,1..m]; //已分配资源数量
int Need[1..n,1..m]; //进程还需的各种资源数量
int Work[1..m]; //工作向量
bool Finish[1..n]; //进程是否结束
while(true){
    for(i = 1; i <= n; i++) {
        if(Finish[i] == false && Need[i] <= Work) {
            Work = Work + Allocation[i];
            Finish[i] = true;
            break;
        else {
            goto end;
        }
        }
    }
}//以上和banker算法一样
End: for(i = 1; i <= n; i++)
         if(Finish[i] == false) deadlock = deadlock + {i};

但是在选择哪些进程回滚(是选择优先级高的还是占用资源多的?)和如何实现回滚(已经修改的文件如何处理?)的问题上处理十分棘手。

综上,实际上许多通用操作系统,如PC机上安装的Windows和Linux,都采用死锁忽略方法。

死锁忽略

就像没有出现死锁一样。死锁出现不是确定的,且可以荣国重启动来处理死锁

内存使用与分段

计算机工作的开始是将程序放到内存中,PC指向开始地址。物理地址=基址+逻辑地址

逻辑地址是已知确定的,但是基址是会变化的,每个进程的都不一样,如何定位以及在何处定位就成为一个难题。最合适的应当是在运行时重定位,每个进程各自的基地址放在PCB中,执行指令时第一步先从PCB中取出基地址,然后再加上逻辑地址得出物理地址。

一整个程序分成很多段,例如代码段、数据段等等。每段分别独立储存在内存上的空闲区间,每一段的基地址放在LDT表中。

内存分区与分页

内存如何分割?分区?
  • ?固定分区:等分内存。但是因为段有大有小,需求不一样,所以此方法不可用
  • !可变分区:根据实际所需内存大小进行分割。分割方法有最先适配、最佳适配、最差适配。
可变分区造成的问题

内存不断分区后会造成内存碎片,而如果现在需要请求的内存空间大于所有的碎片的长度,导致无法使用。这时就需要将空闲分区合并,需要移动段,内存紧缩

为了解决内存分区导致的内存效率问题,引入了分页。

分页方法

针对每个段内存请求,系统以页为单位分配给段。页面尺寸为4K。

例:mov[0x2240], %eax,逻辑地址0x2240如何计算得出物理地址?首先用2240除以4K得到2,得知在页2,根据CR3页表找到页框号,假设此处页框号为3,则得出物理地址0x3240。

多级页表与快表

为了提高内存空间利用率,每一页应当小,但是页小,页表项变多,页表就会变大。比如,页面尺寸通常为4K,而地址是32位的,所以就有2的20次方个页面,需要4M内存,系统中并发10个进程就需要40M内存。

尝试

由于实际上许多逻辑地址并不会用到,就引出了一种想法,只存放用到的页。

  • 但是这样做会导致页表中的页号并不连续,在页表里根据页号查找页框号时需要比较查找,时间开销较大。

那么如何才能让页表既连续又让它占用内存少呢?

多级页表

页目录表+页表

每张表的大小:
2 10 个 目 录 项 ∗ 4 字 节 地 址 = 4 K 2^{10} 个目录项*4 字节地址 = 4K 2104=4K
总共四张表,一张一级页表,三张二级页表,所以需要空间是16K,远小于4M。

在这里插入图片描述

多级页表提高了空间效率,但是增加了访存的次数。

快表

TLB是一组相联快速存储寄存器,比较昂贵,可以存储最近使用的页号与其对应的页框号。这是硬件装置,可以通过硬件电路相联直接找到页号而不需要查找多级页表。如果命中失败,再去查找多级页表。

在这里插入图片描述

段页结合的实际内存管理

段的管理方式是程序分段再对应到内存,页的管理方式是分页后再对应到内存,想要结合这两种方式可以通过一个虚拟内存割出段,让段对应到页,页再真正地映射到物理内存。段面向用户,页面向硬件

重定位(地址翻译)

在这里插入图片描述

  1. 分配虚存,建段表

    fork()调用sys_fork调用copy_process

    //在linux/kernel/fork.c中
    int copy_process(int nr, long ebp, ...)
    {
        ...
        copy_mem(nr,p);
        ...
    }
    
    int copy_mem(int nr, task_struct *p) //*p就是pcb表
    {
        unsigned long new_data_base;
        new_data_base = nr*0x4000000; //64M*nr
        set_base(p->ldt[1],new_data_base);
        set_base(p->ldt[2],new_data_base);//每个进程占64M的虚拟地址空间,互不重叠
    }
    
  2. 分配内存,建页表

    //接上面的代码
    int copy_mem(int nr, task_struct *p)
    {
        unsigned long old_data_base;
        old_data_base = get_base(current->ldt[2]);
        copy_page_tables(old_data_base,new_data_base,data_limit);
    }
    //这一段没听懂
    

    为了父子进程物理内存公用,又不会相互影响,在子进程copy完父进程的内存后,写在与父进程不同的页上。

内存换入换出

虚拟内存就是将用到的资源复制进内存中处理,没用的不分配内存,这样就会减少内存的占用情况,给用户内存很大的错觉。

内存换入
  1. 根据逻辑地址算出虚拟地址:页号+偏移。
  2. 如果查找页表没有该映射,则需要调页,出现缺页中断(14号中断:Page fault,页不在内存)。找到内存中的空闲页,建立映射。
  3. 根据页表中的映射找到实际的物理内存地址。
内存换出

内存是有限的,必须要选择一些页淘汰,换出磁盘。这样才能有空闲的内存空间分配给新的要用的进程。

  • FIFO页面置换

    举个例子:

    在这里插入图片描述

    换页过于频繁,相比较之下C的使用比较少,可以考虑将C换出,这就引出了MIN算法

  • MIN页面置换:选择最远将使用的页淘汰

    同样用上面的例子,模拟后发现只导致了5次缺页。但是这种方法需要预知未来,这并不合理。

  • LRU页面置换:选最近最长一段时间没有使用或最少使用的页淘汰

    同样用上面的例子,发现也只有五次缺页。

    • 用时间戳:每页维护一个时间戳,选择最小时间戳的页淘汰。但是该算法会导致,每次访问地址都要修改一次时间戳,还有进行比较最小值,实现代价太大。
    • 用页码栈:选择栈底页淘汰。每次访问都要修改栈,实现代价依然很大

    所以LRU算法准确实现的代价太大。

  • LRU近似实现:将时间计数变为是和否。

    SCR ( Second Chance Replament ):每个页加一个引用位,每次访问一页,硬件自动设置该位为1。在选择淘汰页时,挨个扫描,是1时清0,是0时淘汰该页。

    在这里插入图片描述

    上述方法也有弊端,在缺页很少的时候,所有的R=1,循环队列扫描时会旋转一圈后,将调入页插入循环一圈前的起点,这时就会退化成FIFO方法。

    方法改造:定时清除R位。这时还需要一个移动速度快的扫描指针。

  • 给每个进程分配多少页框(帧frame)?

    分配的太多,请求调页导致的内存高效利用就失去了作用。分配的太少会导致颠簸(磁盘内不停地换入换出,所以CPU只能多次等待,利用率降低)现象,在进程数达到最大值后,CPU的利用率会突然急剧下降。所以需要找一个折中页框值。

在这里插入图片描述

I/O

终端设备包括显示器和键盘(其实还有鼠标等等其他的)。

显示器

让外设工作的原理:CPU给外设中(显卡、磁盘)相应的寄存器或存储发一个指令,相应的控制器操作电路设备显示在屏幕上。然后控制器会向CPU发中断信号表示已经完成,CPU再处理中断。

向设备控制器的寄存器写的过程,需要查寄存器地址、内容的格式和语义…所以操作系统要给用户提供一个文件视图

在这里插入图片描述

老师在这一段讲了很多代码,但是感觉不太基础也很琐碎就不列出来了。

在这里插入图片描述

键盘

对于使用者:敲键盘、看结果。

对于操作系统:等待敲键盘,一敲键盘就会中断(21号中断),根据扫描码得到ASCII码,放到read_q队列中,经过转译处理放入secondary中。

在这里插入图片描述

磁盘管理驱动

CPU向磁盘控制器中的寄存器读写数据,盘控制器完成工作后向CPU发送中断信号。

生磁盘的使用

磁盘访问单位是扇区,每个扇区大小为512字节。磁盘的结构示意图如下:

在这里插入图片描述

读/写磁盘上1个字节的过程:控制器->寻道->旋转->传输。

  1. 将磁头移动到指定的磁道上,磁道开始旋转。
  2. 旋转到相应扇区位置,磁信号变成电信号,将电信号读到内存缓冲区。
  3. 写或修改该字节后,通过电生磁,将修改的一个字节写到磁盘中。
最直接的磁盘使用法

要确定具体的读写位置需要往控制器中写入柱面、磁头、扇区、缓存位置。

在这里插入图片描述

但是这种方法需要知道的信息太多了。

通过盘块号读写磁盘(一层抽象)

磁盘驱动负责从block计算出cyl©, head(H), sec(S)。扇区编号连续顺序排放。

盘块号 block = C * (Heads * Sectors) + H * Sectors + S

由上式知道扇区 S = block % Sectors

示意图:在这里插入图片描述

多个进程通过队列使用磁盘(第二层抽象)

在这里插入图片描述

磁盘调度目标是平均访问延迟小。具体使用的磁盘调度算法应该是什么?

  • FCFS(先来先服务)

    弊端:可能磁头原位置在60,移动到了一个指定位置160后又需要在下一个处理进程移动回61,这样会浪费掉很多时间。考虑是否可以先处理邻近的扇区。

  • SSTF(Shrtest-seek-time First)

    弊端:一般会对磁盘中间磁道的访问比较频繁,这种算法会导致磁头反复在磁道中间部分滑动,两端的请求很有可能会被一直滞后处理,导致饥饿问题。

  • SCAN

    SSTF+中途不回折:一路向磁道的端移动,一边移动一边处理相应位置的请求

    在这里插入图片描述

  • C-SCAN(电梯算法)

    SCAN+直接移到另一端:电梯也是如此,如果有下楼请求,先移动到最高楼层接乘客,再一楼一楼下降接乘客。

    在这里插入图片描述

生磁盘的使用整理
  1. 进程得到盘块号block(根据文件得到),根据上面的公式算出扇区号S
  2. 用扇区号和需要用到的内存缓冲区地址作出磁盘请求make req,用电梯算法add_request放入请求队列
  3. 进程sleep_on (接下去的动作都由硬件完成)
  4. 磁盘中断处理do_hd_request,算出cyl, head, sector
  5. hd_out调用outp完成端口写
  6. end_request(1)唤醒进程
从生磁盘到文件

用户在实际使用中不会使用盘块号,所以没有办法用生磁盘。所以需要在盘快上引入更高一层次的抽象概念,将生磁盘变为熟磁盘,即文件,这样使用起来更为直观使用更方便。

过程:根据文件形成映射表FCB,按顺序将字符存放到磁盘上,根据映射表得到磁盘上的位置即可取得字符。

上面按顺序存放字符流的方式适合于顺序读写,但是不适合于动态修改的文件。所以文件的映射还有不同的方法。例如,链式结构,存取慢,但是动态修改快;索引结构,读写和动态修改都比较快。

实际系统中用的是多级索引,这样既可以表示很大的文件,又可以保证读写修改的速度。

在这里插入图片描述

代码实现

在fs/read_write.c中调用了file_write函数。

file_write(inode, file, buf, count)的工作过程:

  1. file参数中的读写指针是开始地址,count是指明字符数量。确定了是哪一段字符。
  2. 通过调用create_block函数根据inode找到要写的盘块号。
  3. 用盘块号、buf等形成请求根据电梯算法放入队列。
目录与文件系统
  • 树状目录

    在这里插入图片描述

  • 磁盘中除了上面图中的FCB数组和数据盘块合集还需要存放

    • inode节点位图:哪些节点空闲,哪些被占用

    • 盘块位图:显示哪些空闲,不同硬盘大小这个位图的大小也不同

      例如:0011110011101表示磁盘块2、3、4、5、8、9、10、12空闲

    • 超级块:记录节点位图和盘块位图和超级块的大小,这样就知道在哪个区间查找了。

    • 引导块:其长度区间是固定的

    在这里插入图片描述

目录解析代码实现
//在linux/fs/open.c中
int sys_open(const char* filname, int flag)
{
    i = open_namei(filename, flag, &inode);
    ....
}
int open_namei(...)
{
    dir = dir_namei(pathname, &namelen, &basename);
    ...
}
static struct m_inode *dir_namei()
{
    dir = get_dir(pathname);
    ...
}

get_dir完成真正的目录解析:

static struct m_inode *get_dir(const char *pathname)
{
    if((c = get_fs_byte(pathname)) == '/') {//如果文件路径以/开头
        inode = current->root;//代表以当前目录为根
        pathname++;
    }else if(c) {
        inode = current->pwd;
    }
    while(1) {
        if(!c) return inode; //函数出口
        bh = find_entry(&inode, thisname, namelen, &de);//从目录中读取目录项
        int inr = de->inode;//inr是目录项中的索引节点号
        int idev = inode->i_dev;
        inode = iget(idev,inr);//读下一层目录
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值