程序地址空间
void* malloc(size_t size) malloc返回类型是void*,所以一般要强制转换。
程序地址都是低地址到高地址都是这样排的。

用代码来验证。
1 //程序地址空间的测试
2 #include<stdio.h>
3 #include<stdlib.h>
4
5 int g_val=10;
6 int g_unval;
7
8 int main(int argc,char*argv[],char*env[]){
9 char *s="hellobite";
10 printf("字符串地址:%p",s);
11
12 printf("代码段%p\n",main);
13 printf("初始化数据%p\n",&g_val);
14 printf("未初始化数据%p\n",&g_unval);
15
16 int *heap1=(int*)malloc(sizeof(int)*4);
17 int *heap2=(int*)malloc(sizeof(int)*4);
18 int *heap3=(int*)malloc(sizeof(int)*4);
19 int *heap4=(int*)malloc(sizeof(int)*4);//开堆空间.
20
21
22 printf("堆地址:%p\n",heap1);
23 printf("堆地址:%p\n",heap2);
24 printf("堆地址:%p\n",heap3);
25 printf("堆地址:%p\n",heap4);
26
27
28
29 printf("stack 地址:%p\n",&heap1);
30 printf("stack 地址:%p\n",&heap2);
31 printf("stack 地址:%p\n",&heap3);
32 printf("stack 地址:%p\n",&heap4);
33
34 for(int i=0;i<argc;i++){
35 printf("命令行参数:%p\n",&argv[i]);
36
37 }
38
39 for(int i=0;env[i];i++){
40 printf("环境变量:%p\n",&env[i]);
41
42 }
43
44
45 return 0;
46
47 }
查看地址符合要求,还有这个字符串地址。跟数据地址很近。这就说明。不能*s="c",修改字符串。因为常量不能修改。对于这种常量修改运行时会报错。如果加了const,还修改就是编译器报错。对于static也是吧变量存在数据段区,生命周期变成全局。作用域不变。
bash
字符串地址:0x55e81dabe004代码段0x55e81dabd189
初始化数据0x55e81dac0010
未初始化数据0x55e81dac0018
堆地址:0x55e8314266b0
堆地址:0x55e8314266d0
堆地址:0x55e8314266f0
堆地址:0x55e831426710
stack 地址:0x7ffc11ce0520
stack 地址:0x7ffc11ce0528
stack 地址:0x7ffc11ce0530
stack 地址:0x7ffc11ce0538
命令行参数:0x7ffc11ce0668
环境变量:0x7ffc11ce0678
环境变量:0x7ffc11ce0680
环境变量:0x7ffc11ce0688
环境变量:0x7ffc11ce0690
环境变量:0x7ffc11ce0698
环境变量:0x7ffc11ce06a0
环境变量:0x7ffc11ce06a8
环境变量:0x7ffc11ce06b0
环境变量:0x7ffc11ce06b8
环境变量:0x7ffc11ce06c0
环境变量:0x7ffc11ce06c8
环境变量:0x7ffc11ce06d0
环境变量:0x7ffc11ce06d8
环境变量:0x7ffc11ce06e0
环境变量:0x7ffc11ce06e8
环境变量:0x7ffc11ce06f0
环境变量:0x7ffc11ce06f8
环境变量:0x7ffc11ce0700
环境变量:0x7ffc11ce0708
环境变量:0x7ffc11ce0710
环境变量:0x7ffc11ce0718
环境变量:0x7ffc11ce0720
环境变量:0x7ffc11ce0728
环境变量:0x7ffc11ce0730
环境变量:0x7ffc11ce0738
环境变量:0x7ffc11ce0740
环境变量:0x7ffc11ce0748
进程虚拟地址
对于之前外面创建了子进程。父子进程的代码和数据共享(在子进程没有发生修改的情况下)。
对于同一个变量。当我们修改子进程的数据的时候。父子进程打印的变量的地址一样。但数据不一样。因为发生了写时拷贝。c/c++我们看见的地址都是虚拟地址。不是物理内存上的地址。

对于上面我们代码测试的数据都是通过mm_struct的虚拟地址在页表中通过硬件mmu查找物理地址。
理解虚拟地址
对于虚拟地址就是通过数据结构描述的一个结构体。先描述再组织。
对于mm_struct里面有哪些成员了。对于里面就是对数据段,代码段。栈,堆地址开始与结束的描述

