1. 回顾“程序”地址空间
在学习c语言的时候,我们为了更好的理解,把它叫做程序地址空间,从低地址到高地址,依次是,常量区,数据区,堆区,栈区,而且没有共享映射区和内核空间的概念,我们那个只能说是在语言方面的理解。是残缺的。
当我们学习了c++和操作系统,就该对他有更深层次的理解。所以下面这张图才是相对完整的。
所以他的名字也应该叫做进程地址空间。
我们写一段代码,测试一下他的地址。
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int g_val=0;
5 int u_g_val;
6 int main(int argc,char* argv[],char* env[])
7 {
8 printf("code address:%p\n",main);
9 printf("init val address:%p\n",&g_val);
10 printf("uninit val address:%p\n",&u_g_val);
11 char* p=(char*)malloc(10);
12 printf("heap address:%p\n",p);
13 printf("stack address:%p\n",&p);
14 printf("opt address:%p\n",argv[0]);
15 printf("opt address:%p\n",argv[argc-1]);
16
17 printf("env address:%p\n",env[0]);
18 return 0 ;
19 }
从低地址到高地址,所输出的结果可以看到是地址递增。我们可以看到
- 地址确实是递增的。
- 堆栈中间差距很大很大,这中间就是共享区,共享区里主要存放动态库和共享内存。
- 代码段并不是从0地址开始的
2. 进程地址空间
其实就是,操作系统给进程划分了一块内存,进程在这个内存里,这个内存就是地址空间,进程知道这个空间该怎么用,他给空间划分了一块块区域,但是进程并不是每一刻都在使用这个空间所有的区域。
2.1 虚拟地址空间
1 #include<stdio.h>
2 #include<unistd.h>
3 int g_val=100;
4 int main()
5 {
6 pid_t id=fork();
7 if(id==0)
8 {
9 //child
10 while(1)
11 {
12 printf("g_val:%d,child: g_val:address:%p\n", g_val, &g_val);
13 sleep(1);
14 }
15
16 }
17 else if(id>0)
18 {
19 //father
20 while(1)
21 {
22 printf("g_val:%d,father: g_val:address:%p\n ",g_val, &g_val);
23 sleep(1);
24 }
25
26 }
27 else{
28 //error
29 }
30 return 0;
31 }
意料之中的运行结果
当我们在子进程中把val的值改成50(在child中令val=50
),输出的结果不一样,但是地址确实一样的
所以他一定不是物理地址,因为假如是内存中的物理地址,地址相同值是一定相同的。
这里我们就要引入一个叫做虚拟内存的概念。
进程只能看到虚拟内存,子进程继承父进程,当我们代码中没有写更改数据的指令,虚拟内存是相同的且经过页表,映射到同一块物理内存
当子进程中发生更改数据,操作系统重新开辟一断空间,但是虚拟内存地址仍然没有变化,变的只是页表中的映射关系。
现在问自己3个问题,当你能回答着三个问题就说明对于地址空间这个概念就理解透彻了。
- 什么是地址空间
内存中存在多个进程,所以一定会存在多个地址空间,操作系统就要将他先描述,在组织。在pcb中是有着地址空间指针的。
描述:地址空间也是一个数据结构。包含我们所划分区域的起始。
struct mm_struct
-
为什么要有地址空间。
假如没有地址空间,我们直接对物理内存进行操作会产生什么情况呢,进程在内存当中的时候,pcb+代码+数据,内存中是会有多个进程存在的,可以从两个问题来看,假如几个进程在物理内存中存放的很近,假如其中某个进程进行了操作产生新数据,后面放不下,他的空间就会不连续。而且还增加了指针越界的概率,野指针等,从而写坏别的进程的数据。
所以就出现了进程地址空间(虚拟),它是一块线性连续的地址空间(代码段,数据段,堆,共享区,栈参数环境变量,)操作系统会创建一个页表来映射虚拟内存和物理内存的地址。所以我们看到的虚拟地址空间是连续的,至于物理内存是否连续,不用管,os会帮我们在页表中进行映射。而且当我们非法访问别人的数据时,页表没有这种映射关系的话,操作系统也会检测然后终止。那假如我们想访问全局数据,不小心对常量字符串进行了修改,页表中虽然有这种映射关系,但是由于常量字符串是只读的页表中也有对应的只读标识也会失败的。 -
地址空间怎么工作
当一个进程访问地址空间,地址空间会将虚拟地址通过页表映射到物理地址,从而拿到数据。
当编译的时候地址空间中的数据和指令处于虚拟内存中,当程序运行起来它会被加载到物理内存中成为进程,同时页表将虚拟地址映射到物理内存地址之上,分配空间。
3. 进程总结
- 什么是进程
进程是加载在内存的程序,由进程和常见数据结构(task_struct(控制块),mm_struccct(进程地址空间)
)代码和数据构成。 - task struct内容
标识符:描述本进程的唯一标识符,用来区别其他进程
状态:任务状态,退出代码,退出信号
优先级:相比与其他进程的优先级
程序计数器:程序中即将被执行的下一条指令的地址
内存指针:包含程序代码和进程相关数据的指针,还有其他进程共享的内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包含显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息:可能包括处理器的时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
4. 简单了解运行队列,等待队列
task_struct中包含了许多进程链接信息。
运行队列:
本质是把进程pcb进行排队等待运行的过程。举个例子,有很多进程想要占用cpu资源。操作系统维护了一个runqueue
队列。在这个队列中存放的是进程的pcb。
等待队列:
假如当前进程需要访问磁盘进行读写,他需要在一个等待队列中等待,轮到他的时候被唤醒加载到cpu当中执行硬盘读写。也是存放的pcb。