进程地址空间

张开发
2026/4/20 2:57:17 15 分钟阅读

分享文章

进程地址空间
一个程序#includeiostream #includeunistd.h using namespace std; int main() { int val0; pid_t pidfork(); if(pid0) { val10; cout我是子进程pid是 getpid()父进程id是getppid() val是val val的地址是valendl; } else { cout我是父进程pid是 getpid()val是val val的地址是valendl; } return 0; }运行结果我们发现父子进程的中val的地址都是一样的但是打印出来的结果却不一样原因如下打印的地址是逻辑地址并非是val的物理地址在创建子进程的时候子进程会将父进程的代码数据继承过去如果数据没有改变他们的数据会指向同一个地址但是如果子进程改变了数据就会发生写时拷贝逻辑地址不变但是将实际的数据拷贝一份放入新的物理地址图片如下再来解释一下刚开始的时候每个进程都有自己的PCB每一个PCB也就是一个结构体当创建一个子进程的时候会将mm_struct拷贝一份这个时候父子进程数据的页表中的虚拟地址和物理地址都是一样的但是当子进程改变了数据的话子进程的数据的物理地址会改变的进程的创建我们来探讨一下写时拷贝相关的原理子进程的创建本质上就是父进程代码和数据的拷贝当然页表中的数据也是一样会被拷贝的页表中除了有虚拟地址到物理地址的映射还有数据的读写权限当子进程想要修改数据的时候发现数据是只有读的权限的时候会触发错误系统检测到发生了错误就会判断是否要进行写时拷贝如果是写时拷贝的话就会修改页表申请空间之类的操作进程的终止main是有返回值的我们写代码都是很清楚的但是为什么要返回要有返回值嘞这个返回值就是错误码是用来方便父进程知道子进程的运行情况的如果返回的是0就表示这个程序运行成功了如果是非0的话就表明这个程序运行失败了这也是为什么我们的在写main程序的时候是返回0的原因了下面我们来介绍几个命令echo $?这个命令返回的是最近的一个进程结束的错误码errno和strerror这两个函数一个是错误码一个是错误码对应的错误信息通常在程序里面使用#includeiostream #includecstring #includeerrno.h using namespace std; int main() { FILE *fdfopen(./no_such_file.txt,w); couterrnoerrno 错误信息strerror(errno)endl; return 0; }运行结果我们可以来打印一看各种状态码对应的状态信息#includeiostream #includecstring #includeerrno.h using namespace std; int main() { for(int i0;i20;i) { couterrnoi 状态码strerror(i)endl; } return 0; }运行结果进程的终止进程的终止有下面两种方式1.main函数里面的return 02.exit#includeiostream #includecstring #includestdlib.h #includeerrno.h using namespace std; void func() { cout你好endl; exit(0); } int main() { func(); couthello worldendl; return 0; }函数运行结果从代码的执行情况来看exit函数无论在哪一层栈帧只要出现就会终止整个进程mian函数里面的hello world都没有打印和_exit函数的区别exit函数在会将缓冲区里面的内容打印在出来_exit不会#includeiostream #includecstring #includestdlib.h #includeerrno.h #includecstdio using namespace std; int main() { couthello worldendl; exit(0); }运行打印hello world#includeiostream #includecstring #includestdlib.h #includeerrno.h #includecstdio #includeunistd.h using namespace std; int main() { printf(hello world/n); _exit(0); } //不会打印hello world进程等待在父子进程当中如果子进程运行完毕但是父进程没有运行完的话父进程就会卡在哪里去先去回收子进程类似于scanf函数一样等待用户输入介绍一个函数pid_t wait(int *status),父进程要使用这个函数阻塞等待去回收子进程status返回子进程的状态信息等待成功返回子进程的pid失败返回-1#includeiostream #includecstring #includestdlib.h #includeerrno.h #includecstdio #includeunistd.h #includesys/wait.h #includeunistd.h using namespace std; int main() { pid_t pidfork(); if(pid0) { printf(hello world\n); sleep(5); exit(5); } else { int status0; wait(status); printf(我是父进程\n); printf(%d,status); } exit(0); }等待子进程运行完5秒之后父进程才开始运行waitpid方法#include sys/types.h#include sys/wait.hpid_t waitpid(pid_t pid, int *status, int options);1.pid- 指定要等待的子进程pid 0等待进程ID等于pid的特定子进程pid -1等待任意子进程等同于waitpid 0等待与调用进程在同一进程组的任意子进程pid -1等待进程组ID等于|pid|的任意子进程2.status- 存储子进程退出状态如果不为NULL存储状态信息可用宏解析状态WIFEXITED(status)是否正常退出WEXITSTATUS(status)获取退出码WIFSIGNALED(status)是否被信号终止WTERMSIG(status)获取终止信号WIFSTOPPED(status)是否被信号暂停WSTOPSIG(status)获取暂停信号3.options- 控制行为选项0阻塞等待直到子进程结束WNOHANG非阻塞若无子进程退出立即返回0WUNTRACED也返回被暂停的子进程状态WCONTINUED也返回被恢复的子进程状态Linux 2.6.10返回值成功返回状态变化的子进程ID失败返回 -1设置errnoWNOHANG且无子进程退出返回0#includeiostream #includecstring #includestdlib.h #includeerrno.h #includecstdio #includeunistd.h #includesys/wait.h #includeunistd.h using namespace std; int main() { pid_t idfork(); if(id0) { printf(我是子进程pid是%d\n,getpid()); exit(123); //自定义退出码 } else { int status0; pid_t retwaitpid(id,status,0); //0第三个参数为0阻塞到子进程结束 if(ret-1) //等待失败 { perror(waitpid); exit(1); } //子进程正常退出 printf(我是父进程,子进程正常退出退出码是%d\n,status8); } return 0; }运行结果在上面打印退出码的时候status需要右移动8位才能得到真正的退出码因为次高八位才是存储状态码的地方低八位是退出的信号值正常退出8-15位 退出状态码0-255bits 0-7: 全为 0被信号终止bits 0-6: 终止信号编号1-31bit 7: 是否产生core dump通常为1bits 8-15: 0再实际使用中如果子进程一直没有运行完毕那需要一直让父进程去等待他吗答案是否定的因此我们需要设置第三个参数#includeiostream #includecstring #includestdlib.h #includeerrno.h #includecstdio #includeunistd.h #includesys/wait.h #includeunistd.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); } }运行结果这样子进程没有运行结束父进程就可以去做其他的事情了这叫做非阻塞等待进程程序替换下面我们来介绍几个函数excelint execl(const char *path, const char *arg, ...);参数说明path可执行文件的完整路径如/bin/lsarg可变参数列表依次传递命令行参数第一个参数通常是程序名惯例最后一个参数必须是(char *)NULL作为结束标记返回值成功不返回当前进程被替换失败返回 -1#includeiostream #includeunistd.h using namespace std; int main() { execl(/bin/ls,ls,-a,-l,nullptr); return 0; }运行结果注意这里是程序替换也就是说是用参数里面的可执行程序去替代现在的进程我们还可以用来执行自己的程序#includeiostream #includeunistd.h using namespace std; int main() { execl(/home/LiHao/practice/test202645/helloworld,helloworld,nullptr); return 0; }其中helloworld是helloworld.c的可执行程序里面的内容是打印hello worldexeclvint execv(const char *path, char *const argv[]);参数含义path可执行文件的路径绝对路径或相对路径-3-8argv[]参数数组第一个元素通常是程序名最后一个必须是NULL比如下面的int main() { const char *argv[]{ls,-a,-l,NULL}; execv(/bin/ls,argv); }execlpint execlp(const char *file, const char *arg, ...);关键特性不会返回执行成功时execlp不会返回当前进程的代码被新程序完全替换查找路径通过PATH环境变量查找file无需写绝对路径参数格式第一个arg通常是程序名可自定义后面的arg依次对应命令行参数最后必须以(char *)NULL结尾返回值成功不返回失败返回-1并设置errno#includeiostream #includeunistd.h using namespace std; int main() { execlp(ls,ls,-a,-l,nullptr); return 0; }运行结果但是会不会觉得第一个参数和第二个参数有点重复了不是这样的第一个参数是给内核和操作系统看的用来定位磁盘上的文件。第二个参数是给新启动的程序看的是它的“人设”或第一个输入数据。所以这两个参数是可以不同的#includeiostream #includeunistd.h using namespace std; int main() { execlp(ls,echo,-a,-l,nullptr); return 0; }我们将第二个参数变成echo发现运行结果是一样的execvpint execvp(const char *file, char *const argv[]);这个函数也不需要带绝对路径只是需要将参数放在一个数组里就行了其他的返回值之类的和上一个函数是一样的#includeiostream #includeunistd.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环境变量

更多文章