程序地址空间和进程的一些操作

程序地址空间

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调用其他程序,再把环境变量传递过去。

相关推荐
江畔柳前堤4 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore5 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考7 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX8 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面8 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号8 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽9 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI9 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者10 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者10 小时前
【征程6】校准量化中HistogramObserver解析
算法