栈和堆地址的大小是运行时决定的。
这些地址的描述是对虚拟地址描述。因为虚拟地址更有序。
如果对于堆地址中间free一部分,那剩余的地址又该怎么描述了。不可能还是简单的开始和结束。


为什么要有虚拟地址

对于我们直接对物理内部进行管理的话。进程a的物理地址,系统需要管理。b的也要。太麻烦。如果有虚拟地址。我们只需要给每个进程分配4GB虚拟地址。不管他们这么映射的。



对于页表还有一个权限标志。对于数据段就是r权限。只能读。当我们修改的时候就会直接报错。
不会修改内存。
还有当我们进行malloc的时候。会在虚拟地址申请一块空间。但是物理内存上没有申请。只有当需要的时候会触发页表缺页。然后申请物理内存,加载代码和数据

进程的创建
fork,创建进程会把内核数据结构和代码和数据拷贝给子进程。但内核数据结构也会有不同pid等。

对于修改之前假如我们这个数据段是可读写的,当我们fork之后权限会变成只读。如果我们子进程要修改数据。会触发缺页异常,操作系统介入。判断内存里面的数据可以修改。然后就写时拷贝。重新开辟一块内存。父进程要修改读写变成只读的部分也是一样,都会重新生成一块空间
为什么要写时拷贝
提高内存使用率。延迟技术。只有子进程修改的时候才拷贝。不提前开辟空间。使得内存也可以被其他先使用。
为什么是拷贝。当写实拷贝的时候。就开辟一块空间,把原来的数据拷贝过来。
进程终止
进程的退出码有很多种


对于return.是在main函数返回退出码,其他函数就只是函数返回值。
bash
1 #include<stdio.h>
2
3
4 int print(int b)
5 {
6
7 printf("%d\n",b);
8 return 1;
9
10 }
11
12 int main()
13 {
14
15 //对于return退出码。一般是main函数返回
16
17
18 int a=0;
19 a=print(a);
20
21 return 23;
22
23 }

对于exit。其他函数exit退出了。整个程序就终止了。后面的代码不会执行。所以退出码是23。

bash
#include<stdio.h>
2 #include<stdlib.h>
3
4 void add(int a,int b)
5 {
6 int c=a+b;
7 exit(23);
8
9 }
10
11 int main()
12 {
13
14 //对于exit和return 的区别
15 add(2,3);
16 exit(2);
17
18 }

对于父进程获取子进程的信息除了退出码还有退出信号。
常见的退出信号。

进程终止通常有三种情况。
代码跑完,结果正确。(退出码=0,退出信号也是0)
代码跑完,结果不正确(退出码!=0,退出信号为0)
代码没有跑完 异常了(退出信号!=0)
_exit()和exit()的区别


如果修改成exit(1);那么就会输出

这是为什么,因为exit是函数。_exit是系统调用。exit提供把缓冲区的内容输出

进程等待的方法
之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存
泄漏。
•
另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的kill -9 也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程。
•
最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
不对,或者是否正常退出。
•
⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息

进程等待就是解决僵尸进程。因为不管父进程是循环,只要父进程写了wait(),就会获取子进程的退出信息

这种不能获取。必须调换顺序。

我们用wait获取pcb的退出信息怎么查看了?

所以对于退出码可以status变量右移8为&0xff。信号就是&0x7f.退出码的范围是[0,255]。因为退出码的范围是8位.


