Linux学习笔记(linux 0.11完全注释)
阅读原文时间:2021年04月20日阅读:1

第一章:概述

GNU计划:旨在开发一个类似UNIX并且是自由软件的完整操作系统。

POSIX标准:是由IEEE和ISO/IEC开发的一簇标准。该标准基于UNIX的实践和经验,描述了操作系统的调用和服务接口。用来保证编制的应用程序能在源代码一级上在多个操作系统上移植和运行。

Linux-0.11 版本发布时包括以下文件:

bootimage.Z     --  具有美国键盘代码的压缩启动映像文件

rootimage.Z     --  以 1200KB 压缩的根文件系统映像文件

linux-0.11-tar.Z     --  内核源码文件。大小为 94KB, 展开后也仅有 KB

as86.tar.Z     --  Bruce Evans' 的二进制执行文件。是 16 位的汇编程序和装入程序

INSTALL-0.11    --  更新过的安装信息文件

bootimage.Z 和 rootimage.Z 是 压缩的软盘映像文件。 bootimage 是引导启动 Image 文件,主要包括磁盘引导扇区代码、 操作系统加载程序和内核执行程序。 PC 启动时 ROM BIOS 中的程序会把默认启动驱动器上的引导扇区代码和数据读入内存, 而引导扇区代码负责把操作系统加载程序和内核执行代码读入内存中,然后把操作系统的控制权交给操作系统加载程序去进 一步准备内核的初始化操作,最终加载程序会把控制权交给内核代码。内核若要正常运行就需要文件系统的支持, rootimage 就是用于向内核提供最基本支持的根文件系统。这 2 个盘合起来就相当于一张可启动的 dos 操作系统盘。 这些文件的下载地址如下:

http://oldlinux.org/Linux.old/images/

http://oldlinux.org/Linux.old/kernels/

http://oldlinux.org/Linux.old/bochs/

http://oldlinux.org/Linux.old/Linux-0.11/

第一部分:1至4章,基础知识部分,包括微机原理和80x86保护模式编程。

第 二部分: 5至7章,描述内核引导启动和32位运行方式的准备阶段。

第 三部分: 8至13章,内核代码的主要部分。

第 四部分: 14至16章,作为第三部分源代码阅读的参考信息。

第 五部分:针对linux 0.11内核的各种实验活动。

第二章:微机组成

总线插槽是数据总线、地址总线、控制线与扩展设备控制器的标准连接接口。总线接口标准通常有: ISA 、 EISA 、 PCI 、 AGP 、 PCIE 。

现代 PC 机主要使用 2 个超大规模芯片构成的芯片组或芯片集组成: southbridge 、 northbridge 。 Northbridge 用于与 CPU 、内存和 AGP 视频接口,这些接口具有高传输速率; northbridge 还起存储器控制作用,故 Intel 称之为 MCH ( memory controller hub );南桥芯片用于管理中低速器件,如 PCI 总线接口、 IDE 硬盘接口、 USB 端口等,故南桥芯片称之为 ICH ( I/O controller hub );

I/O 访问

I/O 控制器包含数据端口、命令端口、状态端口。 I/O 编址分 2 种:统一编址和独立编址。

统一编址中访问 I/O 跟访问内存一样,独立编址中有单独的 I/O 地址空间,用专门的指令进行访问。

IBM pc 主要使用独立编址, I/O 地址空间为 0~0x3ff; 也部分使用了统一编址,如显存就直接占用了内存空间 0xb800~0xbc00 。

在普通 Linux 系统下可以通过查看 /proc/ioports 可以得到相关控制器或设置使用的 I/O 地址范围。

接口数据传输有 3 种方式:查询、终端、 DMA 。

主存、 BIOS 、 CMOS 存储器

当计算机上电初始化时,物理内存被设为从 0 地址开始的连续区域。除了地址从 0XA0000 到 0XFFFFF(640K 到 1M) 和 0XFFFE0000 到 0xffffffff ( 4G 的最后 64K )范围外所有的内存都可以用作系统内存。这 2 个特定范围被特定用作 I/O 设备和 BIOS 程序。

