【Linux 0.11】第五章 Linux 内核体系结构

691 阅读47分钟

赵炯;《Linux 内核完全注释 0.11 修正版 V3.0》

操作系统:硬件、操作系统内核、操作系统服务、用户应用程序。

  • 操作系统服务程序: X 窗口系统、shell 命令解释系统、内核编程接口...
  • 操作系统内核:对硬件资源的抽象和访问调度。

5.1 内核模式

操作系统内核模式:

  • 单内核模式(Linux),代码结构紧凑、执行速度快、结构性不强
  • 微内核模式

单内核模式下,操作系统提供服务的流程: 应用程序 --【参数、init x80】-- 【CPU: 用户态切换到核心态】-- 系统调用服务程序 -- 底层支持函数 -- 【完成特定功能】 -- 【CPU: 核心态切换到用户态】 -- 应用程序。

在这里插入图片描述

5.2 Linux 内核系统体系结构

Linux 内核主要模块:

  1. 进程调度:控制进程对 CPU 资源的使用;
  2. 内存管理:控制进程对 内存 资源的使用、虚拟内存:允许进程使用比实际空间更多的内存容量;
  3. 文件系统:控制进程对 外存、外设 资源的使用、虚拟文件系统:对所有外存提供一个通用的文件接口,屏蔽硬件设备细节;
  4. 进程间通信
  5. 网络接口

在这里插入图片描述 在这里插入图片描述

单内核模式下,操作系统提供服务的流程: 应用程序 --【参数、init x80】-- 【CPU: 用户态切换到核心态】-- 系统调用服务程序 -- 底层支持函数 -- 【完成特定功能】 -- 【CPU: 核心态切换到用户态】 -- 应用程序。

5.3 Linux 内核对内存的管理和使用

  1. 物理内存分布
  2. 分段、分页
  3. CPU 多任务操作和保护方式
  4. 虚拟地址、线性地址、物理地址

5.3.1 物理内存

在这里插入图片描述

Intel 80386 及以后的 CPU 中提供了两种内存管理(地址变换)系统:内存分段(Segmentation System)和分页系统(Paging System)。

5.3.2 内存地址空间

三种地址:

  1. 程序(进程)的虚拟和逻辑地址;
  2. CPU 的线性地址;
  3. 实际物理内存地址;

在这里插入图片描述 Intel 80X86 CPU 共可以索引 16384 个选择符,若每个段的长度取最大值 4G,则最大的虚拟地址空间范围是 16384*4G=64T。 在 Linux0.11 内核中,给每个程序(进程)都划分了总容量为 64MB 的虚拟内存空间。

5.3.3 内存分段机制

jiaming.blog.csdn.net/article/det…

逻辑地址 --【分段机制】-- 线性地址。

  • GDT(GDTR)
  • IDT(IDTR):保存在内核代码段中。
  • LDT(LDTR):每个任务的局部描述符表 LDT 也是由 GDT 中描述符定义的一个内存段,该段中存放着对应任务的代码段和数据段描述符,因此 LDT 段很短,其段限长通常只要大于 24字节 即可;每个任务的任务状态段 TSS 也是由 GDT 中描述符定义的一个内存段,其段限长只要满足能够存放一个 TSS 数据结构就够了。

对于任务的切换:CPU 把寄存器等信息保存至该任务的 TSS 段中,同时 CPU 使用新切换进任务的 TSS 段中的信息来设置各寄存器,以恢复任务的执行环境。 Linux0.11 内核中没有使用到 GDT 表中第4个描述符(syscall 描述符项)

在这里插入图片描述

5.3.4 内存分页管理

jiaming.blog.csdn.net/article/det…

线性地址 --【分页机制】-- 物理内存

  • 对于 Linux0.11,内核设置全局描述符表 GDT 中的段描述符项数最大为 256,其中 2 项空闲、2 项系统使用,每个进程使用两项。系统最多容纳 (256-4)/2=126个 任务,虚拟地址范围 ((256-4)/2)*64MB=8GB,但是人工定义最大任务数 64个,每个任务逻辑地址范围是 64M,各个任务在线性地址空间的起始位置是 (任务号)*64MB。
  • 实际上,Linux0.11 中所有任务的指令空间(I)和数据空间(D)都合用一块内存,即一个进程的所有代码、数据和堆栈部分都处于同一段内存段中。
  • 任务 0、1 的代码段和数据段长度都是从线性地址 0 开始的 640KB 范围。

在这里插入图片描述 在这里插入图片描述

进程逻辑地址空间中代码段和数据段的概念与 CPU 分段机制中的代码段和数据段不是同一个概念?

  • 进程逻辑地址空间中的代码段和数据段则是由编译器在编译程序和操作系统在加载程序时规定的在进程逻辑空间中顺序排列的代码区域、初始化和未初始化的数据区域以及堆栈区域。
  • CPU 分段机制中段的概念确定了在线性地址空间中一个段的用途以及被执行或访问的约束和限制,每个段可以设置在 4GB 线性地址空间中的任何地方,可以相互独立,也可以重叠。

在这里插入图片描述

5.3.5 CPU 多任务和保护方式

  • 内核代码和数据由所有任务共享。
  • 每个任务都有自己的代码和数据区,不能访问其它任务。
  • 进程执行系统调用而陷入内核代码中执行时(内核运行态),使用进程的内核栈。

在这里插入图片描述

5.3.6 虚拟地址、线性地址和物理地址之间的关系

内核代码和数据的地址

  • head.s 程序的初始化操作中将内核代码段和数据段设置为长度 16MB 的段、重叠;
  • 含有内核所有的代码、内核段表(GDT、IDT、TSS)、页目录表、二级页表、局部数据、内核临时堆栈(用于任务 0 的用户堆栈);

在这里插入图片描述

  • 默认情况下,Linux0.11 内核最多管理 16MB 的物理内存,共有 4096 个页帧,每个页面 4KB,若机器只有 4MB(甚至 2MB)物理内存就可以完全运行 Linux0.11 系统,4-16MB 地址范围会映射到不存在的物理内存地址上,但是不会使用,若机器有大于 16MB 内存,在初始化阶段会限制 16MB 以上内存的使用;
  1. 内核代码段和数据段区域在线性地址空间和物理地址空间中一样,有助于简化内核的初始化操作;
  2. 除任务 0 以外,其它任务均需要在主内存区中为它们作映射操作;

任务 0 的地址对应关系

  • 是系统中人工启动的第一个任务;
  • 代码段和数据段长度 640KB;
  • 代码和数据确定与内核代码和数据中(0~640KB),不需要再分配内存页;
  • 任务状态段 TSS0 代码确定,并且位于任务 0 数据结构信息中;
  • 内核态堆栈和用户态堆栈空间也都在内核代码区中;

