1.程序地址空间
(1)结构图:进程地址空间不是内存
(2)代码模拟实现程序地址的变化
//程序地址空间
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<malloc.h>
int g_unval; //未初始化
int g_val = 100; //初始化
int main(int argc,char *argv[],char *envp[])
{
//代码地址
printf("code addr:%p\n",main);
//字符常量区
char *str = "hello world"; //str为指针常量
printf("read only addr:%p\n",str);
printf("init addr:%p\n",g_val);
printf("uninit addr:%p\n",g_unval);
//堆区地址
int *p = malloc(10);
printf("heap addr:%p\n",p);
//栈区地址
printf("stack addr:%p\n",&str);
printf("stack addr:%p\n",&p);
//运行参数的地址
for(int i = 0; i < argc; i++)
{
printf("args addr:%p\n",argv[i]); //ls -a -l
}
int i = 0;
while(envp[i])
{
printf("env addr:%p\n",envp[i]);
i++;
}
return 0;
}
地址从下往上变高
2.虚拟地址,也叫线性地址
以下程序,父子进程的地址一样,但是读出的值不一样,说明所读取的不是物理地址,实际上是不同的虚拟地址
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<malloc.h>
#include<sys/types.h>
int g_val = 100; //初始化
int main(int argc,char *argv[],char *envp[])
{
pid_t id = fork();
if(id == 0)
{
g_val = 200;
printf("child:pid %d,ppid:%d,g_val:%d,&g_cal:%p\n",getpid(),getppid(),g_val,&g_val);
}
else
{
sleep(2);
printf("father:pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
}
return 0;
}
在语言层面上见到的地址都不是物理地址
详细解答:
子进程的地址空间是拷贝的父进程的地址空间,所以他们映射的虚拟地址是一样的,但是子进程又写入了其它数据,则会在物理内存上重新开辟一块空间存放数据,不影响父进程的任何地址(每个进程都是独立的),这也是写时拷贝的一种应用。
虚拟地址的理解
举例理解
(1)结构区域化
struct ruler{
unsigned long start_left;
unsigned long end_left;
unsigned long start_right;
unsigned long end_right;
}
(2) 区域数据化
ruler r = {0, 10, 10, 20};
(3) 0~10之间的所有刻度,都可以看作是一个地址,即虚拟地址
在左边放一支笔,笔的长度,start:6,end:8,
即**基地址+偏移量**
另外需要注意:
(1)扩大区域
可把划分的线重新划分,一边占小一点,一边占大一点,即:若堆区想扩大区域,则栈区可以缩小内存区域
(2)磁盘上的可执行程序(.exe文件)与物理内存的对应
exe文件是被划分好的(可执行程序是分段的),若要执行代码,则页表会自动找到物理内存代码段的地址
Linux可执行的格式是:ELF
结论:
a. 进程地址空间本质是内存中的一种内核数据结构(mm_struct)
b. 在划分区域之前,必须得有“大的区域”,32位,4GB,2^32 字节,2^10 * 2^10 * 2*10 * 2^2(1KB→1MB→1GB→4GB)
c. 2^32空间大小与物理内存本身有直接关系吗?
没有
232空间大小:相当于直尺的长度为20,这是人为抽象定义的(这也类似于进程地址空间,计算机能访问的最大内存空间232)
物理内存本身:相当于直尺,几乎看不到物理内存
进程地址空间具体划分:
d.(1)扩大访问的范围:stack的end减小,heap的end增大
(2)可执行程序已经被划分好了区域(code、readonly、uninit、init等),编译器按照区域内存进行加载进程
e. 创建进程:肯定会有struct task_struct,struct mm_struct,页表的创建
e.为什么要有地址空间?
① 不会有任何系统级别的越界问题存在,错误地访问物理内存(若存在野指针,在页表内不会有它的映射,就不会映射到物理内存),虚拟地址+页表:主要是保护内存
②每个进程都认为看到的是相同的空间范围,包括构成和顺序(同一种方式处理内存空间)
③每个进程都认为自己在独占内存,更好的完成进程独立性以及合理使用空间(例如:给每个人画大饼,相当于自己都认为自己有很多财富) 将进程调度与内存管理进行解耦或者分离(动态地调用,需要哪就调用哪)
3.调度队列(runqueue)
80 nice [-20,19]
nice修正值:[0]-[40] (也叫优先级数组)
nice修正值相同的进程要链接成一个链表队列,优先级均相同,但是都是从序号0开始,直到0号进程的队列没有进程后,再选择1号的队列,依次选择下去……
4.活动队列
结构图:
nr_active:总共有多少个运行状态的进程
时间片还没有结束的所有进程都按照优先级放在该队列
queue[40]:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则排队调度,数组下标就是优先级,从0开始遍历queue[140](虽然复杂度是常数,但是效率比较低)
改进:
利用bitmap[5]:一共140个优先级,一共140个进程队列,可以用5*32个比特位表示队列是否为空,提高查找非空队列的效率
5.过期队列
结构图:
过期队列上放的进程都是时间片耗尽的进程
当活跃队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
6.active指针和expired指针
active指针永远指向活动队列
expired指针永远指向过期队列
活动队列上的进程会越来越少,过期队列上的进程会越来越多(原因:进程时间片到期时一直都存在),在合适的时期,交换active指针和expired指针的内容,就相当于有了一批新的活动进程