当上电或者复位时, CPU 会把程序指针知道 4G 内存空间的最后 64K 的最后 16 字节处。 BIOS 会在这儿存放一条 JMP 指令跳转到 BIOS 代码中 64KB 范围内的某一条指令开始执行。 BIOS 在执行一系列硬件检测和初始化后,就会把原来 pc 兼容的 64KB 的代码和数据复制到内存低端 1M 末端的 64K 处,然后跳转到这个地方进入真正的实地址模式工作,如图 2-5 所示。最后, BIOS 就会从硬盘或者其他存储介质把操作系统引导程序加载到内存 0x7c00 处,并跳转到这个地方继续执行引导程序。

注:由于效率原因, Linux 只利用了 BIOS 的一些最基本的信息,并不使用 BIOS 。

在 PC/AT 机中,还使用了很小容量(只有 64 到 128 字节)的 CMOS 来存储计算机的实时时钟信息和硬件配置信息。

软盘和硬盘控制器

………………..

第三章 内核语言和编程环境

Ø As86汇编器+ld86链接器 boot/bootsect.s boot/setup.s Intel汇编

Ø GNU as汇编器+GNU链接器 除as针对的2个文件,包括c生成的汇编 AT&T汇编 默认输出为a.out

嵌入式汇编基本格式

Linux 0.11目标文件格式

第4章 80X86保护模式及其编程

4.1 80x86系统寄存器和系统指令

标志寄存器:标志寄存器EFLAGS中的系统标志和IOPL用于控制I/O访问、可屏蔽硬件中断、调试、任务切换以及虚拟8086模式。

内存管理寄存器:共4个内存管理寄存器,GDTR、LDTR、IDTR和TR,用于指定分段内存管理所使用的系统表的基地址。

GDTR:记录GDT(全局描述符表)的基地址和表长度。是用指令LGDT和SGDT分别用于load和store。

LDTR:记录LDT(局部描述符表)的基地址和表长度。是用指令LLDT和SSDT分别用于load和store。在任务切换是改变。

IDTR:记录IDT(中断描述符表)的基地址和表长度。是用指令LIDT和SIDT分别用于load和store。

TR:任务寄存器,在任务切换时改变。

控制寄存器:包括CR0~3

CR0中含有控制处理器操作模式和状态的系统控制标识;CR1保留不用;CR2含有导致也错误的线性地址。CR3含有页目录表物理内存基地址,故也称作页目录基地址寄存器PDBR.

CR0:PE—protection enable;PG—Paging

CR3:含有存放页目录表页面的物理地址,因为该地址是页对齐的,所以仅高20位有效。

4.2 保护模式内存管理

任何完整的内存管理系统都包含2个关键部分:保护和地址变换。

逻辑地址包含段选择符和偏移量,通过段选择符在段描述符表中查到段描述符(段描述符表[段选择符])。段描述符指定:段基址、段大小、访问权限和特权级、段类型。

分页机制即是把段再分为固定大小(常为4k)的页,存储在2个地方:1 物理内存,需要使用时直接使用; 2 硬盘或flash,需要使用时调入内存。

保护

任务之间的保护:全局地址空间,所有的任务都能访问,所有任务使用相同的虚拟地址访问映射到全局地址空间的物理地址,所有段的段描述符存于GDT众。局部地址空间,属于各个任务私有的虚拟地址空间,段描述符存在该私有任务对应的LDT中。

特权级保护:定义数据段的特权级。

4.3 分段机制

逻辑地址转成线性地址的步骤:

段描述符表:

4.4 分页机制

分页机制是实现虚拟存储的基础。

最近访问的页目录和页表会被放到处理器的缓冲器件中,该缓冲器件被称为转化查找缓冲区TLB(Translation Lookaside Buffer)。

页目录表常驻内存,页表可以存在Flash或硬盘上。并且任何时候,仅有一部分页表存储于物理内存中。

页目录表和页表的表项格式

4.5 保护

保护分段级保护和页级保护。

段级保护

段级保护依靠特权级:

数据访问的保护:

程序访问的保护:

1 调用或跳转:RET、JMP、CALL指令的近转移形式只是在当前代码段中执行程序的控制转移,因此不会执行特权级检查。远转移形式会变更代码段,所以会执行段转移。

2 门描述符:为了对不同特权级的代码提供受控的访问,处理器提供称之为门描述符的特殊描述符集。共有4种:Call Gate,Trap Gate,Interrupte Gate,Task Gate.

页级保护

分页机制只识别2类权限:特权级3为用户级,特权级0/1/2为超级用户级。

