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

程序地址空间

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

相关推荐
阿Y加油吧1 小时前
二刷 LeetCode:两道经典贪心题复盘
算法·leetcode·职场和发展
Java成神之路-2 小时前
【LeetCode 刷题笔记】35. 搜索插入位置 | 二分查找经典入门题
算法·leetcode
MediaTea12 小时前
AI 术语通俗词典:C4.5 算法
人工智能·算法
Navigator_Z12 小时前
LeetCode //C - 1033. Moving Stones Until Consecutive
c语言·算法·leetcode
WBluuue12 小时前
数据结构与算法:莫队(一):普通莫队与带修莫队
c++·算法
风筝在晴天搁浅13 小时前
n个六面的骰子,扔一次之后和为k的概率是多少?
算法
MATLAB代码顾问14 小时前
Python实现蜂群算法优化TSP问题
开发语言·python·算法
代码飞天14 小时前
机器学习算法和函数整理——助力快速查阅
人工智能·算法·机器学习
jiushiapwojdap14 小时前
LU分解法求解线性方程组Matlab实现
数据结构·其他·算法·matlab