Linux内核设计与实现笔记

news/2025/2/26 13:20:52

第3章 进程管理
进程
进程描述符及任务结构
分配进程描述符,通过slab分配器分配task_struct,达到对象复用和缓存着色的目的
task_struct
thread_info
每个任务的thread_info结构在他的内核栈的尾端分配,结构中task域中存放的是指向该任务task_struct的指针
进程描述符的存放
进程状态
设置当前进程状态
set_task_state(task, state)
必要时,会设置内存屏障来强制其他处理器做重新排序
进程上下文
一般程序在用户空间进行,当一个程序执行系统调用或触发了异常,陷入内核空间,则称其处于进程上下文中
系统调用和异常处理程序时对内核明确定义的接口
进程家族树
全部由init进程生成

进程创建
Linux采用了两个单独的函数执行,fork(),exec()
fork()通过拷贝当前进程创建一个子进程
exec()函数负责读取可执行文件并将其载入地址空间开始运行
写时拷贝(copy-on-write),先以只读方式共享,当发生写入是再拷贝。
fork的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符
fork()
fork,vfork,__clone库函数通过各自的参数标志去调用clone(),clone()调用do_fork()
do_fork完成了创建过程中的大部分工作。其定义在kernel/fork.c文件中。其调用了copy_process()函数
调用dup_task_struct()为新进程创建内核栈、thread_info结构和task_struct,这些值与当前进程值相同
检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制
。。。。。
vfork()
除了不拷贝父进程的页表项外(共享地址空间CLONE_VM),vfork()系统调用和fork()的功能相同。父进程被堵塞,直到子进程退出或执行exec()。有了写时拷贝之后,vfork基本没用

线程在Linux的实现
Linux不为线程特别准备调度算法和数据结构。而是将其视为与其他进程共享某些资源的进程,每个线程结构体沿用task_struct()
创建线程
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
共享 地址空间 文件系统信息 打开的文件 信号处理函数及被阻断的信号
相比之下呢,
普通fork:clone(SIGCHLD, 0)
vfork:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
内核线程
与普通进程的区别是内核线程没有独立的地址空间,实际上指向地址空间的mm指针被设置为NULL。只在内核空间运行。和普通进程同样可以被调度,可以被抢占
创建:kthread_creat()默认不可运行状态。
运行:wake_up_process()唤醒
宏:kthread_run(),将上面两个合起来

进程终结
内核释放其所占用的资源并通知其父进程。大部分调用do_exit()函数。调用后其所有资源都被释放掉。其唯一剩余的内存就是内核栈,thread_info结构和task_struct结构
删除进程描述符
调用do_exit()函数后尽管线程已经僵死不再运行,但系统仍保留其进程描述符。这样可以让系统有办法在子进程终结后仍能获得其信息
父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
孤儿进程造成的进退维谷

