Linux管理子系统

来源:互联网 发布:阿里云注册域名手机号 编辑:程序博客网 时间:2024/05/17 03:55

内存管理子系统

内存是Linux内核所管理的最重要的资源之一,内存管理子系统是操作系统中最重要的部分之一。对于立志从事内核开发的工程师来说,熟悉Linux的内存管理系统非常重要。

地址类型:
(1)物理地址:物理地址是指出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。
(2)逻辑地址:程序代码经过编译后,出现在汇编程序中的地址。
(3)虚拟地址:线性地址又名虚拟地址,在32位的CPU架构下,可以表示4G的地址空间,用16进制表示就是0x00000000到0xffffffff。

地址转换:
CPU要将一个逻辑地址转换为物理地址,需要两步:首先CPU利用段式内存管理单元将逻辑地址转换为线性地址,再利用页式内存管理单元,把线性地址最终转化为物理地址。


段式管理:16位CPU内部拥有20位的地址线,它的寻址范围就是2的20次方,也就是1M的内存空间,但是16位的CPU用于存放地址的寄存器(IP,SP...)只有16位,因此只能访问65536个存储单位,64K。
为了能够访问1M的内存空间,CPU就采用了内存分段的管理模式,并在CPU内部加入了段寄存器。16位的CPU把1M内存空间分为若干个逻辑段,内个逻辑段的要求如下:(1)逻辑段的起始地址(段地址)必须是16的倍数,即最后4个进制位必须全为0;(2)逻辑段的最大容量为64K;
物理地址的形成方式:由于短地址必须是16的倍数,所以值得一般形式为XXXX0H,即前16位的二进制位是变化的,后四位是固定的0,鉴于段地址的这种特性,可以只保存前16位二进制位来保存整个段基地址,所以每次使用时要用段寄存器左移补4个0来得到实际的段地址。
逻辑地址=段基地址+段内偏移量
由逻辑地址得到物理地址的公式为:PA=段寄存器的值*16+逻辑地址

段寄存器是为了对内存进行分段管理而增加的,16位CPU有4个段寄存器,程序可同时访问四个不同含义的段。
(1)CS+IP:用于代码段的访问,CS指向存放程序的段基地址,IP指向下条要执行的指令在CS段的偏移量,用这两个寄存器就可以得到一个内存物理地址,该地址存放着一条要执行的指令。
(2)SS+SP:用于堆栈段的访问,SS指向堆栈段的基地址,SP指向栈顶,可以通过SS和SP两个寄存器直接访问栈顶单元的内存物理地址。
(3)DS+BX:用于数据段的访问。DS中的值左移4位得到数据段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。
(4)ES+BX:用于附加段的访问。ES中的值左移4位得到附加段起始地址,再加上BX中的偏移量,得到一个存储单元中的物理地址。

32位pc的内存管理仍然采用分段的管理模式,逻辑地址同样由段地址和偏移量两部分组成,和16位的相比有相同之处,也有不同之处。因为32位pc采用了两种不同的工作模式:实模式和保护模式。
(1)实模式:在实模式下,32位CPU的内存管理和16位CPU是一致的。
(2)保护模式:段基地址长达32位,每个段的最大容量可达4G,段寄存器的值使段地址的“选择器”(Selector),用该“选择器”从内存中得到一个32位的段地址,存储单位的物理地址就是该段地址加上偏移量,这与32CPU物理地址的计算方式完全不同。



页式管理:
分页管理:从管理和效率的角度出发,线性地址被分为固定长度的组,成为页(page),例如32位的机器,线性地址最大可为4G,如果用4KB为一个页来划分,这样整个线性地址就被划分为2的20次方个页。
另一类“页”称之为物理页,或者是页框、页帧,分页单元把所有的物理内存也划分为固定长度的管理单元,它的长度一般与线性地址页式相同的。

1.分页单元中,页目录的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2.每一个进程,都有其独立的虚拟地址空间,运行一个进程,首先需要将它的页目录地址放到cr3寄存器中,将其他的进程保存下来。
3.每一个32位的线性地址被划分为3个部分:页目录索引(10位):页表索引(10位):偏移(12位)

依据以下步骤进行地址转换:
(1)装入进程的页目录地址(操作系统在调度进程时,把这个地址装入cr3)。
(2)根据线性地址前十位,在页目录中,找到对应的索引项,页目录中的项是一个页表的地址。
(3)根据线性地址的中间十位,在页表中找到页的起始地址。
(4)将页的起始地址与线性地址的最后12位相加,得到物理地址。

Linux内存管理:
逻辑地址可以理解为偏移量。





