目录
概念
复习:
#问:什么叫进程?
进程 = 内核相关的数据结构 + 进程相关的代码和数据
1. 动态运行属性:
进程不仅仅是将磁盘中的程序加载到内存,然后此时执行代码和数据就行了。实际上,因为操作系统要管理所谓的进程,所以其必须先描述再组织。
即:每一个进程都要有其内核相关的数据结构。
----------------------------------------------------------------
1.1 学到现在的内核数据结构:
进程的PCB、进程的虚拟地址空间(上下文)、进程的相关页表、进程CPU的上下文数据、进程打开的文件、进程的通讯、进程的信号、进程访问的各种资源(只要进程需要访问某种资源 -> 操作系统只要对进程本身或者是访问的资源进行管理 -> 就会有对应的数据结构来进行描述 -> 进程和某种资源的访问关系就变成了某种数据结构上的关系)
----------------------------------------------------------------
1.2 学到现在的进程代码和数据:
代码和数据通过页表来进行对应的映射,而页表分为用户级页表、内核级页表。内核级页表每个进程共享,用户级页表每个进程独有一份。
栈区(有寄存器的概念保存栈顶栈底 -> 容易区分起始和结尾)和代码区是被整体使用的。而堆区看起来是一个整体,但实际上是被零散化的(malloc / new的申请都在不同区域)。
我们申请堆空间的时候,向来都是告述其我们申请多大,但是从来为告述其该堆空间什么时候结束。所以在这里其实还需要细粒度的控制 —— 即:堆区的哪一部分区域是属于谁的。
所以在内核结构中,针对于地址空间,组合式的还有一个结构:vm_area_struct
vm_area_struct
结构体里对于地址空间当中的某一个区域,进行更细粒度的划分。
struct vm_area_struct
{
/* The first cache line has the info for VMA tree walking.
第一个缓存行具有VMA树移动的信息*/
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address
每个任务的VM区域的链接列表,按地址排序*/
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
此VMA左侧最大的可用内存间隙(以字节为单位)。
在此VMA和vma-> vm_prev之间,
或者在VMA rbtree中我们下面的一个VMA与其->vm_prev之间。
这有助于get_unmapped_area找到合适大小的空闲区域。
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here.
第二个缓存行从这里开始*/
struct mm_struct *vm_mm; /* 我们所属的address space*/
pgprot_t vm_page_prot; /* 此VMA的访问权限 */
unsigned long vm_flags; /* Flags, see mm.h. */
/*
对于具有地址空间(address apace)和后备存储(backing store)的区域,
链接到address_space->i_mmap间隔树,或者链接到address_space-> i_mmap_nonlinear列表中的vma。
*/
union
{
struct
{
struct rb_node rb;
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear;
} shared;
/*
在其中一个文件页面的COW之后,文件的MAP_PRIVATE vma可以在i_mmap树和anon_vma列表中。
MAP_SHARED vma只能位于i_mmap树中。
匿名MAP_PRIVATE,堆栈或brk vma(带有NULL文件)只能位于anon_vma列表中。
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock
由mmap_sem和* page_table_lock序列化*/
struct anon_vma *anon_vma; /* Serialized by page_table_lock 由page_table_lock序列化*/
/* 用于处理此结构体的函数指针 */
const struct vm_operations_struct *vm_ops;
/* 后备存储(backing store)的信息: */
unsigned long vm_pgoff; /* 以PAGE_SIZE为单位的偏移量(在vm_file中),*不是* PAGE_CACHE_SIZE*/
struct file *vm_file; /* 我们映射到文件(可以为NULL)*/
void *vm_private_data; /* 是vm_pte(共享内存) */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU映射区域 */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* 针对VMA的NUMA政策 */
#endif
};
与mm_struct中含有大量的start与end是类似的,只不过mm_struct中是描述大空间的,而堆区有很多我们申请的小空间(第一次new、第二次new……)。堆区的空间就会划分的很细,所以使用vm_area_struct结构体。
其中,此处不讲全,只讲解一些重点理解。
见上图,vm_start与vm_end即是小空间细致的划分。vm_next与vm_prev即是经典的双链表整理结构。
如此mm_struct中的堆区只表示堆区整体的起始和结束,但是堆区的详细信息,比如从哪里开始哪里结束。其中就可以每一次取一个vm_area_struct。当我们上层malloc然后调用系统调用(malloc -系统调用-> brk),其就帮我们申请一个vm_area_struct。其中就有start表示虚拟地址起始,end表示虚拟地址结束,然后将其对应的虚拟地址经过页表映射到内存当中。
而堆区的特点就是连续,只要我们申请了就是连续的。所以当我们需要使用申请的堆空间,只需要在vm_area_struct构成的双链表结构中查找到对应的start,便可知道end,即可知道堆的范围。
Note:
地址空间内的地址划分是粗粒度的,而其是可以使用再补充数据结构来保证可以进行细粒度的划分。
操作系统是可以做到让进程进行资源的细粒度划分。
上层所拿的所谓的地址来进行操作,该地址就是虚拟地址,而虚拟地址通过 -> 用户级页表 + MMU内存管理单元(Memory Manager Unit,硬件)-> 映射物理内存。(MMU是集成于CPU内部 -> 从CPU出来访问的就是物理地址)CPU读取到的都是虚拟地址,出来的时候就到了访问的物理内存当中。
以前我们说所就是页表映射到物理内存,现在我们需要探讨到底:
#问:如何从虚拟地址映射到物理内存的?
- .exe就是一个文件(二进制文件)。
- 我们的可执行程序本质就是按照地址空间方式进行编译的。
- 可执行程序,其实在磁盘中按照区域也已经被划分成为了以4KB为单位。(可执行程序编译形成的二进制文件的格式)
如同,一个工厂生产商品一样,在工厂里就将商品包装好了,放在厂库里。每一个商品的大小就是 "4KB" ,一次提货的量就可以说是n * 4KB。
同样的物理内存其实也早被划为了4KB为单位的块(软件上的划分)。这也就是文件系统中,操作系统IO的时候是以4KB为单位的。
假如物理内存的大小为4GB,我们将其划分成为了4KB为单位的空间。操作系统如何知晓那些空间(4KB)被占,哪些没有被占用。哪些空间的访问时间是什么时候、装载的是代码还是数据等 —— 操作系统需要知道物理内存当中每个内存块对应的属性
而4GB的分割为4KB:4GB / 4KB = 104.8576w,并且每个内存块都具有其自己的属性,所以操作系统对于内存块的管理是十分重要的 —— 先描述,再组织。
对4KB内存块管理的结构:struct Page
struct Page
struct page中使用了大量的联合体union。
struct page {
/* First double word block */
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
/* Second double word */
struct {
union {
pgoff_t index; /* Our offset within mapping. */
void *freelist; /* slub first free object */
};
union {
/* Used for cmpxchg_double in slub */
unsigned long counters;
struct {
union {
atomic_t _mapcount;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
atomic_t _count; /* Usage count, see below. */
};
};
};
/* Third double word block */
union {
struct list_head lru; /* Pageout list, eg. active_list
* protected by zone->lru_lock !
*/
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
#ifdef CONFIG_64BIT
int pages; /* Nr of partial slabs left */
int pobjects; /* Approximate # of objects */
#else
short int pages;
short int pobjects;
#endif
};
};
/* Remainder is not double word aligned */
union {
unsigned long private; /* Mapping-private opaque data:
* usually used for buffer_heads
* if PagePrivate set; used for
* swp_entry_t if PageSwapCache;
* indicates order in the buddy
* system if PG_buddy is set.
*/
#if USE_SPLIT_PTLOCKS
spinlock_t ptl;
#endif
struct kmem_cache *slab; /* SLUB: Pointer to slab */
struct page *first_page; /* Compound tail pages */
};
}
其中,此处不讲全,只讲解一些重点理解。
flags:是否有效、是否可以直接访问、是否有数据等,描述page的状态和其他信息。
描述方式上可见,即组织:struct page mem[1048576]; 利用数组的方式组织起来,并且利用数组也就默认的为每一个内存块配上了编号。
于是便将对于特定内存块管理,变为了特定数据结构的管理。
其中我们将,磁盘中用4KB为单位划分的代码和数据(内容),称作为:页帧。物理内存中用4KB为单位划分的内容,称作为:页框。
操作系统(文件系统)和磁盘进行IO的基本单位是4KB:页帧装进页框。
Note:
如此的划分方式,对应的从外设加载到内存时,是以4KB为单位,就算只加载1Bit也是进行的4KB的加载。
缺页中断
说白了就是:操作系统在通过页表进行寻址时,发现我们需要访问的对应的内存区域不在内存当中,所以就要引发缺页中断去:
- 先确认申请对应的内存。
- 在磁盘当中找到我们需要加载的目标数据对应的地址。
- 将磁盘中目标数据加载到指定的内存的位置。
- 重新填充页表。
- 返回到用户,让给用户继续进行访问。
#:需要访问的不在内存区域当中,在磁盘中
#:引发缺页中断
1. 先确认申请对应的内存。
2. 在磁盘当中找到我们需要加载的目标数据对应的地址。
3. 将磁盘中目标数据加载到指定的内存的位置。
4. 重新填充页表。
5. 返回到用户,让给用户继续进行访问。
(这个动作用户是零感知的,用户顶多感受到第一次访问速度与后面访问速度的快慢)
总结
磁盘与物理内存IO的大小为4KB并不是简单的一说,而是操作系统与编译器的支持:
- 编译器:编译的时候就必须将对应的代码和数据,按照4KB来划分好。
- 操作系统:对内存进行管理的时候,就必须按照4KB为单位来进行管理。
深入理解补充的概念
前面的是概念的铺垫,现在才是虚拟地址如何映射到物理地址的真正理解。
前面的学习我们只是说,虚拟地址映射到物理地址是通过页表的kv结构。但是其实这仔细看起来是十分荒谬的。如果我们的平台大小为32位,那么也就是说有:2^32的地址个数,那么页表的映射关系表就有2^32行。据我们简单的推测:一个物理地址对一个地址 + 一个字节的大小表示各种状态和其他信息。
也就是说:为了存储页表就需要消耗内存大小 = 2^32 * 9Byte = 36GB。
8bit = 1Byte
1024Byte = 1KB
1024KB = 1MB
1024MB = 1GB
1024GB = 1TB
页表不是一个简单的结构。而是几个结构组合而成。
以32为操作系统为例:
32位的页表分为:一级页表、二级页表。
#:一级页表
内核层,将虚拟地址的前10个bit位进行映射,成一个一级页表,而前10bit映射的不是物理地址而是二级页表:
一级页表全使用所占空间:
空间 = 2^10 * 9Byte = 36KB。
#:二级页表
二级页表全使用所占空间:
空间 = 2^10 * 9Byte = 36KB。
#:页表结构的原理
所以:所谓的页表,严格意义上建立的不是虚拟地址到物理地址的映射。更准确的说法是,所谓的页表,建立的是虚拟地址到特定页的映射。
这是能够保证通过这个方法可以找到对应的数据。因为编译器编译代码的时候,就是按照4KB为单位,把可执行程序(按虚拟地址)规制好的。