第4章 进程调度
Linux进程调度
2.5之后采用O(1)调度
2.6.23采用CFS(完全公平调度算法)
策略
I/O消耗型进程
处理器消耗型进程
进程优先级
Linux采用了两种不同的优先级范围
第一种是用nice值,范围从-20~+19,越高意味着优先级越低,默认为0。Linux系统中,nice值代表时间片比例
第二种是实时优先级,可配置,范围0~99,越高意味着优先级越高任何实时进程的优先级高于普通的进程
调度策略设置
Linux主要提供完全公平调度器CFS、实时调度器RT、截止时间调度器DL。
并且以此调度DL,RT,CFS
CFS参考nice[-20,19]
RT参考实时优先级(也称静态优先级)[0, 100)
时间片
Linux调度算法
调度器类
Linux调度器是以模块方式提供的,目的是允许不同类型的进程可以有针对性的选择调度算法
这种模块化结构称为调度器类,允许多种不同的可动态调度添加的调度算法并存,调度属于自己范畴的进程
公平调度
Linux调度的实现
时间记账
调度器实体结构sched_entity
虚拟实时
调度周期,默认值sched_latency为6ms,如果任务数量过多,导致一个周期内每个任务分得的时间片较短,则设置平均时间片最小为0.75ms,调度周期为sched_min_granularity * nr_running(平均时间片*当前任务数)
确定调度周期后,计算第i个任务的动态时间片
time_slice_i = sched_latency * weight_i / weight_rq。
weight_i表示当前任务权重,weight_rq表示当前运行队列中任务的权重之和。
vruntime_i = vruntime_i + weight_nice0 / weight_i * real_runtime
进程选择
选择具有最小vruntime的任务,使用红黑树组织可运行进程队列
调度器入口schedule()
通常需要一个具体的调度类相关联
会调用pick_next_task()找到一个最高优先级的调度类
睡眠和唤醒
进程把自己标记为休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。唤醒相反。
抢占和上下文切换
上下文切换就是从一个可执行的进程切换到另一个可执行进程
由context_switch()完成,两项基本工作
switch_mm()负责把虚拟内存映射切换到新进程中
switch_to()负责吧上一个进程的处理器状态切换为新进程处理器状态,包括保存恢复栈信息、寄存器信息,以及其他架构相关信息
用户抢占
内核即将返回用户空间时,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占
在以下情况产生:
从系统调返回用户空间时
从中断处理程序返回用户空间时
内核抢占
没有持有锁时,说明内核可以进行抢占,每个进程的thread_info引入preempt_count计数器
在以下情况产生:
中断处理程序正在执行,且返回内核空间之前
内核代码再一次具有可抢占性的时候
如果内核中的任务显示调用schedule()
如果内核中的任务阻塞(这也会调用schedule())
实时调度策略
SCHED_FIFO和SCHED_RR
SCHED_FIFO没有时间片,先进先处理
SCHED_RR时间片轮转
实现都基于静态优先级,范围为0-99。前面的nice值对应在100-140
静态优先级越大优先级越高
nice越小优先级越高
与调度相关的系统调用
与调度策略和优先级相关的系统调用
与处理器绑定有关的系统调用

第5章 系统调用
与内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。主要作用:
给用户空间提供一种硬件的抽象接口
保证了系统的稳定和安全
为实现多任务和虚拟内存
API、POSIX和C库
系统调用syscall

第6章 内核数据结构

第7章 中断和中断处理
中断,本质是一种特殊的电信号,由硬件设备发给处理器
异常和中断不同,在产生时必须考虑与处理器时钟同步,异常也称同步中断
中断处理程序
中断处理程序是被内核调用来响应中断的,运用中断上下文
第一部分是中断处理程序,另一部分是下半部
上半部和下半部对比
上半部接收到一个中断立即执行,且只做有严格的时限的工作,如中断应答、复位硬件等
能够被允许稍后进行的工作将推迟到下半部
注册中断处理程序
request_irq(unsigned int irg, irq_handler_t handler, unsigned long flag, const char* name, void* dev)
中断程序标志
中断上下文
与进程上下文对比
进程上下文是一种内核所处的操作模式,此时内核代表进程执行。
当执行一个中断处理程序时,内核处于中断上下文
进程上下文可以睡眠,也可以调度程序。中断上下文不可睡眠,具有较为严格的时间限制
中断处理程序可以打断其他的代码
先前中断处理程序共享所中断进程的内核栈。之后因为内核栈的减小,重新分配了一个中断栈,每个处理器一个
中断处理机制的实现
中断控制
禁止和激活中断
local_irq_disable()
local_irq_enable()
禁止指定中断线
void disable_irq(unsigned int irq)
void disable_irq_nosync(unsigned int irq)
void enabele_irq(unsigned int irq)
void synchronize_irq(unsigned int irq)
中断系统的状态

