进程地址空间
2026/4/20 2:57:15 网站建设 项目流程

一个程序

#include<iostream> #include<unistd.h> using namespace std; int main() { int val=0; pid_t pid=fork(); if(pid==0) { val+=10; cout<<"我是子进程"<<"pid是"<<" " <<getpid()<<"父进程id是"<<getppid()<<" "<<"val是"<<val<<" " <<"val的地址是"<<&val<<endl; } else { cout<<"我是父进程"<<"pid是"<<" "<<getpid()<<"val是"<<val<<" "<<"val的地址是"<<&val<<endl; } return 0; }

运行结果

我们发现,父子进程的中val的地址都是一样的,但是打印出来的结果却不一样,原因如下打印的地址是逻辑地址,并非是val的物理地址,在创建子进程的时候,子进程会将父进程的代码数据继承过去,如果数据没有改变,他们的数据会指向同一个地址,但是如果子进程改变了数据,就会发生写时拷贝,逻辑地址不变,但是将实际的数据拷贝一份,放入新的物理地址,图片如下

再来解释一下刚开始的时候,每个进程都有自己的PCB,每一个PCB,也就是一个结构体,当创建一个子进程的时候会将mm_struct拷贝一份,这个时候父子进程数据的页表中的虚拟地址和物理地址都是一样的,但是当子进程改变了数据的话,子进程的数据的物理地址会改变的

进程的创建

我们来探讨一下写时拷贝相关的原理

子进程的创建本质上就是父进程代码和数据的拷贝,当然,页表中的数据也是一样会被拷贝的,页表中除了有虚拟地址到物理地址的映射,还有数据的读写权限,当子进程想要修改数据的时候,发现数据是只有读的权限的时候,会触发错误,系统检测到发生了错误就会判断是否要进行写时拷贝,如果是写时拷贝的话,就会修改页表,申请空间之类的操作

进程的终止

main是有返回值的,我们写代码都是很清楚的,但是为什么要返回要有返回值嘞?这个返回值就是错误码,是用来方便父进程知道子进程的运行情况的,如果返回的是0,就表示这个程序运行成功了,如果是非0的话,就表明这个程序运行失败了,这也是为什么我们的在写main程序的时候是返回0的原因了

下面我们来介绍几个命令

echo $?

这个命令返回的是最近的一个进程结束的错误码

errno和strerror

这两个函数一个是错误码,一个是错误码对应的错误信息,通常在程序里面使用

#include<iostream> #include<cstring> #include<errno.h> using namespace std; int main() { FILE *fd=fopen("./no_such_file.txt","w"); cout<<"errno"<<errno<<" "<<"错误信息"<<strerror(errno)<<endl; return 0; }

运行结果

我们可以来打印一看各种状态码对应的状态信息

#include<iostream> #include<cstring> #include<errno.h> using namespace std; int main() { for(int i=0;i<=20;i++) { cout<<"errno"<<i<<" "<<"状态码"<<strerror(i)<<endl; } return 0; }

运行结果

进程的终止

进程的终止有下面两种方式

1.main函数里面的return 0

2.exit

#include<iostream> #include<cstring> #include<stdlib.h> #include<errno.h> using namespace std; void func() { cout<<"你好"<<endl; exit(0); } int main() { func(); cout<<"hello world"<<endl; return 0; }

函数运行结果

从代码的执行情况来看,exit函数无论在哪一层栈帧,只要出现,就会终止整个进程,mian函数里面的hello world都没有打印

和_exit()函数的区别:exit函数在会将缓冲区里面的内容打印在出来,_exit不会

#include<iostream> #include<cstring> #include<stdlib.h> #include<errno.h> #include<cstdio> using namespace std; int main() { cout<<"hello world"<<endl; exit(0); }

运行打印:hello world

#include<iostream> #include<cstring> #include<stdlib.h> #include<errno.h> #include<cstdio> #include<unistd.h> using namespace std; int main() { printf("hello world/n"); _exit(0); } //不会打印hello world

进程等待

在父子进程当中,如果子进程运行完毕,但是父进程没有运行完的话,父进程就会卡在哪里去先去回收子进程,类似于scanf函数一样等待用户输入

介绍一个函数

pid_t wait(int *status),父进程要使用这个函数阻塞等待去回收子进程,status返回子进程的状态信息,等待成功返回子进程的pid,失败返回-1

#include<iostream> #include<cstring> #include<stdlib.h> #include<errno.h> #include<cstdio> #include<unistd.h> #include<sys/wait.h> #include<unistd.h> using namespace std; int main() { pid_t pid=fork(); if(pid==0) { printf("hello world\n"); sleep(5); exit(5); } else { int status=0; wait(&status); printf("我是父进程\n"); printf("%d",status); } exit(0); }

等待子进程运行完5秒之后父进程才开始运行

waitpid方法

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

