Linux内核与编程


一、Linux内核简介

1.Linux是类Unix系统,但它不是Unix。
2.用户界面是操作系统的外在表象,内核才是操作系统的内在核心。
3.在系统中运行的应用程序通过系统调用来与内核通信。应用程序通常通过调用库函数,再有库函数通过系统调用,让内核完成各种不同的任务。当应用程序执行一条系统调用时,我们称内核正代其执行,此时,应用程序通过系统调用在内核空间运行,而内核被成为运行于进程上下文中。
4.每个处理器在任何时间都处于一下三者之一
(1)在用户空间,执行用户进程
(2)在内核空间,处于进程上下文,代表某个特定的进程执行。
(3)在内核空间,处于中断上下文,与任何进程无关。
5.Linux和传统Unix内核比较
6.Linux内核版本
稳定版:副版本号是偶数 开发版:副版本号是奇数


二、从内核出发

1.Linux内核源码树

Linux内核与编程_第1张图片
Linux内核与编程_第2张图片

2.配置内核

三选一时yes no moudle,moudle意味着该配置被选定了,但是是以模块的形式生成,而yes是直接编译进 内核映像中。

3.内核开发的特点

(1)不能用C库也不能使用标准的C头文件
(2)必须使用 GUN C内联函数,内联汇编,分支声明
(3)难以进行浮点数运算
(4)进程的只有一个很小的定长堆栈:32位机有8KB,64位机有16KB
(5)没有内存保护机制
(6)时刻考虑同步和并发
(7)要考虑移植性的重要性

三、进程管理

1.进程

(1)内核调度的对象是线程,而不是进程,进程是处于执行器的程序以及相关资源的总称。

(2)每个进程都拥有一组独立的进程计数器、进程栈、进程寄存器。

(3)Linux进程也叫任务Task

(4)进程有两种虚拟机制:虚拟处理器和虚拟内存

(5)wait4()可以查询子进程是否终结,子进程退出执行后处于僵死状态,直到父进程调用wait()或waitpid()为止。

2.进程描述符

(1)进程的列表放在一个叫任务列表的双向循环链表中,链表的每一项都是一个task_struct类型的成为进程描述符的结构体,描述进程的所有信息。

(2)Linux通过slab动态生成task_struct,thread-info在内核栈尾端生成,Thread_info的task域指向任务实际的task_struct

内核通过进程标识号(PID)标识每个进程

(3)current指向当前正在运行的进程的进程描述符

(4)进程状态

task_struct,state域描述进程状态,有五种状态。

TASK_RUNNING(运行)--进程或者在运行,或者在运行队列等待执行。

TASK_INTERRUPTABLE(可中断)--进程在睡眠,等待某些条件达成,一旦接收到信号便立刻执行或准备执行。

TASK_UNINTERRUPTABLE(不可中断)--除了即使收到信号也不会执行或准备执行之外,其他的与可中断状态相同。

TASK_TRACE--进程被跟踪,比如ptrace调试

TASK_STOPOED--进程停止运行。通常在收到各种停止信号之后。在调试期间收到任何信号也会进入这种状态。

set_task_state(task,state)设置状态

3.进程创建

(1)fork()拷贝当前进程,子进程除了pid ppid以及一些计量值与父进程不一样外,其他都与父进程一样。然后子进程调用exec()读取并加载可执行文件。两个函数组合起来的效果跟单一的函数效果相似。fork之后,exec之前,父子进程拥有共同的物理地址,不同的虚拟地址。内核有意地让子进程先执行

(2)写时拷贝,调用fork()时并不立即拷贝,而是在写入的时候才拷贝。因为大多数子进程会调用exec()去执行其他的可运行文件,这时就不用拷贝了。所以内核有意地让子进程先执行。

(3)进程创建的过程:dup_task_struct为新进程创建一个内核栈,thread_info,task-struct,此时子进程完全与父进程相同。。。

4.线程在Linux中的实现

(1)线程的创建方式与进程的创建类似,只是在调用clone()时需要传递一些参数。

(2)内核线程与普通线程的区别在于内核线程没有独立的地址空间

(3)kthread_create()创建,wake_up-process唤醒。kthread_run将两者合二为一。do_exit()和ktread_stop停止

5.进程终结

(1)do_exit()进程终结

它占用的所有内存就是内核栈、thread_info结构和task_struct结构

(2)wait()->wait4()->release_task();

6.进程创建的过程

【1】通过fork()创建新进程并为新进程分配进程栈、task_struct和thread_info,新进程与父进程除了PID、PPID以及一些计数器不一样外,其他的都一样。fork()返回两次,一次返回父进程另一次返回子进程。fork的调用过程是fork()->clone->do_fork()->copy_process.

【2】写时拷贝,新进程创建后大部分都执行exec运行别的可执行文件。所以在新进程创建时并不立刻进程拷贝。而是在进行写操作的时候才拷贝。内核有意让子进程先运行。