在这里插入图片描述

任务 1 的地址对应关系

  • fork() 会为存放任务 1 的二级页表而在主内存区申请一页内存;
  • 复制了父进程(任务0)的页目录和二级页表项;
  • 线性地址空间:64MB-128MB;
  • 线性地址映射到了物理地址 0-640KB;
  • 会在主内存区域中申请一页内存用来存放它的任务数据结构和用作任务 1 的内核堆栈空间。任务数据结构(PCB)包括任务 1 的 TSS 段结构信息;

在这里插入图片描述

  • 任务 1 的用户态堆栈空间共享任务 0 的用户态堆栈空间;
  • 在刚开始创建任务 1 时,任务 0 的用户态堆栈与任务 1 共享使用,但当任务 1 开始运行时,由于任务 1 映射到用户态堆栈的页表项被设置成只读,使得任务 1 在执行堆栈操作时将会引起写页面异常,从而由内核另行分配主内存区页面作为堆栈空间使用。

其它任务的地址对应关系

  • 对于被创建的从任务2开始的进程,它们的父进程都是 init(任务 1)进程;
  • Linux0.11 中可以有 64 个进程同时存在;
  • 从任务 2 开始,每个任务的开始位置为 nr*64MB 处;
  • 任务代码段和数据段的最大长度为 64MB;
  • 在任务 2 中执行 execve() 函数来执行 shell 程序;
  • 当任务 2 的代码调用 execve() 系统调用开始执行 shell 程序时,该系统调用会释放掉从任务 1 复制的页目录和页表表项以及相应内存页面,然后为新的执行程序 shell 重新设置相关页目录和页表表项。
  • 系统虽然在线性地址空间为任务 2 分配了 64MB 空间,但是内核并不会立刻为其分配和映射物理内存页面;
  • 只有当发生缺页异常时,MM 才会分配一页物理内存并映射(Load on demand);

在这里插入图片描述

5.3.7 用户申请内存的动态分配

  • malloc() 申请内存,动态申请的内存容量由 C 库函数 malloc() 来管理,内核不管理;
  • 因为,内核为每个进程已经确定好了 64MB 的线性地址空间,只要 malloc 导致缺页,内核同样会执行缺页处理;
  • 使用 malloc 并不会为新申请的页面映射物理内存页面,只有当程序寻址到某个不存在对应物理页面的地址时,内核才会映射物理内存页面;
  • free(),C 库会将所释放的内存块标记为空闲,以备程序再次申请使用,在这个过程中内核为该进程所分配的这个物理页面并不会被释放掉,只有当进程最终结束时内核才会全面收回已分配和映射到该进程地址空间范围的所有物理内存页面;

5.4 Linux 系统的中断机制

5.4.1 中断操作原理

  1. 轮询;
  2. 中断,中断请求-中断请求-中断服务过程

可编程中断控制器(PIC)是管理设备中断请求的管理者,硬件中断处理流程:

  • PIC 收集中断服务请求,并对它们进行优先级比较,并选出最高优先级的中断请求进行处理,同时还决定是否抢占;
  • PIC 向处理器的 INT 引脚发出一个中断信号,处理器会立刻停下当时所做的事情并询问 PIC 需要执行哪个中断服务过程;
  • PIC 通过向数据总线发出与中断请求对应的中断号来告知处理器要执行哪个中断服务过程;
  • 处理器则根据读取的中断号通过查询中断向量表取得相关设备的中断向量并开始执行中断服务程序;
  • 当中断服务程序执行结束,处理器就继续执行被中断信号打断的程序;

软件中断: 通过使用 int 指令并使用操作数指明中断号,就可以让处理器去执行相应的中断处理过程;

5.4.2 80X86 微机的中断子系统

jiaming.blog.csdn.net/article/det…

  • 每个 8259A 芯片可以管理 8 个中断源;
  • 通过多片级联方式,8259A 能构成最多管理 64 个中断向量的系统

以下图为例:

  • 从芯片的 INT 引脚连接到主芯片的 IR2 引脚上,即 8259A 从芯片发出的中断信号作为 8259A 主芯片的 IRQ2 出入信号;

在这里插入图片描述

5.4.3 中断向量表(中断描述符表)

  • 80X86 微机支持 256 个中断,对应每个中断需要安排一个中断服务程序;
  • 80X86 实模式运行方式下,每个中断向量由 4 个字节组成,指明了段值和段内偏移值;
  • 中断向量在内存中的位置:0x0000:N4,即对应的中断服务程序入口地址保存在物理内存 0x0000:N4 位置处;
  • Linux 系统中,除了在刚开始加载内核时需要用到 BIOS 提供的显示和磁盘读操作中断功能,在内核正常运行之前则会在 setup.s 程序中重新初始化 8259A 芯片并且在 head.s 程序中重新设置一张中断向量表,完全抛弃了 BIOS 所提供的中断服务功能;
  • 当 Intel CPU 运行在 32 位保护模式下时,需要使用中断描述符表 IDT 来管理中断或异常,类似中断向量表的替代物;
  • Linux 操作系统工作于 80X86 的保护模式下,因此它使用中断描述符表来设置和保存各中断的向量信息;

5.4.4 Linux 内核的中断处理

中断信号:

  1. 硬件中断
    1. 可屏蔽中断:CPU 引脚 INTR
    2. 不可屏蔽中断 :CPU 引脚 NMI
  2. 软件中断
    1. Fault:CPU 重新执行引起错误的指令
    2. Trap:CPU 继续执行后面的指令
    3. Abort:引起这种错误的程序应该被终止
  • 每个中断由 0-255 之间的一个数字来标识;
  • int0 - int31 每个中断的功能由 Intel 公司保留,属于软件中断(异常);
  • int32 - int255 可以由用户自己设定;

在这里插入图片描述 Linux 中,将 int32 - int47 对应于 8259A 中断控制芯片发出的硬件中断请求信号 IRQ0 - IRQ15,并把程序编程发出的系统调用中断设置为 int 0x80 —— 用户程序使用操作系统资源的唯一接口

在这里插入图片描述 在这里插入图片描述

  • 在系统初始化时,内核在 head.s 程序中使用中断描述符对中断描述符表中所有的 256 个描述符进行默认设置,否则可能会发生一般保护性出错;
  • 在设置中断描述符表 IDT时 Linux 内核使用了中断门和陷阱门两种描述符,二者的区别在于对标志寄存器 EFLAGS 中的中断允许标志 IF 的影响;
    • 由中断门描述符执行的中断会复位 IF 标志,(IF=0:避免中断执行被打断),中断结束指令 iret 会从堆栈上恢复 IF 标志的原值;
    • 通过陷阱门执行的中断不会影响 IF 标志。

