5.1 Linux内核模式
层次式的为内核模式
整体式的单内核模式
- Linux 0.11内核,采用但内核模式
- 优点:内核代码结构紧凑、执行速度快
- 缺点:层次结构性不强
- 流程:应用主程序使用指定的参数值执行系统调用指令(int x80),使CPU从用户态(User Mode)切换到核心态(Kernel Mode),然后操作系统根据具体的参数值调用特定的系统调用服务程序,而这些服务程序则根据需要再调用底层的一些支持函数以完成特定的功能。在完成了应用程序所要求的的服务后,操作系统又使CPU从核心态切换回用户态,从而返回到应用程序中继续执行后面的指令。
- 三个层次:调用服务的主程序层、执行系统调用的服务层和支持系统调用的底层函数
5.2 Linux内核系统体系结构
进程调度模块
- 进程调度模块用来负责控制进程对CPU资源的使用
- 调度策略:各进程能够公平合理地访问CPU,同时保证内核能及时地执行硬件操作
内存管理模块
- 用于确保所有进程能够安全地共享机器主内存去,同时,内存管理模块还支持虚拟内存管理方式,使得Linux支持进程使用比实际内存空间更多的内存容量
- 可以利用文件系统把暂时不用的内存数据交换到外部存储设备上去,当需要时再交换回来
文件系统模块
- 用于支持对外部设备的驱动和存储
- 虚拟文件系统模块通过向所有的外部存储设备提供一个通用的文件接口,隐藏了各种硬件设备的不同细节,从而提供并还吃与其他操作系统兼容的多种文件系统格式
进程间通信模块
- 用于支持多种进程间的信息交换方式
网络接口模块
- 提供对多种网络通信标准的访问并支持许多网络硬件
5.3 Linux内核对内存的管理和使用
5.3.1 物理内存
- 物理内存存在告诉缓冲区部分,高速缓冲区可以用于块设备的读写,用来提高读写速度
- 两种内存管理系统:内存分段系统和分页系统
5.3.2 内存地址
——程序的虚拟和逻辑地址,CPU的线性地址,实际物理内存地址
虚拟地址
- 组成:段选择符和段内偏移地址
- 包括:GDT映射的全局地址空间和LDT映射的局部地址空间
逻辑地址
- 由程序产生的与段相关的偏移地址部分
- 有些资料并不区分逻辑地址和虚拟地址的概念,而是将它们统称为逻辑地址
线性地址
- 虚拟地址到物理地址变换之间的中间层,是处理器可寻址的内存空间中的地址
- 线性地址= 段中的偏移地址(程序代码会产生逻辑地址)+ 相应段的基地址
- 开启分页机制,线性地址–>分页转换–>物理地址
- 未开启分页机制,线性地址==物理地址
物理地址
- 出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址
- 线性地址使用页目录和页表中的项变换成物理地址
虚拟存储(虚拟内存)
- 计算机呈现出要比实际拥有的内存大得多的内存量
5.3.3 内存分段机制
-
内存地址 = 段值(放在ds段寄存器中) + 偏移值(寻址寄存器如si)
-
在保护模式下,段寄存器中存放的是一个段描述符表中某一描述符项在表中的索引值
-
为了让CPU能顶为GDT表、IDT表和当前的LDT表,需要为CPU分别设置GDTR、IDTR和LDTR三个特殊寄存器
5.3.4 内存分页管理
- 基本原理:将CPU整个线性内存区域划分为4096字节为1页的内存页面,程序申请使用内存时,系统就以内存也为单位进行分配
- 优点:每个执行中的进程可以使用比实际内存容量大得多的连续地址空间
5.3.5 CPU多任务和保护方式
- 内核代码和数据是由所有任务共享的,因此它保存在全局地址空间中
- 每个进程都有自己的内核栈,当进程执行系统调用而陷入内核代码中执行时,则使用的自己的内核栈。
5.3.6 虚拟地址、线性地址和物理地址之间的关系
内核代码和数据的地址
- 0~16MB,该范围中包括内核所有的代码、内核段表(GDT、IDT、TSS)、页目录表和内核的二级页表、内核局部数据以及内核临时堆栈
- 内核代码段和数据段区域在线性地址空间和物理地址空间中是一样的,这样设置可以大大简化内核的初始化操作
任务0的地址对应关系
任务1的地址对应关系
其他任务的地址对应关系
5.3.7 用户申请内存的动态分配
- malloc分配完成之后,并不会有对应的硬件地址,只有当分配的地址被使用的时候,才会通过内存缺页管理机制自动为寻址对应的页面分配物理内存页面并进行映射操作
- free动态释放已申请的内存卡,但只有进程最终结束时才会全面收回已分配和映射到该进程地址空间范围的所有物理内存页面
5.4 Linux系统的中断机制
5.4.1 中断操作原理
- 可编程中断控制器(PIC)是微机系统中管理设备中断请求的管理者
- 当PIC向处理器的INT引脚发出一个中断信号时,处理器会立刻停下当时所做的事情并询问PIC需要执行哪个中断服务请求,处理器则根据读取的中断号通过查询中断向量表取得相关设备的中断向量并开始执行中断服务程序
- 软件中断:通过使用int指令并使用其操作数知名中断号,就可以让处理器去执行相应的中断处理过程
5.4.2 80X86微机的中断子系统
- 采用的是8259A可编程中断控制器芯片
- 主8259芯片的端口基地址是0x20,从芯片是0xA0
- 8259A芯片可以处于编程状态和操作状态
- 编程状态是CPU使用IN或OUT指令对8259A芯片进行初始化编程状态
5.4.3 中断向量表
- CPU根据中断号获取中断向量值,即对应中断服务程序的入口地址值
- 中断向量表就是内存中建立的一张查询表
- 每个中断向量由4个字节组成,这4个字节指明了一个中断服务程序的段值和段内偏移值
- 在linux下,IDT是Intel8086 – 80186 CPU中使用的中断向量表的直接替代物
5.4.4 Linux内核的中断处理
- 中断门和陷阱门的区别在于对标志寄存器EFLAGS中的中断允许标志IF的影响,由中断门描述符执行的中断会复位IF标志,因此可以避免其它中断干扰当前中断的处理,随后的中断结束指令iret会从堆栈上回复IF标志的原值,而通过陷阱门执行的中断则不会影响IF标志
5.4.5 标志寄存器的中断标志
- 内核代码中使用了cli和sti来皮面竞争条件和中断对临界代码区的干扰
- cli指令用来复位CPU标志寄存器中的中断标志,使得系统在执行cli指令后不会响应外部中断
- sti指令用来设置标志寄存器中的中断标志,以允许CPU能识别并响应外部设备发出的中断
5.5 Linux的系统调用
5.5.1 系统调用接口
- 系统调用是Linux内核与上层应用程序进行交互通信的唯一接口
- 用户程序通过直接或间接调用中断int 0x80,并在eax寄存器中指定系统调用功能号
5.5.2 系统调用处理过程
- 应用程序经过库函数想内核发出一个中断调用int 0x80时,就开始执行一个系统调用
- 寄存器eax中存放着系统调用号,而懈怠的参数可依次存放在寄存柜器ebx、ecx和edx中
- Linux0.11内核中用户能够向内核最多直接传递三个参数,当然也可以不带参数
5.5.3 Linux系统调用的参数传递方式
- 当进入系统中断服务程序而保存寄存器值时,这些传递参数的寄存器也被自动地放在了内核态堆栈上
5.6 系统时间和定时
5.6.1 系统时间
- UNIX日历时间,用户程序可以通过time()来获取,超级用户则可以通过stime()来修改系统时间
#define CURRENT_TIME (startup_time + jiffies/HZ)
通过这个宏定义来确定运行时刻的当前时间值
5.6.2 系统定时
5.7 Linux进程控制
- 程序是一个可执行的文件,而进程是一个执行中的程序实例
- 分时技术的基本原理是把CPU的运行时间划分成一个个规定长度的时间片,让每个进程在一个时间片内运行;当进程的时间片用完时系统就利用调度程序切换到另一个进程去运行
5.7.1 任务数据结构
- 在Linux系统中,进程表项是一个task_struct任务结构指针
- 其中保存着用于控制和管理进程的所有信息
- 主要包括进程当前运行的状态信息、信号、进程号、父进程号、运行时间累计值、正在使用的文件和本任务的局部描述符以及任务状态段信息
struct task_struct{
long state; //任务的运行状态
long counter; //任务运行时间技术,运行时间片
long priority; //运行优先数。任务开始运行时counter=priority,越大运行越长
long signal; //信号,是位图,每个比特位代表一个信号,信号值=位偏移值+1
struct sigaction sigaction[32];//信号执行属性结构,对应吸纳好将要执行的操作和标志信息
long blocked; //进程信号屏蔽码
int exit_code; //任务停止执行后的退出码,其父进程会来取
unsigned long start_code;//代码段地址
unsigned long end_code;//代码长度
unsigned long end_data; //代码长度 + 数据长度
unsigned long brk; //总长度
unsigned long start_stack;//堆栈段地址
long pid; //进程标志号
long father; // 父进程号
long pgrp; // 进程组号
long session; // 会话号
long leader; // 会话首领
unsigned short uid; // 用户标志号
unsigned short euid; // 有效用户id
unsigned short suid; // 保存的用户id
unsigned short gid; // 组标志号(组id)
unsigned short egid; // 有效组id
unsigned short sgid; // 保存的组id
long alarm; // 报警定时值(滴答数)
long utime; // 用户态运行时间(滴答数)
long stime; // 系统态运行时间(滴答数)
long cutime; // 子进程用户态运行时间
long cstime; // 子进程系统态运行时间
long start_time; // 进程开始运行时刻
unsigned short used_math; // 标志:是否使用了协处理器
int tty; // 进程使用tty终端的子设备号,-1表示没有使用
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]; // 文件结构指针表,最多32项,表项号即是文件描述符的值
struct desc_struct ldt[3]; // 局部描述符表, 0-空, 1-代码段cs, 2-数据和堆栈段ds&ss
struct tss_struct tss; // 进程的任务状态段信息结构
5.7.2 进程运行状态
5.7.3 进程初始化
- 在boot/目录中,引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序 init/main.c
- 确定如何分配使用系统物理内存
- 分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理
- 此后程序把自己“手工”移动到任务0中运行
5.7.4 创建新进程
- 寻找任务书中的空项
- 为新建进程申请内存存放其任务数据结构信息
- 刚创建新进程时,需要将其设置为不可中断的等待状态
- 修改任务数据结构
- 设置新任务的代码和数据段基址、限长,并复制当前进程内存分页管理的页表(note:只有当父进程或新集成中任意一个有写内存操作时,系统才会为执行写操作的进程分配相关的肚子使用的内存页面)
- 在GDT中设置新任务的TSS和LDT描述符项,将新任务设置成可运行状态并返回新进程号
- 子程序开始运行新程序时,由于此时内核还没有从块设备上加载该程序的代码,CPU就会立刻产生代码页面不存在的异常,此时内存管理程序就会从块涉笔上加载相应的代码页面,然后CPU又重新执行引起异常的指令
5.7.5 进程调度
- 内核中的调度程序用于选择系统中下一个要运行的程序
- Linux进程是抢占式的
- 进程的抢占只发生在用户态执行阶段,在内核态执行时是不能被抢占的
- Linux0.11采用了基于优先级排队的调度策略
调度程序
- 扫描任务组,比较就绪态的进程的运行时间,优先切换到运行时间较短的进程
- 计算进程需要运行的时间片值counter
- schedule()函数用于扫描任务组中所有处于就绪态的进程
- switch_to()函数执行实际的进程切换操作
- 系统空闲时,会选择任务0来运行
- 进程0会调用pause()将自己置位可中断的睡眠状态并再次调用schedule()函数
进程切换
- 检查要切换的进程是否为当前进程
- 把内核全局变量current置为新任务的指针,然后长跳转到新任务的任务状态段TSS组成的地址处,造成CPU执行任务切换
- CPU会把当前所有寄存器的状态保存到TR中TSS段选择符所指向的当前进程任务数据结构的tss结构中
- 把新任务状态段选择符所指向的新任务数据结构中tss结构中的寄存器信息恢复到CPU中
终止进程
- 释放进程代码段和数据段占用的内存页面
- 关闭进程打开着的所有文件
- 对进程使用的当前工作目录、根目录和运行程序的i节点进行同步操作
- 如果进程有子进程,则让init进程作为所有子程序的父进程
- 设置进程为僵死状态,并向进程的父进程发送SIGCHLD信号,通知其该子进程终止
- 任务终止时,它的任务数据结构仍然保留,因为其父进程会使用
- 父进程使用wait()或waitpid()函数等待某个子进程终止,当等待的子进程被终止或处于僵死状态时,父进程会最终释放该子程序任务数据结构所占用的内存页面,并置空子程序任务数据中所占用的指针项
5.8 Linux系统中堆栈的使用方法
5.9 Linux0.11采用的文件系统
目录 | 说明 |
---|---|
etc/ | 目录主要含有一些系统配置文件 |
dev/ | 含有设备特殊文件,用于使用文件操作语句操作设备 |
bin/ | 存放系统执行程序,例如sh、mkfs、fdisk等 |
usr/ | 存放库函数、手册和其它一些文件 |
usr/bin | 存放用户常用的普通命令 |
var/ | 用于存放系统运行时可变的数据或者是日志等信息 |
5.10 Linux内核源代码的目录结构
5.10.1 内核主目录linux
- 含有Makefile以及所有的子目录
- 用于编译
5.10.2 引导启动程序目录boot
- 含有三个文件,最先被编译的程序
- 主要的功能是当计算机加电时引导内核启动,将内核代码加载到内存中,并做一些进入32位保护运行方式前的系统初始化工作
- bootsect.s程序是磁盘引导块程序,编译后会驻留在磁盘的第一个扇区中,在PC机加电ROM BIOS自检后,将被BIOS加载到内存0x7c00处进行执行
- setup.s程序主要用于读取机器的硬件配置参数,并把内核模块system移动到适当的内存位置处
- head.s程序会被编译连接在system模块的最前部分,主要进行硬件设备的探测设备和内存管理页面的初始设置工作
5.10.3 文件系统目录fs
- 虚框中的程序文件不属于文件系统,带箭头的线条表示引用关系,粗线条表示有相互引用关系
- 高速缓冲区管理、底层文件操作、文件数据访问和文件高层函数
- 本目录中的程序主要用来管理高速缓冲区中缓冲块的使用分配和块设备上的文件系统
- 管理高速缓冲区的程序是buffer.c,其他程序则主要都是用于文件系统管理
5.10.4 头文件主目录include
- 主目录下有13个文件
文件名 | 文件说明 |
---|---|
<a.out.h> | a.out头文件,定义了a.out执行文件格式和一些宏 |
<const.h> | 常数符号头文件,目前仅定义了i节点中i_mode字段的各标志位 |
<ctype.h> | 字符类型头文件,定义了一些有关字符类型判断和转换的宏 |
<errno.h> | 错误号头文件,包含系统中各种出错号 |
<fcntl.h> | 文件控制头文件,用于文件及其描述符的操作控制常数符号的定义 |
<signal.h> | 信号头文件,定义信号符号常量,信号结构以及信号操作函数原型 |
<stdarg.h> | 标准参数头文件,以宏的形式定义变量参数列表 |
<stddef.h> | 标准定义头哦文件,定义了NULL,offsetof |
<string.h> | 字符串头文件,主要定义了一些有关字符串操作的嵌入函数 |
<termios.h> | 终端输入输出函数头文件,主要定义控制一步通信口的终端接口 |
<time.h> | 时间类型头文件,其中最主要定义了tm结构和一些有关时间的函数原形 |
<unistd.h> | Linux标准头文件,定义了各种符号常数和类型,并声明了各种函数 |
<utime.h> | 用户时间头文件,定义了访问和修改时间结构以及utime()原型 |
- 体系结构相关头文件子目录include/asm:主要定义了一些与CPU体系结构密切相关的数据结构、宏函数和变量
文件名 | 文件说明 |
---|---|
<asm/io.h> | io头文件,以宏的嵌入汇编程序行使定义对io端口操作的函数 |
<asm/memory.h> | 内存拷贝头文件,含有memcpy()嵌入式汇编宏函数 |
<asm/segment.h> | 段操作头文件,定义了有关段寄存器操作的嵌入式汇编函数 |
<asm/system.h> | 系统头文件,定义了设置或修改描述符/中断门等的嵌入式汇编宏 |
- Linux内核专用头文件子目录 include/linux
文件名 | 文件说明 |
---|---|
<linux/config.h> | 内核配置头文件,定义键盘语言和硬盘类型(HD_TYPE)可选项 |
<linux/fdreg.h> | 软驱头文件,含有软盘控制器参数的一些定义 |
<linux/fs.h> | 文件系统头文件,定义文件表结构(file,buffer_head,m_inode等) |
<linux/hdreg.h> | 硬盘参数头文件,定义访问硬盘寄存器端口,状态码,分区表等信息 |
<linux/head.h> | head头文件,定义了段描述符的简单结构,和几个选择符常量 |
<linux/kernel.h> | 内核头文件,含有一些内核常用函数的原型定义 |
<linux/mm.h> | 内存管理头文件,含有页面大小定义和一些页面释放函数原型 |
<linux/sched.h> | 调用程序头文件,定义了任务结构task_struct、初始任务0的数据等 |
<linux/sys.h> | 系统调用头文件,含有72个系统调用C函数处理函数,以‘sys_’开头 |
<linux/tty.h> | tty头文件,定义了有关tty_io,串行通信方面的参数、常数 |
- 系统专用数据结构子目录include/sys
文件名 | 文件说明 |
---|---|
<sys/stat.h> | 文件状态头文件,含有文件或文件系统状态结构stat{}和常量 |
<sys/times.h> | 定义了进程中运行时间结构tms以及times()函数原型 |
<sys/types.h> | 类型头文件,定义了基本的系统数据类型 |
<sys/utsname.h> | 系统名称结构头文件 |
<sys/wait.h> | 等待调用头文件,定义系统调用wait()和waitpid()及相关常数符号 |
5.10.5 内核初始化程序目录init
- 该目录中仅包含一个文件main.c,用于执行内核所有的初始化工作,然后移到用户模式创建新进程,并在控制台设备上运行shell程序
- 创建第一个任务
5.10.6 内核程序主目录kernel
块设备驱动程序子目录kernel/blk_drv
- blk.h中定义了3个c程序中共用的块设备结构和数据块请求结构
- hd.c程序主要实现对硬盘数据块的读/写驱动函数,主要是do_hd_request()函数
- floppy.c程序中主要实现了对软盘数据块的读/写驱动函数,主要是do_fd_request()函数
- ll_rw_blk.c中程序实现了低层块设备数据读/写函数ll_rw_block(),内核中所有其他程序都是通过该函数对块设备进行数据读写操作
字符设备驱动程序子目录 kernel/chr_drv
- 该目录含有4个c语言程序和2个汇编程序文件
- 实现了对串行端口rs-232、串行终端、键盘和控制台终端设备的驱动
协处理器方针和操作程序子目录kernel/math
5.10.7 内核库函数目录lib
- 由于完整的C函数库很大,内核代码不能使用标准C函数库及其他一些函数库
- 在lib/目录中共有12个C语言文件
5.10.8 内存管理程序目录mm
- 主要用于管理程序对主内存区的使用
- page.s文件包括内存页面异常中断(int 14)处理程序,主要用于处理程序由于缺页而引起的页异常中断和访问非法地址而引起的页保护
5.10.9 编译内核工具程序目录tools
该目录下的build.c程序用于将Linux各个目录中被分别编译生成的目标代码连接合并成一个可运行的内核映像文件image