//note:CPU首先会执行段级保护,然后执行页级保护。

4.5 中断和异常处理

中断源:1 外部硬件中断,如中断有NMI引脚接收,不可屏蔽,使用固定的中断号2;如由INTR引脚接收,可屏蔽;2 软件中断(如INT)

//note:EFLAGs中的IF标志无法屏蔽INT软中断。

异常源:1 程序错误异常;2 软件产生的异常。异常可被细分为fault、trap、aborts。

中段描述符表(IDT):IDT存放3中类型的描述符:Interrupt Gate、Trap Gate、Task Gate.

当通过IDT中任务门来访问中断和异常处理过程就会导致任务切换

4.7 任务管理

任务是处理器可以分配调度、执行和挂起的一个工作单元。它可用于执行程序、任务或进程、操作系统服务、中断或异常处理过程和内核代码。

80x86中,描述符表中与任务相关的描述符有2类:任务状态段描述符和任务门。当执行权传给这2种描述符时,就会任务切换。

任务不可重入,任务切换不会把任何信息存入堆栈,而是存到任务状态段TSS中。

任务的结构和状态:

当前执行任务的状态由处理器中以下信息构成:

任务的执行:

TSS分动态字段和静态字段。

TSS描述符:TSS描述符只能存在GDT中。

任务门描述符:提供对一个任务间接、受保护地的引用。任务门描述符可以存放于LDT、GDT、IDT中。下图显示这3个表中的任务门描述符时如何指向同一任务。

任务切换的4种方式:

1 当前任务对GDT中的TSS描述符执行JMP或CALL指令。

2 当前任务对GDT或LDT中的任务门描述符执行JMP或CALL指令。

3 中断或异常向量指向IDT的任务门描述符。

4 当EFLAGS中的NT标志置位时当前任务执行RET指令。

任务链:

第5章 Linux内核体系结构

5.1 Linux内核模式

Linux是一个单内核,其层次大概划分如下:

5.2 Linux内核体系结构

Linux内核主要由5个模块组成:进程调度、内存管理、文件系统、进程间通信、网络接口。

5.3 Linux对内存的管理和使用

物理内存

内存地址空间概念:虚拟地址、逻辑地址、线性地址、物理地址。具体见前面的章节。

内存管理的缺页加载机制:当进程引用一个不存在物理内存中的的内存地址时,就会触发CPU产生一个缺页异常,并把需要访问的线性地址存放在CR2寄存器中。中断服务程序就会根据CR2的值,把需要访问的地址所在的页,从二级存储空间(如硬盘或者flash)加载到物理内存中。如果物理内存已全部被占用,就借助二级存储空间上的swap区,把内存中暂时不用的页面置换到二级存储空间,再把需要访问的页面置换到物理内存。

实模式下,段寄存器存放段基址;保护模式下,段寄存器存放段选择符,段选择符用来定位段描述符表中的表项。

而描述符表又分3中类型:GDT、LDT、IDT。

GDT是主要的基本描述符表,对应于全部地址空间。可被所有程序访问,通过段选择符定位到全部地址空间的一部分。

IDT的表项记录的是中断或异常处理程序入口的信息,功能类似于实模式下的中断向量表。

LDT对应于某个具体任务,某个具体的LDT代表对应任务所能访问到地址空间。所有LDT存放的位置,作为GDT的一个表项存放在GDT中。

//注:由GDT访问到LDT,再由LDT寻址到具体的地址空间。TSS段用于在任务切换时,用于保存和恢复相关任务的执行上下文。

内存分页管理:

CPU多任务和保护方式:

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

内核代码和数据的地址:对于Linux0.11来说,head.s中已经把内核数据段和代码段都设置成了长度为16MB的段。所以Linux0.11 在默认情况下只能管理最大为16MB的空间。

(a)内核代码段和数据段在线性地址空间和物理地址空间的地址一致。(b)GDT和IDT属于内核空间,故其线性地址和物理地址也一致。在实模式setup.s 中,必须设置好这2个表,为进入保护模式head.s做好准备。(c)进入head.s后,设置GDTR和IDTR指向这2个表,并重新加载表项,无需更换位置。(d)除任务0外,其他所有任务所需物理内存页面与线性地址不同或部分不同。

任务0的地址对应关系:

