【Linux】进程概念(下)
进程概念
- 一、环境变量
- 1. 命令行参数
- 2. 常见的环境变量
- (1)PATH
- (2)PWD
- (3)HOME
- (4)env 查看所有的环境变量
- 3. 获取环境变量
- (1)通过代码获取环境变量
- (2)通过命令行参数获取
- (3)通过 extern char** environ 获取
- 4. 本地变量和环境变量
- 5. Linux 命令的分类
- 6. 环境变量相关的命令
- 二、程序地址空间
- 1. 程序地址空间分布
- 2. 进程地址空间
- 3. 地址空间和区域划分
- (1)地址空间
- (2)区域划分
- (3)管理地址空间
- 4. 为什么要有地址空间
- 5. 空间分配扩展
一、环境变量
1. 命令行参数
什么是命令行参数呢?首先我们得先知道,主函数是可以传参的!而这个传给主函数的参数就是命令行参数。
我们可以创建一个主函数,并在主函数中接收命令行参数,把命令行参数打印出来观察一下,如下:
图中的 argc 和 argv 就是接收命令行参数的形参,我们观察一下打印出来的数据:
我们看到,打印出来的数据竟然是我们可执行程序的名字,那么 0 又代表什么呢?我们尝试在可执行程序后面加多一些数据,如下:
最后我们得出结论:我们输入的指令中,以空格作为分隔符,被分割成了4个子串,而这4个子串最终会传入主函数中被接收,argc 就是命令行参数的个数,argv 就是被分割的子串;其中 0 号子串一定是我们的可执行程序,后面带的可以说是选项,为什么说是选项呢,因为我们可以通过不同的子串执行不同的代码,例如:
1 #include <stdio.h>2 #include <string.h>3 4 int main(int argc, char* argv[])5 {6 if(argc != 2)7 {8 printf("error!\n");9 return 1; 10 }11 if(strcmp(argv[1], "-a") == 0)12 printf("aaa\n");13 else if(strcmp(argv[1], "-b") == 0)14 printf("bbb\n");15 // ........16 17 return 0;18 }
如以上代码,我们可以通过命令行参数,支持各种指令级别的命令行选项的设置!
2. 常见的环境变量
- 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数;
- 例如我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找;
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性;
常见的环境变量有:
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash
(1)PATH
什么是 PATH 呢?我们平时在 Linux 中写一份代码,想要运行起来首先需要找到这个可执行程序的路径,所以如果这个可执行程序在当前路径下,就需要在前面加上 ./ ,例如下图:
那么通过上面命令行参数的学习,我们知道,Linux 中的指令也是可执行程序,那么为什么它们的指令不用加 ./ 就能正常运行呢?这就和我们的环境变量 PATH 有关了,PATH 是系统默认的搜索路径,只要将我们程序的路径添加到 PATH 中,我们的程序也不需要加 ./ 就能跑啦!
其中我们可以使用指令 which + 指令
可以查看这个指令所在的路径,例如我们需要查看 ll
指令所处的路径:
接下来我们查看一下 PATH 中的内容,使用指令 echo $PATH
查看;其中 $ 相当于解引用查看的含义,这是 shell 的语法;如下:
如果我们想将我们的当前路径添加到 PATH 中呢?我们首先使用 pwd
查看我们当前的路径:
然后我们将我们的路径复制,然后放到以下的位置,可以使用如下指令:
PATH=$PATH:/home/lmy/.mygitee/Linux_Study/study9
此时我们的路径也就添加到 PATH 中了,我们可以查看一下:
如上图,我们确实将路径添加到了 PATH 中;那么我们现在执行当前路径下的可执行程序时就不用在前面加上 ./ 了;如下图:
如果我们想删除当前路径呢?也很简单,只需要将不需要的部分去掉就行了,假设我们将当前路径在 PATH 中去掉,可以复制除了当前路径的其它路径,然后执行以下指令:
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/lmy/.local/bin:/home/lmy/bin
就把当前路径去掉了;注意 PATH 中是以 : 作为分隔符将各个不同的路径分割开来的。
上面是一种方法,还有另外一种方法,就是将我们的可执行程序直接使用 cp
指令拷贝到 PATH 中的某个路径的文件下也可实现,注意这里拷贝需要提升权限,使用 sudo
即可,这里将不再演示。这种方法就叫做程序安装,本质就是将可执行程序拷贝到系统可以找得到的路径下;程序卸载即是将这个可执行程序从路径下删除即可。
如果我们把 PATH 设置为空串,看看会发生什么情况,如下图:
我们可以看到,大部分的指令都不能用了;此时我们可以重新启动 Xshell 即可恢复。所以我们得出,默认更改环境变量,只限于本次登录,重新登录环境变量自动被恢复,这是为什么呢?我们后面再讲。
(2)PWD
我们在使用 pwd
指令的时候,系统怎么知道我们所在的路径在哪里呢?原因是因为 Linux 中也会存在一个环境变量:PWD.
PWD 是给我们当前对应的 bash 内置的一个专门用来记录我们当前所处的路径的一个环境变量。
例如我们先查看一下我们的当前路径:
我们 cd 到上一层,再查看 PWD 环境变量:
如上图,PWD 环境变量也会跟着变化;所以 pwd 的指令其实就是读取 PWD 环境变量中的内容打印出来即可。
(3)HOME
当我们默认登录 Xshell 时,我们所处的路径/目录是 /home/xxx
,例如:
但是如果我们是以 root 用户登录,我们所处的默认目录将会是 /root
;这是为什么呢?
这是因为当我们登录的时候,首先我们需要输入用户名和密码,等待系统认证;认证完毕后,会形成环境变量,此时肯定不止一个环境变量(PATH, PWD, HOME 等);然后会根据用户名初始化 HOME,即 HOME = /root,HOME = /home/xxx;最后只需要执行 cd $HOME 即可。
(4)env 查看所有的环境变量
我们可以使用 env
指令查看所有的环境变量,Linux 中的环境变量非常多,大家可以自行查看。而每一个环境变量都有它自己的特殊用途,用来完成特定的系统功能。
3. 获取环境变量
(1)通过代码获取环境变量
我们有一个接口可以通过代码直接获取环境变量,就是 getenv
,我们可以通过 man 指令查看一下:
我们可以使用一下 getenv
查看一下 PATH 环境变量,如下代码:
1 #include <stdio.h>2 #include <stdlib.h>3 4 int main()5 {6 printf("%s\n", getenv("PATH"));7 return 0; 8 }
执行结果如下:
我们上面所说每一个环境变量都有它自己的特殊用途,如何体现出来呢?下面我们结合 getenv
简单使用一下,如下代码:
#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){char* s = getenv("USER");if(strcmp(s, "root") != 0){printf("%s 是非法用户!\n", s);return 1;}printf("Hello!\n");printf("Hello!\n");printf("Hello!\n");return 0;}
我们使用环境变量 USER 去判断当前用户是否是 root,我们只让root 执行相应的代码;如下运行结果:
当我们是 root 用户:
当我们是普通用户:
(2)通过命令行参数获取
1.通过我们上面所学的命令参数,我们知道命令行参数可以有两个,但是其实还可以有第三个命令行参数,那就是 env!
env 其实就是一张环境变量表,系统启动我们的程序时,可以选择给我们的进程(main)提供两张表:1. 命令行参数表 2. 环境变量表;其中这个第三个参数 env 就是环境变量表,env 是一个指针,指向的是一个函数指针数组,可以参考下图理解:
其中右边一大串的是环境变量;通常 env 的最后一个元素是指向空的;
我们也可以通过代码打印出来观察;我们可以打印当前程序运行的 pid,如下段代码:
1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <unistd.h>5 #include <sys/types.h>6 7 int main(int argc, char* argv[], char* env[])8 {9 int i = 0;10 for(; env[i]; i++)11 {12 printf("pid: %d, env[%d]: %s\n", getpid(), i, env[i]); 13 }14 15 return 0;16 }
运行的结果大家可以自行运行后查看;那么我们为什么要打印出它的 pid 呢?那么我们不妨想想,这个进程的 env 是谁传给我们的呢?就是 bash!我们命令行启动的进程都是 shell(bash) 的子进程,子进程的命令行参数和环境变量,都是父进程 bash 传递的!
那么问题又来了,父进程 bash 的环境变量又从哪里来呢?其实环境变量是以脚本配置文件的形式存在的;我们也可以找到这个文件,例如我们回到我们的家目录下,有一个隐藏文件叫做 .bash_profile
,如下图:
每一次登录的时候,我们的 bash 进程都会读取 .bash_profile
这个文件中的内容,为我们 bash 进程形成一张新的环境变量表信息!这也就能解释了我们上面的一个问题:我们将 PATH 的路径改成空,重启 Xshell 之后就会恢复正常了。
2.另一个问题,我们也可以创建属于自己的环境变量,如下图,直接在命令行中输入即可:
此时我们在环境变量表中查看一下:
发现并没有导入到我们的环境变量表中;或许我们可以直接在我们的可执行程序中查找:
也一样没有,因为我们还没有导入到环境变量表中,我们需要把它导入到环境变量表中,使用命令 export MYENV_LMY
即可,如下:
我们也可以直接在创建环境变量时导入,使用指令
export 环境变量名称=内容
,如下图:
我们可以看到它们两个都出现在了环境变量表中;如果此时我们退出再重新登录,它们两个还存在吗?答案是不存在了!因为以上这两个环境变量并没有写入配置文件中!改变的只是当前 bash 内部的环境变量表,当我们退出重新登录后,bash 会重新读取 .bash_profile
文件从而重新获取环境变量表!上面这种只在 bash 进程内部有效的叫做本地变量!
所以我们想要我们自己的环境变量永远生效,我们需要把它添加到 .bash_profile
配置文件中,如下图所示:
此时我们保存退出,重新登陆后,就可以查到我们对应的环境变量:
这种通过子进程继承的方式继承环境变量表的,就叫做环境变量!
我们知道了命令行参数表和环境变量表都可以通过继承给子进程的方式让子进程继承,这就表明系统环境变量具有全局属性!
(3)通过 extern char** environ 获取
第二种方式获取环境变量需要我们写命令行参数传入,也有一种方式不需要写命令行参数就可以获取环境变量表,就是通过系统给我们提供的 environ 指针;这个指针我们使用的时候需要使用 extern 声明一下,因为不是我们自己定义的,是系统给我们提供的,其实 environ 就是指向 env 的指针,它们的关系如下图:
我们也可以通过 environ 打印出环境变量表,如下段代码:
1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <unistd.h>5 #include <sys/types.h>6 7 int main()8 {9 extern char** environ;10 int i = 0;11 for(; environ[i]; i++)12 {13 printf("pid: %d, environ[%d]: %s\n", getpid(), i, environ[i]); 14 }15 16 return 0;17 }
运行的结果大家可以自行尝试去观察,结果和第二种方法是一样的。
4. 本地变量和环境变量
上面我们也简单地介绍了一下本地变量和环境变量,接下来我们进一步分析它们之间的区别:
- 本地变量:本地变量只在 bash 进程内部有效,不会被子进程继承下去。
- 环境变量:环境变量通过让所有的子进程继承的方式,实现自身的全局性!环境变量是天然让所有子进程继承下去的!
5. Linux 命令的分类
我们首先回忆起当我们把 PATH 设为空时,是不是有一些命令能跑,有一些命令不能跑呢?我们再次尝试一下,如下图:
我们可以看到,确实有些命令是跑不了了,但为什么诸如 pwd、echo 这样的命令还能跑呢?原因是因为 Linux 中的命令可分为两类:
- 常规命令
常规命令是 shell 命令行解释器进行 fork 让子进程执行的。
- 内建命令
内建命令是 shell 命令行的一个函数,建立在 shell 的内部,所以可以直接读取 shell 内部定义的本地变量!
6. 环境变量相关的命令
- echo
echo 是显示某个环境变量值;这个我们在上面也用过,例如需要查看 PATH 的环境变量值:
- export
export 是将本地变量导入到环境变量表中,即设置一个新的环境变量,我们在上面也使用过。例如我们先设置一个本地变量,此时是可以用 echo 查看的,因为 echo 是内建命令;但是在环境变量 env 中没有这个本地变量,如下图:
我们可以将该本地变量导入环境变量中,就能在 env 中查看到:
- unset
unset 是清除环境变量,删除某个环境变量。例如我们要删除上面我们自己定义的环境变量 MYENV,如下:
此时本地变量和环境变量中都没有了。
- env
env 是显示所有环境变量,这个我们上面也介绍过。
- set
set 是显示本地定义的 shell 变量和环境变量。例如我们定义一个本地变量 local_env 和一个环境变量 myenv,此时我们在环境变量中查看的时候只有 myenv,如下图:
但是我们使用 set 查看的时候两个都能看见,如下图:
二、程序地址空间
1. 程序地址空间分布
我们以前大概了解过在 C/C++ 中诸如下图的空间分布图:
其中堆和栈是相对而生的,堆区往上增长,栈区往下增长,其实堆区和栈区中间还有其它空间,我们后面在学;静态数据变量也存在于数据段中,其实静态变量会被编译器修饰成全局变量,所以它会被放到数据段中。
如何证明我们程序的地址是按照以上的空间分布呢?下面我们使用代码验证一下,如下段代码:
1 #include <stdio.h>2 #include <stdlib.h>3 4 int init_val = 1;5 int uninit_val;6 7 int main()8 {9 char* str = "Hello";10 char* heap1 = (char*)malloc(10);11 char* heap2 = (char*)malloc(10);12 char* heap3 = (char*)malloc(10);13 char* heap4 = (char*)malloc(10);14 static int static_val1 = 2;15 static int static_val2;16 17 printf("栈区地址1:%p\n", &heap1);18 printf("栈区地址2:%p\n", &heap2); 19 printf("栈区地址3:%p\n", &heap3);20 printf("栈区地址4:%p\n", &heap4);21 22 printf("堆区地址4:%p\n", heap4);23 printf("堆区地址3:%p\n", heap3);24 printf("堆区地址2:%p\n", heap2); 25 printf("堆区地址1:%p\n", heap1);26 27 printf("未初始化静态变量地址:%p\n", &static_val2);28 printf("未初始化全局数据区:%p\n", &uninit_val);29 printf("已初始化静态变量地址:%p\n", &static_val1);30 printf("已初始化全局数据区:%p\n", &init_val);31 printf("字符常量区地址:%p\n", str);32 printf("代码区地址:%p\n", main);33 34 return 0;35 }
运行结果如下,结果确实是这样的:
我们单独拿栈区出来分析,我们在局部创建的数组、结构体都是在栈区中向下增长开辟空间的,假设我们有一个 a[10] 的数组,一个 struct A = {x, y, z} 结构体,那么 a[0] 和 a[9] 的地址谁大呢?A obj 中,&obj.x 和 &obj.z 谁的地址大呢?下面我们写个程序验证一下,如下代码:
#include <stdio.h>#include <stdlib.h>typedef struct A {int x;int y;int z;}A;int main(){int a[10];A obj;printf("%p\n", &a[0]);printf("%p\n", &a[9]);printf("%p\n", &obj.x);printf("%p\n", &obj.y);printf("%p\n", &obj.z);return 0;}
结果如下:
所以我们得出结论,栈区是往下开辟申请空间,但是使用的时候是局部往上使用的,可以用下图来概括:
如上图,如果我们在栈上定义了一个变量 int b,那么我们知道 int 是占四个字节的,而我们上面的空间中每个空间占一个字节,那么我们 &b 拿的是哪个地址呢?其实是拿最低的地址,然后是通过起始地址 + 偏移量的方式进行访问。
其实除了上面空间分布中的区域外,还有一些我们还没学,所以我们以后再介绍,但是在栈区上面有两个是我们刚学习的区域,就是命令行参数和环境变量。
所以我们上面学的空间分布,它到底是什么呢?它是内存吗?我们下面开始学习。
2. 进程地址空间
首先我们回顾一下我们以前学习 fork 的时候,父子进程之间是怎么运行的,我们这时候想起来还有一个问题还没解决,那就是当子进程修改代码时,会发生写时拷贝,但是一个变量不同的值为什么会有相同的地址呢?这就是我们接下来需要学习的;首先我们把代码再敲出来,如下段代码:
1 #include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 5 int g_val = 100;6 7 int main()8 {9 pid_t id = fork();10 if(id == 0)11 {12 //child13 int cnt = 5;14 while(1)15 {16 printf("i am child, g_val: %d, &g_val=%p\n", g_val, &g_val);17 sleep(1);18 if(cnt == 0)19 {20 g_val=200;21 printf("child change g_val: 100->200\n");22 }23 cnt--;24 }25 }26 else27 { 28 //father29 while(1)30 {31 printf("i am father, g_val: %d, &g_val=%p\n", g_val, &g_val);32 sleep(1);33 }34 }35 sleep(100);36 return 0;37 }
我们定义了一个全局变量 g_val,当子进程对它进行修改时,父进程还是原来的 g_val,因为父子进程之间的数据互不影响,具有独立性;我们观察运行的结果会发现,它们的地址竟然是一样的!如下图:
那么为什么对同一个地址进行读取会得出不同的内容呢?所以通过上图我们得出的结论是,我们在C/C++平常所见到的地址绝对不是物理地址,其实都是虚拟地址/线性地址!
其实,我们上面所学的空间分布的那张图,就是进程地址空间,里面的地址全都是虚拟地址!如下图:
但是我们的进程需要被cpu调度,进程中的数据要被cpu读取识别,就必须加载到内存中,即物理内存中。那么这个过程到底是怎样的呢?其实我们上面的代码所打印出来的地址,全部都是它的进程地址空间的地址,也就是虚拟地址,而这个可执行程序是 bash 的子进程啊!而这个父进程在代码中又创建自己的子进程,也有它自己的进程地址空间,所以我们认为,每一个程序运行之后,都会有一个进程地址空间的存在!
那么我们现在已经有三个重要的角色了,分别是 pcb(task_struct)、进程地址空间、物理内存,其中 task_struct 中肯定有一些字段是指向属于自己的进程地址空间的;而物理内存中也一定需要存储该进程的一些数据的,在物理内存中存储这个数据的地址才叫做物理地址,如下面的 0x11111111;所以它们现在的关系如下图:
那么进程地址空间和物理内存之间是如何联系起来的呢?在操作系统中,为了让进程找到物理内存中自己的数据,会为每个进程维护一张映射表,它要为进程地址空间和物理内存之间构建一种映射关系,而这个表叫做页表。它的左侧放的是虚拟地址,右侧是数据在物理内存中的地址,它们两者之间是一种映射关系。可以结合下图理解:
如上就是页表的简单结构,页表中还有其它字段我们后面再介绍;其中进程通过进程地址空间中的地址可以查找页表找到对应在物理内存中的物理地址。
我们从上面知道,进程在运行之后都会有一个进程地址空间的存在,而在系统层面它们都要有自己的页表映射结构;因为我们的上面的父进程也会有子进程,所以操作系统会将父进程的 pcb 拷贝一份给子进程,所以子进程也会有自己的进程地址空间和页表,都是从父进程那获取的,它们俩互相独立,互不影响,例如下图:
当我们的子进程对数据进行修改时,通过页表找到相应的数据,但是操作系统发现父进程正在使用这个数据,所以子进程不能直接对该数据进行修改,因为它们具有独立性,所以操作系统会将物理内存中的值进行写时拷贝,生成一块新的物理地址,然后将子进程想要修改的数据覆盖之间的数据即可;这时候子进程中的物理地址就发生变化,结合下图理解:
所以我们就理解了为什么它们会有相同的地址而值却不一样,因为它们两个进程都有之间独立的进程地址空间和页表,写时拷贝发生在物理内存中,改变的也是物理地址,虚拟地址并没有改变,所以相同的虚拟地址并不互相影响。
3. 地址空间和区域划分
(1)地址空间
我们从上面知道,物理内存是操作系统直接管理的。当我们的进程需要使用空间时,操作系统会给进程一个虚拟地址空间,这个虚拟地址空间就是地址空间。当我们的进程多起来的时候,既然操作系统要把进程管理起来,那么操作系统也要把地址空间管理起来,因为如果不管理起来就会乱套了。那么如何管理起来呢?我们下面再说。
(2)区域划分
所谓的区域划分,就是用 start 和 end 这样的 int 或者 long long 变量来区分一个线性空间的开始和结束;空间大小可以调整,调整时只需要将 start 和 end 的值进行扩大或缩小即可;不要只看到空间的范围,空间范围内的地址我们都可以使用。
(3)管理地址空间
地址空间要被操作系统管理起来,因为每一个进程都有一个地址空间,系统中一定要把地址空间做管理。如何管理呢?我们以前学过,先描述,在组织,所以地址空间最终一定是一个内核的数据结构对象,就是一个内核的结构体!
我们发现,进程地址空间中的内容全都是区域划分!所以我们就可以进行区域划分进行管理!
在 Linux 中,这个进程/虚拟地址空间叫做 struct mm_struct
,其中它大概就长下面这个样子:
struct mm_struct{long code_start;long code_end;long data_start;long data_end;long heap_start;long heap_end;long stack_start;long stack_end;......}
其中 struct mm_struct
这个结构体是由一个叫做 struct mm_struct* mm
的指针指向的,而这个指针存在于 task_struct
中,所以每个进程被创建的时候都会创建一个 struct mm_struct* mm
,也就有了一个进程地址空间;所以我们的进程地址空间为什么不是内存呢,原因就是因为它只是一个内核数据结构。
4. 为什么要有地址空间
- 让进程以统一的视角看待内存,所以任意一个进程,都可以通过地址空间 + 页表将乱序的内存数据变成有序,分门别类的规划好!
为什么这么说呢?我们思考一下,当我们的程序加载到内存中时,它是有顺序地加载的吗?并不是的,程序加载到内存是乱序的,但是通过地址空间和页表就可以做到将乱序的内存数据变为有序,让进程认为这些数据就是按照它的方式进行分类的!
- 存在虚拟地址空间,可以有效的进行进程访问内存的安全检查!
首先我们再要了解一下,在页表中,还有一列叫做访问权限字段的东西,它的结构就如下:
访问权限字段有什么用呢?它会在访问物理内存的时候检查对应的权限是否满足,或者检查这个地址是否合法,如果权限不满足或者地址非法,就会在页表中直接拦截,就不允许访问物理内存中的地址。
例如以下代码:
int main(){char* s = "hello";*s = 'h';return 0;}
上面这段代码是不能正常运行的,因为我们知道 “hello” 是常量,不可被修改,那么这是为什么呢?这时候我们就知道了,s 的地址是在进程地址空间中的字符常量区的,而通过字符常量区映射的页表中的访问权限字段是只读的,即 r,所以当我们需要写入修改时,在页表就直接被拦截了。
另外,进程进行各种转换、各种访问,这个进程一定是正在运行!所以在 cpu 中有一个寄存器叫做 CR3,会存放页表的地址,这个页表的地址是物理地址。因为这个进程正在cpu 内运行,所以CR3中的内容本质是在该进程的硬件上下文内容当中!所以当该进程切换出去的时候,本质这个CR3寄存器的内容会保存到当前进程的上下文里。所以每个进程都有自己的页表。
- 将进程管理和内存管理进行解耦!
在操作系统层面,当我们在磁盘中有程序需要加载到内存时,首先需要在内存中申请内存,然后填充内容和页表,然后建立映射关系;其中页表中还有一列内容,是专门判断某个地址是否在内存中有分配内存和是否有内容的,里面可能是两个比特位,例如 1/0 表示是/否分配有内存,1/0 表示 是否在内存中有内容;例如下图:
当我们的进程拿着一个虚拟地址来找物理地址的时候,假设这时候内存还没有给它分配物理地址,此时操作系统就会把该进程暂停,并从磁盘中加载相对应的程序到内存中,然后再填充页表,建立映射关系;这个过程叫做缺页中断!这个概念我们以后还会介绍,现在先了解一下。
当我们上面在内存中申请内存,然后填充内容和页表,然后建立映射关系,这一套流程叫做内存管理;而这个过程进程是不需要理会的,而且进程也不知道这个过程;进程需要做的只是该调度的就调度,该访问的就访问,这一套叫做进程管理;进程管理不关心内存管理,所以进程管理和内存管理因为有了地址空间和页表的存在实现了操作系统层面上的模块的解耦!
最后,通过页表,还可以让进程映射到不同的物理内存中,从而实现进程的独立性!
5. 空间分配扩展
我们当前所能用的地址空间都是在用户空间中使用的,在地址空间中,还有一部分空间是要留给操作系统自身用的;其中我们用户用到的空间叫做用户空间,有 3GB;操作系统自身用的空间叫做内核空间,有 1GB. 如下图所示:
在 struct mm_struct
结构体中,其实还有一个指针,它指向的是一个 vm_area_struct
的结构体,它有什么用呢?在我们的地址空间中,被划分成了很多区域,但是总有一些区域还没有被使用的,所以当我们想要划分自己的区域的时候,可以申请一个 vm_area_struct
的对象,这个对象中有 start 、end 两个值,分别是空间的开始和结束,还有一个 next 的指针指向下一个结构体的对象,它们之间构成一个链表的结构,所以有了这样一个结构体,我们就能根据自己的需求划分属于自己的空间了!可以根据下图进行理解:
其中我们的 mm_struct
结构体其实真正叫做内存描述符;而 vm_area_struct
叫做线性空间;这两个概念合起来才叫做地址空间!但是由于方便,我们认为 mm_struct
才是地址空间!
相关文章:

