赞
踩
本文已收录至《Linux知识与编程》专栏!
作者:ARMCSKGT
演示环境:CentOS 7
我们知道对于C/C++程序来说,程序占用的内存地址分为栈区,堆区,静态区等等,每一个程序在编译时都会分配这些空间,这些空间只是规划的虚拟地址空间,并不是分配的真实的物理空间,这样划分虚拟地址空间是操作系统为了更好的管理和使用内存空间而做的,那么 虚拟地址(也称逻辑地址) 是如何与物理地址联系起来的呢?本节将为您介绍!
内存中的每个空间地址是唯一的,对地址空间进行编号是为了进行更好的管理和减少冲突!
变量地址
我们在C/C++学习中,不妨会对变量取地址,然后解引用访问地址中的内容,那么这个地址到底是什么呢?
#include <iostream> using namespace std; int main() { const char* s = "CSDN"; printf("%p\n", s); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
当我们写一个常量字符串时,可以使用const char*指针指向并对其访问!
那么在Linux中有一个概念:父子进程
子进程会拷贝父进程大多数的PCB数据,包括地址信息等等!
如果我们使用fork创建子进程,修改同一个变量会怎样?#include <iostream> #include <unistd.h> using namespace std; int main() { int num = 0; pid_t id = fork(); if(id == 0) { num = 1; //子进程修改共享变量 cout<<"子进程num地址为:"<<&num<<";num="<<num<<endl; exit(1); } cout<<"父进程num地址为:"<<&num<<";num="<<num<<endl; return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
神奇的是父子进程相同的变量地址尽然打出的变量值不一样! 对于物理地址来说,地址是唯一的,这种现象是不可能的!
所以我们得出结论:
因为物理地址是唯一的,父子进程打印的变量地址相同而变量值不同,只能说明父子进程所用的变量并非在同一块地址,而且这个地址也不是真实的物理地址!
分析:
- 当父子进程尝试修改共享的变量时会触发写时拷贝机制(针对数据),同时也证明进程具有独立性
- 语言上的地址并非真实的地址,而是虚拟地址(线性地址)
- 我们无法在语言层面上看到物理地址,物理地址由操作系统统一管理和分配,操作系统具有很多内存空间,可以划分给其他进程(进程地址空间)
地址空间
虚拟地址空间
对于C/C++程序猿来说,程序内部空间划分如下:
但这是程序在内存中的划分,实际物理内存中并非存在这样明确的区域!
虚拟到物理地址的转换
既然是虚拟地址,最终我们肯定要在物理内存上存储数据,那么从虚拟地址到物理地址就需要一个转换过程!
而操作系统在进程的虚拟地址到物理地址的转换中承担着不可缺少的作用!
虚拟地址空间的管理
虚拟空间(线性空间)的实现和管理是通过mm_struct对象实现的,相关代码:
struct mm_struct { //指向线性区对象的链表头 struct vm_area_struct * mmap; /* list of VMAs */ //指向线性区对象的红黑树 struct rb_root mm_rb; //指向最近找到的虚拟区间 struct vm_area_struct * mmap_cache; /* last find_vma result */ //用来在进程地址空间中搜索有效的进程地址空间的函数 unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); unsigned long (*get_unmapped_exec_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); //释放线性区时调用的方法, void (*unmap_area) (struct mm_struct *mm, unsigned long addr); //标识第一个分配文件内存映射的线性地址 unsigned long mmap_base; /* base of mmap area */ unsigned long task_size; /* size of task vm space */ /* * RHEL6 special for bug 790921: this same variable can mean * two different things. If sysctl_unmap_area_factor is zero, * this means the largest hole below free_area_cache. If the * sysctl is set to a positive value, this variable is used * to count how much memory has been munmapped from this process * since the last time free_area_cache was reset back to mmap_base. * This is ugly, but necessary to preserve kABI. */ unsigned long cached_hole_size; //内核进程搜索进程地址空间中线性地址的空间空间 unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */ //指向页表的目录 pgd_t * pgd; //共享进程时的个数 atomic_t mm_users; /* How many users with user space? */ //内存描述符的主使用计数器,采用引用计数的原理,当为0时代表无用户再次使用 atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ //线性区的个数 int map_count; /* number of VMAs */ struct rw_semaphore mmap_sem; //保护任务页表和引用计数的锁 spinlock_t page_table_lock; /* Protects page tables and some counters */ //mm_struct结构,第一个成员就是初始化的mm_struct结构, struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung * together off init_mm.mmlist, and are protected * by mmlist_lock */ /* Special counters, in some configurations protected by the * page_table_lock, in other configurations by being atomic. */ mm_counter_t _file_rss; mm_counter_t _anon_rss; mm_counter_t _swap_usage; //进程拥有的最大页表数目 unsigned long hiwater_rss; /* High-watermark of RSS usage */、 //进程线性区的最大页表数目 unsigned long hiwater_vm; /* High-water virtual memory usage */ //进程地址空间的大小,锁住无法换页的个数,共享文件内存映射的页数,可执行内存映射中的页数 unsigned long total_vm, locked_vm, shared_vm, exec_vm; //用户态堆栈的页数, unsigned long stack_vm, reserved_vm, def_flags, nr_ptes; //维护代码段和数据段 unsigned long start_code, end_code, start_data, end_data; //维护堆和栈 unsigned long start_brk, brk, start_stack; //维护命令行参数,命令行参数的起始地址和最后地址,以及环境变量的起始地址和最后地址 unsigned long arg_start, arg_end, env_start, env_end; unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */ struct linux_binfmt *binfmt; cpumask_t cpu_vm_mask; /* Architecture-specific MM context */ mm_context_t context; /* Swap token stuff */ /* * Last value of global fault stamp as seen by this process. * In other words, this value gives an indication of how long * it has been since this task got the token. * Look at mm/thrash.c */ unsigned int faultstamp; unsigned int token_priority; unsigned int last_interval; //线性区的默认访问标志 unsigned long flags; /* Must use atomic bitops to access the bits */ struct core_state *core_state; /* coredumping support */ #ifdef CONFIG_AIO spinlock_t ioctx_lock; struct hlist_head ioctx_list; #endif #ifdef CONFIG_MM_OWNER /* * "owner" points to a task that is regarded as the canonical * user/owner of this mm. All of the following must be true in * order for it to be changed: * * current == mm->owner * current->mm != mm * new_owner->mm == mm * new_owner->alloc_lock is held */ struct task_struct *owner; #endif #ifdef CONFIG_PROC_FS /* store ref to file /proc/<pid>/exe symlink points to */ struct file *exe_file; unsigned long num_exe_file_vmas; #endif #ifdef CONFIG_MMU_NOTIFIER struct mmu_notifier_mm *mmu_notifier_mm; #endif #ifdef CONFIG_TRANSPARENT_HUGEPAGE pgtable_t pmd_huge_pte; /* protected by page_table_lock */ #endif /* reserved for Red Hat */ #ifdef __GENKSYMS__ unsigned long rh_reserved[2]; #else /* How many tasks sharing this mm are OOM_DISABLE */ union { unsigned long rh_reserved_aux; atomic_t oom_disable_count; }; /* base of lib map area (ASCII armour) */ unsigned long shlib_base; #endif };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
通过上述代码,我们不难发现,之所以程序地址会划分为各个分区并不是没有道理的,因为底层实现就是这个原理!
说明:
- 地址空间信息的存储与进程的task_struct一样使用mm_struct描述和存储
- mm_struct中有许多区间边界值,保护不同区域不会相互越界访问
- 每一个进程都会有一个mm_struct,里面描述一个进程的空间划分
- 如果某个区域需要扩大,例如栈区,则直接调整指针即可,可以让边界值动态的扩大和缩小
- mm_struct中的信息结合页表和MMU单元就能找到进程所处于的真实物理内存空间位置
写时拷贝
通过以上的介绍,大家大概就可以理解为什么父子进程在相同的地址下访问的变量信息不同:
- 父进程在创建子进程时,会将自己的大部分信息拷贝给子进程
- 父子进程各自拥有独立的mm_struct,但因为子进程是从父进程上拷贝的信息,所以会有所相同之处
- 对于父子进程共享的数据,修改时会发生写时拷贝:
– 对于同一个变量,如果不涉及修改(只读),则两者的虚拟地址通过 页表和MMU 转换后指向同一块空间
– 如果发生修改行为,那么谁先修改则此时会在真实空间中再开辟一块空间,拷贝变量值,让其中一个进程的虚拟地址空间页表映射改变,这种行为称为写时拷贝
写时拷贝是Linux中的一种内存高效利用的机制,操作系统一开始默认你不会对数据进行修改,在访问同一个数据时,指向同一块空间,当发生修改行为时,再新开辟空间进行读写!
对于内置类型,这种机制感觉非常鸡肋,但是对于自定义类型,特别是使用模板的自定义类型进行嵌套时,其拷贝构造(特别是深拷贝)需要相当大的性能和资源牺牲,某些情况下,写时拷贝可以减少拷贝构造的次数,尽可能提高效率!
写时拷贝本质是一种按需申请的资源策略!
虚拟地址的意义
进程地址空间又称虚拟地址,在计算机早期还没有虚拟地址这个概念!
早期大多数都是32位电脑,最大内存也就4GB,对于数据的写入和删除都是直接在物理内存上进行的,进程直接和物理内存打交道,但存在以下问题:
- 程序运行成为进程时,因为地址空间结构,每次运行都需要4GB的内存空间,当进行多进程时,容易造成内存不足,效率下降
- 如果出现野指针问题,则直接损坏物理内存上对应的数据,如果损坏了内核区的数据,可能导致操作系统直接崩溃
- 内存的动态申请和释放效率非常低
为了解决以上的问题,操作系统大佬们提出了虚拟地址的概念,即当进程创建时系统会为其分配属于自己的虚拟地址空间,在需要内存时通过MMU寻址的方式访问对应的物理内存上的数据即可!
对于虚拟地址的意义:- 当发生越界或野指针问题时,MMU会直接向操作系统报错,操作系统转而直接终止进程,对越界行为及时制止和拦截,保护物理内存与其他进程
- 每个进程拥有独立的地址空间和相同的mm_struct结构,操作系统可以进行统一高效的管理
- 多进程下互不干扰,保证进程的独立性且提高了执行效率和资源的使用效率
- 将 进程管理 和 内存管理 进行解耦,方便操作系统进行更高效的管理
- 可以让进程以统一的视角看待自己的代码和数据
其实页表并非只有虚拟地址和物理地址的映射,还有读写权限,以及内核数据和用户数据的区分等等!
当然,只有虚拟地址是不够的,不同的操作系统有不同的虚拟地址处理(翻译)机制,对于Linux系统采用的是页表和MMU的方式进行对物理内存进行使用的!
页表和MMU单元
- 关于页表的专业解释:
– 页表(Page Table)也称分页机制,是一种特殊的数据结构,记录着页面和页框的对应关系。(映射表)页表的作用是内存非连续分区分配的基础,实现从逻辑地址转化成物理地址!
–页表是计算机操作系统中的一种数据结构,用于管理虚拟内存和物理内存之间的映射关系。它记录了进程中每个虚拟页的物理地址,以便操作系统将其正确地加载到内存中。通常,页表是一个由操作系统维护的数据结构,每个进程都拥有自己的页表。页表中的每个表项都包含有关虚拟页和物理页的信息,如虚拟页号、物理页号、页状态等。在访问虚拟内存时,操作系统会将页表中的表项与虚拟页号进行匹配,以确定应该将虚拟页映射到哪个物理页中。
- 关于MMU单元的专业解释:
– MMU单元(Memory Management Unit)是一种用于管理计算机内存的硬件模块,通常是由CPU中的特殊芯片实现的。 MMU单元根据程序访问内存的地址,将逻辑地址转换为物理地址,从而将程序代码和数据存储在正确的内存位置。MMU单元负责地址映射、内存保护、虚拟内存、缓存控制等各种内存管理功能,是现代计算机系统中不可或缺的部分。
– MMU单元可以将一个程序所需的内存空间映射到计算机系统中实际的物理内存地址,实现虚拟地址到物理地址的转换。当程序在访问内存地址时,操作系统通过MMU单元转换逻辑地址为物理地址,使得程序可以直接访问到物理内存。
– 此外,MMU单元还可以实现内存保护,以防止不恰当的程序访问特定的内存区域,从而保护系统的稳定性和安全性。另外,MMU单元还可实现虚拟内存技术,即将大于实际物理内存的程序虚拟地址空间映射到磁盘上,从而扩展可用内存。
– MMU单元在CPU中由硬件实现,它能够快速自动地完成地址映射,加快了处理器对内存的访问速度。同时,MMU单元还可以提高系统的稳定性和安全性,增强内存的管理能力,保护程序免于受到恶意攻击和病毒的影响。
内存申请机制
我们在使用C/C++动态内存管理函数例如malloc和new时,操作系统并不会直接分配内存,而是先检查内存是否足够,如果充足则操作系统会在内存申请处记录相应的信息,当操作系统需要使用这块内存时操作系统会进行缺页中断,即操作系统让进程暂时阻塞,然后在内存中开辟内存并将内存与该进程的页表形成映射关系再解除中断让进程继续运行,此时进程就可以使用自己申请的内存空间了!
之所以操作系统会存在这种机制,是因为某些情况下进程会出现申请内存不用的情况,如果每个进程都申请内存空间但是不用则会产生许多闲置内存,是资源利用低效的表现!
malloc本质:申请空间时,操作系统会在你需要时把内存给你,操作系统不允许任何的浪费(或不高效),用户申请内存并不等于立刻会使用,在申请成功之后和你使用之前,就有一段小小的时间窗口,这段空间没有被正常使用,但是别人也用不了,空间处于闲置状态,当你访问写入和读取时,才会申请空间构建映射关系(这也叫缺页中断),所以在创建时会先建立虚拟地址空间,使用时才建立映射关系开辟真实的物理内存空间,因为有代码和映射,所以数据理论上可以放在任何位置,进程并不关心代码和数据在内存的那个地方!
进程地址空间的介绍到这里就告一段落了,相信大家认识了Linux底层的虚拟地址空间后,对很多进程地址相关的问题一定有所解答了;通过父子进程修改共享变量我们认识了写时拷贝等各种底层高效的内存管理机制,正是因为有这些强大的运行机制和策略才能使系统高效稳定的运行和管理!
本次 <Linux进程地址空间> 就先介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/769943
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。