Linux进程地址空间

Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间大小为3G,用户看到和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。
Linux将4G的虚拟地址空间划分为两个部分——用户空间与地址空间。用户空间从0到0xbfffffff,内核空间从3G到4G,用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程通过系统调用访问内核空间。
用户空间对应进程空间,所以每当进程切换,用户空间就会跟着变化。实际点就是页表和页目录会变。
创建进程fork()、程序载入execve()、动态内存分配malloc()等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。
实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由”请页机制”产生“缺页”异常,从而进入分配实际页框程序。该异常是虚拟内存机制赖以生存的基本保证——它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在映射到了物理地址上。

内核内存分配:
在应用程序中,常使用malloc函数进行动态内存分配,而在Linux内核中,通常使用kmalloc来动态分配内存。kmalloc的原型是:
#include<linux/slab.h>
void * kmalloc(size_t size,int flags)
参数:
size:要分配的内存大小
flags:分配标志,它控制kmalloc的行为。

分配标志:最常用的标志是GFP_KERNEL,它的意思是该内存分配是由运行的在内核态的进程调用的。也就是说,调用它的函数属于某个进程,当空闲内存太少时,kmalloc函数会使当前进程进入睡眠,等待空闲页的出现。

GFP_ATOMIC:用来在进程上下文之外的代码(包括终端处理)中分配内存,从不睡眠。
GFP_KERNEL:进程上下文分配。可能睡眠。(16M-896M)
_GFP_DMA:这个标志要求分配能够DMA的内存区(物理地址在16M以下的页帧)
_GFP_HIGHMEM:这个标志表示分配的内存位于高端内存(896M以上)

按页分配:
get_zeroed_page(unsigned int flags):返回指向新页面的指针,并将页面清零。
_get _free_page(unsigned int flags):和get_free_page类似,但不清零页面。
_get_free_page(unsigned int flags,unsigned int order):分配若干个连续的页面,返回指向该内存区域的指针,但也不清零这段内存区域。

释放:
当程序用完这些页,可以使用下列函数之一释放它们
void free_page(unsigned long addr)
void free_pages(unsigned long addr,unsigned long order)
如果释放的和先前分配数目不等的页面,会导致系统错误。


Linux内核地址空间

内核空间是由内核负责映射的,它并不会跟着进程改变,是固定的。
物理内存896M以上的部分称之为高端内存。
直接内存映射区(Direct Memory Region):从3G开始,最大896M的线性地址区间,我们称作直接内存映射区,这是因为该区域的线性地址和物理地址之间存在线性转换关系,线性地址=3G+物理地址。

动态映射区(Vmalloc Region):该区域的地址由内核函数vmalloc来进行分配,其特点是线性空间连续,但对应的物理空间不一定连续。vmalloc分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。

永久内存映射区(PKMap Region):对于896MB以上的高端内存,可使用该区域来访问,访问方法:
(1)使用alloc_page(_GFP_HIGHMEM)分配高端内存页。
(2)使用kmap函数将分配到的高端内存映射到该区域。

固定映射区(Fixing Mapping Region):PKMap区上面,有4M的线性空间,被称作固定映射区,它和4G顶端只有4K的隔离带。固定映射区中每个地址项都服务于特定的用途,如ACPI_BASE等。


Linux内核链表









Linux内核定时器

时钟中断由系统的定时硬件以周期性的时间间隔产生,这个间隔(即频率)由内核根据HZ来确定,HZ是一个与体系结构无关的常数,可配置(50-1200),在x86平台默认是1000,意思是每秒钟产生1000次时钟中断。
每当时钟中断发生时,全局变量jiffies(unsigned long)就加1,因此jiffies记录了自Linux启动后时钟中断发生的次数。驱动程序常利用jiffies来计算不同事件间的时间间隔。

延迟执行:如果对延迟精度要求不高,最简单的实现方法如下--忙等待:
unsigned long j=jiffies+jit_delay*HZ;
while(jiffies<j)
{
/* do things */
}延迟jit_delay秒

定时器用于控制某个函数(定时器处理函数)在未来的某个特定时间执行。内核定时器注册的处理函数只执行一次--不是循环执行的。
内核定时器被组织成双向链表,并使用struct timer_list结构描述。
struct timer_list{
struct list_head entry;/*内核使用*/
unsigned long expires;/*超时的jiffies值*/
void  (*function)(unsigned long);/*超时处理函数*/
unsigned long data;/*超时处理函数的参数*/
struct tvec_base *base;/*内核使用*/
};

操作定时器的有如下函数:
void init_timer(struct timer_list *timer); //初始化定时器队列结构
void add_timer(struct timer_list *timer); //启动定时器
int del_timer(struct timer_list *timer); // 在定时器超时前将它删除。当定时器超时后,系统会自动的将它删除。

0 0
原创粉丝点击