中牟建设工程信息网站,互联网保险销售,网页设计实验报告实验分析,wordpress mnews一、程序地址空间回顾
在学习 C/C 时#xff0c;我们知道内存会被分为几个区域#xff1a;栈区、堆区、全局/静态区、代码区、字符常量区等。但这仅仅是在语言层面上的理解#xff0c;是远远不够的。
如下空间布局图#xff0c;请问这是物理内存吗#xff1f; 不是 时我们知道内存会被分为几个区域栈区、堆区、全局/静态区、代码区、字符常量区等。但这仅仅是在语言层面上的理解是远远不够的。
如下空间布局图请问这是物理内存吗 不是下图是进程地址空间。 结论 进程地址空间不是物理内存。 进程地址空间会在进程的整个生命周期内一直存在直到进程退出。 这也就解释了为什么全局/静态变量的生命周期是整个程序因为全局/静态变量是随着进程一直存在的 二、验证地址空间的基本排布 // checkarea.c
#include stdio.h
#include stdlib.h // mallocint g_unval; // 未初始化数据区
int g_val 10; // 已初始化数据区int main(int argc, char* argv[], char* env[])
{printf(code addr : %p\n, main); // 代码区printf(\n);const char *p hello;printf(read only : %p\n, p); // 字符常量区只读printf(\n);printf(global val : %p\n, g_val); // 已初始化数据区printf(global uninit val: %p\n, g_unval); // 未初始化数据区printf(\n);char *phead (char*)malloc(1);printf(head addr : %p\n, phead); // 堆区向上增长printf(\n);printf(stack addr : %p\n, p); // 栈区向下增长printf(stack addr : %p\n, phead); // 栈区printf(\n);printf(arguments addr : %p\n, argv[0]); // 命令行参数第一个参数printf(arguments addr : %p\n, argv[argc-1]); // 命令行参数最后一个参数printf(\n);printf(environ addr : %p\n, env[0]); // 环境变量return 0;
} 运行结果 三、虚拟地址和物理地址 定义一个全局变量 g_val然后创建子进程父子进程分别打印出变量值和变量地址。 #include stdio.h
#include sys/types.h // getpid
#include unistd.h // getpid, fork
#include stdlib.h // perrorint g_val 0; // 全局变量int main()
{printf(before creating a new process, g_val %d\n, g_val);pid_t ret fork();if (ret 0){// child processprintf( child - pid: %u, g_val: %d, g_val: %p\n, getpid(), g_val, g_val);}else if (ret 0){// father processprintf(father - pid: %u, g_val: %d, g_val: %p\n, getpid(), g_val, g_val);}else{perror(fork);} return 0;
} 运行结果 before creating a new process, g_val 0
father - pid: 23014, g_val: 0, g_val: 0x601058child - pid: 23015, g_val: 0, g_val: 0x601058 通过观察可以发现父子进程打印的变量值和变量地址是一样的因为创建子进程通常以父进程为模版父子进程并没有对变量进行进行任何修改。 如果将代码稍加改动 #include stdio.h
#include sys/types.h // getpid
#include unistd.h // getpid, fork, sleep
#include stdlib.h // perrorint g_val 0; // 全局变量int main()
{printf(before creating a new process, g_val %d\n, g_val);pid_t ret fork();if (ret 0){// child processg_val 100; // 在子进程中对变量进行修改printf( child - pid: %u, g_val: %-3d, g_val: %p\n, getpid(), g_val, g_val);}else if (ret 0){// father processsleep(3); // 父进程休眠子进程一定会先退出让父进程读取变量值和变量地址printf(father - pid: %u, g_val: %-3d, g_val: %p\n, getpid(), g_val, g_val);}else{perror(fork);} return 0;
} 运行结果 before creating a new process, g_val 0child - pid: 25270, g_val: 100, g_val: 0x601058 # 子进程先退出
father - pid: 25269, g_val: 0 , g_val: 0x601058 # 父进程休眠3s后退出 子进程肯定先跑完也就是子进程先修改完成之后父进程再读取。 可以发现父子进程打印的变量值是不一样的但变量地址是一样的。 父子进程代码共享数据各自私有一份写时拷贝。 变量内容不一样说明父子进程中的变量绝对不是同一个变量。打印的变量地址值是一样的说明绝对不是物理地址。因为在同一物理地址处不可能读取出两个不同的值。我们曾经在 C/C 语言或其它语言中学到或看到的地址比如取地址全都是虚拟地址而物理地址用户是一概看不到的由操作系统统一管理。OS 必须负责将虚拟地址转化成物理地址 。 注意程序的代码和数据一定是存在物理内存上的。 因为想要运行程序就必须先将代码和数据加载到物理内存中所以需要操作系统负责将虚拟地址转化成物理地址。 所以之前说 “ 程序的地址空间” 是不准确的准确来说应该是 “ 进程地址空间”。 上图说明同一个变量打印的地址相同其实是虚拟地址相同而内容不同其实是被映射到了不同的物理地址处。 四、理解地址空间
1、举例
假设有一个富豪他有 10 亿美元的家产而他有 3 个私生子但这 3 个私生子彼此之间并不知道对方的存在。这个富豪对他的每个私生子都说过同一句话“儿子这 10 亿的家产未来都是你的”。站在每个私生子的视角来看每个私生子都认为自己可以拥有 10 亿美元。
如果每个私生子都找父亲一次性要 10 个亿那么这个富豪是拿不出来的。但实际上这是不可能的每个私生子找父亲要钱一般只会几千几万这样一点点去要那么这个富豪只要有就一定会给。而如果私生子要的钱太多富豪不给私生子也只会认为是父亲不想给。换而言之这个富豪给每个私生子在大脑中建立一个虚拟的概念都认为自己拥有 10 亿美元。 类比到计算机中 富豪 —— 操作系统私生子 —— 进程富豪给私生子画的 10 亿家产的饼 —— 进程的地址空间 通过上述例子可以得出结论 操作系统默认会给每个进程构建一个地址空间的概念比如在 32 位下把物理内存资源抽象成了从 0x00000000 ~ 0xFFFFFFFF 共 4G 的一个线性的虚拟地址空间假设系统中有 10 个进程每个进程都会认为自己有 4G 的物理内存资源。这里可以理解成 OS 在画大饼 2、认识地址空间 在 Linux 中地址空间其实是内核中的一种数据结构。在 Linux 中OS 除了会为每个进程创建对应的 PCB即 struct task_struct 结构体还会创建对应的进程地址空间即内核中的 struct mm_struct 结构体。 空间的本质无非就是多个区域栈、堆…的集合。
那么在 struct mm_struct 结构体中OS 是如何表述划分这些区域的呢 定义 start 和 end 变量来表示每个区域起始和结束的虚拟地址。然后通过设置这些 start 和 end 的值对抽象出的这个线性的虚拟地址空间在 32 位下是从 0x00000000 ~ 0xFFFFFFFF 共 4G进行区域划分。 struct mm_struct {// ...unsigned long code_start; // 代码区起始虚拟地址比如 0x10000000hunsigned long code_end; // 代码区结束虚拟地址比如 0x00001111hunsigned long init_start; // 已初始化数据区unsigned long init_end;unsigned long uninit_start; // 未初始化数据区unsigned long uninit_end;unsigned long heap_start; // 堆区unsigned long heap_end;// ...
}; 3、什么是地址空间
进程地址空间 地址空间究竟是什么 地址空间的本质操作系统让进程看待物理内存的方式这是抽象出来的一个概念。地址空间是内核中的一种数据结构即 struct mm_struct 结构体。由 OS 给每个进程创建这样每个进程都认为自己独占系统内存资源。 划分区域的本质把线性的地址空间划分成了一个个的区域通过设置结构体内的 start 和 end 的值来表示区域的起始和结束。比如栈区和堆区的增长 为什么要进行区域划分呢 可以通过 [start, end] 进行初步判断访问某个虚拟地址时是否越界访问了。因为可执行程序在磁盘中是被划分成一个个的区域存储起来的所以进程的地址空间才有了区域划分这样的概念方便进程找到代码和数据。 虚拟地址的本质每个区域 [start, end] 之间的各个地址就是虚拟地址之间的虚拟地址是连续的。 五、地址空间和物理内存之间的关系 虚拟地址和物理地址之间是通过页表来完成映射的。 六、存在地址空间的原因
直接让进程去访问物理内存不行吗 早期操作系统是没有进程地址空间的这就导致物理内存暴露恶意程序可以直接通过物理地址来进行内存数据的读取甚至篡改。后来随着操作系统的发展迭代有了进程地址空间虚拟地址由操作系统完成虚拟地址和物理地址之间的转化。 为什么还要存在地址空间呢
1有效的保护物理内存。 因为地址空间和页表是 OS 创建并维护的也就意味着凡是想使用地址空间和页表进行映射也就一定要在 OS 的监督之下来进行访问也保护了物理内存中的所有合法数据包括各个进程以及内核的相关有效数据。 在进程内不能非法访问或映射因为 OS 会进行合法性检测如果非法则终止进程。 通过划分区域中虚拟地址的起始和结束即 start 和 end 的值来判断当前访问的地址是否合法。 比如如果用户想在某个虚拟地址处写入但检测到该虚拟地址在字符常量区的 start 到 end 之间而字符常量区是只读的说明非法越界访问了OS 会直接终止进程。 char *str hello world;
*str H; // error 通过页表中的权限属性来判断当前访问的地址是否合法。页表完成了虚拟地址到物理地址之间的映射而页表中除了有基本的映射关系之外还可以进行读写等权限相关的管理。 比如如果用户想在某个虚拟地址处写入通过页表进行虚拟地址到物理地址的转换时发现该地址处只有读权限说明非法访问了页表拒绝转换OS 直接终止进程。 2将内存管理模块和进程管理模块在系统层面上进行解耦合。 操作系统的核心功能内存管理、进程管理、文件管理、驱动管理。 没有进程地址空间时内存管理必须得知道所有的进程的生命状态创建、退出等才能为每个进程分配和释放相关内存资源。所以内存管理模块和进程管理模块是强耦合的。而现在有了进程地址空间内存管理只需要知道哪些内存区域page是被页表映射的已使用哪些是没有被页表映射的未使用不需要知道每个进程的生命状态。当进程管理想要申请内存资源时让内存管理通过页表建立映射即可想要释放内存资源时通过页表取消映射即可。解耦的本质也就是减少模块与模块之间的关联性所以就是将内存管理模块和进程管理模块进行解耦了。 在物理内存中是否可以对未来的数据进行任意位置的加载 可以。 物理内存的分配可以和进程的管理做到没有关系。 在 C/C 语言上 new/malloc 出一块新的空间时本质是在哪里申请空间的呢 虚拟地址空间。 如果申请了空间但不立马使用这块空间 是不是对空间造成了浪费呢 是的。 所以本质上因为有地址空间的存在所以上层申请空间缺页中断其实是在地址空间上申请的物理内存可以甚至一个字节都不给。而当我们真正进行对物理地址空间访问时才执行内存的相关管理算法来申请内存构建页表映射关系然后再进行内存的访问。 括号内的部分完全由 OS 自动完成用户包括进程完全 0 感知。 在分配内存时采用延迟分配的策略来提高整机的效率。几乎内存的有效使用率是 100% 3通过页表映射到不同的有序区域来实现进程的独立性。
在进程的视角所有的内存分别都可以是有序的。让每个进程以同样的方式来看待代码和数据。这样对于进程的设计是非常好的
可执行程序在磁盘中是被划分成一个个的区域存储起来的比如代码 .txt、已初始化数据 .data、未初始化数据 .bss 等等。
因为可执行程序形成时有一个链接的过程会把用户代码和库的代码合并在一起把用户数据和库的数据合并在一起。否则可执行程序的代码和数据如果是混着存放在一起的会导致链接过程变得很复杂。所以进程的地址空间才有了区域划分这样的概念方便进程找到代码和数据。
分析 如图代码被零散的加载到了内存的各个位置。如果直接让进程去找到代码是非常困难的尤其是找到代码的起始和结束位置。所以我们在进程的地址空间中划分出一个个区域再通过页表把内存中的各个位置的代码给整合到一起使代码的物理地址变成线性的虚拟地址了。然后进程通过其对应地址空间中的代码区区域中虚拟地址是连续的可以很方便的找到代码。同时 CPU 也方便执行代码虚拟地址是连续的这样 PC 指针才能进行加 1 的操作得到下一条指今的地址CPU 才能从上到下顺序执行指令。 地址空间 页表的存在可以将内存分布有序化。结合2进程要访问物理内存中的数据和代码可能目前并没有在物理内存中。同样的也可以让不同的进程映射到不同的物理内存便很容易做到进程独立性的实现。进程的独立性可以通过进程空间 页表的方式实现。 好处 不用在物理内存中找一块连续的区域。站在进程的角度所有进程的代码二进制指令存放的区域虚拟地址是连续的可以被顺序执行。即使物理内存上有可能不连续 七、重新理解什么是挂起
进程和程序有什么区别呢 加载的本质就是创建进程。
那么是否必须立刻将所有程序的代码和数据加载到内存中并创建内核数据结构建立映射关系 不是。 如果在最极端的情况下只有内核结构被创建出来了新建状态。当真正被调度/执行代码时才把外设加载内存里然后再执行代码。 理论上可以实现对程序的分批加载。
如果物理内存只有 4G有一个游戏 16G能否运行 可以运行。 CPU 无论运行多大的程序都需要从头到尾执行每一行指令。即使物理内存有 32G也不会一次性把 16G 的程序加载进来因为内存资源还需要分配给其它进程而是采用延时加载。比如先加载 200M 进来执行完了再覆盖式的加载 200M 进来然后再执行。所以如果物理内存比较小用户可能会感到游戏卡顿。 加载的本质就是换入的过程。
既然可以分批加载那可以分批换出吗 可以。 甚至这个进程短时间不会再被执行比如挂起 / 阻塞。 也就相当于其对应的代码和数据占着空间却不创造价值所以 OS 就可以将它换出一旦被换出那么此时这个进程就叫被挂起。 八、Linux2.6 内核进程调度队列
1、Linux2.6 内核中进程队列的数据结构 2、一个 CPU 拥有一个 runqueue 如果有多个 CPU 就要考虑进程个数的负载均衡问题。 3、优先级 普通优先级100139我们都是普通的优先级想想 nice 值的取值范围可与之对应 实时优先级099不关心 4、活动队列 时间片还没有结束的所有进程都按照优先级放在该队列。nr_active总共有多少个运行状态的进程。queue[140]一个元素就是一个进程队列相同优先级的进程按照 FIFO 规则进行排队调度所以数组下标就是优先级。从该结构中选择一个最合适的进程过程怎么回事的呢 从 0 下表开始遍历 queue[140]。找到第一个非空队列该队列必定为优先级最高的队列。拿到选中队列的第一个进程开始运行调度完成。遍历 queue[140] 时间复杂度是常数但还是太低效了。 bitmap[5]一共 140 个优先级140 个进程队列为了提高查找非空队列的效率就可以用 5*32 个比特位表示队列是否为空这样便可以大大提高查找效率。 5、过期队列 过期队列和活动队列结构一模一样。过期队列上放置的进程都是时间片耗尽的进程。当活动队列上的进程都被处理完毕之后对过期队列的进程进行时间片重新计算。 6、active 指针和 expired 指针 active 指针永远指向活动队列。expired 指针永远指向过期队列。可是活动队列上的进程会越来越少过期队列上的进程会越来越多因为进程时间片到期时一直都存在的。 但在合适的时候只要能够交换 active 指针和 expired 指针的内容就相当于有具有了一批新的活动进程。 7、总结 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数不随着进程增多而导致时间成本增加我们称之为进程调度 O(1) 算法。 【Linux】Linux 的进程优先级 NI 和 PR-CSDN博客