jcm@iZwz9c6v6ajdocnv9xkh8rZ:~$ while :; do ps axj|head -1; ps axj|grep wait;sleep 1;done
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
/usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
551405 551988 551988 551405 pts/0 551988 S+ 1001 0:00 ./wait
551988 551989 551988 551405 pts/0 551988 S+ 1001 0:00 ./wait
551521 551993 551992 551521 pts/3 551992 S+ 1001 0:00 grep wait
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1009 1009 1009 ? -1 Ssl 0 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
551405 551988 551988 551405 pts/0 551988 S+ 1001 0:00 ./wait
551988 551989 551988 551405 pts/0 551988 Z+ 1001 0:00 [wait] <defunct>
551521 551998 551997 551521 pts/3 551997 S+ 1001 0:00 grep wait
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1009 1009 1009 ? -1 Ssl 0 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
551405 551988 551988 551405 pts/0 551988 S+ 1001 0:00 ./wait
551988 551989 551988 551405 pts/0 551988 Z+ 1001 0:00 [wait] <defunct>
551521 552003 552002 551521 pts/3 552002 S+ 1001 0:00 grep wait
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1009 1009 1009 ? -1 Ssl 0 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
551405 551988 551988 551405 pts/0 551988 S+ 1001 0:00 ./wait
551988 551989 551988 551405 pts/0 551988 Z+ 1001 0:00 [wait] <defunct>
551521 552008 552007 551521 pts/3 552007 S+ 1001 0:00 grep wait
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1009 1009 1009 ? -1 Ssl 0 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
551405 551988 551988 551405 pts/0 551988 S+ 1001 0:00 ./wait
551988 551989 551988 551405 pts/0 551988 Z+ 1001 0:00 [wait] <defunct>
551521 552013 552012 551521 pts/3 552012 S+ 1001 0:00 grep wait
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1009 1009 1009 ? -1 Ssl 0 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
551521 552019 552018 551521 pts/3 552018 S+ 1001 0:00 grep wait
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1009 1009 1009 ? -1 Ssl 0 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
对于waitpid

参数pid可以指定等待哪个子进程。options是决定是否位阻塞等待。options=0为阻塞
(-1,status,0)跟wait一样,所以pid=-1的时候等待任意的子进程。对于多个子进程。要等待完
bash
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 int main()
6 {
7
8 pid_t pid[5]={0};
9 for(int i=0;i<4;i++){
10 pid_t id=fork();
11 pid[i]=id;
12
13 if(pid[i]==0){
14 printf("我是子进程:%d\n",getpid());
15 exit(2);
16 }
17
18
19 }
20 for(int i=0;i<4;i++){
21 int status=0;
22 pid_t id=waitpid(pid[i],&status,0);
23 if(id>0){
24 printf("等待成功退出码:%d,id:%d\n",status>>8&0xff,id);
25 }
26
27
28 }
29 return 0;
30 }

非阻塞等待
对于非阻塞等待,我们需要把option参数写成WNOHONG。这样父进程就不会在等待子进程卡死。会执行自己的任务,直到等待子进程成功。
cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
void printlog(){
printf("打印日志\n");
}
void mysql(){
printf("连接数据库\n");
}
typedef void(*task_t)(void);
task_t task[2]={
printlog,
mysql
};
int main(){
pid_t id=fork();
if(id==0){
while(1){
printf("我是子进程\n");
sleep(5);
//野指针
int*n=NULL;
*n=100;
}
}
else if(id>0){
//为了不然父进程结束。要循环。因为这样父进程非阻塞状态不会执行到return 0;
while(1){
int status=0;
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret==0){
//等待期间,子进程没有结束。父进程可以干其他事情.
for(int i=0;i<2;i++){
task[i]();
sleep(1);
}
}
else if(ret<0){
printf("等待失败\n");
break;
}
else{
printf("等待成功\n");
break;
}
}
}
return 0;
}
因为父进程比子进程执行快,所以先打印日志。这就是在等待子进程的时候可以做其他事情。

我们等待成功之后,还可以通过宏WIFEXUTED查看是否正常退出.>0正常退出。==0异常
我们代码设置了野指针所以异常退出。信号为11
cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
void printlog(){
printf("打印日志\n");
}
void mysql(){
printf("连接数据库\n");
}
typedef void(*task_t)(void);
task_t task[2]={
printlog,
mysql
};
int main(){
pid_t id=fork();
if(id==0){
while(1){
printf("我是子进程\n");
sleep(5);
//野指针
int*n=NULL;
*n=100;
}
}
else if(id>0){
//为了不然父进程结束。要循环。因为这样父进程非阻塞状态不会执行到return 0;
while(1){
int status=0;
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret==0){
//等待期间,子进程没有结束。父进程可以干其他事情.
for(int i=0;i<2;i++){
task[i]();
sleep(1);
}
}
else if(ret<0){
printf("等待失败\n");
break;
}
else{
//当我们等待成功了,可以查看信息。
//对于查看信息。我们通过WIFEXITED宏来查看,>0正常退出。等于0异常退出.
if(WIFEXITED(status)){
printf("退出吗:%d\n",WEXITSTATUS(status));
}
else{
printf("异常信号:%d\n",WTERMSIG(status));
}
printf("等待成功\n");
break;
}
}
}
return 0;
}

