进程概念[下]
进程优先级 | 环境变量 | main函数参数 | 环境变量通常具有全局属性 | 程序地址空间&进程地址空间
一、 进程优先级
0x01 什么叫进程优先级
进程访问某种资源的先后顺序(方式:排队)。
0x02 为什么要有进程优先级
因为资源不足,所以优先权高的进程有优先执行权利。
0x03 查看更加详细的进程信息
①运行代码
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(1)
{
cout << "PID:" << getpid() << " " << "PPID:" << getppid() << endl;
sleep(1);
}
return 0;
}
②ps -al 查看更加详细的进程信息
③提示
UID:代表执行者的身份 ;PRI:代表这个进程可被执行的优先级,其值越小越早被执行;NI :代表这个进程的nice值,是优先级的一个修正数据,取值范围是-20至19,一共40个级别。
0x04 调整优先级
1)运行程序;
2)输入top命令,输入r ,输入进程号 (优先级范围 [60,99]);
3)输入nice值 (nice值范围[-20,19])。
如果我们再进行一次调整会是什么样的呢?
我们知道PRI(new)=PRI(old)+nice,但是为什么不是在90上再加5呢?
因为旧的优先级默认为80。
0x05 nice值为何要是一个相对较小的范围呢?
因为若不加限制,那么将自己的进程的优先级调整的非常高,别人的优先级调整的非常低。而优先级越高的进程,优先得到资源,常规的进程则很难享受到CPU资源。从而会导致进程饥饿问题。
0x06 小结
1)系统进程数目众多,具有竞争性,而CPU资源只有少量,甚至只有1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理分配相关资源,便具有了优先级;
2)独立性: 多进程运行期间,互不干扰;
3)并行: 多个进程在多个CPU下同时运行;
4)并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进。
二、环境变量
0x01 什么叫环境变量?
环境变量是系统内置的具有特殊用途的变量,此变量的本质是系统开辟空间,给他名字和内容。
./test,为什么用"./ " ?
因为要帮系统确认对应的程序在哪里。
那为何系统的命令不用路径呢?
因为环境变量(在操作系统中用来指定操作系统运行环境的一些参数)。
PATH 环境变量 的作用是:当你输入一个命令时,系统会按照 PATH 中列出的目录顺序,在这些目录里查找对应的可执行程序。
查看环境变量:
0x02 那么我们可以不用./,就能将程序运行起来吗?
当然可以,我们可以将路径导入到环境变量中去(只要没有改配置文件,那么重启时就会恢复默认)。
三种方法:
1)可以将程序test放入/usr/bin目录中;
2)如果想让导入的环境变量长期有效,可以在vim ~/.bash_profile中去修改;
3)export PATH=$PATH: 路径,将路径导入环境变量(建议)。
0x03 HOME(常用环境变量)
HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)![]()
![]()
0x04 SHELL (常用环境变量)
SHELL:当前Shell版本或者Shell命令,它的值通常是/bin/bash。![]()
0x05 与环境变量相关的命令
1. echo: 显示某个环境变量值 。2. export: 设置一个新的环境变量。3. env: 显示所有环境变量 。4. unset: 清除环境变量 。5. set: 显示本地定义的shell变量和环境变量。![]()
0x06 环境变量的配置文件
当前修改完环境变量后,在下次登录时,环境变量有被初始化,这是为什么?
当前修改只保存在内存中,而登录时从磁盘配置文件重新初始化。要实现永久修改,必须将更改写入相应的配置文件或注册表。
修改配置文件:vim .bash_profile


重新登陆时,修改的环境变量被添加完成:
![]()
0x07 小结
1)环境变量的本质是操作系统在内存或者磁盘文件中开辟的空间,用来保存系统相关的数据;
2)环境变量 = 变量名 + 变量内容;
3)系统上还存在一种变量,是与本次登录有关的变量,只在本次登录有效(本地变量);
4)可以将本地变量导入环境变量(重启也会释放掉这个变量)。
三、main函数参数
0x01 argc&argv
#include<stdio.h>
int main(int argc,char* argv[])
{
for(int i = 0;i < argc;i++)
{
printf("argv[%d] -> %s\n",i,argv[i]);
}
return 0;
}
[wh@bogon lesson10]$ ./test -a -b -c -d
argv[0] -> ./test
argv[1] -> -a
argv[2] -> -b
argv[3] -> -c
argv[4] -> -d

0x02 命令行参数的作用
同一个程序带入不同参数来呈现不同的表现形式 (同一程序输入不同的选项呈现不同的结果)。
下面我们通过命令函参数来做一个简易的计算器。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 int main(int argc,char* argv[])
6 {
7 if(argc != 4)
8 {
9 printf("Usage: %s -[add|sub|mul|div|a|b]\n",argv[0]);
10 return 1;
11 }
12 int a = atoi(argv[2]);
13 int b = atoi(argv[3]);
14 if(strcmp(argv[1],"add") == 0 )
15 {
16 printf("%d + %d = %d\n",a,b,a+b);
17 }
18 else if(strcmp(argv[1],"sub") == 0)
19 {
20 printf("%d - %d = %d\n",a,b,a-b);
21 }
22 else if(strcmp(argv[1],"mul") == 0)
23 {
24 printf("%d * %d = %d\n",a,b,a*b);
25 }
26 else if(strcmp(argv[1],"div") == 0)
27 {
28 printf("%d / %d = %d\n",a,b,(int)a/b);
29 }
30 else{
31 printf("reinpt");
32 }
33 }

