本门课程的主题是LINUX内核
什么是LINUX内核?就是一个类UNIX操作系统
作为操作系统,其有以下几个重要组成部分:
进程管理功能
内存管理功能
虚拟文件系统
一、进程管理功能
进程的概念
定义
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
进程同程序的比较
进程是由PCB、程序段和数据段三部分组成的
动态-静态(动态性是进程最基本的特征)
暂时-永久
同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程。也就是说同一程序可以对应多个进程
进程控制块PCB
名字为task_struct的数据结构
进程的任务结构体是进程存在的唯一标志。
Linux在内存空间中开辟了一个专门的区域存放进程的 任务结构体。
进程结构体又叫做进程描述符
进程上下文
把系统提供给进程的处于动态变化的运行环境总和称为进程上下文。系统中的每一个进程都有它自己 的上下文。
进程因时间片用完或因等待某个事件而阻塞时,进程调度需要把CPU的使用权从当前进程交给另一个进程,这个过程称为进程切换(procdss switching)。
进程的切换又称为上下文切换(context switching)
进程环境
系统上下文
进程栈
linux系统为每个用户进程分配了两个栈:用户栈和内核栈。当一个进程在用户空间执行时, 系统使用用户栈;当在内核空间执行时,系统 使用内核栈。由于内核栈地址空间的限制,内核栈不会分配很大的空间。此外,内核进程只有内核栈,没有用户栈。
当进程从用户空间陷入到内核空间时,首先, 操作系统在内核栈中记录用户栈的当前位置, 然后将栈寄存器指向内核栈;内核空间的程序 执行完毕后,操作系统根据内核栈中记录的用 户栈位置,重新将栈寄存器指向用户栈
进程的状态
运行态:进程正在使用CPU运行的状态
可运行态:就绪态
等待态:又称睡眠态,它是进程正在等待某个事件或某个资源时所处的状态。
暂停态:进程需要接受某种特殊处理而暂时停止运行所处的状态。
僵死态:进程的运行已经结束,但它的任务结构体仍在系统中。
Task_struct结构的描述:
进程标识
进程状态(State)
进程调度信息和策略
进程通信有关的信息(IPC)
进程链接信息(Links)
时间和定时器信息(Times and Timers)
文件系统信息(Files System)
处理器相关的上下文信息
进程的控制
标识一个进程
使用PID (Process ID,PID)进程标识符的数来标识进程
获得当前进程pid:sys_getpid
current宏获取当前正在运行的进程描述符的指针,current->pid返回当前正在运行的进 程的PID值
进程链表
for_each_task宏扫描整个进程链表
SET_LINKS和REMOVE_LINKS宏用来 分别在进程链表中插入和删除一个进程 描述符
进程的切换
两步:
切换页全局目录以安装一个新的地址空间;
切换内核态堆栈和硬件上下文 ——硬件上下文提供了内核执行新进程所需要的所有信息,包括cpu寄存器
硬件上下文
进程共享的cpu的寄存器
switch_to宏执行进程切换
这个宏和函数的被调用关系: schedule() --> context_switch() --> switch_to -- > __switch_to()
切换全局页表项这个切换工作由 context_switch()完成。其中switch_to和__switch_to() 主要完成切换内核态堆栈和硬件上下文
进程的创建
Linux提供了几个系统调用来创建和终止进程,以及执行新程序
Fork,vfork和clone系统调用创建新进程
exec系统调用执行一个新程序
exit系统调用终止进程(进程也可以因收到信号而 终止)
进程的撤销
撤销过程分为
进程终止:释放进程占有的大部分资源 一般方式是exit()系统调用
exit()调用_exit()系统调用
_exit() 调用do_exit()释放进程所占资源,终止进程
进程删除:彻底删除进程的所有数据结构
do_exit()
删除内核对终止进程的大部分引用:
删除信号量队列中的进程描述符 PCB
删除进程描述符中与分页、文件系统、打开文件描 述符和信号处理相关数据结构
减小进程所用模块的引用计数
将进程描述符的exit_code字段设置为终止代号
更新父子进程的亲属关系,强制将自己的子进 程作为其它某个进程的子进程,以等待该父进 程进行删除
调用schedule()进行调度
进程删除
父进程调用wait()类系统调用检查子进程是否终止
若子进程包含终止代号,则父进程通过 release()释放僵死进程的描述符
释放进程id
从进程链表中删除进程描述符PCB
释放存放进程描述符的内存区
进程调度
进程调度的基本概念
衡量进程调度性能的指标有:
周转时间:任务到达到完成的时间
响应时间
CPU-I/O执行期
调度算法
先进先出算法
谁先来给谁cpu,用完再释放。
最短CPU运行期优先调度算法(SCBF--Shortest CPU Burst First)
选出下一个“CPU执行期最短”的进程,为之分配处理机
该算法虽可获得较好的调度性能,但难以准确地知道下一个CPU执行期,而只能根据每一个进程的执行历史来预测。
最高优先权(FPF)优先调度算法
该算法总是把处理机分配给就绪队列中具有最高优先权的进程。常用以下两种方法来确定进程的优先权
静态优先权
动态优先权
轮转法
分时系统下的时间片轮转法
系统将所有就绪进程按FIFO规则排队,按一定的时间间隔把处理机分配给队列 中的进程。
这样,就绪队列中所 有进程均可获得一个时间片的处理机而运行。
进程调度的功能
(1)记录系统中所有进程的执行情况
系统中各进程的执行情况和状态特征记录在各进程的PCB表中
(2)选择占有处理机的进程
(3)进行进程上下文切换
—个进程的上下文(context)包括进程的状态、有 关变量和数据结构的值、机器寄存器的值和PCB以及 有关程序、数据等。
当正在执行的进程由于某种原因要让出处理机时,系 统要做进程上下文切换,以使另一个进程得以执行。
调度的时机
时机1:进程状态发生变化时
运行态下的进程要等待某种资源
运行态下的进程在程序执行完毕后,通过调用内核函数do_exit()终止运行并转入僵死态。
运行态下的进程转入暂停态 (这三种都是要被换下去)
暂停态下的进城成为可运行态
处于等待态的进程被唤醒后,将加入到可运行队列中时 (这两种有可能要被换上去)
时机2 当前进程时间片用完时
时机3 进程从系统调用返回到用户态时
时机4 中断处理后,进程返回到用户态时
Linux进程调度
Linux的进程调度是基于优先级的调度
Linux的进程分为普通进程和实时进程,在基于优先级的算法下实时进程的优先级高于普通进程。
(Linux对实时进程和普通进程采用不同的调度策略)
Linux中进程的优先级是动态的,调度程序 周期性的调整他们的优先级,避免进程饥饿
普通进程按照SCHED_OTHER调度策略(时间片)进行进程调度。
实时进程按照SCHED_FIFO或SCHED_RR(时间片)策略进行调度。
RR和FIFO都只用于实时任务。
创建时优先级大于0(1-99)。
按照可抢占优先级调度算法进行。
就绪态的实时任务立即抢占非实时任务。
policy 进程调度策略,可通过系统调用 sys_sched_setscheduler()更改
priority
进程静态优先级,给出进程每次获取cpu后可使用 的时间(按jitty计算)。通过系统调用 sys_setpriority()、nice()改变。
进程动态优先级,Linux对普通的进程,根据动态优 先级进行调度。 普通进程的动态优先级是静态优先级调整过来 的。
rt_priority
实时进程的优先级,可通过系统调用 sys_sched_setscheduler()改变.
Counter
表示进程当前还可运行多久
进程开始运行时被赋为priority值,以后,每隔一 个tick(时钟中断)递减1,减到0时引起新一轮调度。
重新调度将从runqueue队列中选出运行运行权值 最大的就绪进程获得cpu。
创建一个新的进程时,子进程会继承父进程的一半剩余时 间片
Linux进程调度方法
采用动态优先级法,调度对象是可运行队列
可运行队列中优先级大的进程首先得到CPU
权值最大的进程做为下一个运行进程
2.6内核中采用动态优先级调度,权值即为进程的动态优先级本身
普通进程的动态优先级不小于100 (数越大级别越低)
实时进程不大于99
普通进程
静态优先级决定了进程的基本时间片,优先级越高获得的CPU运行时间片越长
时间片计算公式
周期性地修改进程的动态优先级(避免饥饿)
Linux对普通的进程,根据动态优先级进行调度。而动态优先级是由静态优先级 (static_prio)调整而来
静态优先级对用户隐藏,但可通过nice值更改
static_prio=MAX_RT_PRIO +nice+ 20
nice值的范围是-20~19,nice数值越大就使得 static_prio越大,最终进程优先级就越低
动态优先级值的范围是100(最高静态优先级) ~139(最低静态优先级)
动态优先级计算公式
活动进程&过期进程
交互进程和批处理进程
为提高交互进程的性能,用完时间片的 活动批处理进程 总是变成过期进程
用完时间片的活动交互进程通常仍然是活动进程, 调度程序重填时间片把它留在活动进程集合中
如果最老的过期进程等待了很长时间,或者过期进程比交互进程的优先级高,调度程序把用完时间片的活动交互进程移到过期进程集合中
实时进程
实时进程的动态优先级数范围1-99之间(1为最 高优先级)
每个实时进程都与一个实时优先级有关
实时进程总是活动进程
调度程序总是让优先级高的进程运行
几个进程优先级相同,调度程序选择FIFO
实时进程运行的过程中,禁止低优先级进程的执行
实时进程运行后会一直占用CPU资源,只有下列情况在会被另一进程替代:
被更高优先级的实时进程取代
执行了阻塞操作并进入睡眠
停止或被杀死
自愿放弃cpu(可以通过调用系统调用sched_yield() 放弃CPU)
基于时间片轮转的实时进程用完了时间片
基本时间片(base time quantum)
每个进程有一个基本时间片
可以通过nice、setpriority系统调用调整进程的基本时间片
新进程总是继承父进程的基本时间片
小结:
Linux的进程根据优先级排队
根据特定的算法计算出进程的优先级,用一个值表示
这个值表示把进程如何适当的分配给CPU
可运行进程被放在可运行队列中,一个CPU有一个可运行队列
可运行队列分为过期进程集合和活动进程集合
进程集合按进程的优先级,通过链表进行组织
进程按优先级调度 (可运行队列的关键数据结构runqueue)
优先级位图
进程链表 v
Linux中进程的优先级是动态的
调度程序会根据进程的行为周期性的调整进程的优先级 l
较长时间未分配到CPU的进程,通常↑ l
已经在CPU上运行了较长时间的进程,通常↓
二、内存管理功能
进程地址空间
Linux把进程地址空间分成内核区和用户区两部分
操作系统内核的代码和数据等被映射到内核区
进程可执行映像(代码和数据)映射到虚拟内存的用户区
一个进程所需的虚拟空间中的各个部分未必连续,这通常会形成若干离散的虚存“区间”(VM area)
一个虚拟“区间”是进程虚拟空间的一部分,这部分的虚拟空间是连续的并且有相同的一些属性
内核态和用户态分配内存的不同
内核中的函数以直接了当的方式获得动态内存
内核是操作系统中优先级最高的成分。
内核信任自己
采用前面介绍的页面级内存分配和小内存分配以及非连续内存区
给用户态进程分配内存时
请求被认为是不紧迫的
用户进程不可信任
因此,当用户态进程请求动态内存时,并没有立即获得实际的物理页框,而仅仅获得对一个新的线性地址区间的使用权
这个线性地址区间会成为进程地址空间的一部分,称作线性区(memory areas)
进程在访问某个线性空间之前,必须获得该线性空间的许可
进程最多能访问4GB的线性地址空间
内核使用线性区资源来表示线性地址空间
每个线性区由起始线性地址、长度和一些存取权限描述
一个进程的地址空间是由允许该进程访问的全部线性地址组成
内存描述符
与进程地址空间有关的全部信息都包含在一个叫做内存描述符的数据结构中
每个进程有且只有一个mm_struct结构,描述进程的虚拟内存
进程描述符的mm字段指向mm_struct结构
mm_users:共享mm_struct的轻量级进程的个数
mm_count:内存描述符的主使用计数器
mm_users次使用计数器的所有用户在mm_count中只作为一个单位
每当mm_users减少时,内核都要检查它是否变为0,若是,则解除这个内存描述符
如果把内核描述符暂时借给一个内核线程,则增加mm_count
内核线程的内存描述符
内核线程仅运行在内核态,不会访问低于TASK_SIZE(等于0xc0000000)的地址
内核线程不使用线性区,不拥有内存描述符
进程描述符PCB中的两个内存描述符mm和active_mm
mm:指向进程拥有的内存描述符
active_mm:指向进程运行时所使用的内存描述符
对普通进程而言,这两个字段存放相同的指针
对内核线程而言
由于不拥有任何内存描述符,mm字段总为NULL
当内核线程运行时,active_mm被初始化为前一个运行进程的active_mm的值
获得当前进程的内存描述符
进程描述符中有一个mm域,这里边存放的就是该进程使用的内存描述符,通过current->mm便可以指向当前进程的内存描述符
通常,每个进程都有唯一的mm_struct结构体
内存描述符的释放
在进程退出时,exit_mm()函数被调用
首先做一些常规清除工作,更新一些内核全局统计数据
接着调用mmput(),减少内存描述符的mm_users域
如果mm_users域变成0,就调用mmdrop()函数减mm_count域
如果mm_count域变成0,就由free_mm()宏调用kmem_cache_free()函数把mm_struct返还给mm_cachp指向slab缓存
线性区(memory region)
由vm_area_struct结构体描述
描述地址空间内连续区间上的一个独立内存范围
线性区的开始和结束都必须4KB对齐
进程只能访问某个有效的线性区
每个线性区由一个vm_area_struct结构来表示
这个结构描述了一段给定的内存区间
区间中的地址都有同样的属性,比如同样的存取权限和相关的操作函数
用这个结构可以表示各种线性区,比如映射可执行的二进制代码的线形区、用作用户态堆栈的线形区等等
线性区链表
进程所拥有的线性区通过链表连接在一起。
链表中的线性区按内存地址的升序排列,线性区可由未用的内存地址区隔开。
内核通过进程的内存描述符的mmap字段来查找线性区, mmap字段指向链表中的第一个线性区描述符。
线性区的红黑树结构
mmap指向的线性区链表用来遍历整个进程的地址空间
红黑树mm_rb用来定位一个给定的线性地址落在进程地址空间中的哪一个线性区中
通过内存描述符中的两个域mmap和mm_rb都可以访问线性区。事实上,它们都指向了同一个vm_area_struct结构,只是链接的方式不同
处理线性区
内核进程需要对一个线性区进行处理,比如确定一个给定线性地址是否存在于一个线性地址空间中
查找线性区
查找空闲的进程地址区间
向内存描述符链表插入线性区
分配/释放线性地址区间
查找给定地址的最邻近线性区
查找与给定地址线性区相重叠的线性区
查找一个空闲的地址区间
向内存描述符链表插入一个线性区
创建一个线性区间
do_mmap(),为当前进程创建并初始化一个新的线性区。
如果创建的地址区间和已存在的地址区间相邻,且有相同的访问权限,则将区间合并
删除一个线性区间
缺页异常
内核只是通过mmap()等调用分配了一些线性地址空间给进程,并没有真正的把实际的物理页框分配给进程
do_page_fault()函数分析
读取并确定引起缺页的线性地址
判断缺页的线性地址是否在内核空间
程序检查异常发生时是否内核正在执行一些关键例程或内核线程
......
请求调页
一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止,也就是说一直推迟到进程要访问的页不在RAM中为止,由此引起缺页异常
写时复制机制
进程复制父进程的整个地址空间非常耗时:
为子进程的页表分配页框
为子进程的页分配页框
初始化子进程的页表
把父进程的页复制到子进程相应的页中
这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期;并且这样做经常毫无意义:
许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的空间
写时复制:
父进程和子进程共享页框而不是复制页框。
共享的页框不能被修改
父进程和子进程何时试图写一个共享的页框,就产生一个异常,这时内核就把这个页复制到一个新的页框中并标记为可写。
原来的共享页框仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页框的唯一属主,如果是就把这页框分配给它
这样一段时间后进程就会有自己的空间,并且按需分配。
创建进程地址空间
当创建一个新的进程时内核调用copy_mm()函数:该函数通过建立新进程的所有页表和内存描述符来创建进程的地址空间。
需要明确的几个问题
内核与普通进程获取内存时有何不同?
以直接了当的方式获得动态内存,采用页面级内存分配和小内存分配以及非连续内存区
进程的地址空间?
用户进程的地址空间用线性区来表示,内核进程不使用线性区。
进程何时会获得新的线性区?
刚刚创建的新进程
使用exec系统调用装载一个新的程序运行
将一个文件(或部分)映射到进程地址空间中
当用户堆栈不够用的时候,扩展堆栈对应的线性区
进程的线性区如何组织?
靠mmap或者mm_rb来组织,一种是映射的方式,一种是红黑树的方式。
何时为进程分配页框?
缺页异常引发请求调页
三、虚拟文件系统
Linux文件系统
Linux支持多种文件系统,包括ext2、ext3、vfat、ntfs、iso9660、jffs、romfs和nfs等,为了对各类文件系统进行统一管理,Linux引入了虚拟文件系统VFS(Virtual File System),为各类文件系统提供一个统一的操作界面和应用编程接口。
VFS的作用
VFS是一个软件层,用来处理与Unix标准文件系统相关的所有系统调用。
能为各种文件系统提供一个通用的、统一的接口
VFS中通用文件模型概念
VFS的基本思想:引入一个通用文件模型,这个模型能够表示所有支持的文件系统。
通用文件模型有下列对象类型组成
超级块对象(superblock object)
存放文件系统相关信息:例如文件系统控制块
索引节点对象(inode object)
存放具体文件的一般信息:文件控制块/inode
目录项对象(dentry object)
存放目录项与文件的链接信息
文件对象(file object)
存放已打开的文件和进程之间交互的信息:代表进程打开的一个文件
每个主要对象都包含一个操作对象,它描述了内核针对主要对象可以使用的方法
三个不同进程打开同一个文件,其中两个进程使用同一个硬链接。每个进程都有自己的文件对象,每个硬链接对应一个目录项对象。三个文件对象只需要两个目录项对象,这两个目录项对象对应同一个索引节点对象,这个索引节点对象标识的是超级块对象以及普通磁盘文件。
VFS对象的数据结构
超级块对象
索引节点对象
Inode对象内包含了内核在操作文件或目录时需要的全部信息,文件名可以更改,但inode对文件是唯一的,且随文件的存在而存在。
一个Inode代表文件系统中的一个文件,它可以是设备或管道这类特殊文件,故Inode中会包含特殊的项。
目录项对象
VFS把每个目录看作一个文件,如在路径/tmp/test中,tmp和test都是文件, tmp是目录文件,而test是普通文件; tmp和test都有一个inode对象表示。
每一个文件除了有一个inode数据结构外,还有一个dentry数据结构与之关联,该结构中的d_inode指针指向相应的inode结构。
dentry数据结构可以加快对文件的快速定位,改进文件系统的效率。
Dentry描述文件的逻辑属性,它在磁盘上没有对应的映像; inode结构记录文件的物理属性,在磁盘上有对应的映像。
与目录项相关的操作对象由dentry_operations结构描述
文件对象
文件对象在磁盘上没有映像,在文件被打开时创建由一个file结构组成。
文件对象中的信息主要是文件指针,即文件中当前的位置,下一个操作将在该位置发生。
file结构除保存文件当前位置外,还把指向该文件inode的指针放在其中,并形成一个双项链表,称系统打开文件表。
与进程相关的文件
用户打开文件的相关信息files_struct
进程的当前的工作目录和根目录的相关信息fs_struct
PCB
*files
files_struct数据结构(用户打开文件表)
进程手里的,记录文件的fdtable
用户打开文件表和系统打开文件表的区别
注意区分系统打开文件表和用户打开文件表
前者是由file对象组成的链表,它在内核态,由内核控制。
后者是文件描述符表,在用户进程空间,是进程的私有数据。
因而,应用程序只能使用文件描述符作为参数的系统调用才能访问系统打开文件表。
Linux文件系统逻辑结构
Linux中的每个进程都有两个数据结构来描述进程与文件的相关信息,
一个进程所处的位置是由fs_struct 结构来描述,它包含了两个指向VFS dentry的指针,分别指向根.目录节点(root)和当前目录节点(pwd) ;
而进程打开的文件是由files__struct 结构来描述,最多能同时打开的文件为OPEN_ MAX(默认值为256)个,由fd[0]~fd[ 255]所表示的指针指向对应的file结构。
每当打开一个文件时,就从files__struct 结构中找一个空闲的文件描述符,使它指向打开文件的描述结构file,对文件的操作通过file结构中定义的文件操作函数和VFS inode 的信息来完成。整个系统打开的文件则由file 结构来描述。
文件系统的挂载
根文件系统
在系统初始化过程中被直接mount
提供系统初始化脚本以及基本命令
如果一个文件系统的根目录是系统目录树的根目录,那个这个文件系统就是根文件系统
其他文件系统可以挂载到系统的目录树上
这样的目录称为挂载点(mount点,安装点)
文件系统之间的挂载关系对应文件系统之间的父子关系
打开文件实质上是在进程与文件之间建立连接,而打开文件描述符唯一地标识着这个连接。
open
该系统调用成功后将返回一个文件描述符,也就是文件对象指针数组的一个索引;系统调用不成功时返回-1。
read write
两个系统调用都返回所成功传送的字节数,或者发一个错误信号并返回-1