任务0是系统人启动的第一个任务,代码长度和数据长度均设为640K。tss0也是手工预设好的。如图所示:

任务1的地址对应关系:

任务1的代码也存在内核代码区域中。

任务1的用户堆栈空间将直接共享使用处于内核代码和数据区域中任务0的用户堆栈空间user_stack[]。创建任务1时,任务0和任务1共用user_stack[],当任务1开始运行时,任务1映射到user_stack[]的页表项被设为只读,使得任务在执行堆栈操作时会引起页面异常,从而使内核另行分配主内存区域的页面作为堆栈空间使用。

其他任务的地址对应关系

其他所有的任务(除任务0和任务1以外的任务),父进程都是init进程(任务1)。从任务2开始,如果任务号用nr来表示,那么nr在线性地址空间的起始地址为nr*64MB。

在任务2被创建后,将在其中运行execve()函数来执行shell程序。execve()会释放掉从任务1复制过来的页目录和页表表项,执行shell程序会重新设置相关的页目录和页表项。

注:在执行execve()系统调用时,系统虽然在线性地址空间为任务2分配了64MB的空间范围,但内核并不立刻为其分配和映射物理内存页面。当任务2开始执行,由于发生缺页异常才会分配和映射物理内存。这种方法称为需求加载(load on demand).同理,用malloc分配内存时,也没有进行物理内存的映射。而free也只是把相应内存块标记会空闲,并没有释放物理内存。

5.4 Linux中断机制

5.5 Linux的系统调用

系统调用时Linux内核与上层应用进行交互通信的唯一接口,其实质是通过int0x80实现。

系统调用的返回值为负表示错误,为0表示成功。错误时,错误码被存在全局变量errno中。调用库函数perror能将其打印出来。

在Linux内核中,每个系统调用都具有一个唯一的一个系统调用功能号。这些功能号定义在include/unistd.h中。这些调用号实质是sys_call_table的索引值。

系统调用的处理过程为:_syscalln-->系统调用函数(此处会有int0x80)—>system_call.s,通过功能号找到内核系统调用实现的入口地址。-->功能实现函数

注:前2步在用户空间,后2部在内核空间。

Linux系统调用最多传递3个参数。这3个参数被放在ebx、ecx、edx中,进入系统中断服务程序后,这几个寄存器会被自动保存到堆栈。

5.6 系统时间和定时

全局变量startup_time记录系统开始运行的时间,jiffies记录系统运行时间。故:

jiffies单位为10ms,hz为100。CURRENT_TIME的单位为s。

5.7 Linux进程控制

5.7.1 PCB(Process Control Block)

PCB保存着用于控制和管理进程的所有信息。主要包括进程当前运行的状态信息、信号、进程号、父进程号、运行时间累计值、正在使用的文件和本任务使用的局部描述符以及任务状态段信息。

进程运行状态:

5.7.3 进程初始化

在boot目录中(含三个汇编文件),引导程序把内核内核从磁盘加载到内存中,并让系统进入保护模式后,然后启动执行init/main.c。该程序首先分配物理内存,然后调用内核各部分的初始化函数对内存管理、中断处理、块设备和字符设备、进程管理以及对硬盘和软盘硬件进行初始化操作。然后把自己手工移动到进程0中运行,并使用fork创建出进程1。然后进入进程1中进行,此后进程0只会执行pause函数,其中又会去执行任务调度函数。

5.7.4 创建新进程

使用fork创建新进程时,首先在任务数组中寻找一个未被任何任务使用的空槽,并为新进程申请一页的内存来存放PCB。然后把新进程设置为TASK_UNINTERRUPTIBLE,修改复制过来的PCB。然后设置新任务的代码段和数据段,但不分配物理内存,使用copy on write技术。新进程在执行execve后才真正运行。

5.7.5 进程调度

Linux进程时抢占式的,被抢占的进程仍然处于TASK_RUNNING状态。内核态执行的进程不允许被抢占。

任务调度程序schedule首先扫描任务数组,比较处于TASK_RUNNING状态的任务的counter,执行宏swith_to切换至剩余时间片多的任务。如果无进程运行,将执行进程0。

进程切换如下图所示,整个过程从长跳转至新任务的TSS指定的地址处,造成CPU的任务切换操作(保存当前tss,加载新tss至寄存器)。

5.7.6 终止进程