0x03 env[]
main函数还有第三个参数char* env[ ]
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc,char* argv[],char* env[])
{
for(int i = 0;env[i]; i++)
{
printf("%d -> %s\n",i,env[i]);
}
return 0;
}

环境变量具有全局属性的方法:
Bash(父进程)进程创建子进程时,会创建两张表(命令行参数表和环境变量表),然后向main函数中传参,子进程会通过main函数的第三个参数进行接收。因此环境变量会被子进程和孙子进程继承,所以环境变量具有全局属性。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 int main(int argc,char* argv[],char* env[])
7 {
8 printf("I am a process: PID: %d, PPID: %d\n",getpid(),getppid());
9 for(int i = 0;env[i]; i++)
10 {
11 printf("%d -> %s\n",i,env[i]);
12 }
13 pid_t id = fork();
14 if (id == 0)
15 {
16 printf("I am a child process: PID: %d, PPID: %d\n",getpid(),getppid());
17 for(int i = 0;env[i]; i++)
18 {
19 printf("%d -> %s\n",i,env[i]);
20 }
21 }
22 sleep(1);
23 }

当然也可以不用main函数的第三个参数的方式,查看环境变量,也可以通过第三方变量environ来获取
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc,char* argv[])
{
extern char** environ;
for(int i = 0;environ[i]; i++)
{
printf("%d -> %s\n",i,environ[i]);
}
return 0;
}
0x04 getenv()
用来获取环境变量。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char* argv[])
{
printf("PATH: %s\n",getenv("PATH"));
return 0;
}
![]()
识别用户小游戏:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 int main(int argc,char* argv[],char* env[])
7 {
8 const char* username = getenv("USER");
9 if(strcmp(username,"wh") == 0)
10 {
11 printf("You are a user\n");
12 }
13 else
14 {
15 printf("None");
16 }
17 return 0;
18 }
![]()
0x05 environ(通过第三方变量获取环境变量)

1 #include<stdio.h>
2
3 int main()
4 {
5 extern char** environ;//声明
6 int i = 0;
7 for(;environ[i];i++)
8 {
9 printf("env[%d]: %s\n",i,environ[i]);
10 }
11 return 0;
12 }

0x06 本地变量的设置与查看

因此,为什么每次登录时默认所在路径都是在家目录下?
因为系统首先要读取家目录下的环境变量的配置文件。
五、程序地址空间&进程地址空间
0x01 进程地址空间布局
#include<stdio.h>
#include<stdlib.h>
int g_unval;
int g_val = 100;
int main()
{
const char* s = "hello world";
printf("code addr: %p\n",main);
printf("Character constant addr: %p\n",s);
printf("uninit addr: %p\n",&g_unval);
printf("init addr: %p\n",&g_val);
char* heap = (char*)malloc(10);
printf("heap addr: %p\n",heap);
printf("stack addr: %p\n",&s);
printf("stack addr: %p\n",&heap);
int a = 10;
int b = 30;
printf("stack addr: %p\n",&a);
printf("stack addr: %p\n,%b");
return 0;
}


0x02 虚拟地址
此时我们会疑惑,上面打印的地址是物理地址?
下面我们可以通过这段代码来印证:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int g_val = 100;
int main()
{
if(fork() == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child,times: %d,g_val = %d,&g_val = %p\n",cnt,g_val,&g_val);
cnt--;
sleep(1);
if(cnt == 3)
{
printf("############child change before!##############\n");
g_val = 200;
printf("############child change after!##############\n");
}
}
}
else
{
//parent
while(1)
{
printf("I am parent,g_val = %d,&g_val = %p\n",g_val,&g_val);
sleep(1);
}
}
return 0;
}

从上述结果来看,因为写时拷贝,俩个进程互不干扰,所以当子进程修改值时,父进程并没有影响,但是我们也可以发现他们的地址是完全一样的。
但是地址怎么会没有变化呢?
如果C/C++打印出来的地址是物理内存的地址,这种现象可能存在吗?
当然不可能,所以我们使用的地址,绝对不是物理地址,即不是内存地址空间,而是进程虚拟地址空间,所以说,我们之前说的程序地址空间是有些不准确的,准确的应该说是进程地址空间。我们用C/C++语言所看到的的地址,全部都是虚拟地址,物理地址用户是看不到的,由操作系统统一管理,将虚拟地址转化成物理地址。
0x03 进程虚拟地址空间
①进程&操作系统