【Linux】进程概念(下)
进程概念 一、环境变量1. 命令行参数2. 常见的环境变量(1)PATH(2)PWD(3)HOME(4)env 查看所有的环境变量 3. 获取环境变量(1)通过代码获取环境变量(…...

基于Spring Boot的本科生就业质量设计与实现
摘 要 信息化爆炸的时代,互联网技术的指数型的增长,信息化程度的不断普及,社会节奏在加快,每天都有大量的信息扑面而来,人们正处于数字信息化世界。数字化的互联网具有便捷性,传递快,效率高&am…...
238. 除自身以外数组的乘积 --力扣 --JAVA
题目 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请不要使用除法,且在 O(n) 时间复…...
如何判断一个类是线程安全的
线程安全 一个类或者程序提供的接口,多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是不必考虑同步问题。 或者说一段代码可能会被多个线程同时执行,如果每次运行的结果和单线程执行的结果是一样的,并且其他变量的…...
MyBatis的各种查询功能
文章目录 情景查询一个实体类对象查询一个List集合查询单个数据查询一条数据为map集合查询多条数据为map集合方法一方法二 情景 如果查询出的数据只有一条,可以通过 实体类对象接收List集合接收Map集合接收,结果{password123456, sex男, id1, age23, us…...

【Tomcat】如何在idea上部署一个maven项目?
目录 1.创建项目 2.引入依赖 3.创建目录 4.编写代码 5.打包程序 6.部署项目 7.验证程序 什么是Tomcat和Servlet? 以idea2019为例: 1.创建项目 1.1 首先创建maven项目 1.2 项目名称 2.引入依赖 2.1 网址输入mvnrepository.com进入maven中央仓库->地址…...
Three.js 材质的 blending
Three.js 材质的 blending // blending modes export type Blending | typeof NoBlending| typeof NormalBlending| typeof AdditiveBlending| typeof SubtractiveBlending| typeof MultiplyBlending| typeof CustomBlending;// custom blending destination factors export t…...
关于pcl 给new出的数据赋值报错问题
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>); for (size_t i 0; i < cloud->points.size (); i) //填充数据 { cloud->points[i].x 1024 * rand () / (RAND_MAX 1.0f); cloud->points[i].y 1024…...

window11 更改 vscode 插件目录,释放C盘内存
由于经常使用vscode开发会安装一些代码提示插件,然后C盘内容会逐渐缩小,最终排查定位到vscode。这个吃内存不眨眼的家伙。 建议:不要把插件目录和vscode安装目录放在同一个位置,不然这样vscode更新后,插件也会消失。 v…...

【PyQt学习篇 · ⑥】:QWidget - 事件
文章目录 事件消息显示和关闭事件移动事件调整大小事件鼠标事件进入和离开事件鼠标按下和释放事件鼠标双击事件鼠标按下移动事件 键盘事件焦点事件拖拽事件绘制事件改变事件右键菜单输入法 事件转发机制案例一案例二案例三 事件消息 显示和关闭事件 showEvent(QShowEvent)方法…...
Vue、jquery和angular之间区别
aVue、jquery、angular之间区别 angular与jquery区别angular和Vue angular与jquery区别 三个版本的输入数据绑定,都是单页面应用。 Angular <body ng-app><input type"text" ng-model"name"><p>{{name}}</p></body…...
MATLAB算法实战应用案例精讲-【图像处理】机器视觉(基础篇)(六)
目录 前言 几个高频面试题目 工业相机与普通相机的差别 一、 工业相机与普通相机的区别...
硬件知识积累 RS232 接口
1. RS232 是什么 RS-232标准接口(又称EIA RS-232)是常用的串行通信接口标准之一,它是由美国电子工业协会(Electronic Industry Association,EIA)联合贝尔系统公司、调制解调器厂家及计算机终端生产厂家于1970年共同制定࿰…...

机器人入门(四)—— 创建你的第一个虚拟小车
机器人入门(四)—— 创建你的第一个虚拟小车 一、小车建立过程1.1 dd_robot.urdf —— 建立身体1.2 dd_robot2.urdf —— 添加轮子1.3 dd_robot3.urdf —— 添加万向轮1.4 dd_robot4.urdf —— 添加颜色1.5 dd_robot5.urdf —— 添加碰撞检测(Collision …...

部署K8S
防火强的初始化: [rootk8s-node-12 ~]# systemctl stop firewalld NetworkManager [rootk8s-node-12 ~]# systemctl disable firewalld NetworkManager Removed symlink /etc/systemd/system/multi-user.target.wants/NetworkManager.service. Removed symlink /et…...

[NSSCTF 2nd] web刷题记录
文章目录 php签到MyBox非预期解预期解 php签到 源代码 <?phpfunction waf($filename){$black_list array("ph", "htaccess", "ini");$ext pathinfo($filename, PATHINFO_EXTENSION);foreach ($black_list as $value) {if (stristr($ext, …...
MyBatis获取参数值的两种方式(重点)
文章目录 简介单个字面量类型的参数多个字面量类型的参数map集合类型的参数实体类类型的参数使用Param标识参数总结 简介 MyBatis获取参数值的两种方式:${}和#{}${}的本质就是字符串拼接,#{}的本质就是占位符赋值${}使用字符串拼接的方式拼接sql&#x…...

Cesium弹窗可随地图移动
目录 项目地址实现效果实现方法 项目地址 https://github.com/zhengjie9510/webgis-demo 实现效果 实现方法 handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) handler.setInputAction((click) > {if (listener) {listener()listener undefinedthis.v…...
MySQL WITH AS及递归查询
MySQL WITH AS及递归查询 WITH AS 官网:WITH 是 SQL 中的一个关键字,用于创建临时表达式(也称为 Common Table Expression,CTE),它允许你在一个查询中临时定义一个表达式,然后在后续的查询中引…...

Harbor私有镜像仓库搭建
本文基于:https://zhuanlan.zhihu.com/p/143779176 1.环境准备 IP:192.168.10.136/24 操作系统:centos7 2.安装Docker、Docker-compose 2.1安装Docker-CE $ wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.re…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
安卓基础(Java 和 Gradle 版本)
1. 设置项目的 JDK 版本 方法1:通过 Project Structure File → Project Structure... (或按 CtrlAltShiftS) 左侧选择 SDK Location 在 Gradle Settings 部分,设置 Gradle JDK 方法2:通过 Settings File → Settings... (或 CtrlAltS)…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

高分辨率图像合成归一化流扩展
大家读完觉得有帮助记得关注和点赞!!! 1 摘要 我们提出了STARFlow,一种基于归一化流的可扩展生成模型,它在高分辨率图像合成方面取得了强大的性能。STARFlow的主要构建块是Transformer自回归流(TARFlow&am…...