使用exit系统调用,会执行内核函数do_exit().释放内存,把所有的子进程的父进程设成init进程。释放控制终端。设置状态为TASK_ZOMBIE.并向原父进程发SIGGHLD信号。父进程通常在调用wait或waitpid等待这个信号。

5.8 linux的堆栈

Linux使用4中堆栈:boot的临时堆栈;进入保护模式后的堆栈,也是后来任务0的用户态堆栈;任务内核态堆栈;任务用户态堆栈。

bootsect.s被加载到0x7c00时,不使用堆栈;bootsect.s被移至0x9000时,设置栈顶为0x9000:0xff00;setup.s也使用该堆栈段;

进入实模式head.s,设置堆栈为user_stack的栈顶。

初始化main.c,在执行move_to_user_mode之后,main.c的代码被切换到任务0中进行。任务0继续使用以上堆栈作为自己的用户态堆栈。

5.8.2 任务的堆栈

每个任务有用户态和内核态堆栈,内核态堆栈小,不能超过4k-PCB字节数。而用户态堆栈可以向用户的64M空间延伸。

任务的用户态堆栈:

任务的内核态堆栈:由ss0和esp0指定。与PCB在同一页面。

任务态和用户态的堆栈切换

5.10 Linux内核源码目录结构

5.10.2 boot

bootsect.s编译后驻留在磁盘第一个扇区。开机后被BIOS加载到0x7c00处。

setup.s读取机器硬件配置参数,并把内核system模块移到适当的内存位置。

head.s是system模块的最前部分。

5.10.3 文件系统目录fs

分4个部分:高速缓冲区管理、底层文件操作、文件数据访问、文件高层函数。如图:

5.10.4 include(略)

5.10.5 init

仅包含main.c。

5.10.6 kernel

asm.s处理系统硬件异常引起的中断,调用traps.c进行中断处理。

exit.c处理进程终止。包括进程释放、会话终止、杀死进程、终止进程、挂起进程等。

fork.c给出了系统调用sys_fork使用的2个c函数:find_empty_process和copy_process.

panic.c显示内核出错信息并停机。

sched.c调度相关的函数。如wake_up、sleep_on、schedule。

signal.c信号处理相关,含do_signal()函数。

sys.c系统调用函数,部分还未实现。

system_call.s Linux系统调用的接口处理。

块设备驱动子目录 kernel/blk_drv:

hd.c针对硬盘;floppy。c针对软盘;ll_rw_blk.c针对其他块设备。

字符设备驱动子目录 kernel/chr_dev:

tty_io.c 包含tty字符设备的读写函数tty_read和tty_write。还包含为中断类型为读字符的处理函数do_tty_interrupt。

console.c 控制台初始化程序和写函数con_write。以及键盘和显示器中断的初始化设置con_init。

rs_io.s 串口的中断处理程序。

serial.c 串行通信处理。

tty_ioctl.c 实现tty的io控制接口函数tty_ioctl。

keyboard。s 键盘中断处理。

5.10.7 内核库函数lib

为main.c和运行在用户态的任务0和1提供库支持。内核库函数是提取了c库函数的很小一部分,并经过优化。

5.10.8 内核管理程序目录mm

page.s包括内存页面异常中断处理,主要是缺页异常和写保护异常。

memory.c包括mem_init函数,和供page.s调用do_no_page()和do_wp_page()。

5.10.9 编译内核工具目录tools

该目录下的build.c用于将编译生成的.o连接合成可运行的image。

5.11 内核与APP的关系

Linux内核对用户程序提供2方面的支持:(1)对任务0和1提供内核lib函数支持;(2)提供系统调用

系统调用主要是提供给系统软件编程或者用于库函数编程。而一般用户开发程序则是通过调用像libc等库函数来访问内核资源。这些库函数被称为API。一个API可以对应于一个系统调用;也可以调用多个系统调用来实现某个功能。也有API不使用系统调用个,及不使用内核功能。

从移植性考虑,应用程序应该访问API,而不是系统调用。

5.12 Makefile

第6章 系统引导程序

6.1 总体功能

6.2 bootsect.s程序

bootsect.s被BIOS加载到0x7c00运行,bootsect.s运行期间,把自己移到0x90000处,然后加载setup.s至0x90200,然后调用BIOS中断13,显示loading。。。;然后加载其余system至0x10000处。