5.4.5 标志寄存器的中断标志

  • 为了避免竞争条件和中断对临界代码区的干扰,在 Linux0.11 内核代码中许多地方使用了 cli 和 sti 指令;
  • cli:IF=0,不会相应外部中断;
  • sti:IF=1,允许 CPU 识别并响应外部设备发出的中断;

5.5 Linux 的系统调用

5.5.1 系统调用接口

  • 系统调用(syscalls)是 Linux 内核与上层应用程序进行交互通信的唯一接口
  • 用户程序通过直接或间接(通过库函数)调用 int 0x80,并在 eax 寄存器中指定系统调用功能号,即可使用内核资源;
  • 通常应用程序都是使用具有标准接口定义的 C 函数库中的函数间接地使用内核地系统调用;

在这里插入图片描述

  • 系统调用使用函数形式进行调用,可以带有一个或多个参数;

  • 系统调用执行的结果,会在返回值中表示出来,负值表示错误,0 表示成功;

  • 错误的类型码被存放在全局变量 errno 中,通过调用库函数 perror(),我们可以打印出该错误码对应的出错字符串信息;

  • 在 linux 内核中,每个系统调用都具有唯一的一个系统调用功能号,定义在 include/unistd.h 中。这些系统调用功能号实际上对应于 include/linux/sys.h 中定义的系统调用处理程序指针数组表 sys_call_table[] 中项的索引值;

  • 系统调用处理函数的名词基本上都是以符号 sys_ 开始的。

5.5.2 系统调用处理过程

  • 当应用程序经过库函数向内核发出一个中断调用 int 0x80 时,就开始执行一个系统调用;
  • 寄存器 eax 中存放着系统调用号,携带的参数依次存放在寄存器 ebx、ecx 和 edx 中;
  • 处理系统调用中断 int0x80 的过程是程序 kernel/system_call.s 中的 system_call。
  • 内核源码在 include/unistd.h 文件中定义了宏函数 _syscalln(),其中 n 代表携带的参数个数,可以分别 0 至 3。若需要传递大块数据给内核,则可以传递这块数据的指针值;
// 对于read()系统调用,其定义是:
// unistd.h
#define __NR_read	3
int read(int fd, char *buf, int n);

// 我们在用户程序中直接执行对应的系统调用,那么该系统调用的宏的形式为:
#define _LIBRARY_
#include <unistd.h>

_syscall3(int, read, int, fd, char *, buf, int, n)

// 我们可以直接在用户程序中使用上面的 syscall3() 来执行一个系统调用read(),而不通过C 函数库作中介

对于 include/unistd.h 中给出的每个系统调用宏,都有 2+2*n 个参数,其中第一个参数对应系统调用返回值的类型;第二个参数是系统调用的名称;随后是系统调用所携带参数的类型和名称。这个宏会被扩展成包含内嵌汇编语句的C函数;

// unistd.h
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
	return (type) __res; \
errno=-__res; \
return -1; \
}
int read(int fd, char *buf, int n)
{
	long _res;
	__asm__ volatile (
		"int $0x80"
		:"=a" (_res)	// eax 中存放返回值,存放了实际读取的字节数
		:"0"(_NR_read), "b"((long)(fd)), "c"((long)(buf)), "d"((long)(n))); // eax 中存放 NR_read,即系统调用号,携带的参数分别放在 ebx,ecx,edx中
	if (_res>=0)
		return int __res;
	errno =- _res;	// 读出错,将出错号取反后放入全局变量 errno 中,并向调用程序返回 -1 值。
	return -1;
}

可以看出,这个宏经过展开就是一个读操作系统调用的具体实现。

当进入内核中的系统调用处理程序 kernel/system_call.s后,system_call的代码会首先检查eax中的系统调用功能号是否在有效系统调用号范围内,然后根据sys_call_table[]函数指针表调用执行相应的系统调用处理程序。

call _sys_call_table(,%eax,4)

这句汇编语句操作数的含义是间接调用地址在 _sys_call_table+%eax*4 处的函数,由于 sys_call_table[] 指针每项 4 个字节,因此这里需要给系统调用功能号乘上 4。然后用所得的值从表中获取被调用处理函数的地址。

5.5.3 系统调用的参数传递方式

  1. 通用寄存器传递方法;
  2. 系统调用门(System Call gate),在进程用户态堆栈和内核态堆栈自动复制传递的参数;

5.6 系统时间和定时

为了让操作系统自动地准确提供当前时间和日期信息,PC/AT 微机系统中提供了用电池供电的实时钟RT电路支持。通常这部分电路与保存系统信息的少量CMOS RAM集成在一个芯片上,因此这部分电路被称为RT/CMOS RAM电路。

在初始化时,Linux 0.11内核通过init/main.c程序中的time_init()函数读取这块芯片中的当前时间和日期信息,并通过kernel/mktime .c 程序中的kernel/mktime()函数转换成从1970年1月1日0时开始记起的以秒为单位的时间——start_time。用户程序可以通过调用time()来读取startup_time的值。

通过从系统启动开始计数的系统滴答值 jiffies,程序可以唯一地确定运行时刻的当前时间值。

5.6.2 系统定时

可编程定时芯片 Intel 8253(8254)被设置为每 10 毫秒就发出一个时钟中断请求(IRQ0)信号,这个时间节拍是操作系统运行的脉搏,称之为 1 个系统滴答,每经过 1 个时钟滴答时间,系统就会调用一次时钟中断处理程序。

时钟中断处理程序 timer_interrupt 主要通过 jiffies 变量来累计自系统启动以来经过的时钟滴答数。每当发生一次时钟中断 jiffies 值增加 1,然后调用 C 语言函数 do_timer() 作进一步处理。

do_timer() 函数根据特权级对当前进程运行时间作累计,如果 CPL=0,表示进程运行在内核态时被中断,内核会把进程的内核态运行时间统计值 stime+1,否则把进程用户态时间统计值+1。

**时间片是一个进程在被切换掉之前所能持续运行的 CPU 时间,其单位是滴答数。**如果进程时间片值递减后还大于 0,表示其时间片还没有用完,于是退出 do_timer() 继续运行当前进程。如果此时进程时间片已经递减为 0,表示该进程已经用完了此次使用 CPU 的时间片,于是程序就会根据被中断程序的级别来确定进一步处理的方法。若被中断的当前进程是工作在用户态的,则 do_timer() 就会调用调度程序 schedule() 切换到其他程序去运行。如果被中断的当前进程工作在内核态,也即在内核程序中运行时被中断,则 do_timer() 会立刻退出。这样的处理方式决定了 Linux 系统的进程在内核态运行时不会被调用程序切换。即进程在内核态程序中运行时是不可抢占的,当处于用户态程序中运行时则可以被抢占。