【3】当进程结束时,调用exit()->do_exit退出进程并释放占有的资源。之后所占有的仅仅是进程栈、thread_info和task_struct,此时进程存在的唯一目的就是向父进程提供消息。

【4】父进程收到消息后,调用wait->wait4(0->release_task()来删除子进程的进程标识符。

四、进程调度

1.多任务

(1)多任务操作系统就是能同时并行地执行多个交互的进程的操作系统。分为非 抢占式多任务和抢占式多任务。Linux时抢占式。

2.Linux的进程调度

(1)CFS:完全公平调度算法,更倾向于优先调度I/O消耗型进程

(2)nice值,-20---+19,值越大,优先级越小。nice代表时间片比例。动态优先级是0到99,值越大优先级越大。任何实时进程的优先级都高于普通的进程?

调度器实体结构作为一个名为se的成员变量,嵌入在进程描述符内。

(3)调度器类:Linux调度器以模块化的方式提供,允许对不同的进程动态地选择调度器,这种模块化的结构被称为调度器类。调度器类有优先级。系统会遍历调度器类,选择胜出者选择下一个要执行的进程。CFS针对普通的进程调度类,SCHED_NORMAL.

(4)CFS

时间片底限:1ms

任何进程所获得时间片是由他自己和其他所有可运行进程的nice值相对差决定的。

时间记账:

vruntime变量记录虚拟运行时间,单位是ns,时间是经过标准化的时间,记录进程已经运行了的时间和还需要再运行多久

update_curr会被定期地调用并对运行时间进行加权运算更新vruntime

进程选择:

选择vruntime最小的进程,使用红黑树来组织进程队列。

调度器入口:

schedule()选择一个最高优先级的调度器,每一个调度器类都有自己的进程队列,然后再选择一个最高优先级的进程。

CFS是普通进程的调度器类,而大部分是普通进程。

唤醒和睡眠

进程把自己标记成休眠状态,从可执行红黑树中移除,加入等待队列。唤醒的过程刚好相反。

3.抢占和上下文切换

(1)上下文切换由context_switch()实现,每当选择一个新进程执行时,shedule()就会调用该函数。分为两部分:switch_mm():将虚拟内存从上一个进程映射到新的进程中。switch_to():将处理器状态从上一个进程切换到新进程,包括保存、恢复栈信息和寄存器信息。

内核在返回用户空间以及从中断返回时会检查need_resched标志,如果被设置,内核会在继续执行之前调用进程调度程序。每一个进程都有need_reshed标志。因为访问进程描述符的值要比访问一个全局变量快。

(2)用户抢占

内核即将返回用户空间时,如果need_rescheld被设置,导致schedule()被调用,此时就会发生用户抢占。即:从系统调用返回用户空间时或从中断处理程序返回用户空间时。

(3)内核抢占

只要没有锁,内核就可以抢占。发生内核抢占的时间:

【1】中断处理程序正在执行,返回内核空间之前。【2】内核代码再一次具有可抢占性的时候。【3】内核任务显示调用Schedule。【4】内核中任务阻塞

4.实时调度策略

实时调度:

SCHDE_FIFO,优先级大于SCHDE_NORMAL,除非自己受阻塞或显示地释放处理器,只有更高级别的SCHDE_RR或SCHDE_FIFO才能抢占。

SCHDE_RR,本质上和FIFO差不多,只不过具有时间片,轮流地调用

非实时:

SCHDE_NORMAL:

5.Linux进程调度的过程

(1)内核根据进程的nice值与其他进程的nice值的绝对值计算出为进程分配的时间片
(2)时间记账,vruntime记录当前进程运行的虚拟时间(经过标准化的时间),update_curr会定期调用来更新vruntime
(3)进程选择,当需要选择下一个运行进程时,CFS选择一个最小的vruntime进程来运行。CFS通过红黑树来组织进程。
(4)调度器入口,当需要重新调度时,会用schedule找到一个优先级最高的调度类,然后调度类pick_next_task选择队列中优先级最高的进程。因为CFS是普通进程的调度类,而大多数进程是普通进程,所以schedule对CFS做了优化。
(5)睡眠和唤醒。进程把自己标记为睡眠状态从红黑树中移除并加入等待队列,等待队列是等待某些事件发生的进程组成的链表。唤醒,当事件发生时,wake_up唤醒整个队列上的所有进程,并加入红黑树中。如果唤醒的进程比当前正在运行进程的优先级高,则设置need_resched标志。



五、系统调用

1.与内核通信

(1)系统调用是用户进程与硬件设备之间的中间层,主要作用

【1】是不同硬件设备的抽象接口

【2】起到保护系统的作用

【3】系统调用是用户空间访问内核的唯一合法入口

2.API、POSIX和C库

C库是POSIX标准的API

3.系统调用

(1)通过C库函数访问系统调用,系统调用需要一或多个参数,返回一个long型变量,0表示成功。

(2)每个系统调用都绑定一个系统调用号、一旦分配就不能更改,sys_call_table中记录着系统调用号

4.系统调用处理程序

用户空间不能直接调用内核代码,所以要通过软中断引发异常陷入内核来调用异常处理程序即系统调用处理程序 通过Int $x080指令调用system_call();系统调用号存储在eax寄存器中,其他参数存储在别的寄存器中。

5.系统调用的实现

(1)尽量简洁、参数尽量少、考虑向前向后的兼容性。

(2)参数验证。【1】参数必须指向用户空间【2】参数指向的地址必须在进程地址内【3】如果是读,要有读的权限,如果是写要有写的权限。通过capable()验证是否有权限返回0表示无权操作。copy_to_user()将数据拷贝到用户空间。copy_from_user()将数据拷贝到内核空间。假设进程的页被换出,可能会发生阻塞。

(3)权限验证

(4)绑定系统调用号

6.系统调用上下文

(1)系统调用处于进程上下文中,cureent指向当前指针。系统调用返回后仍然处于system_call()的控制权中,最终切换到用户空间。系统调用可以休眠并且可以被中断。这两点为毛重要?

(2)绑定系统调用的步骤

【1】在系统调用表(位于entry.s文件)中添加一个表项。【2】分配系统调用号,必须在中定义。【3】把系统调用的具体实现放在kemel/下的一个相关文件夹,并编译进系统内核映像。

7.系统调用处理程序的处理流程

(1)陷入内核,通过$0x80触发128号软中断,内核调用system_call

(2)系统调用号和参数传递,系统调用号通过eax寄存器传递,参数通过其它五个寄存器传递,当参数超过五个时,选择一个单独的寄存器存放指向用户参数的指针

(3)系统调用处理程序验证系统调用号,是否大于NR_syscalls,执行系统调用函数并把返回值带回用户空间



六、中断处理

1.中断

(1) 中断控制器,将多路中断管线采用复用技术只通过一个管线与处理器通信。
(2)每一个 IRQ线都关联一个数值量,有些是动态分配的。
(3) 异常与时钟同步,而中断不同步。

2.中断处理程序

(1)中断处理程序是设备驱动程序的一部分,设备驱动程序是内核代码,是C函数

(2)中断上下文执行代码不能被阻塞

(3)中断处理程序分为上半部和下半部,上半部处理具有较强时间要求的任务,比如通知设备接收到中断。

3.注册中断处理程序

(1)驱动程序Request_irp()分配一条中断线成功返回0,可以注册中断处理程序并激活响应的中断线,卸载驱动程序时要注销中断处理程序并释放中短线。

(2)request_irq会调用kmallox()可能会睡眠,所以不能在中断上下文和其他不允许阻塞的地方调用该函数。

(3)初始化硬件与注册中断处理程序顺序必须正确,防止硬件未初始化就执行中断处理程序。

(4)释放中断处理程序free_irq()

(5)中断是不可重入的,当一个中断处理程序正在执行时,该中短线在所有处理器上都会被屏蔽掉,防止同一个中断线接受另一个新的中断。不怕处理器接收到新的中断吗?

4.编写中断处理程序

(1)staitc irqreturnt intr_handler(int irq,void *dev)

(2)内核收到一个中断后,会依次调用中断线上的每一个处理程序,因此中断处理程序必须知道是否应该对这个中断号负责。

5.中断上下文

(1)中断上下文与任何进程毫无瓜葛,与curent宏也无关,不可以睡眠。

(2)中断处理程序可能是打断了另一中断线上的另一中断处理函数吗?

(3)中断处理程序拥有自己的中断栈,每个处理器一个,大小为一页。

6.中断处理机制的实现

(1)设备发出通过总线向中断处理器发出中断信号,如果是中断线是激活的,中断处理器把中断发给处理器,除非处理器禁止该中断,否则处理器会停下当前正在做的事,关闭中断,然后跳到内核预定义的中断处理程序入口。

(2)入口点检索IRQ号并放在栈中,调用do_IRQ().do_IRQ判断该中断线上是否有该中断处理程序,如果存在则在该栈上运行中断处理程序,否则返回内核运行中断的代码。

(3)中断处理程序处理完之后返回do_IRQ,清理工作并返回到入口点,入口点再调到ret_from_intr().

Linux内核与编程_第3张图片




7.中断控制

Linux提供一组接口用来控制中断状态,比如禁止中断或者屏蔽某条中断线。

七、下半部和推后执行的工作

1.下半部

(1)并没有明确规定什么任务要在那个部分执行,但是,【1】一个任务对时间敏感【2】一个任务与硬件有关【3】一个任务不允许被中断。以上三种情况放在上半部,否则考虑放在下半部。
(2) 下半部执行的时候允许所有的中断。
(3)2.6版本中内核提供了3种下半部的实现机制:软中断(用的少),tasklets(用得多),任务队列。

2.软中断

(1)软中断是由 softirq_action结构表示,被注册的软中断占据softirq_vec的一员,最多有32个,当前只用了9个
(2)软中断处理程序, softirq_handler(struct softirq_action *),一个软中断不能抢占另一个软中断,但可以在不同的处理器上执行,只有中断处理程序才可以抢占软中断
(3) 执行软中断:do_softirq【1】从一个硬件中断代码处返回时【2】从ksoftirqd内核线程中【3】显示检查执行待处理软中断的代码中
(3) 添加软中断
1】分配索引,在编译期间定义一个枚举类型来静态地声明软中断
【2】注册软中断处理程序,在运行期间调用open_softirq()注册软中断处理程序
【3】触发软中断,raise_softirq可以将一个软中断设置为挂起状态,在下一次调用do_softirq时投入执行。

3.tasklet

(1)tasklet和软中断接近, 接口简单、锁保护要求低,被广泛使用
(2)tasklet由两类软中断组成,HI_SOFTIRQ和TASKLET_SOFTIRQ,前者优先级更高
(3)tasklet的结构体:tasklet_struct
(4) 调度tasklet,已调度的tasklet(等同于被触发的中断)存放在两个单处理器数据结构tasklet_vec和tasklet_hi_vec,都是有tasklet_struct结构构成的链表,tasklet_schedule和tasklet_hi_shcedule函数调度tasklet_schedule()的执行步骤:
【1】检查tasklet的状态是否为TASKLET_STATE_SCHED。 如果是,说明tasklet已经被调度过了,函数立即返回(保证同一时间里只有一个给定类别的tasklet被执行,因此tasklet的代码不需要可重入)。
【2】调用_tasklet_schedule()。
【3】把需要调度的 tasklet加到每个处理器一个的 tasklet vec 链表或 tasklet_hi_vec 链表的表 头上去。
【4】唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该 taskIet。
(5) 使用tasklet的步骤
【1】声明tasklet:DECLARE_TASKLET()
【2】编写tasklet处理程序:void tasklet_handler,两个不同的tasklet绝不会同时执行,因为靠软中断实现,所以不能睡眠,不能使用信号量巴拉巴拉的。
【3】调度tasklet:tasklet_schedule()
(6) ksoftirq,每一个处理器都有一组辅助处理软中断的内核线程。软中断触发频率很高的时候可能导致用户进程无法获得足够的处理器时间,所以当软中断很多的时候,内核唤醒一组内核线程来处理这些负载,这些线程在最低的优先级下运行( 19)。

4.工作队列

(1)工作队列可以把工作推后,在 进程上下文中运行,因此可以重新调度甚至睡眠。如果推后工作的任务需要睡眠就使用工作队列,否则使用tasklet
(2)工作队列子系统是一个用于创建内核线程的接口,通过它创建进程负责内核其部分排到队列里的任务,创建这些内核线程称作工作者线程。
(3)工作队列子系统提供一个缺省的工作者线程来处理这些工作,缺省的工作线程叫events/n。
(4)工作者线程用workqueue_struct结构表示,工作用work_struct结构体表示
(5)工作队列实现机制总结:位于最高一层的是工作者线程。系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者钱程。内核中有些部分可以根据需要来创建工作者钱程,而在默认情况下内核只有event这一种类型的工作者线程。每个工作者线程都由一个cpu_workqueue_struct结构体表示。而workqueue_struct结构体则表示给定类型的所有工作者线程。工作work_struct处于最底层。你的驱动程序创建这些需要推后执行的工作。它们用work_struct结构来表示。这个结构体中最重要的部分是一个指针,它指向一个函数,而正是该函数负责处理需要推后执行的具体任务。 工作会被提交给某个具体的工作者钱程。 然后这个工作者线程会被唤醒并执行这些排好的工作。
(6)尽管操作处理函数运行在进程上下文中,但它不能访问用户空间。
(7)工作队列的实现
【1】创建一个专门的工作者线程来处理需要推后的工作,缺省为event.工作者线程用worqueue_struct结构表示,其中cpu_workqueue_struct域表示每个cpu的工作者线程
【2】工作者线程被视为普通的线程,可以中断和睡眠
【3】当工作者线程被激活时,会执行工作队列中的工作,当工作被执行完毕,就将相应的work_struct对象从链表中移去

5.下半部机制的选择

(1)tasklet基于软中断实现,所以两者非常接近,工作队列靠内核线程实现。
(2)有休眠需要就用工作队列,否则最好用tasklet,如果必须专注于性能的提高就用软中断。
Linux内核与编程_第4张图片
6.在下半部之间加锁
(1)使用tasklet的好处在于,它自己负责执行的序列化保障:两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行。但当两个不同类型的tasklet共享同一数据时,需要正确使用锁机制。
(2)如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权。
(3)如果中断上下文和一个下半部共享数据,在访问数据之前,你需要禁止中断并得到锁的使用权。
(4)任何在工作队列中被共享的数据也需要使用锁机

八、定时器和时间管理

1.内核中时间的概念

(1)硬件为内核提供了一个系统定时器来计算流逝的时间,内核可将其看成一个电子时间资源。系统定时器以一定的频率定期触发时钟中断,该频率可以通过编程预定。时钟中断发生时,内核通过一种特殊的中断处理程序对其进行处理。
(2)节拍率(tick rate),节拍(tick)两次中断的时间差 。内核通过已知的时钟中断计算墙上时间和运行时间

2.节拍率

(1)X86中节拍率是100
(2)高节拍率:优点【1】定时器更高精度【2】依赖定时的时间调用更精准【3】资源消耗和系统时间的测量更精准【4】提高进程抢占准确度缺点【1】频繁时钟中断【2】耗电
(3)Linux支持无节拍

2.jiffies

(1)jiffies记录系统启动后的总节拍数,是无符号长整数,32位系统是32位,64位系统是64位
(2)USER_HZ定义为100,如果HZ与USER_HZ不相等,由宏完成转换

3.硬时钟和定时器

(1)体系结构提供了两种设备进行计时-- 系统定时器和实时时钟
(2)实时时钟(RTC)是持久存放系统时间的设备,即使关闭后也可以通过主板的微电池保持计时,RTC与CMOS继承在一起与BIOS使用同一块电池。
(3)系统定时器提供一种周期性触发中断的机制。

3.时钟中断处理程序

(1)与体系结构相关的历程作为系统定时器的中断处理程序而注册到内核中。需要执行以下工作
【1】获得xtime锁,保护jiffies_64和xtime
【2】需要时应答或重新设置系统时钟
【3】周期性地使用墙上时间更新实时时钟
【4】调用体系结构无关的历程tick_periodic()
(2)与体系结构有关的历程tick_periodic()
【1】给jiffes_64+1
【2】更新资源消耗的统计值
【3】执行已到期的动态定时器
【4】执行sheduler_tick哈书,负责减少当前运行进程的时间片并在需要的时候设置need_reshed值
【5】更新墙上时间,该时间存放在xtime变量中。【6】计算平均负载值

4.定时器

(1)定时器是管理内核流逝时间的基础,能让内核在指定的工作点执行。
(2)设置一个超市时间,指定超时之后执行的函数,然后激活定时器
(3)定时器结构:timer_list my_timer,init_timer(&my_timer),设置timer,add_timer(&timer).mo_timer来修改已经激活了的定时器,del_timer删除定时器
(4)实现定时器,内核在时钟中断之后执行定时器,定时器作为软中断在下半部上下文中执行。
(5)使用定时器
【1】创建定时器前要先定义它 struct timer_list my_timer
【2】初始化 init(my_timer)
【3】填充结构中需要的值
my_timer.expires = jiffies + delay; /* 定时器超时时的节拍数 */
my_timer.data = 0; /* 给定时器处理函数传入0值 */
my_timer.function = my_function; /* 定时器超时时调用的函数*/
【4】最后激活定时器add_timer(&mytimer)
【5】当前节拍计数等于或大于指定的超时时,内核就开始执行定时器处理函数。虽然内核可以保证不会在超时时间到期前运行定时器处理函数,但是有可能延误定时器的执行。
【6】定时器的实现,内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。

5.延迟执行

(1)忙等待,精确度要求不高时可以用
(2)短延迟,延迟时间往往小于1ms不到一个节拍
(3)shedule_timeouter(),让需要延迟的任务睡,睡到指定的延迟时间好紧后再重新执行。

十二、内存管理

1.页区

(1)大多数32位体系结构支持4kB的页,64位体系结构支持8KB的页,struct page结构表示系统中的物理页
struct page {
unsigned long flags;//页的状态,是不是脏的,可同时描述 32种状态
atomic_t count;//被引用的次数
atomic_t mapcount;
unsigned long private;
struct address_space *mapping;
pgoff t index;
stroct list head lru;
void *virtual; //虚拟地址}
(2)Linux将内存分为四个区
ZONE_DMA:用来执行DMA,在X86上包含的页都在0-16MB的内存范围
ZONE_DMA32:用来执行32位的DMA
ZONE_NORMAL:正常映射的页,在x86上,是从16M到896M的所有物理内存
ZONE_HIGHMEM:高端内存,其中的页不能永久地映射到内核地址空间,在32位X86系统上,为高于896M的所有物理内存。在其他体系结构上为空
DMA必须从ZONE_DMA中进行分配,但是一般用途既能从ZONE_DMA分配也能从ZONE_NORMAL分配,但是不允许跨界。
page结构与物理页相关,并非与虚拟页相关。该结构对页的描述只是暂时的,即使页中包含的数据继续存在,由于交换的原因,他们可能并不再和同一个page结构相关联。


2.获得页

(1)以页为单位分配内存,最核心的函数是
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)//该函数分配
2order(1<
 
   
void * page_address(struct page * page)//把给定的页转换为逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask. unsigned int order)//与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,所以其他页也会紧随其后。
struct page * alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)//如果只需要一页
 
   
unsigned long get_zeroed_page(unsigned int gfp_mask)//返回的页的内容全为0
void __free_pages(struct page *page, unsigned int order) 
   
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
   
 
  
(2)释放时要谨慎,只释放属于你的页。触底了错误的struct page或地址,用错了order值,会导致系统崩溃。

3.Kmalloc()

void * kmalloc(size_t size, gfp_t flags)
(1)行为修饰符、区修饰符及类型标志

行为修饰符:说明是否可阻塞、执行I/O
区修饰符:内从区从何处分配
类型标志:GFP_KERNEL可能引起睡眠,GFP_ATOMIC不能睡眠
(2)只有alloc_pages才可以分配高端内存,因为高端内存可能根本没有逻辑地址
(3)kfree()

4.vmalloc(unsigned long size)

(1)虚拟地址是连续的,物理地址无需连续,也是用户空间分配函数的工作方式。
(2)vfree(const void*addr)

5.slab

(1)空闲链表相当于对象高速缓存,slab分配器扮演了通用数据结构缓存层的角色
(2)slab为不同类型的对象分配高速缓存组,高速缓存分为多个slab,一个slab是物理上连续的页组成。
(3)当内核需要一个对象时,先从部分满的slab中进行分配,再从空的slab中分配,如果没有空的,就新创建一个slab

6.在栈上静态分配

(1)静态分配很危险,栈溢出悄无声息,会产生严重的问题

7.高端内存的映射

(1)在x86上,高端内存中的页被映射到3GB-4GB
(2)void * kmap(strcut page * page)将获得的页映射到高端内存,永久映射,不用的时候要释放
(3)临时映射可以用在不能睡眠的地方。有一组保留的映射,内核可以把高端内存原子地映射到某个保留的映射中。
void * kmap_automic(struct *page,enum km_type type)//type描述了映射的目的
void kunmap_automic(void *kvaddr,enum km_type)//解除映射

8.分配函数的选择

(1)如果需要使用连续的物理地址,就使用kmalloc,是内存分配的常用方式
(2)如果要使用高端内存,使用alloc_pages(),返回指向page的指针而不是逻辑地址
(3)如果需要连续虚拟地址,就vmalloc(相对kmalloc有一定损失),内存虚拟地址是连续的,不保证物理地址来内需
(4)如果需要创建和撤销很多大的数据结构,就考虑slab创建高速缓存

十五、进程地址空间

1.地址空间

(1)每个进程都有一个32位或64位的平坦地址空间,如果进程访问了无效地址,内核会终止进程并返回段错误
(2) 内存区域包含:代码段、数据段(以初始化的全局变量)、bss段(未初始化的全局变量),栈,被映射的文件。进程用户空间栈从零页分配,所以局部变量为0.
(3)内存描述符 mm_struct
mm_users使用线程数目,mm_count主引用进程数。mmap和mm_rb描述该地址空间中全部内存区域,前者以链式存放,后者以红黑树存放
(4)进程描述符的mm域指向进程使用的内存描述符,所以current->mm指向当前进程的内存描述符。fork()利用copy_mm()复制父进程的内存描述符。子进程中的 mm_struct结构体从slab缓存中分配。内核线程的mm为空
(5)当进程退出时,内核调用exit_mm()函数,减少mm_users统计量,当减少为零时就mmdrop()减少mm_cout使用计数。再为0,则free_mm()归还到slab缓存中。
(6)内核进程的内存描述符用前一个进程的内存描述符

2.虚拟内存区域

(1)虚拟内存区域vm_area_struct描述了指定地址空间内连续区间上的一个独立内存范围,vm_star是开始地址vm_end是结束地址
(2)vm_mm域指向与之相关的mm_struct域
(3)VMA标志、VM_READ、VM_WRITE和VM_EXEC读写和执行权限。VM_SHARED多进程共享

(4)VMA操作

(5)C库是共享的内存区域,0页里全是0

3.mmap()和do_mmap()

(1)内核通过do_mmap()创建新的线性空间,如果和相邻的空间权限一致,就会合并为一个。

(2)用户空间通过mmap2()调用do_mmap

4.mummap()和do_mummap() 删除地址空间

5.页表

Linux使用三级页表,页全局目录(PGD)、中间页目录(PMD)、简单也目录(PTE)

十三、虚拟文件系统

1.通用文件系统接口

VFS为用户提供通用的文件访问接口

2.文件系统抽象层

(1)VFS抽象层之所以能衔接各种各样的文件系统,是因为定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。而实际的文件系统将接口与VFS保持一致。

(2)ret=write(fd,buf,len).首先给通用的系统调用sys_write处理,sys_write函数找到fd所在的文件系统实际给出哪个写操作然后再执行。

3.Unix文件系统

(1)提供了四个抽象概念:文件、目录项、索引节点、安装点。

(2)

【1】文件系统安装在安装点上。

【2】文件是基于字节流的文件,有头有尾有名字。

【3】文件通过目录组织起来,相当于文件夹,目录还可以包含子目录,VFS把目录当成文件看待

【4】文件的元数据存在单独的数据结构里inode,称为索引结点

4.VFS对象极其数据结构

(1)VFS采用面向对象的设计思想,却是使用C语言编写的,数据结构都使用结构体实现,结构体中拥有指向函数的指针

(2)VFS存在四个主要对象

【1】超级块对象,代表一个具体的文件系统,存储特定文件系统的信息

【2】索引结点,代表一个文件(磁盘上的文件,内存存储不连续),存储内核在操作文件或目录时所需要的全部信息

【3】目录对象,代表一个目录项

目录项对象有三种有效状态:被使用、未被使用和负状态

被使用说明正在被使用,不能删除。未被使用说明没被使用,但是依然保存在缓存中以便在需要时再使用它。一个负状态的目录项没有对应的有效索引接点,但是依然保留以便快速解析以后的路径查询。

【4】文件对象,代表一个由进程打开了的文件(内存中的文件,存储连续)

(3)四个操作对象,每个主要对象对应一个操作对象,表示内核可以对主要对象进行的操作。super_oprations对象,inode_operations对象,dentry_operations对象,file_operations对象

(4)目录项对象有三种有效状态:被使用、未被使用和负状态

被使用说明正在被使用,不能删除。未被使用说明没被使用,但是依然保存在缓存中以便在需要时再使用它。一个负状态的目录项没有对应的有效索引接点,但是依然保留以便快速解析以后的路径查询。

(5)目录项缓存,内核将目录项缓存在目录项缓存中(dcache)

(6)文件对象,代表一个文件。同一个文件可能有多个文件对象,对应的索引结点和目录项是唯一的

5.和文件系统相关的数据结构

(1)file_system_type描述每种文件系统的功能和行为

(2)vfsmount结构体在安装点被创建,代表文件系统的一个实例,也就是安装点。

6.与进程有关的数据结构

(1)进程描述符的files域指向file_struct,所有与进程相关的信息都在其中。

(2)进程描述符中的fs域指向fs_struct,包含文件系统和进程相关的信息

(3)进程描述符中的mmt_namespace指向namespace结构体,使每一个进程在系统中都看到唯一的文件系统。

十四、块IO层

1.剖析一个块设备

(1)块设备最小的寻址单元是扇区,常见为512字节,块设备无法对比扇区还小的单元进行寻址和操作。

(2)内核块大小要是扇区大小的2的整倍数,但不能超过一页,通常为512B,1KB或4KB

2.缓存区与缓存区头

(1)块被调入内存后,存储在缓存区中,一个缓存区对应一个块,缓存区头buffer_header存储相关控制信息,缓存区头在于描述磁盘块与物理内存缓存区之间的映射关系

3.bio结构体

缓冲区头仅能描述单个缓冲区,当作为所有IO的容器使用时,缓冲区头会促使内核把对大块数据的IO操作分解为多个缓冲区头结构,这样造成了负担和空间浪费。

(1)目前内核中块I/O操作的基本容器由bio结构表示,代表正在现成的以片段(segment)链表形式组织的IO操作,一个片段是一小块连续的缓冲区域。

(2)bi_io_vec指向一个bio_vec结构体数组,表示一个操作所需要的片段集合,每个bio_vec结构是一个形式如(pages,offset,len)的向量

(3)bi_vcnt描述bio_io_vec所指向的bio_vec数组中的向量数目

(4)bi_idx指向数组的当前索引

(5)总而言之,每一个块I/O请求都通过一个bio结构体表示。每个请求包含一个或多个块,这些块存储在bio_vec结构体数组中。这些结构体描述了每个片段在物理页中的实际位置,并且像向量一样被组织在一起。I/O操作的第一个片段由b_io_vec结构体所指向,其他的片段在其后依次放置,共有bi_vcnt个片段。 当块I/O层开始执行请求、需要使用各个片段时,bi_idx域会不断更新,从而总指向当前片段。

(6)bio结构与缓冲区头的差别

bio结构体代表的是I/O操作,它可以包括内存中的一个或多个页;buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块。
缓冲区头负责描述磁盘块到页面的映射。bio结构体不包含任何和缓冲区相关的状态信息——它仅仅是一个矢量数组,描述一个或多个单独块I/O操作的数据片段和相关信息。

4.请求队列

(1)块设备将他们挂起的块IO请求放在请求队列中request_queue,高层的代码将请求放进去,设备驱动程序从中获取请求并执行。

(2)请求由request表示,可能操作多个连续的磁盘块,所以可能对应多个bio结构体

5.IO调度程序

(1)内核不会按接受顺序直接给硬盘。而是先执行合并与排序操作。在内核中称提交IO请求的子系统为IO调度程序

(2)调度程序将请求队列中挂起的请求进行合并排序,进行优化。合并,多个请求合并为一个。排序,按照扇区增长方向排序,即电梯调度

(3)Linus电梯

【1】 如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已经存在的请求合并成一个请求。
【2】如果队列中存在一个驻留时间过长的请求,那么新请求将被插入到队列尾部,以防止其 他旧的请求饥饿发生。
【3】如果队列中以扇区方向为序存在合适的插入位置,那么新的请求将被插入到该位置,保 证队列中的请求是以被访问磁盘物理位置为序进行排列的。
【4】如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部。

(4)最后期限IO调度程序

电梯调度会带来饥饿问题。

【1】所以为每个请求设置一个超时时间,默认情况下读超时为500ms,写为5s。

【2】最后期限调度在合并与排序方法上跟电梯调度一样。

【3】但是最后期限IO调度会按照请求的类型将其插入到额外的队列中。读请求按次序被插入到特定的读FIFO队列中,写请求被插入到特定的写FIFO队列中。

【4】如果FIFO队列头超时了,调度程序便从中提取请求进行服务

Linux内核与编程_第5张图片

(5)预测IO调度程序

最后期限虽然略屌,但是降低了系统的吞吐量。预测IO调度程序妄想保持良好的响应的同时提供良好的全局吞吐量

【1】预测调度程序基于最后期限,增加了预测启发能力

【2】请求提交后并不直接返回处理其他请求,而是会有意空闲片刻,在这空闲期间,任何相邻磁盘位置的请求都会立刻得到处理。

【4】预测IO调度程序更踪并且统计每个应用程序块IO的习惯行为,以便正确预测。

(6)完全公平的排队IO调度程序

【1】请求按照进程组成队列并执行合并排序操作

【2】以时间片轮转调度队列,每个队列中选区请求数,然后进行下一轮调度

(6)空操作的IO调度程序

【1】基本什么都不做,不进行排序。

【2】但是执行合并操作,维护一个近似于FIFO的队列

【3】它之所以啥也不做是因为他打算用在真正的随机访问设备,比如闪存卡。

十六、页高速缓存和页回写

1.缓存手段

(1)页高速缓存是由内存中的物理页面组成的,对应物理磁盘上的物理块,能够动态地改变大小。

(2)写缓存有三种策略【1】不缓存【2】自动更新缓存同时更新硬盘文件【3】回写,先把缓存中页面标记为脏,周期性地写回。

(3)缓存回收策略:选择干净的页进行写回,如果没有干净的页则强制地进行回收操作。如果选择写回的页:

【1】最近最少使用LRU

【2】双链策略:Linux实现的是一个修改过得LRU。维护两个链表、活跃链表和非活跃链表,非活跃链表上的页面可以换出。页面从尾部加入,从头部移除。如果活跃链表变得过多而超过了非活跃链表,那么活跃链表的头页面将被重新移回到非活跃链表中。

2.Linux页高速缓存

(1)address_space结构体

当一个文件可以被10个vm_area_struct结构体标识(比如有5个进程,每个调用mmap()映射它两次),那么这个文件只能有一个address_space数据结构——也就是文件可以有多个虚拟地址,但是只能在物理内存有一份。

(2)address_space操作

这些方法指针指向那些为指定缓存对象实现的页I/O操作,这里面readpage()和writepage()两个方法最为重要。

(3)查找页

find_get_page()方法负责完成这个检查动作。一个address_space对象和一个偏移量会传给find_get_page()方法,用于在页高速缓存中搜索需要的数据:

page * find_get_page(mapping , index);

(4)当文件被修改

SetPageDirty(page);

3.缓冲区高速缓存

(1)独立的磁盘块IO缓冲也要被存入页高速缓存。

4.flusher线程

(1)在以下3种情况发生时,脏页被写回磁盘

【1】当空闲页内存低于一个特定的阈值时,内核必须将脏页写回磁盘以释放内存。因为只有干净内存才可以被回收,当内存干净后,内核就可以从缓存清理数据然后收缩缓存最终释放出更多的内存。

【2】当脏页在内存中驻留时间超过一定阈值时,内核必须将超时的脏页写回磁盘。

【3】当用户调用sync()和sync()系统调用时。

(2)一群flusher线程执行三种工作

【1】flusher线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新写回磁盘。当可用物理内存低的时候写回脏数据。当空闲内存比dirty_background_radio还低时,内核便会调用函数flusher_threads唤醒一个或多个线程写回脏页。直到满足以下两个条件{1、已经有指定的最小数目的页被写回,2、空闲内存说已经上升,超过了阈值dirty_background_ratio}

【2】flusher线程会周期性地被唤醒。

(3)膝上型计算机模式

特殊的写回策略,将硬盘转动的机械行为最小化。flusher会根据时机写回数据


你可能感兴趣的:(Linux)