进程优先级
什么是优先级
优先级:指定进程获取某种资源的先后顺序
task_struct,也叫进程控制块,本质是个结构体,有很多内部字段(进程的优先级就是其中的一只内部字段)
int prio = ??
Linux中优先级数字越小,优先级越高
优先级和权限有什么区别呢?
首先,权限是决定我们能不能获取资源,而优先级是决定获取资源的顺序
为什么要有优先级
为什么要有优先级捏?
我们知道进程访问的资源(CPU)始终都是有限的
系统中进程大部分情况都是有较多的
操作系统关于调度和优先级的原则:分时操作系统,保证基本的公平,如果进程长时间不被调度,就会造成饥饿问题(插队破坏规则)
Linux优先级特点&&查看方式
myprocess:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("I am a process,pid:%d\n",getpid());
sleep(1);
}
return 0;
}
makefile:
bin = myprocess.exe
src = myprocess.c
$(bin) :$(src)
@gcc -o $@ $^
@echo "compiled $(src) to $(bin)..."
.PHONY:clean
clean :
@rm -f $(bin)
@echo "clean project"
在Linux中查看进程的优先级可以用这条命令:
ps -l
但是会发现其实查不到刚刚在运行的进程:
因为该命令只会显示当前终端下的进程,那怎么查看全部呢?
带个a就好了:
ps -al
PRI、NI
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :进程可被执行的优先级(默认优先级),其值越小越早被执行
NI:当前进程优先级的修正数据,nice值,新的优先级=优先级+nice,达到对于进程优先级动态修改的过程
那么如何对进程的优先级进行调整呢?
这不是有nice么,可以帮助我们在启动前或者运行时做动态调整
用top可助我(雷公助我!)
怎样对正在运行的进程做动态调整呢?
输入r:
再输入欲调整进程的PID:
调了个一百,出来一看:
哎怎么个事?
怎么变成99和19了?
为什么捏?
其实是因为nice值不可任意调整,是有范围的(为了保证平衡)
那范围是多少呢?
一般来说,nice值是-[20,19],总共四十个数字,所以刚调成一百实际上是把nice值变成19了(选取最接近的取值范围的数)
改完咯:
tips:进程优先级是不支持被频繁修改的,所以普通用户再使用的时候会有些限制(普通用户只要频繁切换身份就好了,超级用户要考虑的事可就很多了!)
关于优先级的调整,我们每次调整的时候都是从80开始的,在80的基础上对数据做修正(无需关注过往,看想要什么就好)虽然并不推荐改优先级嘞,,,
经过之前的学习,当只有一个CPU,一套寄存器时,多进程在特定时间范围内被调度时需要通过上下文切换的方式保证多个进程的代码得以推进,我们把这个过程叫做并发(CPU运行速度很快,能在很短的时间内将这些东西都进行一遍)(卡顿本质是CPU调度进程操作不过来,所以不要挂太多后台)
一些概念:
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发
并行时并发和并行也是同时存在的(能力越大责任越大嘛)
游戏本渲染性能就是会好(虽然4090都出了,可是我独爱着我的4060独显)
命令行参数
main函数带参不啊?
带哎!
是这样的:
#include<stdio.h>
int main(int argc,char* argv[])
{
return 0;
}
不过平时没写,那说明main函数的参数可带可不带
那么这些参数都是干啥的呢?
打印出来看看就知道了:
#include<stdio.h>
#include<unistd.h>
int main(int argc, char* argv[])
{
for (int i = 0; i < argc; i++)
{
printf("argv[%d]->%s\n", i, argv[i]);
}
return 0;
}
通过观察我们可以发现:
main函数的参数是包括程序名称在内及其他参数
在命令行中输入的./myprocess.exe -a -b -c -d,,是命令行字符串,第一个永远都是要执行的程序的名称,后面的被称为选项,argv是变长数组哎!
数组的元素个数由argc表示(c:count,v:vector)
char*指向的是字符串,在进行命令行输入的时候会有一些程序,会帮我们将输入的整体的字符串打散,输入的是以空格作为分隔符的数组(然后把空格转换成\0就相当于把字符串打散嘞)(将字符串指针放到数组里,以参数的形式传递给main函数,最终就有了argv,有几个字符串,argc的个数就是几,但argv最后必须以NULL结尾,不论传递了几个指针)
我们暂时关心两个问题:为什么要这么干+谁干的!
上面的代码还等价于:
#include<stdio.h>
#include<unistd.h>
int main(int argc,char* argv[])
{
int i=0;
for(i=0;argv[i];i++)
{
printf("argv[%d]->%s\n",i,argv[i]);
}
return 0;
}
判断条件是argv[i],当argv[i]到最后的时候也就变为NULL也就是0,也退出循环
再来写一段代码验证下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("Usage:%s -[a,b,c,d]\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-a") == 0)
{
printf("this is function1\n");
}
else if (strcmp(argv[1], "-b") == 0)
{
printf("this is function2\n");
}
else if (strcmp(argv[1], "-c") == 0)
{
printf("this is function3\n");
}
else if (strcmp(argv[1], "-d") == 0)
{
printf("this is function4\n");
}
else
{
printf("no this function!\n");
}
return 0;
}
有了这段代码,编译后运行:
直接运行给出报错提醒:用法是继续再命令行输入相应参数
所以按照相应要求给出输入:
所以这个功能就很明了了:可以通过输入不同的选项去执行同一个程序内的不同功能!
就是这样!你现在全场领先!
命令行参数的本质是交给我们不同程序不同的选项,用来定制不同的程序功能(命令中会携带很多的选项)
那么下个问题来了:这是谁干的?
拜托,我可是main函数哎,谁闲的没事帮我形成argv[ ]这样的数组呢?
再来看一段代码吧:
#include<stdio.h>
#include<unistd.h>
//#include<string.h>
int g_val = 100000;
int main()
{
printf("I am father process,pid: %d,ppid: %d ,g_val:%d\n", getpid(),getppid(), g_val);
sleep(5);
pid_t id = fork();
if (id == 0)
{
while (1)
{
printf("I am child process,pid: %d,ppid: %d , g_val: %d\n", getpid(),getppid(), g_val);
sleep(1);
}
}
else
{
while (1)
{
printf("I am father process,pid: %d,ppid: %d , g_val: %d\n", getpid(), getppid(), g_val);
sleep(1);
}
}
return 0;
}
编译运行:
那这能说明些什么呢?
能说明不论是父进程还是紫禁城,看g_val都是100000
父进程的数据,默认能被紫禁城看到并访问
假设不创建紫禁城,继续运行:
#include<stdio.h>
#include<unistd.h>
int g_val = 100000;
int main()
{
printf("I am father process,pid: %d,ppid: %d ,g_val:%d\n", getpid(),getppid(), g_val);
return 0;
}
一个炫酷的老是存在的Bash!
带入之前的代码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char* argv[])
{
printf("I am father process,pid: %d,ppid: %d , g_val: %d\n", getpid(), getppid(), g_val);
if (argc != 2)
{
printf("Usage:%s -[a,b,c,d]\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-a") == 0)
{
printf("this is function1\n");
}
else if (strcmp(argv[1], "-b") == 0)
{
printf("this is function2\n");
}
else if (strcmp(argv[1], "-c") == 0)
{
printf("this is function3\n");
}
else if (strcmp(argv[1], "-d") == 0)
{
printf("this is function4\n");
}
else
{
printf("no this function!\n");
}
return 0;
}
编译运行之后:
命令行中启动的程序都能变成进程, 而这些都是bash的紫禁城
所以命令行中输入的数据默认是输入给父进程Bash的(Bash:命令行解释器,得对命令行解释啊!所以会把输入的参数变成进程参数,定义好一个argc,再创建个表argv[ ],定义完后再由Bash传递给紫禁城)
那传递怎么传的呢?
父进程开辟空间然后巴拉巴拉,,,父进程的数据默认能被紫禁城看到并访问
环境变量
都是可以带选项的,为什么ls就不用带路径,我的process就一定要带路径呢?
ls在执行的时候也可以带路径的
那为什么会产生这样的差别呢?
是Linux中存在一些全局的设置,表明告诉命令行解释器应该去哪些路径下去寻找可执行程序
echo $PATH
系统中的很多配置,在我们登录Linux系统的时候,就已经加载到了Bash进程中(内存)
PATH:环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
打印环境变量的内容:$PATH
Bash内部维护了上面的一堆路径(以:作为分隔符的),这是Bash在执行时的搜索路径(Bash在执行命令的时候,需要先找到命令,因为未来要加载),如果找不到命令,就会显示不存在
所以ls不用带路径是因为在Bash的搜索路径中(usr/bin)
那假如想要我们的程序和系统指令一样该怎么办呢?
直接拷贝吧:
cp myprocess.exe /usr/bin/
蒽:
这相当于是把我们写的程序安装到了Linux系统里了
可是我多大面啊?
算了吧,容易污染指令集
rm /usr/bin/myprocess.exe
这是卸载
安装和卸载的本质是拷贝和删除
那万一不小心把Bash里面的路径改了改咋整捏?(也太不小心了)
把PATH改了,我嘞个骚刚:
但是莫慌,你重新登录下呢?
原来是须鲸一场:
环境变量在登录时被加载到Bash内部,所以莫事啦~
这种是内存级的环境变量
那怎么样将指定路径添加都环境变量里呢?
PATH=$PATH:/root/ice
这样就是添加不是覆盖了(妈妈再也不用担心我执行命令要加./啦)
由于是内存级的,那怎样永久保留啊?(放一只执念诗心龙就好了:你根本不理解我)
服啦,刷到最后都没刷到,居然是队友刷到的,帮我锁了下,我根本不是先天龙圣体!
最开始的环境变量有自己的数据来源,不是在内存中,是在系统对应的配置文件中
在每个用户的家目录下, 会存在两个隐藏文件:.bash_profile,.bashrc
我看看怎么个事:
bashprofile会判断配置文件是否存在(.bashrc)
这是在把bashrc的内容导入到Bash进程的上下文里:
. ~/.bashrc
这就是PATH环境变量 :
PATH=$PATH:$HOME/bin
这是.bashrc:
作用是把系统的bashec也导入一遍(系统的bashrc也有其他的环境变量,可以理解成大蛇)
具体过程参考下图:
看一下系统下的bashrc吧:
vim /etc/bashrc
里面都是些脚本, 执行的操作就是Bash在登录时把所有的环境变量都导入进来
环境变量默认是在配置文件中的,所以如果想要将自己的路径添加到配置文件且永久保留,就可以把这个:
改成:
然后就永久的可执行了:
但最好别瞎改,我已经继续默认了
在Windows中也是有环境变量的:
就喜欢偷我表情包:
除了PATH肯定还有别的环境变量嘞
查看系统的环境变量:env
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash
HOME是这样的捏:
系统pwd能知道我当前处于哪个路径是因为系统存在可以随着路径变化而变化的环境变量:
在启动时要创建一个命令行SHELL让它来为我提供命令行解释的服务
怎么知道我是什么shell呢?
查查就好:
还有个环境变量叫HISTSIZE:
历史大小: 上下翻可以记录历史指令,1000是能记录的历史指令数
history能查看使用过的历史指令:
那我们怎样自己定义环境变量呢?
可以这样:
export name=value
我导入环境变量没啥用,也根本不会影响后面的工作(听君一席话,如听一席话)
想取消就这样:
unset name
还有种变量:
没有被系统导入到环境变量,但可查,这个叫本地变量
环境变量和代码有什么关系呢?
怎样通过代码的方式获取环境变量呢?
在C中默认给我们提供环境变量:errno
在Linux中系统也默认给我们提供了一个全局变量:environ
这个环境变量的类型是二级指针,从这个二级指针即可获取环境变量
#include<stdio.h>
#include<unistd.h>
int main()
{
extern char** environ; //声明一哈
int i = 0;
for (i = 0; environ[i]; i++)
{
printf("env[%d]->%s\n", i, environ[i]);
}
return 0;
}
让我看看怎么个事:
这些环境变量就是刚刚shell内部的环境变量
环境变量默认也是可以被紫禁城拿到的!(环境变量们默认在Bash内部)
有磁盘有内存
磁盘的内容包含了环境变量
.bash_profile
.bashrc
/etc/bashrc
在启动的时候会把磁盘中的内容导入到内存中
bash也就拿到了这些环境变量,环境变量的本质是数据
父进程的数据默认能被紫禁城看到并访问
所以环境变量的内容被紫禁城看到并不奇怪
那环境变量很多,bash内部如何组织呢?
bash会维护一张表:char* env[ ]
环境变量本质是字符串,bash进程启动的时候,默认会给紫禁城形成两张表:argv[ ]命令行参数表(从用户输入命令行获取),env[ ]环境变量表(从OS的配置文件表),bash通过各种方式交给紫禁城
所以这个是char**也就不奇怪了:
extern char** environ;
因为维护的表里面的内容是char*啊!
导入环境变量的本质是把内容添加到表里:env
环境变量具有系统级的全局属性:环境变量会被紫禁城继承
紫禁城再fork,孙进程(我扯淡的,,,)也能获取到环境变量
getenv:
putenv:
可以这样获取环境变量:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc,char* argv[],char* env[])
{
char* path = getenv("PATH");
if (path == NULL)
{
return 1;
}
printf("path:%s\n", path);
return 0;
}
所以目前已知的环境变量的获取方式:
extern char** environ;
通过main函数参数
getenv(“PATH”);
但是有点怪哎:
export love=trust
这不会创建紫禁城吗?
可是紫禁城不该被Bash看到哎,但它也确实是被导给了Bash (进程有独立性)
内建命令:export,echo
在命令行中执行的命令有80%都是Bash创建紫禁城执行的
内建命令是由Bash亲自执行(Bash也是C写的,里面有些函数,如果命令是这些函数就不创建紫禁城了)
前面已经证明过它的存在了:PATH改掉后touch跑不了,echo接着跑,,,
定义出的本地变量可以被修改,没在环境变量表中可以导入至环境变量表中
本地变量只在本Bash内部有效,无法被紫禁城继承下去,导成环境变量才能被获取
本地变量只能使用内建命令才能看到
进程的地址空间
来看段代码吧:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_val = 100;
int main()
{
printf("father is running,pid: %d,ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
//child
int cnt = 0;
while (1)
{
printf("I am child process,pid: %d,ppid: %d\n", getpid(), getppid());
printf("g_val: %d,&g_val: %d\n", g_val, &g_val);
sleep(1);
cnt++;
if (cnt == 5)
{
g_val = 300;
printf("I am child process,change:%d -> %d\n", 100, 300);
}
}
}
else
{
//father
while (1)
{
printf("I am father process,pid: %d,ppid: %d\n", getpid(), getppid());
printf("g_val: %d,&g_val: %d\n", g_val, &g_val);
sleep(1);
}
}
return 0;
}
进程执行之后是这样的捏:
发现父子进程g_val不一样,由于父子进程具有独立性
进程:内核数据结构(task_struct)+ 代码和数据
紫禁城没有代码不要紧,因为代码的属性是只读的
紫禁城写入对父进程没有影响,但是为什么后面地址是一样得嘞?
地址一样,内容却不同?
应从系统层面上理解了!
这个地址不是物理地址
那不是物理地址是什么呢?
这种地址在系统层面上被称为虚拟地址:
那么我们如何理解这种现象呢?
地址空间(在OS内部)长这样:
地址空间是虚拟地址,存在页表将地址空间的虚拟地址转化到物理内存(建立映射关系)
g_val是已初始化的全局变量,有虚拟地址,经过映射找到物理地址
创建紫禁城,紫禁城有自己的task_struct,也有自己的虚拟地址空间,也会有自己对应的页表
每个进程都有自己的地址空间和页表
那操作系统该怎样管理这么多地址空间和页表呢?
地址空间本质上是内核数据结构(结构体对象)
紫禁城会把父进程的很多内核数据结构拷贝一份(紫禁城的地址空间和页表都来自父进程)
所以在映射的时候只会进行浅拷贝,由于紫禁城修改数据可能会对父进程产生影响,进程在运行时具有独立性(设计原则)
真实情况是紫禁城尝试对数据进行修改的时候,操作系统(自主完成,写时拷贝)会重新开辟一块空间(在物理内存上),把老数据拷贝到新空间中,把新地址放入页表,重新构建映射,地址指向新空间,再进行写入工作
如果父子进程不是写的,那么未来一个全局变量,默认是被父子共享的,代码是共享的(只读)
那为什么要这样干呢?
因为进程具有独立性啊!
不过具有独立性并不是进行写时拷贝的理由
那么可不可以把数据在创建紫禁城的时候,全给紫禁城拷贝一份?
不这样干是因为,有很多东西紫禁城并不会去修改,比如说一些环境变量
没必要改占据空间还很大,直接全拷贝一份浪费资源
写时拷贝的优点:可以按需申请,就不会过分浪费地址空间
这种方式并不慢,而是通过调整拷贝的时间顺序,达到有效节省空间的目的
接下来迎来四个问题:
如何理解地址空间?
首先要知道什么是划分区域:就和三八线差不多(可是从没经历过哎)
划分三八线就是在做区域划分,可是有的三八线不公平哎,要不公平起见重新划一下呢?
重新划的过程是在做区域的调整
上面的场景用计算机语言怎样表述呢?
#include<stdio.h>
struct area
{
int start;
int end;
};
struct destop
{
struct area left;
struct area right;
};
int main()
{
struct destop d;
d.left.start = 1;
d.left.end = 50;
d.right.start = 50;
d.right.end = 100;
return 0;
}
Lock Noob 和 HelpfulLockPicker 的教学
随着您的进步,BosnianBill 为那些了解基础知识的人提供了许多好视频。
LockPickingLawyer 是最著名的开锁 YouTuber,主要展示了他在数百把锁上的强大技能以及如何利用各种锁设备中的漏洞
区域的本质就是上面的start和end
地址空间本质是内核的一个struct的结构体!(叫struct mm_struct的一个结构体对象)内部很多的属性都是start,end的范围
有图有真相:
你说的对,你太深情了小猫
范围对应的整体是可以被使用的
举个:
在遥远的漂亮国, 有个大富翁王老板,王老板有四个私生子,但是这四个私生子真的很私生!(他们甚至真的不知道彼此,甚至都觉得是王老板唯一的儿子/女儿)《重生之我是继承人》狗血戏码居然真的要上演了么!所以王老板某天去找了大宝,和大宝讲(不要坑蒙拐骗了阿喂(#`O′)):“好大儿,附耳过来,且听老爸给你讲讲,你爹我现在身价十个亿!!!,哼,这可是在漂亮国,你以为我会用其它货币衡量我的身价么?(要是津巴布韦币或者欢乐豆就好玩了)肤浅!老子要是走了,这十个亿都是你的!听说你进来在外面跑生意啊,挺能干我大儿子”,在不久之后,王老板又去找了二宝,二宝是个女娃,在海外留学读博,法律专业,王老板对二女儿也说:“娃你好好念,你学位证下来了也有能力了,你一直是个挺独立有主见的女娃,等到时候老爸驾鹤西去,就靠你帮老爸打理这十亿资产了,这些都是你的”(我拿着丘比特之箭追呀追,你穿防弹背心飞呀飞,你说你喜欢白马王子,真不巧,我是黑马胖子,因为“这些都是你的”突然联想到电影片段发个颠,莫在意),又过了不久,王老板又去找小老三,三宝也是个小女孩,在从事珠宝生意,这都是真事,我的串就是从三宝那买的,有图有真相!(其实只是想显摆下我的串):
咳咳,言归正传,王老板对小老三说:“闺女啊,最近生意咋样啊,你好好干,老爸现在身价十亿,如果到时候老爸不在了,这十个亿都是你的” ,小老三听完也特别高兴(当然前面两只亦然),后来王老板又去忽悠(bushi)四宝,来看看四宝吧:
四宝现在还是高中生,预备考大学,王老板也和这个小儿子说:“好儿砸,你要是能考上清华(就不用烤地瓜),将来出息了,咱家这十亿家产都是你哒!”,所以大富翁老王成功给了每个私生子承诺,有天老大找王老板,(哈哈哈大家还记得老大是做什么的吗),他来找王老板说:“爸我要出去谈个很重要的生意,现在差点东西撑场面,我想要那个劳力士手表,但是现在资金暂时周转不过来,咱家这么有钱能不能先给我十万块让我资金周转一下?”,王老板一听,蛐蛐十万,不足挂齿,真是小钱,直接打钱!于是老大成功白嫖十万并被鄙视为什么都做生意了十万块还要从家借
二宝因为还在读博也要生活费嘛,于是老王也要给二宝每个月打一百的生活费,小老三做珠宝生意难免和赌石这些沾边,但是别赌,你以为的概率其实是很多人眼里的尘埃落定,发财的机遇也并发着无望的深渊(我们只是教给人们对纸牌和赌博要有一种合理的恐惧,而我们为这种卑微的服务收取一点报酬)
果不其然,赌输且欠债,欠十个亿,于是小老三去找王老板请求帮助,王老板一听,好个败家女,钱没挣着还欠一屁股债,滚一边去,没你这样的闺女!(小老三内心OS:老登那么有钱还不给我爆金币),于是小老三退而求其次,和老爹撒娇买了点化妆品(花4900),然后四宝也要钱,五万块买学习资料(王老板回家看到自家思维尺学习机和最新款威神普肉陷入沉思),于是就这样,每个私生子都会向王老板要钱,每个子女都觉得自己老爹身价十亿,公司也是上市还在继续运行,要这点小钱毛毛雨不算什么,但事实上平均下来每个人只有2.5亿,但他们不知道彼此的存在并对自己将会有十个亿这件事深信不疑
上面的过程简化成计算机术语就是:操作系统(王老板)为每个进程(私生子)划了一份进程地址空间(十万遗产大饼)
tips:十个亿==物理内存
上面的故事中,王老板只有四个私生子,但是要知道有些土豪莫说是四个,四十个私生子都有可能啊喂!那饼画多了到底要不要管理起来呢?(月薪三千八还要搞内部竞争,上演职场甄嬛传),大富翁画了这么多饼,当然要管理起来了!
那么我们提出一个大胆的假设,假设王老板很爱装杯杯,明明自己最多只有十个亿,非说自己有一百亿,那这几个私生子会不会相信?
肯定会啊!
带入真实场景,把王老板假设成银行(一个土豪提款机罢了),往里面存钱,我们会在乎最初的100元去向吗?不会啊,我们会坚定地相信,这转化成了一串数字,我们最后能凭借这串数据将钱取出来就好了,银行总不可能没钱吧?
但是故事还是要继续的啦,我们都知道进程地址空间有很多,回顾下:
当我们有了对应的区域后就可以进行对应区域的划分啦!地址空间就是一个对应的范围,可以通过页表将虚拟地址映射到物理内存,这就是地址空间
为什么要有地址空间?
故事继续,王老板为什么要给他的儿子画那么多大饼呢?(地址空间为何存在?)
先说说没有的情况吧,如果没有地址空间,进程的代码和数据就要直接加载到物理内存里(需要直接找到代码和数据),就需要把进程的PCB记下来,如果有很多进程同时都在物理内存中加载,那就需要每个进程把自己在物理内存中对应的位置记下来,如果直接使用的就是物理地址,那对多方的负担都比较大(很容易出现访问越界,修改到其他进程的代码和数据...具体可以参考你刚接触C的时候怎么使用指针来参考这错误的触发概率),而且如果用物理内存直接转移进程的代码和数据,很可能出现皮片儿的(东一块,西一块)情况,那就都要记录下来,且多个进程混在一起非常不方便做统一管理
看图,自己PPT搞出来的,有点小丑别介意:
实际的物理内存中,代码区,数据区,堆区,栈区,共享区,命令行参数和环境变量是乱序的,但在地址空间的这个虚拟概念这,这些永远有序
所以地址空间存在的第一个意义:让无序变成有序,让进程以统一的视角看待物理内存及自己运行的各个区域
假设正文代码已经被执行了1M,还有1M需要执行,但操作系统发现物理内存整体空间不够了,,要把闲置空间释放掉,那检测到了前1M的代码已经执行完,可以被释放掉了, 如果先预设申请堆空间就不急着申请物理内存(不急着使用,要了再给,得到了立马用),这样内存的使用率就比较高
第二个意义就也很显著啦:进程管理模块和内存管理模块进行解耦(延后给和直接给能一样嘛?!)
对进程而言,假设我们今天要访问代码区一部分,或者访问的区域在地址空间本就不存在或者访问越界,查页表也就不存在对应的地址,不存在对应关系,操作系统会直接拦截本次访问请求
第三个意义:拦截非法请求,避免向物理内存中写入废旧数据,对物理内存进行保护
地址空间不会整体使用,一般只会使用有一部分
如何进一步理解页表和写时拷贝?
CPU内有一个简单的工作单元:MMU
当然CPU内还有很多寄存器,页表的地址就是通过CR3这个寄存器保存在CPU内的,MMU(内存管理单元)结合虚拟地址和页表快速的把虚拟地址转化成物理地址
页表还是很复杂滴,比如进程挂起到外设,页表就会帮助标记
来看段代码:
#include<stdio.h>
int main()
{
char* str = "hello world";
*str = 'H';
return 0;
}
这段代码能成功运行么?
当然是不能了!编都编不过,进行写入的时候会崩溃(如果cpp的话则是直接出现语法错误,因为cpp类型检查较为严格)
那为什么这段代码会崩溃呢?
因为字符串常量区不能被修改对吧,那为什么字符串常量区不能被修改?
因为每一个虚拟的区域都通过页表进行映射,而页表在进行映射时存在权限管理(rwx权限,没r权限写个蛋)
转化都转不了,,,还修改什么(所以一个进程崩溃不会影响其他进程,因为没办法进行写入)
在操作系统层面上如何支持我们进行写时拷贝呢?
父进程对于全局变量的权限是rw,当父进程创建紫禁城,为了支持写时拷贝,操作系统会修改页表中父子进程的权限(rw --> r),如果有人尝试写入,操作系统会直接识别到错误:
1.判断错因是否是数据不在物理内存(缺页中断)
2.判断数据是否需要写时拷贝(若是则进行写时拷贝)
3.如果都不是,进行异常处理
如何理解虚拟地址?
我们该怎样理解虚拟地址呢?
尝试联想一下,在最开始的时候,地址空间、页表里的数据从哪来?
程序里本身就有地址(在编译成二进制程序)
怎么看?转到反汇编啊,看我干嘛:
objdump -S file //后面跟的是可执行程序哦
程序里面的地址就是虚拟地址(逻辑地址)
所以加载的时候是直接从程序读的(页表,地址空间,虚拟地址,,,),这种编译方式叫平坦模式
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
while (1)
{
printf("I am a child,%d,%p\n", id, &id);
sleep(1);
}
}
else if (id > 0)
{
while (1)
{
printf("I am father,%d,%p\n", id, &id);
sleep(1);
}
}
return 0;
}
为什么id可以 = 0 也可以 > 0 呢?
之前不理解的现在该理解啦
当我们fork的时候不论是父进程还是紫禁城都会return,而return的本质就是对id进行写入,当fork()return的时候创建紫禁城,发生写时拷贝(虚拟地址相同,物理内容不一样)
要不要和我一起看重庆森林?