在 Linux 0.11 内核中定时器同时最多可以有 64 个。sched.c。

5.7 Linux 进程控制

程序是一个可执行的文件,而进程是一个执行中的程序实例。利用分时技术,在 Linux 操作系统上同时可以运行多个进程。对于 Linux 0.11 内核来讲,系统最多可以有 64 个进程同时存在,除了第一个进程用手工建立以外,其余的都是现有进程使用系统调用 fork 创建的进程。进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。每个进程只能执行自己的代码和访问自己的数据及堆栈区。进程之间的通信需要通过系统调用来进行。

一个进程可以在用户态或者内核态下执行,并且分别使用各自独立的内核态堆栈和用户态堆栈,用户堆栈用于进程在用户态下临时保存调用函数的参数、局部变量等数据;内核堆栈则含有内核程序执行函数调用时的信息。

5.7.1 任务数据结构

内核程序通过进程表对进程进行管理,每个进程在进程表中占有一项,在 Linux 系统中,进程表项是一个 task_struct 任务结构指针。任务数据结构定义在头文件 include/linux/sched.h

// P72
struct task_struct {
/* these are hardcoded - don't touch */
	long state;														/* -1 unrunnable, 0 runnable(ready), >0 stopped */
	long counter;													// 任务运行时间计数,递减
	long priority;													// 运行优先级,越大运行越长
	long signal;													// 位图,每个比特位代表一种信号
	struct sigaction sigaction[32];									// 信号执行属性,对应信号将要执行的操作和标志信息
	long blocked;	/* bitmap of masked signals */					// 进程信号屏蔽码
/* various fields */
	int exit_code;													// 任务停止执行后的退出码,父进程用
	unsigned long start_code,end_code,end_data,brk,start_stack;		// 代码段地址、代码段长度、代码段+数据长度、总长度、堆栈段地址
	long pid,father,pgrp,session,leader;							// 进程标识号、父进程号、进程组号、会话号、会话首领
	unsigned short uid,euid,suid;									// 用户标识号、有效用户id、保存的用户id
	unsigned short gid,egid,sgid;									// 组标识号、有效组id、保存的组id
	long alarm;														// 报警定时值(滴答数)
	long utime,stime,cutime,cstime,start_time;						// 用户态运行时间、系统态运行时间、子进程用户态运行时间、子进程系统态运行时间、子进程开始运行时刻
	unsigned short used_math;										// 标志,是否使用了协处理器
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */		// 进程使用tty终端的子设备号
	unsigned short umask;											// 文件创建属性屏蔽位
	struct m_inode * pwd;											// 当前工作目录i节点结构指针
	struct m_inode * root;											// 根目录i节点结构指针
	struct m_inode * executable;									// 执行文件i节点结构指针
	unsigned long close_on_exec;									// 执行时关闭文件句柄位图标志
	struct file * filp[NR_OPEN];									// 文件结构指针表
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];										// 局部描述符表
/* tss for this task */				
	struct tss_struct tss;											// 进程的任务状态段信息结构
};

5.7.2 进程运行状态

进程状态保存在进程任务结构的 state 字段中。在 linux 系统中,睡眠等状态被分为可中断和不可中断的等待状态。

在这里插入图片描述

  • 可中断睡眠状态:当系统产生一个中断、释放了进程正在等待的资源、收到一个信号,都可以唤醒进程转换到就绪状态。
  • 不可中断睡眠状态:只有使用 wake_up() 函数明确唤醒时才能转换到可运行的就绪状态。该状态通常在进程不受干扰地等待或者所等待时间会很快发生时使用。
  • 暂停状态:当进程收到信号 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 进入暂停状态,发送 SIGCONT 信号转换到可运行状态。linux 0.11 中,还未实现对该状态地转换处理,处于该状态的进程将会被作为进程终止来处理。
  • 僵死状态:当进程已经停止运行,但是其父进程还没有调用 wait() 询问其状态时,称该进程处于僵死状态。

当一个进程的运行时间片用完,系统就会使用调度程序强制切换到其它的进程去执行,而进程进入睡眠状态。只有当进程在内核运行态时,内核才会进行进程切换操作,在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态,为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。

5.7.3 进程初始化

在 init/main.c 中完成所有初始化函数之后,系统各部分已经处于可运行状态。此后程序把自己手动移动到进程 0 中运行,并使用 fork() 调用首次创建出进程 1,在进程 1 中程序将继续进行应用环境的初始化并执行 shell 登陆程序。而原进程 0 则会在系统空闲时被调度执行,此时任务 0 仅执行 pause() 系统调用,其中又会去执行调度函数。

移动到任务 0 中执行这个过程由宏 move_to_user_mode(include/asm/system.h)完成。它把 main.c 程序执行流从内核态移动到了用户态的任务 0 中继续运行。在移动之前,系统在对调度程序的初始化过程(sched_init())中,首先对任务 0 的运行环境进行了设置。这包括人工预先设置好人物 0 数据结构各字段的值、在全局描述符表中添入任务 0 的任务状态段描述符和局部描述符表的段描述符,并把它们分别加载到任务寄存器 tr 和局部描述符表寄存器 ldtr 中。

内核初始化是一个特殊的过程,任务 0 的代码段和数据段分别包含在内核代码段和数据段中,内核初始化程序 main.c 也即是任务 0 中的代码,只是在移动到任务 0 之前系统正以内核态特权级 0 运行着 main.c 程序。宏 move_to_user_mode 的功能就是把特权级从内核态的 0 级变换到用户态的 3 级,但是仍然继续执行原来的代码指令流。

在移动到任务 0 的过程中,宏 move_to_user_mode 使用了中断返回指令造成特权级改变的方法。使用这种方法进行控制权转移是由 CPU 保护机制造成的。CPU 允许低级别代码(特权级3)通过调用调用门或中断、陷阱门来调用或转移到高级别代码中运行,反之则不行。因此内核采用了这种模拟 IRET 返回低级别代码(用户态)的方法。该方法的主要思想是在堆栈中构筑中断返回指令需要的内容,把返回地址的段选择符设置成任务 0 代码段选择符,其特权级为 3。此后执行中断返回指令 iret 时将导致系统 CPU 从特权级 0 跳转到特权级 3 上运行。

在这里插入图片描述

