Linux内核设计与实现 - 第3版
一、Linux内核简介
内核态和被其保护起来的内存空间,统称为内核空间
应用程序通过系统调用在内核空间运行,则内核被称为运行于进程上下文。另一个上下文是中断上下文,与进程上下文无关。
每个处理器任何时间点上的活动,为下列之一:
- 运行于用户空间,执行用户进程;
- 运行于内核空间,处于进程上下文,代表某个特定的进程执行;
- 运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。
进程上下文:系统调用,内核线程
单内核,微内核
- 单内核,就是把它从整体上作为一个单独的大过程来实现,同时也运行于一个单独的地址空间上。这样内核之间的通信是微不足道的;
- 微内核,功能被划分为多个独立的过程,每个过程叫做一个服务器。理想情况下,只有强烈请求特权服务的服务器才运行在特权模式,其它运行在用户空间。所有服务器都保持独立且运行在各自的地址空间上,需要IPC机制。
Linux是单内核,但其支持模块化设计,抢占式内核,支持内核线程,动态装载内核模块,内核本身可调度。
- 抢占式内核是指内核抢占,即内核执行某个函数时,如果出现了更高优先级的任务,那么就执行更高优先级的任务。
进程,线程:都一样,只是有些共享资源。
二、从内核出发
1.git clone git://git.kernel.org/pub/scm/linux/kernel/torvalds/linux-2.6.git; git pull
2. 内核源码树
arch 特定体系结构的源码
block 块设备IO层
drivers
firmware 使用某些驱动程序而需要的设备固件
fs VFS和各种文件系统
include 内核头文件
init 内核引导和初始化
kernel 像调度程序这样核心子系统
lib 通用内核函数
mm 内存管理子系统和VM
usr 早期用户空间代码(所谓的initramfs)
3. 配置, 编译,安装内核
#make config // #make menuconfig // #make gconfig
#make
#x86系统,把arch/i386/bzImage拷贝到/boot木下,像vmlinuz-version这样命名,编辑/etc/grub/grub.conf,然后#make modules_install
编译时会在内核源代码的根目录下创建一个System.map文件,这是一份符号对照表,用以将内核符号和他们的起始地址对应起来。
4.内核中的内存都不分页。也就是说,你没用掉一个字节,物理内存就减少一个字节。 -- 不分页,说明内存不能换出。
三、进程管理
创建进程的时候, 会分配一个内核栈空间,其底部是 struct thread_info,能找到struct task_struct;
线程看起来就是一个普通进程,只是共享某些资源,比如地址空间,也同样有一个struct task_struct;
内核线程: - #ps -ef
- 内核经常需要在后台执行一些操作。这种任务可以通过内核线程完成 - 独立运行在内核空间的标准进程。
- 内核线程没有独立的地址空间,从来不切换到用户空间去;
- 可以被调度,可以被抢占;
四、进程调度
IO消耗型和CPU消耗型,吞吐量和响应时间之间的平衡。
1. 进程优先级
nice : -20 ~ 19,对别人友好
prio: 0 ~ 99,实时优先级
#ps -eo state,uid,pid,ppid,rtprio,time,comm
如果rtprio列为-,则说明其不是实时进程。
2. 调度器类
Linux调度器以模块的方式提供,每个调度器有一个优先级。调度器会按照优先级遍历每个调度器类。
CFS:公平调查,运行最少的优先调度。SCHED_NORMAL
实时调度:SCHED_FIFO, SCHED_RR
五、系统调用
在Linux中,系统调用是用户空间访问内核的唯一手段:除异常和陷入外,他们是内核唯一的合法入口。
提供机制而不是策略
asmlinkage:这个是一个限定词,是一个编译器指令,通知编译器仅从栈中提取该函数的参数。
系统调用,通过软中断,通知内核,内核读取系统调用号和参数,然后执行。
copy_to_user(), copy_from_user() --- 可能引起阻塞
六、内核数据结构
struct head_list,是链表头指针。
队列:kfifo
七、中断和中断处理
硬件 ----- 电信号 ------> 中断控制器 --------- 电信号 -------- > 处理器
一般PC机上:IRQ0 - 时钟中断, IRQ1 - 键盘, 对于PCI总线上的设备,中断是动态分配的。
中断和异常
- 异常产生时,必须考虑与处理器时钟同步。实际上,异常也常常称为同步中断。在处理执行到由于编程失误而导致的错误指令,比如除0,或者执行期间出现特殊情况,比如缺页,必须靠内核来处理的时候,处理器就会产生一个异常。系统调用就是一种特殊的异常。
- 中断 : 随时产生
在响应一个终端的时候,内核会执行一个函数,叫做中断处理程序,或者中断服务例程ISR。一个设备的中断处理程序是它驱动程序的一部分。
中断上下文,也称原子上下文,不能阻塞,不能睡眠。
注册中断处理程序,request_irq() - 可能会睡眠,必须在设备初始化完成后,采取注册ISR;
共享中断先,如何区分不同设备,*dev
Linux中的中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有的处理器上都会被屏蔽掉,以防止在同一个中断线上接收到另一个新的中断。但一般来说,其它中断线都是打开的。
中断栈,每个CPU一个,大小为一页。
#cat /proc/interrupts
in_interrupt() - 返回非0,处于上半部或者下半部
in_irq() - 返回非0,处于上半部
八、下半部和推后执行的工作
1. 软中断 - 可以响应中断,但不能休眠
struct softirq_action, 共计32个,在编译时确定
一个软中断不会抢占另一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。
不过,其它的软中断(甚至是类型相同的软中断)可以在其它处理器上同时执行。
触发:raise_softirq();
2. tasklet - 基于软中断 - 不能睡眠,不能使用阻塞函数
两个相同的tasklet不会同时执行,即使存在多处理也是一样。
处理函数有时还会自行重复触发,他们不会立即处理重新触发的软中断。
- 内核线程:ksoftirqd/n,每个处理器一个,当大量软中断出现的时候,给内核线程就会处理。
3. 工作队列 - 进程上下文,允许重新调度,甚至睡眠,可以使用信号量
- events/n 内核线程
选择:
- 要睡眠,工作队列
- 否则tasklet
- 必须专注于性能的提高,再考虑软中断
九、内核同步介绍
要给数据而不是代码加锁。
十、内核同步方法
1.原子操作,保证操作完整性
2.自旋锁 - spinlock
适合轻量加锁,不可递归;
可以用在中断处理程序中,但获取锁前,先要禁止本地中断(当前处理上面的中断请求),否则中断处理程序可能就会打断持有锁的内核代码,然后去争用这个锁。这样一来,中断处理程序就会自旋,等待该锁重新可用。
3. rwlock - 读写自旋锁 - 照顾读操作
4.信号量 struct semaphore, up(), down(), down_interruptable()
5.读写信号量 - 照顾读操作
6.互斥体 - mutex - 二值信号量
7. 完成变量 - complete(), wait_for_complete()
8. 顺序锁 - 照顾写,适用于写操作很少时,write_pending时不允许读
9. 顺序和屏障
持有自旋锁时,不允许抢占 ? 还是担心中断处理程序。
十一、定时器和时间管理
系统定时器是一种可编程硬件芯片,他能以固定频率产生中断,- 所谓的定时器中断。
HZ = 1000,即每秒系统定时器中断的次数。
全局变量jiffies用来记录自系统启动以来产生的节拍总数。
jiffies回绕问题:time_after, time_before
体系结构提供了两种设备进行计时:
- 系统定时器:提供一种周期性触发的中断机制。在x86中,主要采用可编程中断时钟PIT
- 实时时钟(RTC):用来持久存放系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。
定时器: - 动态定时器,内核定时器
- 推后一定时间,执行某个任务。
- 其实现使用软中断
- 延迟执行,无论如何不能在持有锁和禁止中断的情况下发生
短延迟:udelay(), mdelay(), ndelay() -- BogoMIPS - 在系统启动信息中可以看到
十二、内存管理
找了两篇文章
http://www.cnblogs.com/wuchanming/p/4339770.html
http://blog.csdn.net/yusiguyuan/article/details/45155035
struct page
内存管理单元MMU使用页作为内存管理的基本单位。struct page描述物理内存本身,而不是内存里面的内容。
内核需要知道谁拥有这个页,1.用户空间进程,2.动态分配的内核数据,3.静态内核代码,4.页高速缓存(mapping字段)。
struct zone
由于硬件限制,内核不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。由于这些限制,所以内核把页划分为区。
一般包括:DMA,Normal,HighMem。这个是和体系结构相关的。比如x86-32上面,分界线是16M,896M。HighMem中的页,不能永久地映射到内核地址空间。
在x86-64的体系结构中,可以映射和处理64位的内存空间,所以没有HighMem。
注意:区的划分没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑上的分组。
内存分配:
内存的分配,不能跨区。可以按页分配,也可以按字节。
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) // 返回2的order次幂个连续物理页,并返回第一个页的page结构体指针。
void * page_address(struct page * page) // 返回页的逻辑地址,但对于HighMem中分配的内存,则不能获取逻辑地址。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) // 直接返回逻辑地址。
如果只需要一个页,可以调用下面的函数:
struct page * alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)
获得填充为 0 的页
unsigned long get_zeroed_page( unsigned int gfp_mask ) // gfp_mask 的类型怎么不一致 ?
====== 返回逻辑地址的分配函数,不能在HighMem区分配。
释放内存
void __free_pages( struct page * page, unsigned int order )
void free_pages( unsigned long addr, unsigned int order ) // 释放函数和分配函数好像不对应 ?
void free_page(unsigned long addr) // 释放一个页
====== 在程序开始时就先进行内存分配是有意义的,否则对于内存分配可能陷入困境。
按字节分配
void * kmalloc( size_t size, gfp_t flags ) // 物理上是连续的
void kfree( const void * ptr ) // 释放不是kmalloc分配的内存,释放已经释放的内存,都会导致严重后果。
void * vmalloc( unsigned long size ) // 分配的内存,虚拟地址连续,但物理地址未必连续。
void vfree( const void * addr )
虽然某些情况下才需要物理上连续的内存,但在内核中,出于性能考虑,都使用kmalloc,而不是vmalloc,因为vmalloc分配的内存必须一个页一个页进行映射。
gfp_mask 标志:
分为3类:
- 行为修饰符:表示内核应当如何分配所需的内存
__GFP_WAIT : 分配器可以睡眠, __GFP_HIGH : 分配器可以访问紧急事件缓冲池
__GFP_IO:分配器可以启动磁盘IO,__GFP_FS:分配器可以启动文件系统IO
__GFP_COLD:分配器应该使用高速缓存中快要淘汰出去的页,__GFP_NOWARN:分配器将不打印警告
__GFP_REPEAT:分配器在分配失败时重复进行分配,但是这次分配仍然可能失败,__GFP_NOFALL:分配器将无限地重复进行分配,分配不能失败。
__GFP_NORETRY:分配器在分配失败时绝不会重新分配
__GFP_NO_GROW:由slab层使用,__GFP_COMP:添加混合页元数据,在hugetlb的代码内部使用。
- 区修饰符:表示内存应当从哪个区分配
-- __GFP_DMA:强制内核从ZONE_DMA分配
-- __GFP_HIGHMEM:优先从ZONE_HIGHMEM分配,或者从ZONE_NORMAL分配
-- 如果没有指定任何标志,则内核优先从ZONE_NORMAL分配,接下来尝试ZONE_DMA。
不能给__get_free_pages() 和 kmalloc() 指定 ZONE_HIGHMEM ,因为他们返回逻辑地址。可能还没有映射到内核的虚拟地址空间,也就可能还没有逻辑地址。
- 类型标志: 前两者的组合,一般使用类型标志即可。
ATOMIC, NOWAIT, NOIO, NOFS, KERNEL, USER, HIGHUSER, DMA
一般来说,在进程上下文,且可睡眠,则使用KERNEL,否则使用ATOMIC即可,DMA用在需要DMA内存的设备驱动程序中。
slab层 - 高速缓存
对于频繁分配的对象,空闲链表很好用,但内核无法控制,引入了slab层,通过简单的接口,实现对象的分配,释放。比如进程描述符task_struct, inode等。
高速缓存由若干个slab组成,每个slab有若干个物理上连续的页组成,每个slab可以包含若干同类对象。
slab分为:满,部分满,空闲。分配时先找部分满的slab,没有的话就找空闲slab,没有的话就需要分配新的slab。
kmalloc在slab层上实现。
接口: - 注意,可能睡眠的接口,只能在进程上下文中调用。
struct kmem_cache * kmem_cache_create( const char * name, // 告诉缓存的名字,比如 “task_struct”
size_t size, // 每个对象的大小,比如 sizeof(struct task_struct)
size_t align, // slab内第一个对象偏移,进行页内对齐,通常0即可
unsigned long flags, // 标识,见后面
void (*ctor) (void *) ) // 构造函数,NULL即可
int kmem_cache_destroy( struct kmem_cache * cachep )
void * kmem_cache_alloc( struct kmem_cache * cachep, gfp_t flags )
void kmem_cache_free( struct kmem_cache* cachep, void * objp )
slab高速缓存创建标志
- SLAB_HWCACHE_ALIGN : 命令slab层把一个slab内的所有对象按照高速缓存行对齐,可以提高性能,代价是增加了一定内存开销。
- SLAB_POISON :命令slab层使用已知的值(a5a5a5)条从slab,所谓的中毒。
- SLAB_RED_ZONE:命令slab层在已经分配的内存周围插入“”红色警戒区“以探测缓冲越界”
- SLAB_PANIC :分配失败时提醒slab层,在要求分配只能成功时使用。
- SLAB_CACHE_DMA :
高端内存映射
根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。
在x86上,高端内存中的页被映射到3GB ~ 4GB。
- 永久映射 - 数量是有限的
void * kmap(struct page *page) - 可能睡眠,在高端内存或者低端内存中都能用。
void kunmap(struct page *page)
- 临时映射 - 原子映射
用于必须创建一个映射而当前的上下文又不能睡眠时
void *kmap_atomic( struct page * page, enum km_type type )
void kunmap_atomic ( void * kvaddr, enum km_type type )
每个CPU数据
十三、虚拟文件系统
VFS是对各个具体文件系统的抽象,提供统一的接口,具体实现则由具体的文件系统实现,这样各种文件系统之间就可以交流。
当然VFS是按照unix文件系统风格设计的,对于那些不支持目录项、inode等概念的文件系统,需要模拟出相应的概念才行,一般都需要在使用时进行某些处理。
1.Unix文件系统
文件系统包含文件、目录和相关的控制信息。文件系统被安装在一个特定的安装点上,所有已经安装的文件系统都作为根文件系统树的枝叶出现在系统中。
Unix文件系统是面向字节流的,不同于面向记录的文件系统(OpenVMS的File-11)。
文件、目录:属于普通文件
目录项,目录条目:路径中的每一个都是目录项,包含最后的文件。比如:/home/wolfman/butter,/,home,wolfman,butter都是目录项。
文件相关信息,称作文件的元数据:inode,不同于文件内容本身。
文件系统的控制信息在超级快中。
2. VFS对象及其数据结构
VFS中有4个主要对象,每个对象结构体中都有操作对象,包含其操作函数。
超级块对象:super_block,代表一个具体的已经安装的文件系统
索引节点对象:inode,代表一个具体的文件(文件和目录)
目录项对象:dentry,代表一个目录条目,是路径的组成部分
文件对象:file,代表由进程打开的文件
另外
file_system_type :每个注册的文件系统
vfsmount :每个安装点
3. 超级块对象 - super_block
超级块用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或者文件系统控制块。对于并非基于磁盘的文件系统(如sysfs),他们会在使用现场创建超级块并将其保存到内存中。
超级块操作:super_operations
4.索引节点对象 - inode
索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于Unix风格的文件系统,这些信息可以从磁盘索引节点直接读入。
一个索引节点,可以代表普通文件(文件,目录),也可以代表特殊文件(块设备,字符设备,管道,套接字)
其操作inode_operations中的mknod用来创建特殊文件。
int mknod( struct inode *dir, struct dentry *dentry, int mode, dev_t rdev )
要创建的文件存放在dir目录中,目录项是dentry,关联的设备是rdev
5.目录项对象 - dentry
VFS把目录当作文件对待,虽然路径中每一个部分都可以由inode表示,但VFS经常需要执行目录相关的操作,比如路径名查找等。路径名查找需要解析路径中的每一个组成部分,不但要确保它有效,而且还需要再进一步寻找路径中的下一个部分。
为了方便查找操作,VFS引入了目录项的概念。
与超级块对象和索引节点对象不同,目录项对象没有对应的磁盘数据结构,使用时根据路径名现场创建,不会写到磁盘。
目录项有3中状态:
- 被使用:目录项对应一个有效的inode节点,由d_inode指向,d_count为正值。
- 未被使用:目录项对应一个有效的inode节点,但d_count = 0
- 负状态:目录项没有对应的有效的inode节点,d_inode = NULL
目录项缓存:-----
6. 文件对象 - file
文件对象表示进程已打开的文件,是已打开文件在内存中的表示,该对象不是物理文件。
由于多个进程可以打开和操作同一个文件,因此同一个文件也可能存在多个对应的文件对象。
文件对象仅在进程观点上代表已打开的文件,它反过来指向目录项对象(f_path -> f_dentry),而目录项对象指向索引节点对象,其实目录项对象才表示已打开的实际文件。
文件操作 - file_operations
ioctl - 当文件是一个被打开的设备节点时,可以通过它进行设置操作。
int mmap( struct file *file, struct vm_area_struct * vma ) - 将给定的文件映射到制定的地址空间上。
unsigned long get_unmapped_area() - 获取未使用的地址空间来映射给定的文件。
sendfile() , sendpage,从一个文件拷贝内容到另一个文件,在内核中进行,不经过用户空间。
7. 文件系统 - file_system_type - vfsmount
描述文件系统的功能和行为。每种文件系统,不管有多少个实例安装到了系统中,都只有一个file_system_type对象。
当文件系统被安装时,将有一个vfsmount对象在安装点被创建,该结构体表示文件系统的实例,代表一个安装点。
8. 其它数据结构 - file_struct, fs_struct, namespace
系统中的每个进程都有自己的一组打开的文件,像根文件系统,当前工作目录,安装点等。
进程描述符中的files指向file_struct,所有与单个进程相关的信息(如打开的文件及文件描述符)都包含在其中。其中的fd_array,指向已打开的文件对象,默认64,如果进程打开的文件过多,则需要动态分配,性能稍有影响。
进程描述符中的fs指向fs_struct,它包含文件系统和进程的相关信息。该结构体也包含了当前进程的当前工作目录和根目录。
2.4内核后,单进程命名空间被加入到内核中,它使得每一个进程在系统中都看到唯一的安装文件系统 - 不仅是唯一的根目录,而且是唯一的文件系统层次结构。
对于大多数进程来说,他们的进程描述符都指向唯一的files_struct和fs_struct结构体,但对于那些使用克隆标志CLONE_FILES,CLONE_FS创建的进程,则会共享这两个结构体,每个结构体都会维护使用计数。
对于namespace,默认情况下,所有的进程都共享同样的命名空间(也就是,它们都从相同的挂载表中看到同一个文件系统层次结构)。只有在进行clone()操作时使用CLONE_NEWS标志,才会给进程一个唯一的命名空间结构体的拷贝。
十四、块IO层
1.基本概念
系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的硬件设备称为块设备。这些固定大小的数据片称为块。按照字符流方式被有序访问的设备,称为字符设备。他们的区别在于能否随机访问。
扇区是块设备中最小的可寻址单元,常见512字节,这是设备的物理属性,块设备无法对比扇区还小的单元进行寻址和操作,但可能一次对多个扇区进行操作。
块是文件系统的一种抽象,只能基于块来访问文件系统。块不小于扇区,不大于页。扇区又叫硬件块,设备块,而块又叫文件块,IO块。
2.缓冲区和缓冲区头
当一个块被调入内存时,他要存储在一个缓冲区中,每个缓冲区与一个块对应,它相当于磁盘块(文件系统块)在内存中的表示。一个页可以容纳多个块。
每个缓冲区都有一个对应的缓冲区描述符,存储缓冲区的控制信息,叫做缓冲区头,buffer_head。里面包含缓冲区的状态,关联的设备,保存在哪个页中等。缓冲区头用于描述磁盘块和物理内存缓冲区之间的映射关系。
对内核来说,更倾向于操作页面结构。若使用缓冲区头进行IO操作,需要将对页的操作分解到若干个对缓冲区头的操作。
3.bio结构体 - block IO
内核中块IO操作的基本容器由bio结构体表示。该结构体代表了正在现场的(活动的)以片段(segment)链表形式组织的IO操作。一个片段就是一小块连续的内存缓冲区。像这样的向量IO就是所谓的聚散IO。
struct bio结构体包含bio_vec链表,每个对应一个页面的IO操作,包括页面指针,缓冲区起始位置,长度。
相对于buffer_head,struct bio可以表示多个离散的IO操作,且是一个简单的结构体。
4.请求队列
块设备将它们挂起的块IO请求保存在请求队列中。其中的请求由request表示。由于磁盘寻址比较慢,内核会对请求进行合并和排序,这可以极大提高系统的整体性能。
IO调度程序 - 电梯调度:
- Linus电梯
- 最终期限 - deadline:每个请求都有一个最终期限,读500ms,写5s
- 预测IO调度程序 - as:利用程序的局部性,等待一会儿,看看能否合并;
- 完全公正的排队调度程序:cfq:每个进程一个队列,不同队列按照时间片轮转;
- 空操作的IO调度程序 - noop:除了合并,不做其它操作。适用于完全随机的设备。
十五、进程地址空间
内核除了管理本身的内存外,还必须管理用户空间中进程的内存,这个内存称为进程地址空间,也就是系统中每个用户空间进程所看到的内存。
进程地址空间由进程可寻址的虚拟内存组成,而且更为重要的特点是:内核允许进程使用这种虚拟内存中的地址。
1.地址空间
在32位系统上,尽管一个进程可以寻址4GB的虚拟内存,但这并不代表它就有权访问所有的虚拟地址。在地址空间中,我们更关心的是一些虚拟内存的地址区间,这些可被访问的合法地址空间称为内存区域(memory areas)。通过内核,进程可以给自己的地址空间动态地添加或者减少内存区域。内存区域不能相互重叠。
进程只能访问有效内存区域内的内存地址。每个内存区域也有相关的权限。如果一个进程访问了不在有效范围中的内存区域,或者以不正确的方式访问了有效地址,那么内核就会终止该进程,并返回“段错误”信息。
内存区域可以包含各种内存对象:代码段、数据段、未初始化全局变量段bss,用于进程用户空间栈的零页内存映射、任何内存映射文件、任何共享内存段,任何匿名的内存映射(比如由malloc分配的内存)
2.内存描述符 - mm_struct,由task_struct->mm指向
内核使用内存描述符结构体表示进程的地址空间.其中包含 - struct vm_area_struct *mmap - 字段,是内存区域链表。两个线程可能共享地址空间(clone时设置CLONE_VM)。
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中的mm为NULL。事实上,这也正是内核线程的真实含义 - 他们没有用户上下文。
3.虚拟内存区域
内存区域由vm_area_struct结构体描述,Linux内核中也经常成为虚拟内存区域(virtual memory areas - VMAs),它描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存区域作为一个单独的内存对象管理,每个区域都拥有一致的属性,比如访问权限等,另外,相应的操作也一样。
VMA标志,读写执行权限,VM_SHARED表明该VMA可以在多个进程间共享,则成为共享映射,否则称为私有映射。
查看实际的内存区域:cat /proc/pid/maps或者#pmap pid
4.VMA操作
find_vma():找到一个内存地址属于哪个VMA
5、创建地址区间,即VMA
mmap() -> do_mmap()创建一个新的VMA,如果新的VMA和一个已有的VMA相邻,且具有相同的访问权限的话,两个区间将合并成一个。do_mmap()函数将一个地址区间加入到进程的地址空间中 - 无论是扩展已存在的内存区域还是创建一个新的内存区域。
unsigned long do_mmap( struct file * file, unsigned long addr, unsigned long len , unsigned long prot,
unsigned long flag, unsigned long offset )
如果file = NULL,且offset = 0,那么就代表这次映射没有和文件相关,称作匿名映射(annoymous mapping)。如果指定文件名和偏移量,那么该映射称为文件映射(file-backed mapping)
6、删除地址区间 -- munmap() - do_munmap()
7、页表
虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当应用程序访问一个虚拟地址时,首先必须将虚拟地址转换成物理地址,然后处理器才能解析地址访问请求。
Linux通过3级页表完成转换 - PGD,PMD - PTE每个进程都有自己的页表,线程会共享页表,内存描述符的pgd指向的就是进程的页表。
搜索内存中的物理地址的速度很悠闲,因此为了加快搜索,多数体系结构都实现了一个翻译后缓冲器(translate lookaside buffer, TLB),TLB作为一个将虚拟地址映射到物理地址的硬件缓存。
十六、页高速缓存和页回写
页高速缓存(cache)是Linxu内核实现的磁盘缓存。它主要用来减少磁盘的IO操作。具体讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。
- 访问磁盘和访问内存时好几个数量级的差别,ms - ns
- 程序的局部性原理,一旦被访问,可能再次被访问
1.缓存手段
页高速缓存是由内存中的物理页面组成的,其内容对应磁盘上面的物理块。页高速缓存的大小能够动态调整 - 它可以通过占用空闲内存以扩张大小,也可以自我收缩以缓解内存的使用压力。我们称正被缓存的存储设备为后备存储。
写缓存策略:
-- 不缓存
-- 写透缓存(write-through cache),写操作将自动更新内存缓存,同时也更新磁盘文件
-- 回写:写操作直接写到缓存中,后端存储不会立即更新,而是将也告诉缓存标记为“脏”,由回写进程周期写到磁盘。
缓存回收策略:
-- 最近最少使用 - LRU
-- 双链策略
2. Linux页高速缓存
页高速缓存,缓存的是内存页面。缓存中的页来自对正规文件、块设备文件和内存映射文件的读写。如此一来,页高速缓存就包含了最近被访问过的文件的数据块。在执行一个IO操作前,内核会检查数据是否已经在页高速缓存中了,如果在,就不需要再从磁盘读取了。
在页高速缓存中的页,可能包含了多个不连续的物理磁盘块(扇区/设备块,还是文件块 ?似乎是文件块)。
Linux页高速缓存使用了一个新对象管理缓存项和页IO操作 - address_space结构体,它是 vm_area_struct的物理地址对等体。当一个文件被10个VMA结构体标识(比如5个进程,每个进程调用mmap()映射2次),那么这个文件只能有一个address_space数据结构,也就是文件可以有多个虚拟地址,但是只能在物理内存有一份。
address_space的host字段是其owner,如果和文件对应,那就是inode节点,否则为NULL。
3.缓冲区高速缓存
独立的磁盘块通过IO缓冲也要被存入页高速缓存。一个缓冲区是一个物理磁盘块在内存里面的表示。缓冲的作用就是映射内存中的页面到磁盘块,这样一来页高速缓存在块IO操作时也减少了磁盘访问,因为它缓存磁盘块和减少IO操作。这个缓存通常称为缓冲区高速缓存。
缓冲区和页高速缓存现在是统一的。缓冲区请参考ch14,它是用页映射块的,所以正好在页高速缓存中。
十七、设备与模块
1. 设备类型:在所有unix系统中为了统一普通设备的操作所采用的分类
三种类型:块设备、字符设备、网络设备
块设备通过称为“块设备节点”的特殊文件来访问,并且通常被挂在为文件系统。 - 应用程序通过与文件系统交互 ?
字符设备是通过称为字符设备节点的特殊文件访问的。与块设备不同,应用程序通过直接访问字符设备节点与字符设备交互。
网络设备打破了所有东西都是文件的设计原则,它不是通过设备节点来访问,而是通过套接字API这样的特殊接口来访问的。
Linux也提供了不少其它设备类型。
并不是所有设备驱动都表示物理设备。有些设备驱动是虚拟的,仅提供访问内核功能而已 - 伪设备(pseudo device),最常见的如内核随机数发生器/dev/random,空设备/dev/null等。
2. 模块
Kconfig,Makefile,obj-m,fishing-objs
如果不在内核代码树中维护,那么需要指定内核源代码的目录
make modules_install # 将编译后的模块安装到 /lib/modules/version/kernel/ 目录下,比如 /lib/modules/version/kernel/drivers/char/fishing.ko
模块依赖性:depmod or depmod -A,模块依赖信息保存在 /lib/modules/version/modules.dep 中
载入模块:insmod, rmmod, modprobe
3. 设备模型
统一设备模型(device model)最初是为了以正确的顺序关闭设备的电源。
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent; // 指向其parent
struct kset *kset; // 其所属子系统
struct kobj_type *ktype; // 一类kobject所具有的普遍属性。
struct sysfs_dirent *sd; // 在sysfs中表示这个kobject,从sysfs内部看,是一个inode
......
}
struct kobj_type {
void (*release) (struct kobject *); // 引用计数减至0时调用
const struct sysfs_ops * sysfs_ops; // show, store,读写函数
struct attribute ** default_attrs; // 每个属性,在sysfs都对应一个文件
}
struct kset {
struct list_head list; // 连接集合中所有的kobject
spinlock_t list_lock;
struct kobject kobj; // 该集合的基类 ***&&&&
struct kset_uevent_ops * uevent_ops; // 用于处理集合中kobject对象的热插拔操作;
}
kset是一个容器,把kobject集合到一个集合中,而ktype描述相关类型kobject所共有的特性。他们之间的重要区别在于:具有相同ktype的kobject可以被分组到不同的kset。就是说Linux内核中,只有少数一些的ktype,却有多个kset(每个kset代表一个子系统 ?)。
管理操作kobject
kmaloc / memset / kobject_init / - > kobject_create
4. sysfs
sysfs文件系统是一个处于内存中的虚拟文件系统,他为我们提供了kobject对象层次结构的视图。sysfs的诀窍是把kobject对象与目录项(directory entries)紧急联系起来,这点是通过kobject对象中的dentry字段实现的。kobject对象被映射到目录项。
kobject_add // kobject_create_and_add // kobject_del
为kobject创建新属性
sysfs_create_file / sysfs_create_link / sysfs_remove_file / sysfs_remove_link
内核事件层
内核事件层实现了内核到用户的消息通知系统,借助kobject和sysfs实现。内核事件层把事件模拟为信号 - 从明确的kobject对象发出,所以每个事件源都是一个sysfs路径。
每个事件都被赋予了一个动作或者字符串表示信号,比如“被修改过”,“未被挂载”等。每个事件都有一个可选的负载(payload),内核事件层使用属性作为负载。
kobject_uevent()触发内核事件,经netlink传送给用户空间的daemon,事件不使用字符串,而使用枚举值,避免打字错误。如KOBJ_MOUNT, KOBJ_UNMOUNT, KOBJ_ADD, KOBJ_REMOVE, KOBJ_CHANGE等。
十八、调试
引用空指针会导致产生一个oops,但垃圾数据可能导致系统崩溃。
printk() - 除了arch相关的启动阶段,几乎任何地方都能使用,可以指定等级。 0 -> 7 降低。被保存到一个环形队列中,一般为16kb,可以在编译时通过 CONFIG_LOG_BUF_SHIFT 调整其大小。
printk() -> /proc/kmsg -> 用户空间守护进程 klogd -> 用户空间守护进程 syslogd -> /var/log/messages ,可以通过 /etc/syslog.conf 重定向。
OOPS 会打印寄存器信息和call track
ksymoops - 需要编译内核时产生的 System.Map 才能解码 OOPS
kallsyms - 通过 CONFIG_KALLSYMS ,编译进内核,这样内核中就会保存那些地址的函数名称( CONFIG_KALLSYMS_ALL 包括函数和其它符号)。
神奇的系统请求键 - Magic SysRq key
该功能可以通过定义 CONFIG_MAGIC_SYSRQ 配置选项来启用。这时,无论内核处于什么状态,都可以通过特殊的组合键跟内核进行通信。
除了配置选项,还要通过一个 sysctl 标记该特性的开关:echo 1 > /proc/sys/kernel/sysrq
#sysrq -h // help
#sysrq -s // 将脏缓冲区跟硬盘交换分区同步
#sysrq -u // 卸载所有的文件系统
#sysrq -b // 重启设备
详细见文档:Documentations/sysrq.txt。实际的实现,在 /drivers/char/sysrq.c 中
十九、移植
二十、社区