第8章 下半部和推后执行的工作
下半部
下半部环境
下半部有多种方法
软中断
一个软中断不会中断另一个软中断,唯一可以抢占软中断的是中断处理程序。不过其他的软中断可以在其他处理器上执行
软中断保留给系统对于时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的
使用软中断
分配索引
注册处理程序
open_softriq(NET_TX_SOFTIRQ, net_tx_action)
参数:软中断的索引号和处理函数。
参数:软中断的索引号和处理函数
触发软中断
raise_softirq(NET_TX_SOFTIRQ)
中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出
tasklet
最常用的下半部处理方法,可以动态生成,由于对加锁的要求不高,所以使用方便。
当然,对于时间要求严格并能自己高效完成加锁工作的应用,软中断是正确选择
tasklet的实现
通过软中断实现,有两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ
tasklet结构体
调度tasklet
使用 tasklet
声明自己的tasklet
编写自己的tasklet处理程序
调度自己的tasklet
工作队列
将工作推后,交给一个内核线程执行

第10章 内核同步方法
原子操作
原子整数操作
64位原子操作
原子位操作
自旋锁
防止多于一个执行线程同时进入临界区
自旋锁方法
DEFINE_SPINCLOCK(mr_lock)
spin_lock(&mr_lock)
spin_unlock(&mr_lock)
可用于中断处理程序(此处不能用信号量,因为会导致睡眠)
对代码加锁回事程序难以理解,并且引发竞争条件。应该对数据加锁,而不是代码
读写锁
DEFINE_RWCLOCK(mr_rwlock)
read_lock(&mr_rwlock)
read_unlock(&mr_rwlock)
write_lock(&mr_rwlock)
write_unlock(&mr_rwlock)
自旋锁提供了快速简单的锁实现方法,如果加锁时间不长且代码不会睡眠,利用自旋锁是最佳的。
信号量
如果加锁时间可能很长或者代码在持有锁时可能睡眠,那么最好用信号量
读写信号量
互斥体/互斥锁
信号量 适用于较为复杂的、未明情况的互斥访问。为实现简单的睡眠自旋锁,引入了互斥体
BLK:大内核锁
全局自旋锁,只可以用在进程上下文,不能用在中断上下文。递归锁。持有BLK的任务仍可以睡眠
顺序锁,简称seq锁
禁止抢占
顺序和屏障
rmb()提供读屏障,确保跨越rmb()的载入操作不会发生重排序
wmb()提供写屏障,确保跨越wmb()的存储操作不会发生重排序

第12章 内存管理

内核把物理页作为内存管理的基本单位
内核用struck page结构表示系统中的每个页
struck page{
unsigned long flag;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head index;
void virtual;
}
flag域用来存放页的状态。这些状态包括页是否是脏的,是否被锁定在内存中等。每一位单独表示一种状态,定义在<linux/page-flags.h>
_count域存放页的引用计数
mapcount页的映射技术
virtual域是页的虚拟地址
通常他就是页在虚拟内存中的地址。有些内存即所谓的高端内存,并不永久的映射到内核地址空间上,这是virtual为NULL,需要的时候必须动态映射这些页
这个数据结构描述当前时刻在相关的物理页中存放的东西。其目的在于描述物理内存本身,而不是描述包含在其中的数据