在这里插入图片描述

当执行 iret 指令时,CPU 把返回地址送入 CS:EIP 中,同时弹出堆栈中标志寄存器内容。由于 CPU 判断出目的代码段的特权级是 3,与当前内核态的 0 级不同,于是 CPU 会把堆栈中的堆栈段选择符和堆栈指针弹出到 SS:ESP 中。由于特权级发生了变换,段寄存器 DS、ES、FS 和 GS 的值都变得无效,此时 CPU 会把这些段寄存器清零。因此在执行了 iret 指令后需要重新加载这些寄存器,此后,系统就开始以特权级 3 运行在任务 0 的代码上。所使用的用户态堆栈还是原来在移动之前使用的堆栈。而其内核态堆栈则被指定为其任务数据结构所在页面的顶端开始(PAGE_SIZE+(long)&init_task)。由于以后在创建新进程时,需要复制任务 0 的任务数据结构,包括其用户堆栈指针,因此要求任务 0 的用户态堆栈在创建任务 1 之前保持干净状态。

5.7.4 创建新进程

Linux 系统中创建新进程使用 fork() 系统调用。所有进程都是通过复制进程 0 而得到的,都是进程 0 的子进程。

在创建新进程的过程中,系统首先在任务数组中找出一个还没有被任何进程使用的空项。如果系统中已经有 64 个进程在运行,则 fork() 系统调用会因为任务数组表中没有可用空项而出错返回。 然后系统为新建进程在主内存区中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。为了防止这个还未处理完成的新建进程被调度函数执行,此时应该立刻将新进程状态置为不可中断的等待状态。 随后对复制的任务数据结构进行修改。把当前进程设置为新进程的父进程,清除信号位图并复位新进程各统计值,并设置初始运行时间片值为 15 各系统滴答数(150ms)。 设置任务状态段 TSS 中各寄存器的值,tss.eax=0 新建进程内核态堆栈指针 tss.esp0 被设置成新进程任务数据结构所在内存页面的顶端,而堆栈段 tss.ss0 被设置成内核数据段选择符。 tss.ldt 被设置成局部表描述符在 GDT 中的索引值。如果当前进程使用了协处理器,则还需要把协处理器的完整状态保存在新进程的 tss.i387 结构中。 此后系统设置新任务的代码和数据段基址、限长,并复制当前进程内存分页管理的页表。注意,此时系统并不为新的进程分配实际的物理内存页面,而是让它共享其父进程的内存页面。只有当父进程或新进程中任意一个有写内存操作时,系统才会为执行写操作的进程分配相关的独自使用的内存页面。(Copy On Write) 如果父进程中有文件是打开的,则应该将对应文件的打开次数增 1。接着在 GDT 中设置新任务的 TSS 和 LDT 描述符项,其中基地址信息指向新进程任务结构中的 tss 和 ldt。最后再将新任务设置成可运行状态并返回新进程号。

创建一个新的子进程和加载运行一个执行程序文件是两个不同的概念。当创建子进程时,它完全复制了父进程的代码和数据区,并会在其中执行子进程部分的代码。而执行块设备上的一个程序时,一般是在子进程中运行 exec()系统调用来操作的,在进入 exec() 后,子进程原来的代码和数据区就会被清掉。**待该子进程开始运行新程序时,由于此时内核还没有从块设备上加载该程序的代码,CPU 就会立刻产生代码页面不存在的异常,**此时内存管理程序就会从块设备上加载相应的代码页面,然后 CPU 又重新执行引起异常的指令。到此时新程序的代码才开始真正被执行。

5.7.5 进程调度

选择系统中下一个要运行的进程。这种选择运行机制是多任务操作系统的基础。调度程序(在所有处于运行状态的进程之间分配 CPU 运行时间的管理代码)。Linux 进程是可抢占式的,进程的抢占发生在进程处于用户态执行阶段,在内核态执行时是不能被抢占的。

调度程序(基于优先级排队调度策略)

schedule() 函数会扫描任务数组,通过比较就绪态任务的运行时间(counter)来确定运行时间较少的进程,使用任务切换宏函数切换到该进程运行。

如果此时所有处于就绪态的进程的时间片都已经用完,系统就会根据每个进程的优先权值 priority,对系统中的所有进程(包括正在睡眠的进程)重新计算每个任务需要运行的时间片值 counter。

counter=counter2+prioritycounter = \frac{counter}{2} + priority

对于正在睡眠的进程,当它们被唤醒时就有着极高的 counter。然后 schedule() 函数重新扫描任务数组中所有处于就绪状态的进程,并重复上述过程直到选出一个进程为止,最后调用 switch_to() 执行实际的进程切换操作。如果没有其它进程可运行,系统就会选择进程 0 运行,对于 Linux0.11 来说,进程 0 会调用 pause() 把自己置为可中断睡眠状态并再次调用 schedule()。只要系统空闲就调度进程 0 运行。

进程切换

switch_to() 定义在 include/asm/system.h。该宏会把 CPU 的当前进程状态替换成新进程的状态。在进行切换之前,switch_to() 首先检查要切换到的进程是否就是当前进程,如果是则什么也不做,直接退出,否则就首先把内核去那句变量 current 置为新任务的指针,然后长跳转到新任务的任务状态段 TSS 组成的地址处,造成 CPU 执行任务切换操作。此时 CPU 会把其所有寄存器的状态保存到当前任务寄存器 TR 中 TSS 段选择符所指向的当前进程任务数据结构的 tss 结构中,然后把新任务状态段选择符所指向的新任务数据结构中 tss 结构中的寄存器信息恢复到 CPU 中,系统就正式开始运行新切换的任务了。

5.7.6 终止进程

当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程占用的系统资源。这包括进程运行时打开的文件、申请的内存等。

当一个用户程序调用 exit() 系统调用时,就会执行内核函数 do_exit()。该函数会首先释放进程代码段和数据段占用的内存页面,关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程序的 i 节点进行同步操作。如果进程有子进程,则让 init 进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,则释放控制终端,并向属于该会话的所有进程发送挂断信号,这通常会终止该会话中所有进程,然后把进程状态置为僵死状态,并向其父进程发送 ISGCHLD 信号,通知其某个子进程已经终止。最后 do_exit() 调用调度函数去执行其他进程,进程在被终止时,它的任务数据结构仍然保留着。

在子进程在执行期间,父进程通常使用 wait() 或 waitpid() 函数等待某个子进程终止。当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己进程中,最终释放已终止子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。

5.8 Linux 系统中堆栈的使用方法