1.pid- 指定要等待的子进程

  • pid > 0:等待进程ID等于pid的特定子进程

  • pid = -1:等待任意子进程(等同于wait

  • pid = 0:等待与调用进程在同一进程组的任意子进程

  • pid < -1:等待进程组ID等于|pid|的任意子进程

2.status- 存储子进程退出状态

  • 如果不为NULL,存储状态信息

  • 可用宏解析状态:

    • WIFEXITED(status):是否正常退出

    • WEXITSTATUS(status):获取退出码

    • WIFSIGNALED(status):是否被信号终止

    • WTERMSIG(status):获取终止信号

    • WIFSTOPPED(status):是否被信号暂停

    • WSTOPSIG(status):获取暂停信号

3.options- 控制行为选项

  • 0:阻塞等待直到子进程结束

  • WNOHANG:非阻塞,若无子进程退出立即返回0

  • WUNTRACED:也返回被暂停的子进程状态

  • WCONTINUED:也返回被恢复的子进程状态(Linux 2.6.10+)

返回值

  • 成功:返回状态变化的子进程ID

  • 失败:返回 -1(设置errno)

  • WNOHANG且无子进程退出:返回0

#include<iostream> #include<cstring> #include<stdlib.h> #include<errno.h> #include<cstdio> #include<unistd.h> #include<sys/wait.h> #include<unistd.h> using namespace std; int main() { pid_t id=fork(); if(id==0) { printf("我是子进程,pid是%d\n",getpid()); exit(123); //自定义退出码 } else { int status=0; pid_t ret=waitpid(id,&status,0); //0:第三个参数为0,阻塞到子进程结束 if(ret==-1) //等待失败 { perror("waitpid"); exit(1); } //子进程正常退出 printf("我是父进程,子进程正常退出,退出码是%d\n",status>>8); } return 0; }

运行结果

在上面打印退出码的时候status需要右移动8位才能得到真正的退出码

因为次高八位才是存储状态码的地方,低八位是退出的信号值

正常退出

8-15位: 退出状态码(0-255)
bits 0-7: 全为 0

被信号终止

bits 0-6: 终止信号编号(1-31)
bit 7: 是否产生core dump(通常为1)
bits 8-15: 0

再实际使用中,如果子进程一直没有运行完毕,那需要一直让父进程去等待他吗,答案是否定的,因此我们需要设置第三个参数

#include<iostream> #include<cstring> #include<stdlib.h> #include<errno.h> #include<cstdio> #include<unistd.h> #include<sys/wait.h> #include<unistd.h> using namespace std; int main() { pid_t pid = fork(); if (pid == 0) { sleep(5); exit(0); } else { int status; pid_t ret; while ((ret = waitpid(pid, &status, WNOHANG)) == 0) { printf("子进程仍在运行,执行其他任务...\n"); sleep(1); } if (ret == pid) { printf("子进程已结束\n"); } }

运行结果:

这样子进程没有运行结束父进程就可以去做其他的事情了,这叫做非阻塞等待

进程程序替换

下面我们来介绍几个函数

excel

int execl(const char *path, const char *arg, ...);

参数说明

  • path:可执行文件的完整路径(如/bin/ls

  • arg:可变参数列表,依次传递命令行参数

    • 第一个参数通常是程序名(惯例)

    • 最后一个参数必须是(char *)NULL作为结束标记

  • 返回值:成功不返回(当前进程被替换),失败返回 -1

#include<iostream> #include<unistd.h> using namespace std; int main() { execl("/bin/ls","ls","-a","-l",nullptr); return 0; }

运行结果

注意,这里是程序替换,也就是说,是用参数里面的可执行程序去替代现在的进程

我们还可以用来执行自己的程序

#include<iostream> #include<unistd.h> using namespace std; int main() { execl("/home/LiHao/practice/test202645/helloworld","helloworld",nullptr); return 0; }

其中helloworld是helloworld.c的可执行程序,里面的内容是打印hello world

execlv

int execv(const char *path, char *const argv[]);
参数含义
path可执行文件的路径(绝对路径或相对路径)-3-8
argv[]参数数组,第一个元素通常是程序名,最后一个必须是NULL

比如下面的

int main() { const char *argv[]={"ls","-a","-l",NULL}; execv("/bin/ls",argv); }

execlp

int execlp(const char *file, const char *arg, ...);

关键特性

  1. 不会返回:执行成功时,execlp不会返回,当前进程的代码被新程序完全替换

  2. 查找路径:通过PATH环境变量查找file,无需写绝对路径

  3. 参数格式

    • 第一个arg通常是程序名(可自定义)

    • 后面的arg依次对应命令行参数

    • 最后必须(char *)NULL结尾

  4. 返回值

    • 成功:不返回

    • 失败:返回-1,并设置errno

#include<iostream> #include<unistd.h> using namespace std; int main() { execlp("ls","ls","-a","-l",nullptr); return 0; }

运行结果

但是会不会觉得第一个参数和第二个参数有点重复了?不是这样的

第一个参数:是给内核和操作系统看的,用来定位磁盘上的文件。

第二个参数:是给新启动的程序看的,是它的“人设”或第一个输入数据。所以,这两个参数是可以不同的

#include<iostream> #include<unistd.h> using namespace std; int main() { execlp("ls","echo","-a","-l",nullptr); return 0; }

我们将第二个参数变成echo,发现运行结果是一样的

execvp

int execvp(const char *file, char *const argv[]);

这个函数也不需要带绝对路径,只是需要将参数放在一个数组里就行了,其他的返回值之类的和上一个函数是一样的

#include<iostream> #include<unistd.h> using namespace std; int main() { char *argv[]={"ls","-a","-l",nullptr}; //以nullptr结尾 execvp("ls",argv); return 0; }

运行结果

execvpe

这个是带环境变量的,如果需要自己传递环境变量,可以自己定义

int execvpe(const char *file, char *const argv[], char *const envp[]);

具体先不举例子了

上面的函数可能都不好记忆:带l的是列表,带v的是vector,带p的是path,带e的是environment环境变量

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询