操作系统(哈工大李治军老师)课程笔记

操作系统(哈工大李治军老师)32讲

The mind is not a vessel that needs filling, but wood that needs igniting!

文章目录

L 1 什么是操作系统

是计算机硬件和应用之间的一层软件

  • 方便我们使用硬件,如使用显存…
  • 高效的使用硬件,如开多个终端(窗口)
    • 管理哪些硬件
      • CPU管理 内存管理 终端管理 磁盘管理 文件管理 ——五基本功能(本课程重点)
      • 网络管理 电源管理 多核管理

学习操作系统可以有很多层次

  • 从应用软件出发**“探到操作系统”**
    • 集中在使用计算机的接口上
    • 使用显示器:printf;使用CPU:fork,使用文件:open、read…

image-20210810143828758

  • 从应用软件出发**“进入操作系统” (本课程目标)**
    • 一段文字是如何写到磁盘上的…

image-20210810144153520

  • 从硬件出发**“设计并实现操作系统”**
    • 给你一个板子,配一个操作系统…

image-20210810144135154

本课程目标

进入操作系统

  • 能理解真实操作系统的运转!
  • printf(hello")到底怎么回事
  • 能在真实的基本操作系统上动手实践!(能自己动手才是真正学会了…)

为什么要这样干?

  • 学生:我们要成为掌握计算机关键技术的工程师

套用Stanford操作系统课程中的一句话: “Learn OS concepts by coding them!”

L4. 操作系统接口

接口(Interface)
仍然从常识开始… 日常生活中有很多接口:电源插座;汽车油门…

那什么是接口?
连接两个东西、信号转换、屏蔽细节…

什么是操作系统接口?

连接上层用户和操作系统软件

image-20210810184911274

问题:操作系统直接面对用户吗? 即用户是怎么用操作系统的?..

用户如何使用计算机? 命令行 图形按钮 应用程序

命令行是怎么回事?

image-20210810185514927

image-20210810185456287

图形按钮又是怎么回事?

image-20210810185923101

硬件驱动 + 消息机制 + 绘图机制 = 图形按钮

再回到那个问题

用户使用计算机:通过程序(应用软件)

  • 命令行: 命令程序
  • 图形界面:消息框架程序+消息处理程序

操作系统接口:连接谁? 连接操作系统和应用软件;如何连接? C语言程序

  • 普通C代码加上一 些重要的函数

  • 操作系统供这样的重要函数
    这就是操作系统接口了: 接口表现为函数调 用,又由系统供,所以称为系统调用 (system call)

一个概念来回答问题

系统调用! 就好像电源插头一样…

  • 先从认识“插头”开始,这是操作系统的常识
  • POSIX: Portable Operating System Interface of Unix(IEEE制定的一个标准族)
image-20210810191020210

L5. 系统调用的实现

系统调用的直观实现

问题+直观想法…

实现一个whoami系统调用

  • 用户程序调用whoami, 一个字符串“lizhijun”放在操作系统中(系统引导 时载入),取出来打印

为什么不能直接访问该内存,要去操作系统中取?

100: “lizhijun” 都在内存中,这内存不都是我买的吗…

  • 不能随意的调用数据, 不能随意的jmp。
  • 可以看到root密码, 可以修改它…
  • 可以通过显存看到别 人word里的内容…

image-20210810214614866

内核(用户)态, 内核(用户)段

将内核程序和用户程序隔离!!!

用户态执行在用户段下
内核态执行在内核段下

特权级:数字越大,级别越低

image-20210810215217158

硬件提供了“主动进入内核的方法”

  • 对于Intel x86,那就是中断指令
    • int int指令将使CS中的CPL改成0, “进入内核”
    • 这是用户程序发起的调用内核代码的唯一方式 (此时,CPL=3而 DPL=0)
    • 系统调用的核心:
      • (1) 用户程序中包含一段包含int指令的代码
      • (2) 操作系统写中断处理,获取想调程序的编号
      • (3) 操作系统根据编号执行相应代码

系统调用的实现

int 0x80

prinf 使用了 write
write 系统调用中 包含int指令的代码

image-20210810223559132

Linux系统调用的实现细节!

将关于write的故事完整的讲完…

其系统调用从 #define _syscall3(_syscall3(int, write, int, fd, const char *buf, off_t, count)) type 继续说起

image-20210810224819481

显然,__NR_write是系统调用号,放在eax中

在linux/include/unistd.h中
#define __NR_write 4 //一堆连续正整数(数组下标, 函数表索引)

同时eax也存放返回值,ebx,ecx,edx存放3个参数

int 0x80中断的处理

image-20210810225424118

中断处理程序: system_call

_sys_call_table

image-20210810225743239

image-20210810230006254

L7. 我们的任务

我们要学什么? 再具体一些…

image-20210810234156511

L8. CPU管理的直观想法

使用CPU

(计算机组成原理)

CPU工作原理

  • CPU上电以后发生了什么? 自动的取指—执行

image-20210811164101321

管理CPU的最直观方法 设好PC初值就完事

看看这样做有没有问题? 提出问题

int main(int argc, char* argv[])
{
	int i , to, *fp, sum = 0;
	to = atoi(argv[1]);
	for(i=1; i<=to; i++)
	{
		sum = sum + i;
		fprintf(fp,%d”, sum);
	}
}

fprintf 和 普通语句 运行时间比5.7 * 10^5 : 1 等fprintf —— 浪费时间

怎么解决?

image-20210811165625195

多道程序、交替执行,好东西啊!

提高了CPU和外设的利用率

image-20210811165753371

一个CPU面对多个程序

CPU实际工作的样子

image-20210811165920916

一个CPU上交替的执行多个程序:并发

不止修改寄存器PC

运行的程序和静态 程序不一样了

要记录返回地址,要记录ax

每个程序有了一个存放信息的结构: PCB

引入“进程”概念

运行的程序和静态程序不一样!

  • 需要描述这些不一样… (这些不一样就成了 进程概念的外延)
  • 程序 + 所有这些不一样 -> 一个概念 (所有的不一样都表 现在PCB中…)

进程是进行(执行)中的程序

  • 进程有开始、有结束,程序没有
  • 进程会走走停停,走停对程序无意义
  • 进程需要记录ax,bx,…,程序不用

L9. 多进程图像

多个进程使用CPU的图像

  • 如何使用CPU呢? 让程序执行起来
  • 如何充分利用CPU呢? 启动多个程序,交替执行
  • 启动了的程序就是进程,所以是多个进程推进
    • 操作系统只需要把这些进程记录好、要按照合理的次序推进(分配资源、进行调度)
    • 这就是多进程图像…

进程控制块(PCB Process Control Block)

多进程图像从启动开始到关机结束

  • main中的fork()创建了第1个进程
    • init执行了shell(Windows桌面)
  • shell再启动其他进程
int main(int argc, char * argv[]) { 
	while(1) { 
		scanf(%s”, cmd);
		if(!fork()) {
            exec(cmd);
        } 
        wait(); 
    } 
}

一命令启动一个进 程,返回shell再启 动其他进程…

image-20210811191047786

多进程的组织:PCB+状态+队列

有一个进程在执行
有一些进程等待执行 (就绪队列)
有一些进程再等待某事件 (磁盘等待队列)

  • 运行 -> 等待; 运行->就绪; 就绪->运行

image-20210811192153328

  • 该图称为进程状态图
  • 它能给出进程生存期的清晰描述
  • 它是认识操作系统进程管理的一个窗口

交替的三个部分:队列操作+调度+切换

就是进程调度,一个很深刻的话题

  • FIFO?
    FIFO显然是公平的策略
    FIFO显然没有考虑进程执行的任务的区别
  • Priority?
    优先级该怎么设定?可能会使某些进程饥饿
switch_to(pCur,pNew) {
pCur.ax = CPU.ax;
pCur.bx = CPU.bx;
...
pCur.cs = CPU.cs;
pCur.retpc = CPU.pc; //old

CPU.ax = pNew.ax;  //new
CPU.bx = pNew.bx;
...
CPU.cs = pNew.cs;
CPU.retpc = pNew.pc; }

多进程如何影响

多个进程同时在存在于内存会出现下面的问题

image-20210812000335138

解决的办法:限制对地址100的读写
多进程的地址空间分离:内存管理的主要内容

进程带动内存的使用

映射表的使用

image-20210812000659962

多进程图像:多进程如何合作

想一想打印工作过程

  • 应用程序提交打印任务
  • 打印任务被放进打印队列
  • 打印进程从队列中取出任务
  • 打印进程控制打印机打印

从纸上到实际:生产者-消费者实例

image-20210812001207918

两个合作的进程都要修改counter

一个可能的执行序列

P.register = counter;
P.register = P.register + 1;
C.register = counter;
C.register = C.register - 1;
counter = P.register;
counter = C.register;

核心在于进程同步(合理的推进顺序)

写counter时阻断其 他进程访问counter

image-20210812001554670

总结:如何形成多进程图像?

读写PCB,OS中最重要的结构,贯穿始终
要操作寄存器完成切换(L10,L11,L12)
要写调度程序(L13,L14)
要有进程同步与合作(L16,L17)
要有地址映射(L20)

L10 用户级线程

多进程是操作系统的基本图像

image-20210812072627653

是否可以资源不动而切换指令序列?

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

    • 将资源和指令执行分开
    • 一个资源 + 多个指令执行序列
  • 线程: 保留了并发的优点, 避免了进程切换代价

  • 实质就是映射表不变而PC指针变

切换线程——指令切换(PC指针切换),资源不切换(不更换映射表)

多个执行序列+一个地址空间是否实用

一个网页浏览器

  • 一个线程用来从服务器接收数据
  • 一个线程用来处理图片(如解压缩)
  • 一个线程用来显示文本
  • 一个线程用来显示图片

这些线程要共享资源吗?

  • 接收数据放在100处,显示时要读…
  • 所有的文本、图片都显示在一个屏幕上

实现这个浏览器

void WebExplorer() { 
	char URL[] = “http://cms.hit.edu.cn”;
	char buffer[1000];
	pthread_create(..., GetData, URL, buffer); //GetData
	pthread_create(..., Show, buffer); //Show
}
void GetData(char *URL, char *p){...};
void Show(char *p){...};

image-20210812074017791

Create? Yield?

  • 核心是Yield…
  • 能切换了就知道切换时需要是个什么样子
  • Create就是要制造出第一次切换时应该的样子

仔细看Yield,就是100跳到300

image-20210812074244229

两个执行序列与一个栈

跳转的地址混乱,程序出现问题

image-20210812074629082

从一个栈到两个栈

TCB 和 栈 相互配合

image-20210812074906089

204是调用Yield() 才压栈的

jmp 204; 应该去掉,Yieid 执行完的下条指令 ,弹栈204执行jmp 204

两个线程的样子:两个TCB、两个栈、切换的PC在栈中

ThreadCreate的核心就是用程序做出这三样东西

image-20210812075345987

void ThreadCreate(A)
{
	TCB *tcb=malloc();
	*stack=malloc();
	*stack = A;//100
	tcb.esp=stack;
}

将所有的东西组合在一起……

image-20210812075613991

image-20210812075714530

用户级线程和核心级线程

  • 为什么说是用户级线程——Yield是用户程序

    如果用户级进程的某个线程进入内核并阻塞,则 容易造成卡死

    image-20210812080524508

  • ThreadCreate是系统调用,会进入内核,内核知道TCB (不用跳转进程,并发性更好)

    image-20210812080359065

L11 内核级线程

开始核心级线程

image-20210812080359065

多处理器和多核

和用户级相比,核心级线程有什么不同?

  • ThreadCreate是系统调用,内核管理TCB,内核负 责切换线程
  • 如何让切换成型? - 内核栈,TCB
    • 用户栈是否还要用? 执行的代码仍然在用户态,还要进行函数调用
    • **一个栈(用户级) 到一套栈(核心级) ;两个栈(用户级) 到两套栈(核心级) **
    • TCB关联内核栈,那用户栈怎么办? TCB关联一套栈

一套栈

image-20210812131640750

用户栈和内核栈之间的关联

image-20210812132148270

  • 所有中断(时钟、外设、INT 指令)都引起上述切换用户栈

  • 中断(硬件)又一次帮助了操作 系统…

内核线程switch_to的五段论

image-20210812133024181

image-20210812133037620

用户级线程、核心级线程的对比

image-20210812133442884

L12 内核级线程实现

核心级线程的两套栈,核心是内核栈…

五段论

image-20210812135615300

整个故事要从进入内核开始——某个中断开始

fork是系统调用,会引起中断…

image-20210812140100141

切换五段论中的switch_to

Linux 0.11用tss 切换,但也可以 用栈切换,因为 tss中的信息可以 写到内核栈中

L13 操作系统的那棵“树”

The mind is not a vessel that needs filling, but wood that needs igniting!

运转CPU(种子)

image-20210812145412275

CPU没有好好运转

image-20210812145433712

得让CPU好好运转

image-20210812145454155

从A跳到B我们并不陌生

image-20210812145511824

一个栈 一个进程里函数之间跳

一个栈+Yield造成的混乱

image-20210812145541124

面对这样的栈你怎么可能从B顺利的回到A?

对于两个进程来说,一个栈不够用(如图的运行顺序 会造成混乱)

两个栈+两个用户TCB

image-20210812145635347

Yield()找到下一个TCB->找到新的栈->切到新的栈

一直在用户态那怎么行?

image-20210812150147455

引入内核栈的切换

image-20210812150511301

到实现idea的时候了(Linux 0.01)

从一个简单、清晰、明确的目标开始 交替的打出A和B (Linux 0.01)

从用户代码开始

main()
{
	if(!fork()){while(1)printf(“A”);}
	if(!fork()){while(1)printf(“B”);}
	wait();
}

程序是什么?就是人的思维的C表达

main(){
	mov __NR_fork, %eax
	int 0x80        //INT进入内核
100: mov %eax, res
	cmpl res,0
	jne 208
200: printf(“A”)
	jmp 200
208: ...
304: wait()
}

INT进入内核

开始sys_fork

开始copy_process

开始返回…

main继续执行,现在我们有了什么?

main继续,到了哪里?

schedule

switch_to切换

接下来会怎么样?

我们的目标是什么?

时钟中断

有那么一次时钟中断

schedule+switch_to

接下来会怎么样?

我们的目标达到了吗?

又有那么一次时钟中断, 再一次schedule+switch_to

接下来会怎么样?

L14 CPU调度策略

获得next

这两个进程,应该选择哪一个?

FIFO?
谁先进入,先调度谁: 简单有效
一个只简单询问业务的人该怎么办?

Priority?
任务短可以适当优先 (但你怎么知道这个任务将 来会执行多长时间呢?)
这人的询问越来越长怎么办?
那如果一个银行业务很长是因为客户需要填写一 个很长的表,该怎么办?

面对诸多场景,如何设计调度算法?

  • 我们的算法应该让什么更好?
    • 面对客户:银行调度算法的设计目标应该是用户满意
    • 面对进程: CPU调度的目标应该是进程满意
  • 那怎么才能让进程满意呢?时间…
    • 尽快结束任务:周转时间(从任务进入到任务结束)短
    • 用户操作尽快响应:响应时间(从操作发生到响应)短
    • 系统内耗时间少:吞吐量(完成的任务量)
  • 总原则:系统专注于任务执行,又能合理调配任务…

如何做到合理? 需要折中,需要综合…

  • 吞吐量和响应时间之间有矛盾…
    • 响应时间小->切换次数多->系统内耗大->吞吐量小
  • 前台任务和后台任务的关注点不同…
    • 前台任务关注响应时间,后台任务关注周转时间
  • IO约束型任务和CPU约束型任务有各自的特点

image-20210812172327499

I/O多(IO约束型任务) 的CPU长度较短的多 没I/O(CPU约束型) 的CPU长度较短

折中和综合让操作系统变得复杂, 但有效的系统又要求尽量简单

各种CPU调度算法

响应时间 周转时间

  1. First Come, First Served (FCFS)

  2. 如何缩短周转时间? SJF: 短作业优先 (小顶堆)

  3. 响应时间该怎么办?

    image-20210812174221914

    P2(用户)的等待时间非常长,一直没响应 (一点反应没有)

    RR: 按时间片来轮转调度

    时间片大: 响应时间太长;时间片小: 吞吐量小
    折衷: 时间片10-100ms,切换时间0.1-1ms(1%)

  4. 响应时间和周转时间同时存在,怎么办?
    Word很关心响应时间,而gcc更关心周转时间,两类任务同时存在怎么办?

    直观想法: 定义前台任务和后台任务两队列,前台RR,后台SJF,只有前台任务没有时才调度后台任务

如果一直有前台任务

  • 后台任务可能一直得不到运行
  • 后台任务优先级动态升高,但后台任务(用SJF调度)一旦执行,前台的响应时间…
  • 前后台任务都用时间片,但又退化为了RR,后台任务的SJF如何体现?前台任务如何照顾?

还有很多问题…

  • 我们怎么知道哪些是前台任务,哪些是后台任务,fork时告诉我们吗?
  • gcc就一点不需要交互吗?Ctrl+C按键怎么工作? word就不会执行一段批处理吗?Ctrl+F按键?
  • SJF中的短作业优先如何体现?如何判断作业的长度?这是未来的信息…

L15 一个实际的schedule函数

// linux-0.11/kernel/sched.c
void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) {
			if ((*p)->alarm && (*p)->alarm < jiffies) {
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				}
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
			(*p)->state==TASK_INTERRUPTIBLE)
				(*p)->state=TASK_RUNNING;
		}