1)操作系统 相当于 大富翁;
2)物理内存 相当于 大富翁的金库;
3)进程地址空间 相当于 大富翁画的大饼;
4)进程 相当于 大富翁的私生子。
接下来,我们就可以将其串联起来,叙述一个小故事:
背景:假如大富翁有100亿,都存在金库中,因为私生子之间互不认识,所以给私生子们都画了个相同的大饼。大富翁:你们每个人都可以使用100亿,所以私生子们就都在规划这100亿该怎么使用,而三人之间又互不影响,都认为自己独占100亿。
这将其换成进程与操作系统就更加贴切了:
操作系统掌控着所有物理内存空间,给每一个进程都画了一个进程地址空间(独占物理内存)这样的大饼,而每个进程之间又互不影响,都认为自己独占物理内存。
此时又出现了一个问题:
②每个进程都有一个地址空间,而系统中又存在大量的地址空间,那么操作系统应该怎样去管理这些地址空间呢?
先描述,再组织。
1. 先描述:用的是结构体mm_struct来描述地址空间(每个进程都认为mm_struct代表整个内存,都认为进程地址空间是按照4GB划分的);
struct mm_struct
{
unsigned int code_start;
unsigned int code_start;
.....
unsigned int stack_start;
unsigned int stack_end;
}2. 再组织:通过数据结构进行链接;
3. 进程地址空间的本质:特定的数据结构对象(将多个对象通过数据结构进行链接)。
③那么我们该如何进行区域划分呢?
地址空间上进行区域划分时,对应的线性位置虚拟地址。


而进程地址空间的划分如下:
struct mm_struct
{
unsigned int code_start;
unsigned int code_start;
.....
unsigned int stack_start;
unsigned int stack_end;
}作用:
1. 可以判断是否越界;
2. 可以进行扩大或者缩小范围。
④为什么要有页表和进程地址空间?
1)通过页表可以将物理内存空间上不连续、无序的内存空间映射为有序的进程空间地址,让进程以统一的视角看待内存;
2)有了进程地址空间,进程会认为自己独占内存,从而更好的保障进程的独立性以及合理使用内存。这样能将进程管理与内存管理进行解耦合;
3)进程地址空间+页表的设计是保护内存安全的重要手段。
⑤此时我们就可以解释一下为什么父子进程中值不同,但是地址相同的原因:


1)因为子进程是以父进程为模板创建的,所以开始子进程物理地址也是指向父进程g_val位置,所以这也是前3秒,父进程和子进程打印的值相同的原因;
2)之后子进程的值改成了200,又因为进程是具有独立性的,父进程与子进程之间互不干扰,所以此时就发生了写时拷贝,在物理内存中重新开辟了一块空间,给子进程进行使用,然后再重新进行虚拟地址与物理地址之间的映射(写时拷贝的原因);
3)所以父进程与子进程打印的地址是一样的,是虚拟地址没有改变;
4)父子进程一般代码是共享的,此时就可以知道,都是通过映射关系,指向了同一块物理空间中的代码,所以只读的数据,一般可以只有一份,因为这是操作系统维护成本最低的。
0x04 malloc和new 开辟空间
在没学习进程地址空间时,我们会认为开辟空间,是开辟内存的空间。学习之后,我们会认为并不是如此。
1)当进程执行malloc/new操作时,因为该进程并不会立即使用内存,所以OS并不会分配物理内存,而会分配进程地址空间(虚拟地址);
2)当检测到该虚拟内存要进行修改写入操作时,会发生缺页中断,此时OS会更新页表中映射关系,申请物理内存。
提示:
缺页中断是进程访问一个合法的虚拟页时,由于该页当前未被加载到物理内存(页表项标记为无效),由硬件触发、由操作系统处理的中断。操作系统负责将所需的页从磁盘调入内存、更新页表,然后让进程重新执行被中断的访问指令。

作用:
1)充分保证内存的使用率;
2)因为实际没有在内存上开辟空间,所以提升malloc/new的速度。
六、页表及写时拷贝触发机制
页表不仅只有虚拟地址和物理地址,还有其他属性,比如权限。

问题1:我们写代码时,常量不可修改是谁决定?
答:是由操作系统在页表的权限属性上设置为只读决定的,这也是运行时保护的最后一道防线。
当要对数据进行修改或写入时,首先需要将虚拟地址转化为物理地址。转化的过程中,操作系统发现该权限为只读,所以该数据不可修改不可写入。
问题2:const修饰的数据是不是也由页表决定?
答:不是。const的意义是将可能在运行阶段出现的错误提前在编译阶段发现并报错,是一种防御性编程。
问题3:操作系统如何知道何时进行写实拷贝?
答:操作系统通过页错误异常机制来感知写时拷贝的触发时机。
1. 当父进程调用fork()创建子进程后,两者共享相同的物理内存页,但操作系统会故意将对应页表项的权限设置为只读(即使进程的虚拟地址空间描述为可读可写)。
2. 当任一进程尝试写入该共享内存时,CPU的MMU会在地址转换过程中检测到写权限缺失,从而触发页错误异常。
3. 内核的页错误处理程序随后检查异常地址和页表状态,确认该页为写时拷贝共享页后,便执行写时拷贝操作:分配新的物理页、复制原页内容、更新当前进程的页表指向新页并设为可写,而其他进程仍保持只读共享原页。
更多推荐




所有评论(0)