Linux 0.11 系统中共使用了四种堆栈:

  1. 系统引导初始化时临时使用的堆栈;
  2. 进入保护模式之后提供内核程序初始化使用的堆栈:位于内核代码地址空间固定位置处,该堆栈也是后来任务 0 使用的用户态堆栈;
  3. 任务的内核态堆栈:每个任务通过系统调用,执行内核程序时使用的堆栈 ;
  4. 任务的用户态堆栈:位于任务逻辑地址空间近末端处;

为什么有这么多堆栈类型?

  1. 从实模式进入保护模式,使得 CPU 对内存寻址方式发生了变化,因此需要重新调整设置栈区域。
  2. 解决不同 CPU 特权级共享使用堆栈带来的保护问题(当一个任务进入内核态运行时,就会使用其TSS段中给出的特权级0的堆栈指针,即内核态栈,原用户栈指针会保存在内核栈中,而当从内核态返回用户态时,就会恢复使用用户态的堆栈。

5.8.1 初始化阶段

开机初始化时(bootsect.s,setup.s)

当 bootsect.s 代码被 ROM BIOS 引导加载到物理内存 0x7c00 处,并没有设置堆栈。直到 bootsect.s 被移动到 0x9000:0 处时,才把堆栈段寄存器 SS 设置为 0x9000,堆栈指针 esp 寄存器设置为 0xff00,setup.s 程序中也沿用了 bootsect 中设置的堆栈段。

进入保护模式时(head.s)

下述地址位置可以从编译内核时生成的 system.map 文件中查到。

从 head.s 程序起,系统开始正式在保护模式下运行。此时堆栈段被设置为内核数据段(0x10),堆栈指针 esp 设置成指向 user_stack 数组的顶端,保留了 1 页内存(4K)作为堆栈使用。user_stack 数组定义在 sched.c。在这里插入图片描述 初始化时(main.c)

在 init/main.c 程序中,在执行 move_to_user_mode() 代码把控制权移交给任务 0 之前,系统一直使用上述堆栈。而在执行过 move_to_user_mode() 之后,main.c 的代码被切换成任务 0 中执行。通过执行 fork() 系统调用,main.c 中的 init() 将在任务 1 中执行,并使用任务 1 的堆栈,而 main() 本身责备切换成任务 0 后,仍然使用上述内核程序自己的堆栈作为任务 0 的用户态堆栈。

5.8.2 任务的堆栈

每个任务都有两个堆栈,分别用于用户态和内核态程序执行。除了处于不同的 CPU 特权级中,这两个堆栈之间的主要区别在于任务的内核态堆栈很小,所保存的数据量最多不能超过(4096-任务数据结构块)个字节,而任务的用户态堆栈却可以在用户的64MB空间延申。

在用户态运行时

每个任务(除了任务 0 和任务 1)有自己的 64MB 地址空间,当一个任务刚被创建时,**它的用户态堆栈指针被设置在其地址空间的靠近末端部分。**实际上末端部分还要包括执行程序的参数和环境变量,然后才是用户堆栈空间,如图所示。 在这里插入图片描述 应用程序在用户态下运行时就一直使用这个堆栈。堆栈实际使用的物理内存则由 CPU 分页机制确定。**由于 Linux 实现了写时复制功能,因此在进程被创建后,若该进程及其父进程都没有使用堆栈,则两者共享同一堆栈对应的物理内存页面。**只有当其中一个进程执行堆栈写操作时内核内存管理程序才会为写操作进程分配新的内存页面。而进程 0 和进程 1 的用户堆栈比较特殊。

在内核态运行时

每个任务都有其自己的内核态堆栈,用于任务在内核代码中执行期间。其所在线性地址中的位置由该任务 TSS 段中 ss0 和 esp0 两个字段指定。 ss0 是任务内核态堆栈的段选择符,esp0 是堆栈栈底指针。因此每当任务从用户代码转移进入内核代码中执行时,任务的内核态栈是空的。任务内核态堆栈被设置在位于其任务数据结构所在页面的末端,即与任务的任务数据结构放在同一页面内。 这是在建立新任务时,fork() 程序在任务 tss 段的内核级堆栈字段中设置的(kernel/fork.c)。

p->tss.esp0 = PAGE_SIZE+(long)p;
p->tss.ss0 = 0x10;

p 是新任务的任务数据结构指针,tss 是任务状态段结构。内核为新任务申请内存用作保存其 task_struct 结构数据,而 tss 段是 task_struct 中的一个字段。该任务的内核堆栈段值 tss.ss0 也被设置成为 0x10(内核数据段选择符),而 tss.esp0 则指向 task_struct 结构页面的末端。实际上 tss.esp0 被设置成指向该页面上一字节处。Intel CPU 执行堆栈操作时,先递减堆栈指针esp,然后再存入内容。

在这里插入图片描述

为什么从主内存区申请得来的用于保存任务数据结构的一页内存也能被用来设置成内核数据段中的数据呢?

这是因为用户内核态仍然属于内核数据空间。在 head.s 程序的末端,分别设置了内核代码段和数据段的描述符,段长度都被设置成了 16MB,这个长度值是 Linux 0.11 内核所能支持的最大物理内存长度。因此,内核代码可以寻址到整个物理内存范围中的任何位置,当然也包括主内存区。每当任务执行内核程序而需要使用其内核栈时,CPU 就会利用 TSS 结构把它的内核态堆栈设置成由 tss.ss0 和 tss.esp0 这两个值构成。在任务切换时,老任务的内核栈指针 esp0 不会被保存。对于 CPU 来讲,这两个值是只读的。因此每当一个任务进入内核态执行时,其内核态堆栈总是空的。

任务 0 和任务 1 的堆栈

任务 0(空闲进程 idle) 和任务 1(初始化进程 init) 的堆栈比较特殊。

任务 0 和任务 1 的代码段和数据段相同,限长也都是 640KB,但它们被映射到不同的线性地址范围中,任务 0 的段基地址从线性地址 0 开始,而任务 1 的段基地址从 64MB 开始,但是它们全都映射到物理地址 0-640KB 范围中,这个地址范围也就是内核代码和基本数据所存放的地方。

在执行了 move_to_user_mode() 之后,任务 0 和任务 1 的内核态堆栈分别位于各自任务数据结构所在页面的末端,而任务 0 的用户态堆栈就是前面进入保护模式后所使用的堆栈,即 sched.c 的 user_stack[] 数组的位置。

由于任务 1 在创建时复制了任务 0 的用户堆栈,因此刚开始时任务 0 和任务 1 共享使用同一个用户堆栈空间。但是当任务 1 在创建时复制了任务 0 的用户堆栈,因此刚开始时任务 0 和任务 1 共享使用同一个用户堆栈空间。但是当任务 1 开始运行时,由于任务 1 映射到 user_stack[] 处的页表项被设置成只读,使得任务 1 在执行堆栈操作时将会引起写页面异常,从而内核会使用写时复制机制为任务 1 另行分配主内存区页面作为堆空间使用。只有到此时,任务 1 才开始使用自己独立的用户堆栈内存页面。因此任务 0 的堆栈需要任务 1 实际开始使用之前保持“干净”,即任务 0 此时不能使用堆栈,以确保复制的堆栈页面中不含有任务 0 的数据。

任务 0 的内核态堆栈是在其人工设置的初始化任务数据结构中指定的,而它的用户堆栈是在执行 move_to_user_mode() 时,在模拟 iret 返回之前的堆栈中设置的。我们知道,当进行特权级会发生变化的控制权转移时,目的代码会使用新特权级的堆栈,而原特权级代码堆栈指针将保留在新堆栈中。因此这里先把任务 0 用户堆栈指针压入当前处于特权级 0 的堆栈中,同时把代码指针也压入堆栈,然后执行 IRET 指令即可实现把控制权从特权级 0 的代码转移到特权级 3 的任务 0 代码中。

在这个人工设置内容的堆栈中,原 esp 值被设置成仍然是 user_stack 中原来的位置值,而原 ss 段选择符被设置成 0x17,即设置成用户态局部表 LDT 中的数据段选择符。然后把任务 0 代码段选择符 0x0f 压入堆栈作为栈中原 CS 段的选择符,把下一条指令的指针作为原 EIP 压入堆栈。这样,通过执行 IRET 指令即可返回任务 0 的代码中继续执行。

5.8.3 任务内核态堆栈与用户态堆栈之间的切换

在 Linux0.11 系统中,所有中断服务程序都属于内核代码。如果一个中断产生时任务正在用户代码中执行,那么该中断就会引起 CPU 特权级从 3 级到 0 级的变化,(中断发生在用户态)此时 CPU 就会进行用户态堆栈到内核态堆栈的切换操作。 CPU 会从当前任务的任务状态段 TSS 中取得新堆栈的段选择符和偏移值。 因为中断服务程序在内核中,属于 0 级特权级代码,内核态堆栈指针从 TSS 的 ss0 和 esp0 字段中获得。在定位了新堆栈之后,CPU 就会把原用户态堆栈指针 ss 和 esp 压入内核态堆栈,随后把标志寄存器 eflags 的内容和返回位置 cs、eip 压入内核态堆栈。(用户态的堆栈地址保存在内核态堆栈中,以供正确返回)

内核的系统调用是一个软件中断,因此任务调用系统调用时就会进入内核并执行内核中的中断服务代码。此时内核代码就会使用该任务的内核态堆栈进行操作。同样,进入内核程序时,由于特权级别发生了改变,用户态堆栈的堆栈段和堆栈指针以及 eflags 会被保存在任务的内核态堆栈中。 而在执行 iret 退出内核程序返回到用户程序时,将恢复用户态的堆栈和 eflags。 在这里插入图片描述 (中断发生在内核态) 如果一个任务正在内核态中运行,那么若 CPU 响应中断就不再需要进行堆栈切换操作,因此此时该任务运行的内核代码已经在使用内核态堆栈,并且不涉及优先级别的变化,**所以 CPU 仅把 eflags 和中断返回指针 cs、eip 压入当前内核态堆栈,**然后执行中断服务过程。

5.9 Linux 0.11 采用的文件系统

存放文件系统的设备就是文件系统设备,硬盘上按一定规则存放的文件就组成文件系统。而 Linux0.11 内核所支持的文件系统是 MINIX 1.0 文件系统,目前 Linux 系统上使用最广泛的是 ext2 或者 ext3 文件系统。

在软盘上运行的 Linux 0.11 系统,它由简单的 2 张软盘组成,bootimage 盘和 rootimage 盘。bootimage 是引导启动 Image 文件,其中主要包括磁盘引导扇区代码、操作系统加载程序和内核执行代码。rootimage 就是用于向内核提供最基本支持的根文件系统。这两个盘合起来就相当于一张可启动的 DOS 操作系统盘。

当 Linux 启动盘加载根文件系统是,会根据启动盘上引导扇区第 509、510 字节处中的根文件系统设备号从指定的设备中加载根文件系统。

5.10 Linux 内核源代码的目录结构

在这里插入图片描述

5.10.1 内核主目录 linux

包括 14 个子目录,Makefile —— 编译辅助工具软件 make 的参数配置文件。make 工具软件的主要用途是通过识别哪些文件已被修改过,从而自动地决定在一个含有多个源程序文件的程序系统中哪些文件需要被重新编译。

Makefile 还嵌套调用了所有子目录中包含的 Makefile 文件。

5.10.2 引导启动程序目录 boot

含有 3 个汇编语言文件,是内核源代码文件中最先被编译的程序。这 3 个程序完成的主要功能是当计算机加点时引导内核启动,将内核代码加载到内存中,并做一些进入 32 位保护运行方式前的系统初始化相关工作。

  • bootsect.s:as86 软件编译。磁盘引导块扇区。
  • setup.s:as86 软件编译。主要用于读取机器的硬件配置参数。
  • head.s:GNU as 编译。被编译链接在 system 模块的最前部分,进行硬件设备的探测和内存管理页面的初始化。

5.10.3 fs

采用了 1.0 版的 MINIX 文件系统。Linux 是在 MINIX 系统上开发的,采用 MINIX 文件系统便于进行交叉编译,MINIX 对文件系统采用单线程处理方式,而 Linux 则采用了多线程方式,由于采用了多线程处理方式,Linux 程序就必须处理多线程带来的竞争、死锁问题,Linux 文件系统代码要比 MINIX 系统复杂的多。 在这里插入图片描述

5.10.4 include

在这里插入图片描述

5.10.5 内核初始化程序目录 init

包含一个文件 main.c,用于内核所有的初始化工作,然后移到用户模式创建新进程,并在控制台设备上运行 shell 程序。程序首先根据机器内存的多少对缓冲区内存容量进行分配,如果还设置了要使用虚拟盘,则在缓冲区后面也为它留下空间。之后就进行所有硬件的初始化工作,包括人工创建第一个任务,并设置了中断允许标志,在执行从核心态移到用户态之后,系统第一次调用创建进程函数 fork(),创建出一个用于运行 init() 的进程,在该子进程中,系统将进行控制台环境设置,并且在生成一个子进程用来运行 shell 程序。

5.10.6 内核程序主目录 kernel

包含 12 个代码文件和一个 Makefile 文件,另外还有 3 个子目录,所有处理任务的程序都保存在 kernel/ 目录中,其中包括像 fork、exit、调度程序以及一些系统调用程序等。

5.10.7 内核函数目录 lib

与普通程序不同,内核代码不能使用标准 C 函数库以及其它一些函数库。主要原因是由于完整的 C 函数库很大。因此在内核源代码中专门有一个 lib/ 目录提供内核需要用到的一些函数。内核函数库用于为内核初始化程序 init/main.c 运行在用户态的进程提供调用支持。它与普通静态库的实现方法一样。

有12 个 .c 文件。

5.10.8 内存管理程序目录 mm

主要用于管理程序对主内存区的使用,实现了进程逻辑地址到线性地址以及线性地址到主内存中物理内存地址的映射,通过内存的分页管理机制,在进程的虚拟内存页与主内存区的物理内存页之间建立对应关系。

Linux 内核对内存的处理使用了分页和分段两种方式。首先是将 386 的 4G 虚拟地址空间分割成 64 个段,每个段 64MB.所有内核程序占用其中第一个段,并且物理地址与该段线性地址相同。然后每个任务分配一个段使用。分页机制用于把指定的物理内存页面映射到段内,检测 fork 创建的任何重复的拷贝,并执行写时复制机制。

5.10.9 编译内核工具程序目录 tools

build.c 程序用于将 Linux 各个目录被分别编译生成的目标代码链接合并成一个可运行的内核映像文件 image。

5.11 内核系统与应用程序的关系

  • 系统调用接口 int 0x80
  • 开发环境库函数或内核库函数(仅供任务0和任务1使用,最终还是去调用系统调用)

内核对所有用户程序进程实际上只提供系统调用这一种统一的接口。系统调用主要提供给系统软件编程或者用于库函数的实现,而一般用户开发的程序则是通过调用像 libc 等库中函数来访问内核资源,这些库中的函数或资源通常被称为应用程序编程接口。

系统调用是内核与外界接口的最高层。在内核中,每个系统调用都有一个序列号,并且倡议宏的形式实现,应用程序不应该直接使用系统调用,否则会带来较差的移植性。

库函数一般包括 C 语言没有提供的执行高级功能的用户级函数,例如输入/输出和字符串处理函数。某些库函数只是系统调用的增强功能版。标准 I/O 库函数 fopen 和 fclose 提供了与系统调用 open 和 close 类似的功能,但却在更高的层次上。系统调用通常能提供比库函数略微好一些的性能,但是库函数却能提供更多的功能,更具检错能力。

在这里插入图片描述

P192

#
# if you want the ram-disk device, define this to be the
# size in blocks.
#
RAMDISK = #-DRAMDISK=512

AS86	=as86 -0 -a
LD86	=ld86 -0

AS	=gas
LD	=gld
LDFLAGS	=-s -x -M
CC	=gcc $(RAMDISK)
CFLAGS	=-Wall -O -fstrength-reduce -fomit-frame-pointer \
-fcombine-regs -mstring-insns
CPP	=cpp -nostdinc -Iinclude

#
# ROOT_DEV specifies the default root-device when making the image.
# This can be either FLOPPY, /dev/xxxx or empty, in which case the
# default of /dev/hd6 is used by 'build'.
#
ROOT_DEV=/dev/hd6

ARCHIVES=kernel/kernel.o mm/mm.o fs/fs.o
DRIVERS =kernel/blk_drv/blk_drv.a kernel/chr_drv/chr_drv.a
MATH	=kernel/math/math.a
LIBS	=lib/lib.a

.c.s:
	$(CC) $(CFLAGS) \
	-nostdinc -Iinclude -S -o $*.s $<
.s.o:
	$(AS) -c -o $*.o $<
.c.o:
	$(CC) $(CFLAGS) \
	-nostdinc -Iinclude -c -o $*.o $<

all:	Image

Image: boot/bootsect boot/setup tools/system tools/build
	tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) > Image
	sync

