文章目录:
- 程序地址空间
- 进程地址空间
程序地址空间
什么是地址空间:地址空间是内存中可供程序或进程使用的有效地址的范围。也就是说,它是程序或进程可以访问的内存。内存可以是物理的、也可以是虚拟的,用于执行指令和存储数据。
在之前 c/c++ 的学习中,我们所知道的空间布局图如下:
我们可以在 Linux 操作系统中,对上面的空间布局图进行验证:
// 验证程序地址空间的分布
#include<stdio.h>
#include<stdlib.h>
int un_g_val;
int g_val=10;
int main(int argc,char *argv[],char *env[])
{
printf("code addr :%p\n",main);
printf("init global addr :%p\n",&g_val);
printf("uninit global addr :%p\n",&un_g_val);
char *m1 = (char*)malloc(10);
printf("heap addr :%p\n",m1);
printf("stack addr :%p\n",&m1);
for(int i=0;i<argc;++i)
{
printf("argv addr :%p\n",argv[i]);
}
for(int i=0;env[i];++i)
{
printf("env addr :%p\n",env[i]);
}
return 0;
}
测试结果如下,符合上图的分布:
下面来看一段代码及其运行结果:fork() 创建子进程,子进程打印两次之后更改全局变量的值。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int g_val = 10;
int main()
{
pid_t id = fork();
if(id == 0)
{
// child process
int count = 0;
while(1)
{
printf("I am a child process! PID : %d,PPID : %d,g_val = %d,&g_val = %p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
count++;
if(count == 2)
{
g_val = 7;
printf("I am a child process,g_val has changed!\n");
}
}
}
else
{
// parent process
while(1)
{
printf("I am a parent process! PID : %d,PPID : %d,g_val = %d,&g_val = %p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}
运行结果:
🎯根据上述运行结果,我们发现,在子进程还没有修改全局数据的时候,输出的变量和地址是一样的,因为子进程按照父进程为模板,父子进程并没有对变量进行任何修改。但是当子进程将代码中的全局变量更改之后,我们发现,父子进程,输出的地址一样,但是变量的内容却不一样。因此我们得出以下结论:
- 变量的内容不一样,说明父子进程输出的变量绝对不是同一个变量。
- 但是变量对应的地址确实一样的,说明,该地址绝对不是物理地址!在 Linux 操作系统下,我们将此地址叫做 虚拟地址。
- 我们之前在 c/c++ 中所看到的地址,全部都是虚拟地址。物理地址,我们是看不到的,由 OS 同一进行管理。
🎭 OS 需要将 虚拟地址 转化为 物理地址。
进程地址空间
地址空间是计算机内存中的一个空间。进程地址空间是指在内存中为进程分配的空间。每个进程都有一个地址空间。地址空间有两种类型:物理地址空间、虚拟地址空间。
所以之前所说的程序地址空间是不准确的,准确来说应该称为进程地址空间,进程地址空间是内存中的一种内核数据结构。Linux 中由内存描述符(mm_struct)来描述有关进程地址空间的所有信息。
mm_struct 定义在 <linux/sched.h> 下:
206 struct mm_struct {
207 struct vm_area_struct * mmap;
208 rb_root_t mm_rb;
209 struct vm_area_struct * mmap_cache;
210 pgd_t * pgd;
211 atomic_t mm_users;
212 atomic_t mm_count;
213 int map_count;
214 struct rw_semaphore mmap_sem;
215 spinlock_t page_table_lock;
216
217 struct list_head mmlist;
221
222 unsigned long start_code, end_code, start_data, end_data;
223 unsigned long start_brk, brk, start_stack;
224 unsigned long arg_start, arg_end, env_start, env_end;
225 unsigned long rss, total_vm, locked_vm;
226 unsigned long def_flags;
227 unsigned long cpu_vm_mask;
228 unsigned long swap_address;
229
230 unsigned dumpable:1;
231
232 /* Architecture-specific MM context */
233 mm_context_t context;
234 };
内存区域:线性地址的间隔,其特征是初始线性地址、长度和一些访问权限。
Linux 操作系统用 vm_area_struct 来管理内存区域。
vm_area_struct 被定义在 <linux/mm.h> 中:
44 struct vm_area_struct {
45 struct mm_struct * vm_mm;
46 unsigned long vm_start;
47 unsigned long vm_end;
49
50 /* linked list of VM areas per task, sorted by address */
51 struct vm_area_struct *vm_next;
52
53 pgprot_t vm_page_prot;
54 unsigned long vm_flags;
55
56 rb_node_t vm_rb;
57
63 struct vm_area_struct *vm_next_share;
64 struct vm_area_struct **vm_pprev_share;
65
66 /* Function pointers to deal with this struct. */
67 struct vm_operations_struct * vm_ops;
68
69 /* Information about our backing store: */
70 unsigned long vm_pgoff;
72 struct file * vm_file;
73 unsigned long vm_raend;
74 void * vm_private_data;
75 };
当一个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也随之被创建。操作系统可以通过进程的 task_struct 来找进程对应的 mm_struct 。
🎭 如下所示,父进程创建了子进程后,父子进程都有属于自己的进程控制块,父进程和子进程中的进程地址空间中的虚拟地址通过页表映射到物理内存中:
通过此图我们就可以解释上面代码中的问题,当子进程被创建时,子进程和父进程的数据和代码共享。父子进程的代码和数据通过各自的页表映射到物理内存的同一块区域。因此,当子进程和父进程没有对数据进行修改的时候,它们的数据是一样的,是同一份。但是当子进程更改了全局数据,那么就会在内存中某一空间存储一个更改的新数据,并且更改子进程页表中 g_val 的虚拟地址所映射的物理地址。
虚拟地址存在的意义:
- 虚拟内存将主存看作是在磁盘地址空间上的高速缓存,主存中只报保存活动区域并根据需要在磁盘和主存之间来回传送数据。
- 虚拟地址允许我们有内存保护,每个进程的地址空间不被其它程序破坏。
- 允许通过磁盘来扩展物理内存的使用。
- 为进程提供的一致的地址空间简化了内存管理。
对于虚拟地址和物理地址的关系,就简单介绍这些了。