内核使用区对具有相似特性的页进行分组
区的实际使用和分布与体系结构有关
Linux主要有4个区
ZONE_DMA:这个区包含的页能用于执行DMA,物理内存<16MB
ZONE_DMA32:和ZONE_DMA类似,不同的是只能被32位设备访问.
ZONE_NORMAL:包含能正常映射的区,物理内存16-896MB
ZONE_HIGHEM:包含“高端内存,其中的页并不能永久的映射到内核地址空间。物理内存>896MB
每个区用struct zone表示
获得页
核心函数:struct page
alloc_pages(gfp_t gfp_mask, unsigned int order);
分配一个2^order个连续的物理页,并返回指向第一个页的page结构体指针
void *page_address(struct page *page)
返回这个页的逻辑地址
获得填充为0的页
unsigned long get_zeroed_page(unsigned int gfp_mask)
释放页
void free_pages(unsigned long addr, unsigned int order)
要谨慎不要释放错误
kmalloc
与malloc相似,只是多了一个flag参数
kmalloc(sizeof(struct dog), GFP_KEPNEL)
用此获得以字节为单位的一块内核单位,并且获得的内存区物理上是连续的
gfp_mask标志
标志分为三类:行为修饰符,区修饰符,类型
行为修饰符
表示内核以何种方式分配内存
如:分配器可以睡眠,分配器可以启动I/O等
区修饰符
表示内存区应当从何处分配
类型
组合行为修饰符和区修饰符,已完成特殊类型的处理
通常使用
kfree()
释放kmalloc分配出的内存
kfess(buf)
vmalloc()
类似kmalloc(),但是内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式:有malloc()返回的页在进程的虚拟地址空间内是连续的,但不能保证其在物理RAM中也是连续的
kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
内存只有在要被DMA访问的时候才需要物理上连续
vmalloc比kmalloc要慢
slab层
为便于数据的频繁分配和回收,用到了空闲链表。相当于对象高速缓存
slab分配器扮演了通用数据结构缓存层的角色
slab层的设计
slab层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象
例如:一个高速缓存用于存放进程描述符,而另一个高速缓存存放索引节点对象,kmalloc()接口建立在slab层之上,使用一组通用高速缓存
结构模型
高速缓存
slab
对象
对象
对象
slab
对象
对象
slab
slab分配器的接口
一个新的高速缓存通过一下函数创建:
struck kmem_cache * kmem_cache_create(
const char *name,
size_t size,
size_t align,
unsigned long flags,
void (*ctor)(void *))
在栈上的静态分配
高端内存的映射
每个CPU的分配
新的每个CPU接口

第13章 虚拟文件系统(VFS)
为用户空间程序提供了文件和文件系统相关的接口
13.1 通用文件系统接口
13.2 文件系统抽象层
用户空间writr() -> VFS sys_write() -> 文件系统 文件系统的读写方法 -> 物理介质
Unix文件系统
文件、目录项、索引节点和安装点
VFS对象及其数据结构
超级块对象,代表一个具体的已安装文件系统
索引节点对象,代表一个具体文件
目录项对象,代表一个目录项,是路径的组成部分
文件对象,代表由进程打开的文件
13.5 超级块对象
用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块
13.7 索引节点对象
索引节点对象包含了内核在操作文件或目录时需要的全部信息
13.9目录项对象
VFS把目录当成文件对待,即上述索引节点表示,但VFS经常需要执行目录相关操作,如路径名查找,为了方便,VFS引入目录项概念。
13.9.1 目录项状态
有三种状态,被使用、未被使用和负状态
目录项对象释放后也可以保存到slab对象缓存中
13.9.2 目录项缓存
13.11 文件对象
文件对象表示进程已打开的文件

第15章 进程地址空间
内核除了管理本身的内存外,还要管理用户空间中进程的内存。这个内存称为进程地址空间。也就是系统中每个用户空间进程所看到的内存
15.1 地址空间
进程地址空间由进程可寻址的虚拟内存组成,而且更重要的是内核允许进程使用这种虚拟内存中的地址
比如0804800-0804c00,他们可被进程访问。可被访问的合法地址空间称为内存区域
15.2 内存描述符
内核使用内存描述符结构体表示进程的地址空间,包含了与进程地址空间有关的全部信息
15.2.1 分配内存描述符BX
进程描述符中包含的mm域存放着该进程使用的内存描述符。
fork()利用copy_mm()函数复制父进程的内存描述符,而子进程的的mm_struct()结构体就是由mm_cachep slab缓存中分配的。一般每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。
如果父进程希望和子进程共享地址空间,可以在调用clone时,设置CLONE_VM标志。这样的进程就称为线程。当CLONE_VM被指定后,内核就不在需要从slab新分配mm_struct了,而仅仅需要调用copy_mm()时将mm域指向其父进程的内存描述符
15.2.2 撤销内存描述符
15.2.3 mm_struct与内核线程
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中mm域为空。事实上,这正是内核线程的真正意义,他们没有用户上下文。内核线程将直接使用前一个进程的内存描述符。
15.3 虚拟内存区域
内存区域由vm_area_struct结构体描述。内存区域在LInux内核中也称为虚拟内存区域(VMAs)
vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围
15.3.3 内存区域的树型结构和内存区域的链表结构
内存描述符中的mmap和mm_rb域之一访问内存区域,这两个域各自独立的指向与内存描述符相关的全体内存区域对象。包含完全相同的vm_area_struct结构体的指针,仅仅组织方法不同。链表+红黑树
15.3.4 实际使用中的内存区域
注意共享
15.4 操作内存区域
find_vma()用于找到一个给定的内存地址属于哪一个内存区域
。。。。
15.5 mmap()和do_mmap():创建地址区间
15.6 mummap()和do_mummap():删除地址区间
15.7 页表