如果我们除以0也会更改

进程替换

进程替换
⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀
种 exec 函数以执⾏另⼀个程序。当进程调⽤⼀种 exec 函数时,该进程的⽤⼾空间代码和数据完全被
新程序替换,从新程序的启动例程开始执⾏。调⽤ exec 并不创建新进程,所以调⽤ exec 前后该进程
的 id 并未改变。
bash
#include<stdio.h>
2 #include<unistd.h>
3
4
5 int main(){
6 printf("替换之前\n");
7 //execl函数必须以NULL结束
8 execl("/usr/bin/ls","ls","-l",NULL);
9 printf("替换之后");
10 return 0;
11
12 }


对于execl替换不会创建新进程,而是替换原来的数据和代码。
对于exec替换函数可以替换ls,等程序,可以替换自己写的程序吗?
mypro程序
cpp
#include<stdio.h>
2 #include<unistd.h>
3
4
5 int main(){
6 //要加\n或者fflush(stdout),不然数据在缓冲区,只有程序结束才会输出,但是代码和数据都被替换了。所以要加。
7 printf("替换之前\n");
8 printf("pid:%d\n",getpid());
9 //execl函数必须以NULL结束
10 //execl("/usr/bin/ls","ls","-l",NULL);
11 execl("./test","./test",NULL);
12 printf("替换之后");
13 return 0;
14
15 }
test程序
cpp
#include<iostream>
2 #include<unistd.h>
3
4
5 int main(){
6
7 std::cout<<"我是一个c++程序"<<std::endl;
8 std::cout<<"pid"<<getpid()<<std::endl;
9 return 0;
10
11 }

所以程序替换不会替换内部数据结构,只会替换代码和数据,pid不变,不会创建新的进程。

对于函数替换,我们都是创建子进程去调用
cpp
#include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 int main(){
5
6
7 pid_t id=fork();
8 if(id==0){
9
10 execl("/usr/bin/ls","ls","-a","-l",NULL);
11
12 }
13 int status=0;
14 pid_t rid=waitpid(id,&status,0);
15 if(rid>0){
16
17 printf("等待成功");
18
19 }
20 return 0;
21
22 }

对于创建子进程去函数替换,是先会继承父进程的代码和数据。然后子进程在虚拟地址开辟空间。通过页表映射到物理地址开辟空间。最后加载代码和数据
开辟虚拟地址 → 需要时开辟物理内存 ← 同时建立 → 页表映射
替换的几种函数
cpp
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4
5 //char*const argv[]={
6 // "ls",
7 // "-a",
8 // "-l",
9 // NULL
10 //};
11
12 char*const argv[]={
13 "top",
14 "-d",
15 "1",
16 "-n",
17 "3",
18 NULL
19 };
20
21 char*const env[]={
22 "TERM=xterm-256color",
23 "haha=hehe",
24 "HOME=/home",
25 NULL
26 };
27
28 int main(){
30
31
32 pid_t id=fork();
33 if(id==0){
34 //各种替换函数。
35 //execvp("top",argv);
36 //execle("/usr/bin/top","top","-d","1","-n","3",NULL,env);
37 //execv("/usr/bin/ls",argv);//v代表数组
38 //execlp("ls","ls","-a","-l",NULL);//p结尾代表文件
39 //execl("/usr/bin/ls","ls","-a","-l",NULL);
40 //调用系统的环境变量
41 extern char**environ;//声明
42 putenv("haha=hehe");
43 putenv("pwd=home");
44 execle("/usr/bin/top","top",NULL,environ);
45 }
46 int status=0;
47 pid_t rid=waitpid(id,&status,0);
48 if(rid>0){
49
50 printf("等待成功");
51
52 }
53 return 0;
54
55 }

对于我们使用的这些程序替换函数都是库函数。使用时库函数是通过系统调用的execve封装的。所以子进程替换的时候可以不用传递环境变量。系统调用已经传了。
总结:对于系统的环境变量表。是库函数传递给execve.然后execve调用其他程序,再把环境变量传递过去。