disk: Image
	dd bs=8192 if=Image of=/dev/PS0

tools/build: tools/build.c
	$(CC) $(CFLAGS) \
	-o tools/build tools/build.c

boot/head.o: boot/head.s

tools/system:	boot/head.o init/main.o \
		$(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS)
	$(LD) $(LDFLAGS) boot/head.o init/main.o \
	$(ARCHIVES) \
	$(DRIVERS) \
	$(MATH) \
	$(LIBS) \
	-o tools/system > System.map

kernel/math/math.a:
	(cd kernel/math; make)

kernel/blk_drv/blk_drv.a:
	(cd kernel/blk_drv; make)

kernel/chr_drv/chr_drv.a:
	(cd kernel/chr_drv; make)

kernel/kernel.o:
	(cd kernel; make)

mm/mm.o:
	(cd mm; make)

fs/fs.o:
	(cd fs; make)

lib/lib.a:
	(cd lib; make)

boot/setup: boot/setup.s
	$(AS86) -o boot/setup.o boot/setup.s
	$(LD86) -s -o boot/setup boot/setup.o

boot/bootsect:	boot/bootsect.s
	$(AS86) -o boot/bootsect.o boot/bootsect.s
	$(LD86) -s -o boot/bootsect boot/bootsect.o

tmp.s:	boot/bootsect.s tools/system
	(echo -n "SYSSIZE = (";ls -l tools/system | grep system \
		| cut -c25-31 | tr '\012' ' '; echo "+ 15 ) / 16") > tmp.s
	cat boot/bootsect.s >> tmp.s

clean:
	rm -f Image System.map tmp_make core boot/bootsect boot/setup
	rm -f init/*.o tools/system tools/build boot/*.o
	(cd mm;make clean)
	(cd fs;make clean)
	(cd kernel;make clean)
	(cd lib;make clean)

backup: clean
	(cd .. ; tar cf - linux | compress - > backup.Z)
	sync

dep:
	sed '/\#\#\# Dependencies/q' < Makefile > tmp_make
	(for i in init/*.c;do echo -n "init/";$(CPP) -M $$i;done) >> tmp_make
	cp tmp_make Makefile
	(cd fs; make dep)
	(cd kernel; make dep)
	(cd mm; make dep)

### Dependencies:
init/main.o : init/main.c include/unistd.h include/sys/stat.h \
  include/sys/types.h include/sys/times.h include/sys/utsname.h \
  include/utime.h include/time.h include/linux/tty.h include/termios.h \
  include/linux/sched.h include/linux/head.h include/linux/fs.h \
  include/linux/mm.h include/signal.h include/asm/system.h include/asm/io.h \
  include/stddef.h include/stdarg.h include/fcntl.h