Linux 0.11的调度函数schedule()

/* this is the scheduler proper: */

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
			{
				(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
			}
	}
	switch_to(next);
}

counter的作用: 时间片

image-20210812180725315

counter是典型的时间片,所以是轮转调度,保证了响应

counter的另一个作用: 优先级

  • while(--i){ if((*p->state == TASK_RUNNING&&(*p)->counter>c) c=(*p)->counter, next=i; }

    找counter最大的任务调度,counter表示了优先级

  • for(p=&LAST_TASK;p>&FIRST_TASK;--p)
    (*p)->counter=((*p)->counter>>1)+(*p)->priority; //(*p)->priority 初值

    counter代表的优先级可以动态调整

    阻塞的进程再就绪以后优先级高于非阻塞进程,为什么?
    因为 会重复进入while循环 加上初值

    进程为什么会阻塞? I/O,正是前台进程的特征

counter作用的整理

  • counter保证了响应时间的界 c(t) = c(t-1)/2 + p c(0) = p c(∞) = ? < 2P
  • 经过IO以后,counter就会变大;IO时间越长,counter越大(为什么? 被阻塞的多了),照顾了 IO进程,变相的照顾了前台进程
  • 后台进程一直按照counter轮转近似了SJF调度 (短作业的先轮转完)
  • 每个进程只用维护一个counter变量,简单、高效

CPU调度: 一个简单的算法折中了 大多数任务的需求,这就是实际工作的schedule函数

L16 进程同步与信号量

进程合作:多进程共同完成一个任务

实例

司机 while (true){ 启动车辆; 正常运行; 到站停车;}
售票员 while (true){ 关门; 售票; 开门;} 实

等待信号

image-20210813133231337

从纸上到实际:生产者-消费者实例

找到哪些地方要停,什么时候再走?

  • 需要让“进程走走停停”来保证多进程合作的合理有序 这就是进程同步

共享数据

#define BUFFER_SIZE 10
typedef struct { . . . } item;
item buffer[BUFFER_SIZE];
int in = out = counter = 0;

生产者进程

while (true) {
	while(counter== BUFFER_SIZE); //缓存区满,生产者要停; 
	buffer[in] = item;
	in = (in + 1) % BUFFER_SIZE;
	counter++; //生产	发信号让消费者再走…
}

消费者进程

while (true) {
	while(counter== 0); //缓存区空,消费者要停;
	item = buffer[out];
	out = (out + 1) % BUFFER_SIZE;
	counter--; //消费	发信号让生产者再走…
}

只发信号还不能解决全部问题

image-20210813134501885

(1) 缓冲区满以后生产者P1生产一个item放入,会sleep 信号
(2) 又一个生产者P2生产一个item放入,会sleep
(3) 消费者C执行1次循环,counter==BUFFER_SIZE-1, 发信号给P1,P1 wakeup
(4) 消费者C再执行1次循环,counter==BUFFER_SIZE-2,P2不能被唤醒

从信号到信号量

  • 不只是等待信号、发信号? 对应睡眠和唤醒!

  • 还应该能记录一些信息

    • 能记录有“2个进程等待”就可以了…
    • (1) 缓冲区满,P1执行,P1 sleep,记录下1个进程等待
    • (2) P2执行, P2 sleep,记录下2个进程等待
    • (3) C执行1次循环,发现2个进程等待,wakeup 1个
    • (4) C再执行1次循环,发现?个进程等待,再?
    • (5) C再执行1次循环,怎么办? 此时再来P3怎么办?
  • 什么是信号量?

    • 记录一些信息(量),并根据这个信息决定睡眠还是唤醒(信号)。

信号量开始工作

image-20210813135502639

正的——供小于求 负的——供大于求

什么是信号量? 信号量的定义…

  • 信号量: 1965年,由荷兰学者Dijkstra提出的一种特殊整型变量,量用 来记录,信号用来sleep和wakeup
struct semaphore
{
	int value; //记录资源个数
	PCB *queue;
	//记录等待在该信号量上的进程
}
P(semaphore s); //要消费资源	test一下
V(semaphore s); //产生资源

P(semaphore s)
{
	s.value--; //申请资源
	if(s.value < 0) { //没有
		sleep(s.queue);  //阻塞
    }
}

V(semaphore s) //消费者调用
{
    s.value++;
    if(s.value <= 0) { // p中睡眠的 s
        wakeup(s.queue); //唤醒
    }
}

P的名称来源于荷兰语的proberen,即test
V的名称也来源于荷兰语verhogen(increment) 增加

用信号量解生产者-消费者问题

用文件定义共享缓冲区

int fd = open(“buffer.txt”);
write(fd, 0, sizeof(int)); //写in
write(fd, 0, sizeof(int)); //写out 

信号量的定义和初始化

semaphore full = 0;
semaphore empty = BUFFER_SIZE;
semaphore mutex = 1;
Producer(item) {
	P(empty);//判断有无要求
	P(mutex);
	//读入in;将item写入到in的位置上;
	V(mutex);
	V(full); //资源 + 1
}

Consumer() {
	P(full); //判断有无资源
	P(mutex);
	//读入out;从文件中的out位置读出到item;打印item;
	V(mutex);
	V(empty); //要求 + 1
}

L17 信号量临界区保护

温故而知新:什么是信号量? 通过对这个量的访问和修改,让大家有序推进。
哪里还有问题吗?

共同修改信号量引出的问题

竞争条件(Race Condition)

  • 竞争条件: 和调度有关的共享数据语义错误

错误由多个进程并发操作共享数据引起
错误和调度顺序有关,难于发现和调试

解决竞争条件的直观想法

  • 在写共享变量empty 时阻止其他进程也访 问empty

原子操作

image-20210813152049264

临界区(Critical Section)

  • 临界区: 一次只允许一个进程进入的该进程的那几段代码中的一段

一个非常重要的工作: 找出进程 中的临界区代码

读写信号量的代码一 定是临界区…

临界区代码的保护原则

  • 基本原则:

    1. 互斥进入: 如果一个进程在临界区中执行,则其他进程 不允许进入
    • 这些进程间的约束关系称为互斥(mutual exclusion)
    • 这保证了是临界区
  • 好的临界区保护原则

    1. 有空让进: 若干进程要求进入空闲临界区时, 应尽快使一进程进入临界区

    2. 有限等待: 从进程发出进入请求到允许进入, 不能无限等待

进入临界区的一个尝试 - 轮换法

image-20210813154650975

满足互斥进入要求

问题: P0完成后不能接着再次进入,尽管进程P1不在临界区…(不满足有空让进,P1不执行完P0不执行)

进入临界区的又一个尝试 - 标记法

买牛奶

image-20210813155034058

  • 上面的轮换法类似于什么? 值日

  • 更好的方法应该是立即去买,留一个便条

image-20210813155143461

标记法能否解决问题? 无限等待

image-20210813155324500

进入临界区的再一次尝试 - 非对称标记

  • 带名字的便条 + 让一个人更加勤劳

丈 夫 是 那 个 更 勤 劳 的 人

image-20210813155702285

  • 关键: 选择一个进程进入,另一个进程循环等待

进入临界区Peterson算法

结合了标记和轮转两种思想

image-20210813210014657

  • 满足互斥进入: 如果两个进程都进入,则 flag[0]=flag[1]=true, turn==0==1,矛盾!
  • 满足有空让进: 如果进程P1不在临界区,则 flag[1]=false,或者turn=0, 都P0能进入!
  • 满足有限等待: P0要求进入,flag[0]=true;后面的P1不可能一直进入,因为P1执行一次就会让turn=0。

多个进程怎么办? - 面包店算法 (软件)

仍然是标记和轮转的结合

  • 如何轮转: 每个进程都获得一个序号, 序号最小的进入
  • 如何标记: 进程离开时序号为0,不为0的 序号即标记
  • 面包店: 每个进入商店的客户都获得一个号码,号码最小的先得到服务;号码相同时,名字靠前的先服务。

image-20210813210430760

  • 互斥进入: Pi 在临界区内,Pk试图进入,一定有(num[i], i)<(num[k],k),Pk循环等待。

  • 有空让进: 如果没有进程在临界区中,最小序号的 进程一定能够进入。

  • 有限等待: 离开临界区的进程再次进入一定排在最 后(FIFO),所以任一个想进入进程至多等n个进

一个显然的感觉是: 太复杂了,有 没有简单一些的…

临界区保护的另一类解法…

  • 再想一下临界区:**只允许一个进程进入,**进入另一个进程意味着什么?

    • 被调度: 另一个进程只有被调度才能执行,才可能进入临界区,如何阻止调度?

      阻止中断 (中断寄存器)
      只适合单CPU

什么时候不好使? 多CPU(多核)

临界区保护的硬件原子指令法

硬件对 mutex进行保护

方法:把修改mutex的方法 做成 一条指令 —— 原子指令 瞬间锁上

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

while(TestAndSet(& lock)) ; //临界区

lock = false; //剩余区

L18 信号量的代码实现

可以操刀了—从纸上到实际

if 选择最前面的

image-20210813222208237

while + schdule 选择优先级最高的

从Linux 0.11那里学点东西…

image-20210813222241652

L19 死锁处理

  • 我们将这种多个进程由于互相等待对方持有的 资源而造成的谁都无法执行的情况叫死锁

死锁的成因

  • 资源互斥使用,一旦占有别人无法使用

  • 进程占有了一 些资源,又不释放,再去申请其他资源

  • 各自占有的资源和互相申请的资源形成了环路等待

image-20210813233133996

死锁的4个必要条件

  • 互斥使用(Mutual exclusion) 资源的固有特性,如道口
  • 不可抢占(No preemption) 资源只能自愿放弃,如车开走以后
  • 请求和保持(Hold and wait) 进程必须占有资源,再去申请
  • 循环等待(Circular wait) 在资源分配图中存在一个环路

死锁处理方法概述

  • 死锁预防 “no smoking”,预防火灾
    • 破坏死锁出现的条件
  • 死锁避免 检测到煤气超标时,自动切断电源
    • 检测每个资源请求,如果造成死锁就拒绝
  • 死锁检测+恢复 发现火灾时,立刻拿起灭火器
    • 检测到死锁出现时,让一些进程回滚,让出资源
  • 死锁忽略 在太阳上可以对火灾全然不顾
    • 就好像没有出现死锁一样

死锁预防的方法例子

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

问题:为什么使用这两种方法,一定不会死锁?

死锁避免: 判断此次请求是否引起死锁?

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

安全序列: 上面的执行序列P1,…Pn 如何找?

找安全序列的银行家算法(Dijkstra提出)

int Available[1..m]; //每种资源剩余数量
int Allocation[1..n,1..m]; //已分配资源数量
int Need[1..n,1..m];//进程还需的各种资源数量
int Work[1..m]; //工作向量
bool Finish [1..n]; //进程是否结束

image-20210813235617104

O(M * N^2) 成本太高

死锁避免之银行家算法实例

请求出现时: 首先假装分配,然后调用银行家算法(测试是否会造成死锁)

image-20210813235902783

死锁检测+恢复: 发现问题再处理

  • 基本原因: 每次申请都执行O(mn2),效率低。发现问题再处理

    • 定时检测或者是发现资源利用率低时检测

      image-20210813235947518

    • 选择哪些进程回滚? 优先级? 占用资源多的? …

    • 如何实现回滚? 那些已经修改的文件怎么办

死锁忽略的引出

  • 死锁出现不是确定的,又可以用 重启动来处理死锁

  • 有趣的是大多数非专门的操作系统都用它,如 UNIX,Linux,Windows

L20 内存使用与分段

如何让内存用起来?

仍然从计算机如何工作开始…

  • 内存使用:将程序放到内存中,PC指向开始地址

首先程序进入内存

重定位: 修改程序中的地址(是相对地址)

40 —— 逻辑地址(相对地址)

image-20210814073715619

是么时候完成重定位? 编译时 载入时

  • 编译时重定位的程序只能放在内存固定位置
  • 载入时重定位的程序一旦载入内存就不能动了

程序载入后还需要移动…

  • 一个重要概念: 交换(swap)

image-20210814074533011

重定位最合适的时机 - 运行时重定位

  • 在运行每条指令时才完成重定位

image-20210814074756607

  • 每个进程有各自的基地址,放在哪里?

    • PCB
    • 执行指令时第一步先从PCB中取出这个基地址
  • 整理一下思路…

image-20210814152827789

引入分段: 是将整个程序一起 载入内存中吗?

程序员眼中的程序

由若干部分(段)组成,

每个段有各自的特点、
用途:代码段只读,代码/数据段不会动态增长

image-20210814152944631

  • 符合用户观点: 用户可独立考虑每个段(分治)
  • **怎么定位具体指令(数据): <段号, 段内偏移> ** 如mov [es:bx], ax

不是将整个程序,是将各段分别放入内存

image-20210814153558041

  • 再进行运行时重定位会怎么样?
    mov [DS:100], %eax jmpi 100, CS

LTD表 (普通进程)

image-20210814154124003

这个表似曾相识… 真正故事:GDT (操作系统) + LDT (普通进程)

LDTR

image-20210814154245775

L21 内存分区与分页

  1. 程序分段 (编译阶段)
  2. 在内存中划分空闲区域
  3. 磁盘读写,找一段空闲分区读入内存 并用一块表将映射关系做好

接下来的问题是内存怎么割?
这样就可以将程序的各个段载入到相应的内 存分区中了

固定分区 与 可变分区

  • 给你一个面包,一堆孩子来吃,怎么办?
    • 等分,操作系统初始化时将内存等分成k个分区
    • 但孩子有大有小,段也有大有小,需求不一定

可变分区的管理过程 — 核心数据结构

image-20210814162408430

可变分区的管理—释放内存

image-20210814162530737

可变分区的管理—再次申请

  • 又一个段提出内存请求: reqSize=40K, 怎么办?
  • 首先适配——均匀随机的选择 O(1)
    最佳适配——与目标内存差距最小的选择 O(N)
    最佳适配——与目标内存差距最大的选择 O(N)

image-20210814162620654

引入分页: 解决内存分区导致的内存效率问题

可变分区造成的问题

  • 发起请求reqSize=160K怎么办?
    • 总空闲空间>160,但没有一个空闲分区>160,怎么办?
    • 这就是内存碎片
    • 将空闲分区合并,需要移动 1个段(复制内容):内存紧缩
    • 内存紧缩需要花费大量时间,如果复制速度1M/1秒,
      则1G内存 的紧缩时间为1000秒 接近17分钟 (相当于死机17分钟)

从连续到离散… (程序员角度要求分段,物理内存角度要求分页)

  • 让给面包没有谁都不想要的碎末
    • 将面包切成片,将内存分成页
    • 针对每个段内存请求,系统一页一页的分配给这个段

image-20210814163836004

问题:此时需要内存紧缩吗? 最大的内存浪费是多少?

不需要,每个段的最大内存浪费是一页

页已经载入了内存,接下来的事情…

  • 页0放在页框5中,页0中的地址就需要重定位
  • MMU内存管理单元
  • 页表地址存储在PCB中,添加权限 R、W

image-20210814164548733

L22 多级页表与快表

为了提高内存空间利用率,页应该小,但是页小了页表就大了…

页表会很大,页表放置就成了问题…

32位: 总空间[0,4G]

  • 纸上和实际使用总是存在很大差别!

    • 页面尺寸通常为4K,而地址是32位的,有2^20个页面 (32 - 12 = 20)

    • 2^20个页表项都得放在内存中,需要4M内存;系统中并发10个进程,就需要 40M内存

    • 实际上大部分逻辑地址根本不会用到

      只存使用到的页面

      image-20210814174213542

第一种尝试,只存放用到的页

  • 很自然的想法:用到的逻辑页才有页表项

    • 但页表中的页号不连续,就需要比较、查找,折半
      log(2^20)=20,20次什么?

      用4M,4M/4K = 1000,用一次查1000次(顺序查找)
      没执行一次指令,访问内存500次(平均)

      提高方法:折半查找 访问内存10次——仍然慢

    • 32位地址空间+ 4K页面+页号必须连续 => 2^20个页表项 => 大页表占用内存,造成浪费

既要连续又要让页表占用内存少,怎么办?

用书的章目录和节目录来类比思考…

第二种尝试:多级页表,即页目录表(章)+页表(节)

页目录表(章)+页表(节)

既是连续的,又保证了放在内存中的页表项少了许多

210个目录项 * 4字节地址 = 4K,总共需要 16K<<4M

访问 3 * 4M的内存 ,额外开销为16k

页目录号 + 页号 + 页内偏移

image-20210814175222503

多级页表提高了空间效率,但在时间上? TLB

  • 多级页表增加了访存的次数,尤其是64位系统 (上图中的32位增加了一次)
    • TLB是一组相联快速存储,是寄存器 TLB (快表)

image-20210814182139863

TLB得以发挥作用的原因

  • TLB命中时效率会很高,未命中时效率降低

    未命中时 假设为2MA

    image-20210814183338549

  • 要想真正实现“近似访存1次” ,
    TLB的命中率应该很高

  • TLB越大越好,但TLB很贵,通常只有[64, 1024]

为什么TLB条目数可以在64-1024之间?

  • 相比220个页,64很小,为什么TLB就能起作用?

    • 程序的地址访问存在局部性

    • 空间局部性(Locality in Space)

      image-20210814183638572

    • 计算机系统设计时应该充分利用这一局部性

L23 段页结合的实际内存管理

段、页结合: 程序员希望用段, 物理内存希望用页,所以…

image-20210815002853730

虚拟内存 将 页 和 段 的功能 融合在一起

段、页同时存在:段面向用户/页面向硬件

虚拟内存 对用户是 透明的

image-20210815003152865

段、页同时存在是的重定位(地址翻译)

逻辑地址 -> 虚拟地址 -> 物理地址

image-20210815003602685

一个实际的段、页式内存管理

这个故事从哪里开始

  • 内存管理核心就是内存分配,所以从程序放入内存、使用内存开始…
    • 分配段、建段表;分配页、建页表
    • 进程带动内存使用的图谱
    • 从进程fork中的内存分配开始…

段、页式内存下程序如何载入内存?

一共五步

image-20210815004719197

故事从fork()开始

分配虚存、建段表

  • fork() -> sys_fork -> copy_process的路都已经走过了

    在linux/kernel/fork.c中
    int copy_process(int nr, long ebp,...)
    { ...
    copy_mem(nr, p); ...
    
  • 现在开始分析当时那个神秘的copy_mem了

    int copy_mem(int nr, task_struct *p)
    {
    unsigned long new_data_base;
    new_data_base=nr*0x4000000; //64M*nr	分割虚拟内存
    set_base(p->ldt[1],new_data_base); // 5~6行 完成 对段和表的填写
    set_base(p->ldt[2],new_data_base); 
        ...
    

进程0、进程1、进程2的虚拟地址

  • 每个进程的代码段、数据段都是一个段
  • 每个进程占64M虚拟地址空间,互不重叠 (linux0.11 较为简单,实现的是互不重叠,意味着可以不用切换页,直接页内偏移)

image-20210815010023653

问题:这意味着什么样的简化?

接下来应该是什么了?

分配内存、建页表

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);
int copy_page_tables(unsigned long from,unsigned long to, long size) //建页表
{ from_dir = (unsigned long *)((from>>20)&0xffc);
to_dir = (unsigned long *)((to>>20)&0xffc);
size = (unsigned long)(size+0x3fffff)>>22;
for(; size-->0; from_dir++, to_dir++){
from_page_table=(0xfffff000&*from_dir);
to_page_table=get_free_page();

这里的from_dir,to_dir是什么?

from_dir = (unsigned long *)((from>>20)&0xffc);
to_dir = (unsigned long *)((to>>20)&0xffc);
size = (unsigned long)(size+0x3fffff)>>22;
  • from是?
    32位虚拟地址,这个地址的格式是否还记得?
    为什么不是右移20位
    *from>>22得到目录项编号 (from>>22)4每项4字节
    &0xffc 把最后两位清为0
  • from_page_table=(0xfffff000&*from_dir);
    取页号

image-20210815010955733

from_page_table与to_page_table?

for(; size-->0; from_dir++, to_dir++){
to_page_table=get_free_page();	//建立映射	虚拟地址——物理地址
*to_dir=((unsigned long)to_page_table)|7; //赋予页目录的地址

image-20210815011901069

接下来干什么应该猜也猜的到…

开始对内存的拷贝

image-20210815012151109

for(;nr-->0;from_page_table++,to_page_table++){
this_page = *from_page_table;
this_page&=~2;//只读	因为是共享的,所以是只读的,  &~2——把最后两位清0
*to_page_table=this_page;
*from_page_table=this_page;
this_page -= LOW_MEM; this_page >>= 12;
mem_map[this_page]++; //共享 引用+1 }

程序、虚拟内存+物理内存的样子

image-20210815012447516

第五步(总结)

第五步 最终实现对物理内存的读写

*p=7? 父进程*p=7、子进程*p=8?

如何消除父子进程的相互影响

子进程对父进程 只读,如要写,自己开辟内存写

image-20210815012816416

L24 内存换入-请求调页

Swap in

image-20210815003152865

用户眼里的内存!

  • *4GB(大且规整)的“内存” ,可供用户使用, 如char p, p=3G,实际上就是用地址
  • 用户可随意使用该“内存” ,就象单独拥有4G内存
  • 这个“内存”怎么映射到物理内存,用户全然不知

用换入、换出实现“大内存”

  • 左边4G,右边1G怎么办?

    • 访问p(=0G - 1G)时,将这部分映射到物理内存
    • 再访问p(=3G - 4G)时,再映射这一部分
    image-20210815075833194

请求调页

缺页的情况

image-20210815080421061

一个实际系统的请求调页

  • 这个故事从哪里开始?
    • 请求调页,当然从缺页中断开始

image-20210815080642311

void trap_init(void)
{ set_trap_gate(14, &page_fault); }
#define set_trap_gate(n, addr) \
_set_gate(&idt[n], 15, 0, addr);

处理中断page fault

image-20210815080840859

do_no_page

//在linux/mm/memory.c中
void do_no_page(unsigned long error_code,
unsigned long address)
{ address&=0xfffff000; //页面地址
tmp=address–current->start_code; //页面对应的偏移
if(!current->executable||tmp>=current->end_data){
get_empty_page(address); return; }
page=get_free_page();
bread_page(page, current->executable->i_dev, nr); //读文件系统
put_page(page, address); //建立虚拟地址和页的映射
void get_empty_page(unsigned long address)
{ unsigned long tmp = get_free_page();
put_page(tmp, address);}

put_page

//在linux/mm/memory.c中
unsigned long put_page(unsigned long page, //物理地址
unsigned long address)
{ unsigned long tmp, *page_table;
page_table=(unsigned long *)((address>>20)&ffc); //页目录项
if((*page_table)&1)
page_table=(unsigned long*)(0xfffff000&*page_table);
else{
tmp=get_free_page();
*page_table=tmp|7;
page_table=(unsigned long*)tmp;}
page_table[(address>>12)&0x3ff] = page|7; //物理页放在page_table
return page;
}

L25 内存换出

Swap out

get_free_page? 还是…

page=get_free_page(); //申请空闲页
bread_page(page, current->executable->i_dev, nr); //从磁盘读入
  • 并不能总是获得新的页,内存是有限的
    • 需要选择一页淘汰,换出到磁盘,选择哪一页?
    • FIFO,最容易想到,但如果刚换入的页马上 又要换出怎么办?
    • 有没有最优的淘汰方法? MIN
    • 最优淘汰方法能不能实现,是否需要近似? LRU

FIFO页面置换

  • 一实例: 分配了3个页框(frame),页面引用序列为 A B C A B D A D B C B
  • 评价准则: 缺页次数;本实例,FIFO导致7次缺页

MIN页面置换

  • MIN算法: 选最远 (往后看) 将使用的页淘汰,是最优方案
    继续上面的实例: (3frame)A B C A B D A D B C B

    image-20210815083150748

  • 本实例,MIN导致5次缺页

  • 可惜,MIN需要知道将来发生的事… 怎么办?

LRU页面置换

  • 用过去的历史 (往前看) 预测将来。
    LRU算法: 选最近最长一段时间没有使用的页淘汰(最近最少使用)。 对局部性规律的一种认识
    继续上面的实例: (3frame)A B C A B D A D B C B

  • 和MIN完全一样

    image-20210815083150748

  • 本实例,LRU也导致5次缺页

  • LRU是公认的很好的页置换算法,怎么实现?

LRU的准确实现,用时间戳

  • 每页维护一个时间戳(time stamp)

    • 继续上面的实例: (3frame)A B C A B D A D B C B

      image-20210815083802166

    • 每次地址访问都需要修改时间戳,需维护一个全局时钟,需找到最小值 … 实现代价较大

      1、找到最小值 2、更新时间戳 (+1) 3、解决时间挫溢出的情况 4、赋值

LRU准确实现,用页码栈

  • 维护一个页码栈

    • 继续上面的实例: (3frame)A B C A B D A D B C B

      image-20210815084352718

    • 每次地址访问都需要修改栈 (总的修改10次左右栈指针) …
      实现代价仍然较大 -> LRU准确实现用的少

Clock算法

Clock算法 (最近没有使用 近似 之前最远使用)——(LRU近似实现 - 将时间计数变为是和否)

SCR这一实现方法称为 Clock Algorithm 再给一次机会(Second Chance Replacement)

  • 每个页加一个引用位(reference bit)
    • 每次访问一页时,硬件自动设置该位 (设置为 1)
    • 选择淘汰页:扫描该位,是1时清0,并继续扫描;是0时淘汰该页

image-20210815090331639

Clock算法的分析与改造

  • 如果缺页很少,会? 所有的R=1
    • hand scan一圈后淘汰当前页,将调入页插入hand位置,hand前移一位
      退化为FIFO!
    • 原因:记录了太长的历史信息… 怎么办?
    • 定时清除R位… (再来一个扫描指针!)
      • 用来清除R位,移动速度要快! 快指针
      • 用来选择淘汰页,移动速度慢! 慢指针

置换策略有了,还需要解决一个问题

  • 给进程分配多少页框(帧frame) (不够所有进程用时)

    • 分配的多,请求调页导致的内存高效

    • 利用就没用了! 那分配的太少呢?

      image-20210815091857330

    • 称这一现象为颠簸(thrashing)

解释: 系统内进程增多 -> 每个进程的缺页率增大 -> 缺页率增大到一定程度,进程总等待调页完成 ->
CPU利用率降低 -> 进程进一步增多,缺页率更大 …

总结(swap in swap out swap分区管理)

image-20210815092610768

L26 I/O与显示器

让外设工作起来

  • CPU向控制器中的寄存器 读写数据
  • 控制器完成真正的工作, 并向CPU发中断信号
image-20210815130446618

向设备控制器的寄存器写不就可以了吗?

  • 需要查寄存器地址、内容的格式和语义…

  • 操作系统要给用户提供一个简单 视图—文件视图,这样方便

外设驱动的3件事情

  1. 发出out指令(最核心的指令)
  2. 形成文件视图
  3. 形成中断处理

一段操纵外设的程序

int fd = open(/dev/xxx”);
for (int i = 0; i < 10; i++) {
	write(fd,i,sizeof(int));
}
close(fd);

(1) 不论什么设备都是open, read, write, close 操作系统为用户提供统一的接口!

(2) 不同的设备对应不同的设备文件(/dev/xxx) 根据设备文件找到控制器的地址、内容格式等等!

一个统一的视图-文件视图

操作系统两大视图之一

image-20210815130953953

概念有了,开始给显示器输出…

  • 从哪里开始这个故事呢?
    printf(“Host Name: %s”, name);

    • printf库展开的部分我们已经知道:先创建缓存buf将格式化输出都写到那里, 然后再write(1,buf,…)
    在linux/fs/read_write.c中
    int sys_write(unsigned int fd, char *buf,int count) //fd是找到file的索引!
    { struct file* file;
    file = current->filp[fd];  //current不陌生吧,进程带动整个系统的视图
    inode = file->f_inode; 
    
    • file的目的是得到inode,显示器信息应该就在这里

fd=1的filp从哪里来?

  • 因为是被current指向,所以是从fork中来

    int copy_process(...){
    *p = *current;
    for (i=0; i<NR_OPEN;i++)
    if ((f=p->filp[i])) f->f_count++;
    
    • 显然是拷贝来的,那么是谁一开始打开的? shell进程启动了whoami命令,shell是其父进程

      void main(void)
      { if(!fork()){ init(); }
      
      void init(void)
      {open(“dev/tty0”,O_RDWR,0);dup(0);dup(0); //0 1 2open系统调用完成了什么?
      execve("/bin/sh",argv,envp)}
      

open系统调用完成了什么?

在linux/fs/open.c中
int sys_open(const char* filename, int flag) //解析目录,找到inode!
{ i=open_namei(filename,flag,&inode);
cuurent->filp[fd]=f; //第一个空闲的fd
f->f_mode=inode->i_mode;
f->f_inode=inode; //存储下inode的信息
f->f_count=1; return fd; }
  • 核心就是建立这样一个链

image-20210815144407666

准备好了,真正向屏幕输出!

  • 继续sys_write!

    在linux/fs/read_write.c中
    int sys_write(unsigned int fd, char *buf,int cnt)
    { inode = file->f_inode;
    if(S_ISCHR(inode->i_mode))
    return rw_char(WRITE,inode->i_zone[0], buf,cnt); ///dev/tty0的inode中的信息是字符设备
    ...
    
  • 转到rw_char

    在linux/fs/char_dev.c中
    int rw_char(int rw, int dev, char *buf, int cnt)
    { crw_ptr call_addr=crw_table[MAJOR(dev)];
    call_addr(rw, dev, buf, cnt); ...}
    
  • 看看crw_table!

    static crw_ptr crw_table[]={...,rw_ttyx,};
    typedef (*crw_ptr)(int rw, unsigned minor, char
    *buf, int count)
    
    static int rw_ttyx(int rw, unsigned minor, char
    *buf, int count)
    { return ((rw==READ)? tty_read(minor,buf):
    tty_write(minor,buf));}
    
  • 再转到tty_write! //实现输出的核心函数

    在linux/kernel/tty_io.c中
    int tty_write(unsigned channel,char *buf,int nr)
    { struct tty_struct *tty;tty=channel+tty_table; //可以猜测:输出就是放入队列! 写到缓冲区
    sleep_if_full(&tty->write_q);
    ...}
    
  • 看看tty->write

    在include/linux/tty.h中
    struct tty_struct{ void (*write)(struct tty_struct
    *tty); struct tty_queue read_q, write_q; }
    
    • 需要看tty_struct结构的初始化!

      struct tty_struct tty_table[] = {
      {con_write,{0,0,0,0,””},{0,0,0,0,””}},{},};
      
    • 到了con_write,真正写显示器

      在linux/kernel/chr_drv/console.c中
      void con_write(struct tty_struct *tty)
      { GETCH(tty->write_q,c);
      if(c>31&&c<127){__asm__(“movb _attr,%%ah\n\t” //本质上是 out
      “movw %%ax,%1\n\t”::”a”(c),
      ”m”(*(short*)pos):”ax”); pos+=2;}
      

只有一句话:mov pos

  • 完成显示中最核心的秘密就是 mov pos, c

image-20210815150042317

  • pos的修改: pos+=2 为什么加2? 屏幕上的一个字符在显存中除了字符本身还应该有字符的属性(如颜色等)

printf的整个过程

将mov pos, c 指令包装成 统一的文件view
并利用了缓冲技术和 消费者和生产者同步的机制

image-20210815150132310

L27 键盘

终端设备包括显示器和键盘

  1. out指令 发 命令
  2. 形成统一的文件视图
  3. 进行中断处理

关于键盘的故事从哪里开始?

  • 如何使用键盘?
    • 对于使用者(人): 敲键盘、看结果
    • 对于操作系统: “等着”你敲键盘,敲了就中断
    • 所以故事该从键盘中断开始,从中断初始化开始…
void con_init(void) //应为键盘也是console的一部分
{ set_trap_gate(0x21, &keyboard_interrupt); }
在kernel/chr_drv/keyboard.S中
.globl _keyboard_interrupt
_keyboard_interrupt:
inb $0x60,%al //从端口0x60读扫描码   最核心的指令
call key_table(,%eax,4) //调用key_table+eax*4
... push $0 call _do_tty_interrupt
  • 处理扫描码key_table+eax*4

    image-20210815170443476

  • 从key_map中取出ASCII码

  • put_queue将ASCII码放到? con.read_q

键盘处理…

image-20210815170731067

总结

image-20210815170905417

L28 生磁盘的使用

抽象层次比键盘和显示器的多

仍然从硬件开始…

  • CPU向磁盘控制器中的寄存器读写数据
  • 磁盘控制器完成真正的工 作,并向CPU发中断信号

使用磁盘从认识磁盘开始

  • 画一个示意图:

    image-20210815190831542

  • 看看俯视图:

    image-20210815190907020

磁盘的I/O过程

  • 仔细想想磁盘如何读/写1一 个字节? 读——磁生电 写——电生磁
  • 磁盘I/O过程: 控制器->寻道->旋转->传输 (读/写) !

最直接的使用磁盘

  • 只要往控制器中写 柱面 C、磁头 H、扇区 S、缓存位置 (通过指令发送)

    image-20210815191318538

void do_hd_request(void)
{ ...hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,...);
port_write(HD_DATA,CURRENT->buffer,256);}

void hd_out(drive, nsect, sec, head, cyl, cmd...)
{ port = HD_DATA; //数据寄存器端口(0x1f0)
outb_p(nsect,++port); outb_p(sect,++port);
outb_p(cyl,++port); outb_port(cyl>>8,++port);
outb_p(0xA0|(drive<<4)|head, ++port);
outb_p(cmd, ++port); } 

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

  • 磁盘驱动负责从block计算出cyl,head,sec(CHS)

    问题:如何编址?为什么这样编址? 希望 block相邻的盘块可以快速读出

磁盘访问时间 = 写入控制器时间 +
寻道时间(12 ms to 8 ms) + 旋转时间(4 ms) + 传输时间(约0.3ms)

减少寻道次数

从CHS到扇区号,从扇区到盘块

问题:C、H、S得到的扇区号是? C * (Heads * Sectors) + H * Sectors + S

磁盘访问时间 = 写入控制器时间 + 寻道时间 + 旋转时间 + 传输时间 (共计约10ms)

一次多读几个扇区

  • 从扇区到盘块: (空间换时间) 可以每次读写一个盘块
    每次读写1 K: 碎片0.5K;读写速度100K/秒;
    每次读写1 M:碎片0.5M;读写速度约40M/秒

    image-20210815193310131

再接着使用磁盘:程序输出block

计算并输出 block = C*(Heads*Sectors) + H*Sectors + S -> S = block%Sectors

问题:Linux 0.11盘块多大?
两个扇区 req->sector=bh->b_blocknr<<1;

image-20210815194123053

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

image-20210815194337770

  • 多个磁盘访问请求出现在请求队列怎么办?
  • 调度的目标是什么? 调度时主要考察什么?
    目标当然是平均访问延迟小!
    寻道时间是主要矛盾!
  • 给调度算法,仍然从FCFS开始…

FCFS磁盘调度算法

  • 最直观、最公平的调度: 顺序调度
    • 一个实例: 磁头开始位置=53; 请求队列=98, 183, 37, 122, 14, 124, 65, 67

image-20210815194734623

  • FCFS: 磁头共 移动640磁道!
  • 在移动过程中把经过 的请求处理了!

SSTF磁盘调度

  • Shortest-seek-time First: 短寻道优先
    • 继续该实例: 磁头开始位置=53; 请求队列=98, 183, 37, 122, 14, 124, 65, 67

image-20210815195326644

容易在局部晃荡,忽略边缘请求

SCAN磁盘调度

  • SSTF+中途不回折:每个请求都有处理机会
    • 继续该实例: 磁头开始位置=53; 请求队列=98, 183, 37, 122, 14, 124, 65, 67

image-20210815195439642

中间总是较为优先

C-SCAN磁盘调度(电梯算法)

  • SCAN+直接移到另一端 (复位) :两端请求都能很快处理 复位
    • 继续该实例: 磁头开始位置=53; 请求队列=98, 183, 37, 122, 14, 124, 65, 67

image-20210815195542644

多个进程共同使用磁盘

static void make_request()
{ ...req->sector=bh->b_blocknr<<1;
add_request(major+blk_dev,req); } 
static void add_request(struct blk_dev_struct *dev,
struct request *req)
{ struct requset *tmp=dev->current_request;
req->next=NULL; cli(); //关中断(互斥)
for(;tmp->next;tmp=tmp->next)
if((IN_ORDER(tmp,req)||!IN_ORDER(tmp,tmp->next))
&&IN_ORDER(req,tmp->next)) break;
req->next=tmp->next; tmp->next=req; sti();} 
#define IN_ORDER(s1, s2) \         //对柱面进行排序
((s1)->dev<(s2)->dev)||((s1)->dev == (s2)->dev\
&& (s1)->sector<(s2)->sector))

生磁盘(raw disk)的使用整理

image-20210815194337770

(1) 进程“得到盘块号” ,算出扇区号(sector)

(2) 用扇区号make req (本课程略去) ,用电梯算法add_request

(3) 进程sleep_on (进程同步)

(4) 磁盘中断处理

(5) do_hd_request算出cyl,head,sector

(6) hd_out调用outp(…)完成端口写

static void read_intr(void)
{ end_request(1);
do_hd_request(); } //唤醒进程!

L29 从生磁盘到文件

为什么引入文件? —cooked disk

文件 —— 盘块号

引入文件,对磁盘使用的第三层抽象

  • 让普通用户使用raw disk: 许多人连扇区都不知道是什么?要求他 们根据盘块号来访问磁盘…
    • 需要在盘块上引入更高一层次的抽象概念! 文件
    • 首先想一想用户眼里的文件是什么样子? 字符序列 (字符流)
    • 磁盘上的文件是什么样子? 盘块集合
    • 文件: 建立字符流到盘块集合的映射关系

映射的作用

将200-212字符删去

  • 连续结构来实现文件 类似数组(不适合动态变化,适合读取 顺序的)
    问题:200-212在哪里, 如果盘块大小是100?
    test.c中的200-212 字符对应盘块789

    image-20210816004004390

    问题:要回答这个问题, 需要存储什么信息? 文件

image-20210816004136636

  • 链式结构也可以实现文件 类似链表(适合变化)

    image-20210816004443926 image-20210816004626347

    优点: 文件长度增减容易 缺点: 顺序访问慢、可靠性差

文件实现的第三种结构,索引结构

image-20210816004817671
  • 是连续和链式分配的有效折衷, 在实际操作系统中较常用!

实际系统是多级索引

index node —— inode

  • 可以表示很大的文件
  • 很小的文件高效访问
  • 中等大小的文件访问速度也不慢!
image-20210816005022989

L30 文件使用磁盘的实现

再一次使用磁盘,通过文件使用

在fs/read_write.c中
int sys_write(int fd, const char* buf, int count)
{ struct file *file = current->filp[fd];
struct m_inode *inode = file->inode;
if(S_ISREG(inode->i_mode))
return file_write(inode, file, buf, count); }

用open建立这条链,open的核心 是读入inode

image-20210816073437693

file_write的工作过程应该就是…

file_write(inode, file, buf, count);

(1)首先需要知道是些哪段字符?
file中一个读写指针, 是开始地址(fseek就是修改它),再加上count

(2)找到要写的盘块号?
inode就是用来干这事的

(3)用盘块号、buf等形成request放入“电梯”

file_write的实现

int file_write(struct m_inode *inode, struct file
*filp, char *buf, int count)
{ off_t pos;
if(filp->f_flags&O_APPEND)  //用file知道文件流读写的字符区间,从哪到哪!
pos=inode->i_size; else pos=filp->f_pos; //得到字符流中的读写位置
while(i<count){
block=create_block(inode, pos/BLOCK_SIZE); //算出盘块号
bh=bread(inode->i_dev, block);  //放入“电梯”队列!
int c=pos%BLOCK_SIZE; char *p=c+bh->b_data;
bh->b_dirt=1; c=BLOCK_SIZE-c; pos+=c;
... while(c-->0) *(p++)=get_fs_byte(buf++); //一块一块拷贝用户字符,并且释放写出!
brelse(bh); }
filp->f_pos=pos; } //修改pos,使之总是对!

create_block算盘块,文件抽象的核心

while(i<count){
block=create_block(inode, pos/BLOCK_SIZE);  //create=1的_bmap,没有映射时创建映射
bh=bread(inode->i_dev, block);
int _bmap(m_inode *inode, int block, int create)
{ if(block<7){ if(create&&!inode->i_zone[block])
{ inode->i_zone[block]=new_block(inode->i_dev);
inode->i_ctime=CURRENT_TIME; inode->i_dirt=1;}
return inode->i_zone[block];}
block-=7; if(block<512){  //一个盘块号2个字节
bh=bread(inode->i_dev,inode->i_zone[7]);
return (bh->b_data)[block];} ...
struct d_inode{ unsigned short i_mode;...
unsigned short i_zone[9];
//(0-6):直接数据块,(7):一重间接,(8):二重间接 }

m_inode,设备文件的inode

struct m_inode{ //读入内存后的inode       前几项和d_inode一样!
unsigned short i_mode; //文件的类型和属性
...
unsigned short i_zone[9]; //指向文件内容数据块
struct task_struct *i_wait;  //多个进程共享的打开这个inode,有的进程等待…
unsigned short i_count;
unsigned char i_lock;
unsigned char i_dirt; ... }
int sys_open(const char* filename, int flag)
{ if(S_ISCHR(inode->i_mode)) //字符设备
{ if(MAJOR(inode->i_zone[0])==4)  //设备文件
current->tty=MINOR(inode->i_zone[0]);}
#define MAJOR(a)(((unsigned)(a))>>8)) //取高字节
#define MINOR(a)((a)&0xff) //取低字节

伟大的文件视图…

image-20210816075831848

L31 目录与文件系统

文件,抽象一个磁盘块集合

  • 用户眼里文件的样子 —— 字符序列 (字符流)

  • 磁盘上的文件的样子 —— 扇区集合

  • 磁盘文件: 建立了字符流到盘块集合的映射关系

文件系统,抽象整个磁盘(第四层抽象)

目录树

image-20210816081705316

在其他计算机上:应用结构+存储的数据也可以得到那棵文件树,找到文件、读写文件…

故事先从多个文件开始…

  • 所有文件放在一层(一个大集合)
  • 怎么办? 集合划分、分治

引入目录树

  • 将划分后的集合再进行划分: k次划分后,每个 集合中的文件数为O(logkN)
    • 这一树状结构扩展性好、表示清晰,最常用
    • 引入了一个新的东西: 目录,表示一个文件集合

实现目录成为了关键问题…

image-20210816082459355

  • 首先需要回答:目录怎么用?
    • 用“/my/data/a”定位文件a
      问题:更准确的说,是要干什么?
      根据/my/data/a,得到文件a的FCB
    • 问题:那么目录中应该存什么?
      存放目录下的所有文件的FCB吗? 如果 是,解析my要干什么?

问题:有什么办法(目录存什么)让系 统效率更高?

树状目录的完整实现 FCB数组

目录的数据块存的东西 —— 目录项:文件名+ 对应的FCB的 “地址”!

image-20210816083053028

要使整个系统能自举,还需存一些信息

image-20210816083549872

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

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

    空闲位图(位向量)… 0011110011101 表示磁盘块2、3、4、5、 8、9、10、12空闲

  • 超级块: 记录两个位图有多大等信息

“完成全部映射下”的磁盘使用

image-20210816083930047

L32 目录解析代码实现

一个实际运转的文件系统!

PCB 进程控制块 DCT设备控制表 FCB文件控制块

就是将open弄明白…

image-20210816084406172

在linux/fs/open.c中
int sys_open(const char* filename, 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++;}   //PCB  current->root
else if(c) inode=current->pwd;
while(1){if(!c) return inode; //函数的正确出口
bh=find_entry(&inode,thisname,namelen,&de);
int inr=de->inode; int idev=inode->i_dev;
inode=iget(idev,inr); //根据目录项读取下一层inode}}

image-20210816164035918

  • 核心的四句话正好对应目录树的四个 重点:
    (1)root:找到根目录;
    (2)find_entry:从目录中读取目录项;
    (3)inr:是目录项中的索引节点号;
    (4)iget:再读下一层目录

目录解析 — 从根目录开始

inode=current->root;

  • 又是current(task_struct),仍然是拷贝自init进程
void init(void)
{ setup((void *) &drive_info);  //一句看过无数次,又略过无数次的语句
sys_setup(void * BIOS)//在kernel/hd.c中
{ hd_info[drive].head = *(2+BIOS);
hd_info[drive].sect = *(14+BIOS);
mount_root(); ... }
void mount_root(void)//在fs/super.c中
{
mi=iget(ROOT_DEV,ROOT_INO));   //#define ROOT_INO 1
current->root = mi;
}

读取inode — iget

struct m_inode * iget(int dev, int nr)
{ struct m_inode * inode = get_empty_inode();
inode->i_dev=dev; inode->i_num=nr;
read_inode(inode); return inode;} 
static void read_inode(struct m_inode *inode)
{ struct super_block *sb=get_super(inode->i_dev);;
lock_inode(inode);
block=2+sb->s_imap_blocks+sb->s_zmap_blocks+
(inode->i_num-1)/INODES_PER_BLOCK;
bh=bread(inode->i_dev,block);
inode=bh->data[(inode->i_num-1)%INODES_PER_BLOCK];
unlock_inode(inode); }

image-20210816171435923

开始目录解析 — find_entry(&inode,name,…,&de)

  • de: directory entry(目录项)

    struct dir_entry{
    unsigned short inode; //i节点号
    char name[NAME_LEN]; //文件名 }
    
    在fs/namei.c中
    static struct buffer_head *find_entry(struct m_inode
    **dir, char *name, ..., struct dir_entry ** res_dir)
    { int entries=(*dir)->i_size/(sizeof(struct dir_entry));
    int block=(*dir)->i_zone[0];
    *bh=bread((*dir)->i_dev, block);
    struct dir_entry *de =bh->b_data;
    while(i<entries) {
    if(match(namelen,name,de))
    {*res_dir=de; return bh;}de++; i++;} }
    

while(i<entries)…

while(i<entries) //entries是目录项数
{ if((char*)de> = BLOCK_SIZE+bh->b_data){  //#define BLOCK_SIZE1024 //两个扇区
brelse(bh);
block=bmap(*dir,i/DIR_ENTRIES_PER_BLOCK);
bh=bread((*dir)->i_dev,block);
de=(struct dir_entry*)bh->b_data;
} //读入下一块上的目录项继续match
if(match(namelen,name,de))
{*res_dir=de;return bh;}
de++; i++; }

操作系统全图

两大视图

image-20210816172203813

  • 20
    点赞
  • 106
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
操作系统 治军 pdf》是一本由作者治军撰写的关于操作系统的书籍的电子版PDF文件。操作系统是计算机系统中的一个核心组成部分,它负责管理和控制计算机的硬件和软件资源,使得计算机能够高效地运行各种应用程序。 在这本书中,治军详细介绍了操作系统的基本概念、原理和设计原则。他从操作系统的起源和发展历程出发,讲解了多道、分时和实时操作系统等不同类型的操作系统,并深入解析了其内核结构和功能。此外,书中还讨论了进程管理、内存管理、文件系统、输入输出控制等重要的操作系统主题,为读者提供了全面了解和深入学习的机会。 这本书的PDF版本使得读者可以更加便捷地获取其中的内容,无论是在电脑、平板还是手机上都可以进行阅读。通过阅读这本书,读者可以深入了解操作系统的基本原理和相关技术,有助于他们提升对计算机系统的理解和应用。对于学习计算机科学或相关专业的学生和从事软件开发工作的技术人员来说,这本书是一本宝贵的参考资料。 总之,《操作系统 治军 pdf》是一本全面介绍操作系统的书籍的电子版PDF文件,读者可以通过阅读它来深入了解操作系统的原理和技术。这本书的出现为学习和研究操作系统提供了便利,对于对计算机系统有兴趣的人士来说是一本值得阅读的优秀教材。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值