http://www.niftyadmin.cn/n/3172425.html

相关文章

顺序表的个人心得

为什么要有顺序表&#xff1f; 为了有序存储连续的数据。顺序表存储&#xff1a;开辟连续的内存空间&#xff0c;空间里保存真实数据的引用地址&#xff0c;因为地址固定存储四个字节&#xff0c;可以让当前数据结构产生一种规律&#xff0c;不管访问哪个元素&#xff0c;用开始…

day4-用户授权

重置数据库员密码 mysqladmin -hlocalhost -uroot -p password "" 恢复数据库管理员密码 [rootlocalhost ~]# /etc/init.d/mysqld stop [rootlocalhost ~]# /etc/init.d/mysqld start --skip-grant-table mysql>update mysql.user set passwordpassword("新密…

17.动态规划入门

一、算法介绍 1.简介 本次课我们将介绍介绍动态规划&#xff08;Dynamic Programming, DP&#xff09;及其解决的问题、根据其设计的算法及优化。 动态规划是编程解题的一种重要手段&#xff0c;它是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规…

Linux设备驱动程序学习笔记——第七章时间、延迟及延缓操作

Linux设备驱动程序学习笔记 第七章时间、延迟及延缓操作 一、度量时间差 内核通过定时器中断来跟踪时间流 时钟中断由系统定时硬件以周期性的间隔产生&#xff0c;这个间隔由内核根据HZ的值设定&#xff0c;HZ是一个与体系结构相关的常数&#xff0c;定义在<linux/param.h…

Linux设备驱动程序学习笔记——第八章分配内存

Linux设备驱动程序学习笔记 第八章分配内存 一、kmalloc函数的内幕 &#xff08;1&#xff09;flags参数 //kmalloc原型 #include<linux/slab.h> void *kmalloc(size_t size, int flags);//flags分配标志&#xff0c;最常用的是GFP_FERNEL分配标志&#xff1a; GFP_A…

Django 分组查询与ordering字段 巨坑

样例&#xff1a; class Test(Model):class Meta:db_table testordering [字段1&#xff0c; 字段2&#xff0c; 字段3]django里常见的 group by 查询写法 : Test.objects.values(查询字段).filter(过滤条件).annotate(分组字段)遇到的问题&#xff1a; 这个分组查询的巨坑…

MyBatis使用数组作为参数的配置方法

2019独角兽企业重金招聘Python工程师标准>>> 传入的参数是String[] arr; resultMap的值是resultMap的id,也是定义了java属性和表字段对应关系的配置的id collection的值必须为array item是指遍历时的别名, index即下标 <!-- 使用数组为参数查出所有子代理商的订单…

Linux设备驱动程序学习笔记——第九章 与硬件通信

Linux设备驱动程序学习笔记 第九章 与硬件通信 一、I/O端口和I/O内存 每种外设都通过读写寄存器进行控制。 &#xff08;1&#xff09;I/O寄存器和常规内存 注意避免由于CPU或编译器不恰当的优化而改变预期的I/O动作。 有硬件缓存引起的问题很好解决&#xff0c;只要把底层硬…