进程概念
认识冯诺依曼系统
操作系统概念与定位
深⼊理解进程概念,了解PCB
学习进程状态,学会创建进程,掌握僵⼫进程和孤⼉进程,及其形成原因和危害
了解进程调度,Linux进程优先级,理解进程竞争性与独⽴性,理解并⾏与并发
理解进程切换,以及Linux2.6 kernel,O(1)调度算法架构
理解环境变量,熟悉常⻅环境变量及相关指令, getenv/setenv函数
理解C内存空间分配规律,了解进程内存映像和应⽤程序区别, 认识虚拟地址空间。
1.冯诺依曼体系结构
我们常⻅的计算机,如笔记本。我们不常⻅的计算机,如服务器,⼤部分都遵守冯诺依曼体系。
截⾄⽬前,我们所认识的计算机,都是由⼀个个的硬件组件组成
输⼊单元:包括键盘, ⿏标,扫描仪, 写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显⽰器,打印机等
关于冯诺依曼,必须强调⼏点:
•
这⾥的存储器指的是内存
•
不考虑缓存情况,这⾥的CPU能且只能对内存进⾏读写,不能访问外设(输⼊或输出设备)
•
外设(输⼊或输出设备)要输⼊或者输出数据,也只能写⼊内存或者从内存中读取。
•
⼀句话,所有设备都只能直接和内存打交道。
2.操作系统(Operator System)
2-1 概念
任何计算机系统都包含⼀个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,⽂件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
2-2 设计OS的⽬的
对下,与硬件交互,管理所有的软硬件资源
对上,为⽤⼾程序(应⽤程序)提供⼀个良好的执⾏环境
2-3 核⼼功能
在整个计算机软硬件架构中,操作系统的定位是:⼀款纯正的“搞管理”的软件
2-4 如何理解 “管理”
管理的例⼦ - 学⽣,辅导员,校⻓
描述被管理对象
组织被管理对象
总结
计算机管理硬件
- 描述起来,⽤struct结构体
- 组织起来,⽤链表或其他⾼效的数据结构
2-5 系统调⽤和库函数概念
在开发⻆度,操作系统对外会表现为⼀个整体,但是会暴露⾃⼰的部分接⼝,供上层开发使⽤,
这部分由操作系统提供的接⼝,叫做系统调⽤。
系统调⽤在使⽤上,功能⽐较基础,对⽤⼾的要求相对也⽐较⾼,所以,有⼼的开发者可以对部
分系统调⽤进⾏适度封装,从⽽形成库,有了库,就很有利于更上层⽤⼾或者开发者进⾏⼆次开
发。
- 进程
3-1 基本概念与基本操作
课本概念:程序的⼀个执⾏实例,正在执⾏的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
3-1-2 描述进程-PCB
基本概念
进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的⼀种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的⼀种数据结构,它会被装载到RAM(内存)⾥并且包含着进程的信息。
3-2-3 task_ struct
内容分类
标⽰符: 描述本进程的唯⼀标⽰符,⽤来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执⾏的下⼀条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下⽂数据: 进程执⾏时处理器的寄存器中的数据[休学例⼦,要加图CPU,寄存器]。
I∕O状态信息: 包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。
记账信息: 可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。
其他信息
组织进程
3-1-4 查看进程
-
进程的信息可以通过 /proc 系统⽂件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个⽂件夹。
-
⼤多数进程信息同样可以使⽤top和ps这些⽤⼾级⼯具来获取
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
3-1-5 通过系统调⽤获取进程标⽰符
进程id(PID)
⽗进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf(“pid: %d\n”, getpid());
printf(“ppid: %d\n”, getppid());
return 0;
}
3-1-6 通过系统调⽤创建进程-fork初识
运⾏ man fork 认识fork
fork有两个返回值
⽗⼦进程代码共享,数据各⾃开辟空间,私有⼀份(采⽤写时拷⻉)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf(“hello proc : %d!, ret: %d\n”, getpid(), ret);
sleep(1);
return 0;
}
fork 之后通常要⽤ if 进⾏分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//返回的是子进程的pid
pid_t pid = fork();
if(pid<0)
{
perror(“fork”);
return 1;
}
else if(pid == 0)
{
printf(“I am child : %d!, pid: %d\n”, getpid(), pid);
}
else
{ //father
printf(“I am father : %d!, pid: %d\n”, getpid(), pid);
}
sleep(1);
return 0;
}
3-2 进程状态
3-2-1 Linux内核源代码怎么说
为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状
态(在Linux内核⾥,进程有时候也叫做任务)。
下⾯的状态在kernel源代码⾥定义:
static const char *const task_state_array[] = {
“R (running)”, /*0 */
“S (sleeping)”, /*1 */
“D (disk sleep)”, /*2 */
“T (stopped)”, /*4 */
“t (tracing stop)”, /*8 */
“X (dead)”, /*16 */
“Z (zombie)”, /*32 */
};
•
R运⾏状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏
队列⾥。
•
S睡眠状态(sleeping): 意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
•
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个
状态的进程通常会等待IO的结束。
•
T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的
进程可以通过发送 SIGCONT 信号让进程继续运⾏。
•
X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
3-2-2 进程状态查看
1 ps aux / ps axj 命令
•
a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
•
x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
•
j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
•
u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等
3-2-3 Z(zombie)-僵⼫进程
•
僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出并且⽗进程(使⽤wait()系统调⽤,后
⾯讲)没有读取到⼦进程退出的返回代码时就会产⽣僵死(⼫)进程
•
僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。
•
所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程进⼊Z状态
来⼀个创建维持30秒的僵死进程例⼦:
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror(“fork”);
return 1;
}
else if(id > 0){ //parent
printf(“parent[%d] is sleeping…\n”, getpid());
sleep(30);
}else{
printf(“child[%d] is begin Z…\n”, getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
3-2-4 僵⼫进程危害
•
进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我
办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
•
维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,
换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
•
那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数
据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置
进⾏开辟空间!
内存泄漏?是的!
•
如何避免?后⾯讲
⾄此,值得关注的进程状态全部讲解完成,下⾯来认识另⼀种进程
3-2-5 孤⼉进程
•
⽗进程如果提前退出,那么⼦进程后退出,进⼊Z之后,那该如何处理呢?
•
⽗进程先退出,⼦进程就称之为“孤⼉进程”
•
孤⼉进程被1号init进程领养,当然要有init进程回收喽。
3-3 进程优先级
3-3-1 基本概念
•
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
•
优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性 能。
•
还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
3-3-2 查看系统进程
在linux或者unix系统中,⽤ps ‒l命令则会类似输出以下⼏个内容:
我们很容易注意到其中的⼏个重要信息,有下:
•
UID : 代表执⾏者的⾝份
•
PID : 代表这个进程的代号
•
PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
•
PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏
•
NI :代表这个进程的nice值
3-3-3 PRI and NI
•
PRI也还是⽐较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执⾏的先后顺序,此值越⼩进程的优先级别越⾼
•
那NI呢?就是我们所要说的nice值了,其表⽰进程可被执⾏的优先级的修正数值
•
PRI值越⼩越快被执⾏,那么加⼊nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
•
这样,当nice值为负值的时候,那么该程序将会优先级值将变⼩,即其优先级会变⾼,则其越快 被执⾏
•
所以,调整进程优先级,在Linux下,就是调整进程nice值
•
nice其取值范围是-20⾄19,⼀共40个级别。
3-3-4 PRI vs NI
需要强调⼀点的是,进程的nice值不是进程的优先级,他们不是⼀个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
3-3-5 查看进程优先级的命令
⽤top命令更改已存在进程的nice:
•
top
•
进⼊top后按“r”‒>输⼊进程PID‒>输⼊nice值
3-3-6 补充概念-竞争、独⽴、并⾏、并发
•
竞争性: 系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为
了⾼效完成任务,更合理竞争相关资源,便具有了优先级
•
独⽴性: 多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
•
并⾏: 多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
•
并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称
之为并发
3.4 进程切换
CPU上下⽂切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任务 时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的 堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器, 并开始下⼀个任务的运⾏, 这⼀过程就是context switch。
参考⼀下Linux内核0.11代码
3-4 Linux2.6内核进程O(1)调度队列
上图是Linux2.6内核中进程队列的数据结构
3-4-1 ⼀个CPU拥有⼀个runqueue
•
如果有多个CPU就要考虑进程个数的负载均衡问题
3-4-2 优先级
•
普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
•
实时优先级:0〜99(不关⼼)
3-4-3 活动队列
•
时间⽚还没有结束的所有进程都按照优先级放在该队列
•
nr_active: 总共有多少个运⾏状态的进程
•
queue[140]: ⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以, 数组下标就是优先级!
•
从该结构中,选择⼀个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第⼀个⾮空队列,该队列必定为优先级最⾼的队列
- 拿到选中队列的第⼀个进程,开始运⾏,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
•
bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤ 5*32个⽐特位表⽰队列是否为空,这样,便可以⼤ 提⾼查找效率!
3-4-4 过期队列
• 过期队列和活动队列结构⼀模⼀样
• 过期队列上放置的进程,都是时间⽚耗尽的进程
• 当活动队列上的进程都被处理完毕之后,对过期队列的进程进⾏时间⽚重新计算
3-4-5 active指针和expired指针
• active指针永远指向活动队列
• expired指针永远指向过期队列
• 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间⽚到期时⼀直都存在的。
• 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批新的活动进程!
3-4-6 总结
在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成
本增加,我们称之为进程调度O(1)算法!
-
环境变量
4-1 基本概念
• 环境变量(environment variables)⼀般是指在操作系统中⽤来指定操作系统运⾏环境的⼀些参数
• 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪 ⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。
• 环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性
4-2 常⻅环境变量
• PATH : 指定命令的搜索路径
• HOME : 指定⽤⼾的主⼯作⽬录(即⽤⼾登陆到Linux系统中时,默认的⽬录)
• SHELL : 当前Shell,它的值通常是/bin/bash。
4-3 查看环境变量⽅法
echo $NAME //NAME:你的环境变量名称
测试PATH -
创建hello.c⽂件
#include <stdio.h>
int main()
{
printf(“hello world!\n”);
return 0;
} -
对⽐./hello执⾏和之间hello执⾏
-
为什么有些指令可以直接执⾏,不需要带路径,⽽我们的⼆进制程序需要带路径才能执⾏?
-
将我们的程序所在路径加⼊环境变量PATH当中, export PATH=$PATH:hello程序所在路径
-
对⽐测试
-
还有什么⽅法可以不⽤带路径,直接就可以运⾏呢?
测试HOME -
⽤root和普通⽤⼾,分别执⾏ echo $HOME ,对⽐差异
-
执⾏ cd ~; pwd ,对应 ~ 和 HOME 的关系
4-4 和环境变量相关的命令
- echo: 显⽰某个环境变量值
- export: 设置⼀个新的环境变量
- env: 显⽰所有环境变量
- unset: 清除环境变量
- set: 显⽰本地定义的shell变量和环境变量
4-5 环境变量的组织⽅式
每个程序都会收到⼀张环境表,环境表是⼀个字符指针数组,每个指针指向⼀个以’\0’结尾的环境字符串
4-6 通过代码如何获取环境变量
• 命令⾏第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf(“%s\n”, env[i]);
}
return 0;
}
• 通过第三⽅变量environ获取
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf(“%s\n”, environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头⽂件中,所以在使⽤时 要⽤ extern声明。
4-7 通过系统调⽤获取或设置环境变量
• putenv , 后⾯讲解
• getenv , 本次讲解
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf(“%s\n”, getenv(“PATH”));
return 0;
}
常⽤getenv和putenv函数来访问特定的环境变量。
4-8 环境变量通常是具有全局属性的
• 环境变量通常具有全局属性,可以被⼦进程继承下去
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv(“MYENV”);
if(env){
printf(“%s\n”, env);
}
return 0;
}
直接查看,发现没有结果,说明该环境变量根本不存在
• 导出环境变量
export MYENV="hello world“
• 再次运⾏程序,发现结果有了!说明:环境变量是可以被⼦进程继承下去的!想想为什么?
4-9 实验
• 如果只进⾏ MYENV=“helloworld” ,不调⽤export导出,在⽤我们的程序查看,会有什么结果?为什么?
• 普通变量
• 如果时间允许:做⼀下~/.bash_profile && ~/.bashrc修改⽂件级环境变量
5.程序地址空间
5-1 研究平台
• kernel 2.6.32
• 32位平台
5-2 程序地址空间回顾
可是我们对他并不理解!可以先对其进⾏各区域分布验证:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = “helloworld”;
printf(“code addr: %p\n”, main);
printf(“init global addr: %p\n”, &g_val);
printf(“uninit global addr: %p\n”, &g_unval);
static int test = 10;
char heap_mem = (char)malloc(10);
char heap_mem1 = (char)malloc(10);
char heap_mem2 = (char)malloc(10);
char heap_mem3 = (char)malloc(10);
printf(“heap addr: %p\n”, heap_mem); //heap_mem(0), &heap_mem(1)
printf(“heap addr: %p\n”, heap_mem1); //heap_mem(0), &heap_mem(1)
printf(“heap addr: %p\n”, heap_mem2); //heap_mem(0), &heap_mem(1)
printf(“heap addr: %p\n”, heap_mem3); //heap_mem(0), &heap_mem(1)
printf(“test static addr: %p\n”, &test); //heap_mem(0), &heap_mem(1)
printf(“stack addr: %p\n”, &heap_mem); //heap_mem(0), &heap_mem(1)
printf(“stack addr: %p\n”, &heap_mem1); //heap_mem(0), &heap_mem(1)
printf(“stack addr: %p\n”, &heap_mem2); //heap_mem(0), &heap_mem(1)
printf(“stack addr: %p\n”, &heap_mem3); //heap_mem(0), &heap_mem(1)
printf(“read only string addr: %p\n”, str);
for(int i = 0 ;i < argc; i++)
{
printf(“argv[%d]: %p\n”, i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf(“env[%d]: %p\n”, i, env[i]);
}
return 0;
}
5-3 虚拟地址
来段代码感受⼀下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror(“fork”);
return 0;
}
else if(id == 0){ //child
printf(“child[%d]: %d : %p\n”, getpid(), g_val, &g_val);
}else{ //parent
printf(“parent[%d]: %d : %p\n”, getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解呀,因为⼦进程按照⽗进程为模版,⽗⼦并没有对变量进⾏进⾏任何修改。可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror(“fork”);
return 0;
}
else if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再
读取
g_val=100;
printf(“child[%d]: %d : %p\n”, getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf(“parent[%d]: %d : %p\n”, getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
我们发现,⽗⼦进程,输出地址是⼀致的,但是变量内容不⼀样!能得出如下结论:
• 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
• 但地址值是⼀样的,说明,该地址绝对不是物理地址!
• 在Linux地址下,这种地址叫做 虚拟地址
• 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀
管理
5-4 进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看 图:
说明:
• 上⾯的图就⾜矣说明问题,同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
5-5 虚拟内存管理 - 第⼀讲
描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构。
可以说,mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况:
定位mm_struct⽂件所在位置和task_struct所在路径是⼀样的,不过他们所在⽂件是不⼀样的,mm_struct所在的⽂件是mm_types.h。
那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:
- 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
- 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。
linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚 拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进 程快速访问。
struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region vm_region; / NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy vm_policy; / NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
所以我们可以对上图在进⾏更细致的描述,如下图所⽰:
5-6 为什么要有虚拟地址空间
这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的, 也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证 这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存 ⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分 配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分 出110M分配给程序B。
这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。
•
安全⻛险
◦
每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内
存区域,如果是⼀个⽊⻢病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
• 地址不确定
◦ 众所周知,编译完成后的程序是存放在硬盘上的,当运⾏的时候,需要将程序搬到内存当中
去运⾏,如果直接使⽤物理地址的话,我们⽆法确定内存现在使⽤到哪⾥了,也就是说拷⻉
的实际内存地址每⼀次运⾏都是不确定的,⽐如:第⼀次执⾏a.out时候,内存当中⼀个进程
都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程
在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了
• 效率低下
◦ 如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理
内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中,好腾出内
存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷⻉
时间太⻓,效率较低。
存在这么多问题,有了虚拟地址空间和分⻚机制就能解决了吗?当然!
• 地址空间和⻚表是OS创建并维护的!是不是也就意味着,凡是想使⽤地址空间和⻚表进⾏映射,
也⼀定要在OS的监管之下来进⾏访问!!也顺便 ,包括各个
进程以及内核的相关有效数据!
保护了物理内存中的所有的合法数据
• 因为有地址空间的存在和⻚表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置
的加载!物理内存的分配 和 进程的管理就可以做到没有关系, 。
进程管理模块和内存管理模块就完 成了解耦合
◦ 因为有地址空间的存在,所以我们在C、C++语⾔上new, malloc空间的时候,其实是在地址
空间上申请的,物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问
的时候,才执⾏内存的相关管理算法,帮你申请内存,构建⻚表映射关系(延迟分配),这 是由操作系统⾃动完成,⽤⼾包括进程完全0感知!!
• 因为⻚表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的
虚拟地址和物理地址进⾏映射,在进程视⻆所有的内存分布都可以是有序的。