想象力网站建设,阳江房产信息网官网,图片外链生成,wordpress前端开发目录 一、程序地址空间
1.C/C中的程序地址空间
2.进程地址空间
进程地址空间概念
什么是地址空间#xff1f;什么是区域划分#xff1f;
为啥要有地址空间#xff1f; 地址空间的补充
二、进程创建
1.fork函数
2.写时拷贝
3.fork常规用法
4.fork调用失败的原因
…目录 一、程序地址空间
1.C/C中的程序地址空间
2.进程地址空间
进程地址空间概念
什么是地址空间什么是区域划分
为啥要有地址空间 地址空间的补充
二、进程创建
1.fork函数
2.写时拷贝
3.fork常规用法
4.fork调用失败的原因
三、进程终止
1.进程终止的概念 2.进程常见退出方法
3._exit函数
4.exit函数
四、进程等待
1.什么是进程等待
2.为啥要进程等待
3.如何进行进程等待
1.wait方法
2.waitpid方法 3.获取子进程status
五、进程程序替换
1.实现一个简单的进程程序替换
2.进程程序替换原理 3.进程程序替换的函数
4.进程程序替换函数应用场景 一、程序地址空间
1.C/C中的程序地址空间
我们在学习C/C中我们知道这样的空间布局图 我们如何去创建和访问变量呢 本质起始地址 偏移量其实我们的变量的类型就是偏移量 上面的这些是内存吗不是内存 我们下面来做一个小实验
#include stdio.h
#include unistd.h
#include stdlib.h
int g_val 0;
int main()
{pid_t id fork();if(id 0){perror(fork);return 0;}else if(id 0) { //childprintf(child[%d]: %d : %p\n, getpid(), g_val, g_val);}else{ //parentprintf(parent[%d]: %d : %p\n, getpid(), g_val, g_val);}sleep(1);return 0;
} 我们发现输出出来的变量值和地址是一模一样的很好理解呀因为子进程按照父进程为模版父子并没有对变量进行进行任何修改。可是将代码稍加改动。 #include stdio.h
#include unistd.h
#include stdlib.h
int g_val 0;
int main()
{pid_t id fork();if(id 0){perror(fork);return 0;}else if(id 0) { //childg_val 100;printf(child[%d]: %d : %p\n, getpid(), g_val, g_val);}else{ //parentprintf(parent[%d]: %d : %p\n, getpid(), g_val, g_val);}sleep(1);return 0;
} 我们发现父子进程输出地址是一致的但是变量内容不一样 我们可以得出下面的结论
OS必须负责将 虚拟地址 转化成 物理地址 。 2.进程地址空间
进程地址空间概念 上面的图就可以说明问题同一个变量地址相同其实是虚拟地址相同内容不同其实是被映射到了不同的物理地址。 那么我们如何去理解 虚拟地址相同但是物理地址不同呢
父进程有自己的虚拟地址也有自己的页表。子进程在被创建的时候父进程也会将页表拷贝给子进程。子进程在更改数据g_val 200时会发生写时拷贝物理地址改变了但是虚拟地址没有改变。 什么是地址空间什么是区域划分 我们在创建进程的时候不仅要有 pcb也要管理地址空间先描述在组织有一个 struct mm_struct 的结构体。 为啥要有地址空间 我们如何去理解 存在虚拟地址空间可以有效的进行进程内存的安全检查呢
我提一个问题我们 常量区的变量 为啥不能修改呢
我们页表中除了有映射外还有权限的限制当进程要修改常量区的变量时直接在页表就没有权限。 地址空间的补充
每个进程都有自己的页表。 二、进程创建
1.fork函数 在linux中fork函数时非常重要的函数它从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程。 进程调用fork当控制转移到内核中的fork代码后内核做: 当一个进程调用fork之后就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程看如下程序 #include stdio.h
#include unistd.h
#include stdlib.hint main()
{printf(Before: %d\n, getpid()); pid_t id fork();if(id 0){perror(fork);return 0;}else if(id 0) { //childprintf(child: %d \n, getpid());sleep(2);}else{ //parentprintf(parent: %d \n, getpid());sleep(2);}return 0;
}所以fork之前父进程独立执行fork之后父子两个执行流分别执行。注意fork之后谁先执行完全由调度器决定。 2.写时拷贝 通常父子代码共享父子再不写入时数据也是共享的当任意一方试图写入便以写时拷贝的方式各自一份副本。具体见下图 父子进程代码共享数据独有:当任意一方试图写入便以写时拷贝的方式拷贝一份副本 3.fork常规用法
一个父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程等待客户端请求生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从fork返回后调用exec函数。
4.fork调用失败的原因
系统中有太多的进程。实际用户的进程数超过了限制。 我们写一个多进程运行的一个场景
#include stdio.h
#include unistd.h
#include stdlib.h
#include string.h
#include errno.h#define N 10typedef void (*callback_t)(); // 函数指针void Worker()
{int cnt 10;while (cnt){printf(I am child process, pid: %d, ppid: %d, cnt: %d\n, getpid(), getppid(), cnt);sleep(1);cnt--;}
}void createSubPorcess(int n, callback_t cb)
{int i 0;for (i 0; i n; i){sleep(1);pid_t id fork();if (id 0){printf(create child process success: %d\n, i);// childcb();exit(0);}}
}//__StartCTR();int main()
{createSubPorcess(N, Worker);// 只有父进程走到这里sleep(100);return 0;
} 三、进程终止
1.进程终止的概念 main 函数的返回值可以被父进程获取的用来判断子进程的干活的情况 。 查看上一个进程的退出码 echo $? 我们父进程就可以通过这两个数字来判断子进程的退出情况 。 代码异常终止退出码就没有意义了 2.进程常见退出方法 3._exit函数 _exit函数 是系统调用函数。_exit函数 在终止进程的时候不会自动刷新缓冲区。 4.exit函数 exit函数 是库函数。exit函数 在终止进程的时候会自动刷新缓冲区。 exit最后也会调用_exit, 但在调用_exit之前还做了其他工作
执行用户通过 atexit或on_exit定义的清理函数。关闭所有打开的流所有的缓存数据均被写入。调用_exit。 使用_exit系统调用函数 使用exit系统调用函数 四、进程等待
1.什么是进程等待 通过 wait/waitpid 的方式让父进程一般情况对子进程进行资源回收等待过程 2.为啥要进程等待
之前讲过子进程退出父进程如果不管不顾就可能造成‘僵尸进程’的问题进而造成内存泄漏。另外进程一旦变成僵尸状态那就刀枪不入“杀人不眨眼”的kill -9 也无能为力因为谁也没有办法杀死一个已经死去的进程。最后父进程派给子进程的任务完成的如何我们需要知道。如子进程运行完成结果对还是不对或者是否正常退出。父进程通过进程等待的方式回收子进程资源获取子进程退出信息。 3.如何进行进程等待
1.wait方法 #include iostream
#include cstdlib
#include cstdio#include sys/wait.h
#include unistd.hvoid Worker()
{int *p NULL;int cnt 10;while (cnt){printf(I am child process, pid: %d, ppid: %d, cnt: %d\n, getpid(), getppid(), cnt--);sleep(1);}
}
const int n 10;int main()
{pid_t id fork();if (id 0){// childWorker();exit(1);}else{// fatherpid_t rid wait(NULL);if (rid id){printf(wait success, pid: %d, rpid: %d \n, getpid(), rid);}}return 0;
} 1、进程等待可以回收僵尸进程 2、如果子进程没有退出那么父进程会一直阻塞等待直到子进程僵尸了wait自动回收返回了。 看下面结果图发现当父进程调用了waitpid函数时父进程就被阻塞了阻塞期间当子进程运行完毕父进程才执行完毕所以只有子进程退出了父进程才会退出那么子进程就一定不是僵尸进程。 2.waitpid方法 如果子进程已经退出调用wait/waitpid时wait/waitpid会立即返回并且释放资源获得子进程退出信息。如果在任意时刻调用wait/waitpid子进程存在且正常运行则进程可能阻塞。如果不存在该子进程则立即出错返回。 #include iostream
#include cstdlib
#include cstdio#include sys/wait.h
#include unistd.hvoid Worker()
{int *p NULL;int cnt 10;while (cnt){printf(I am child process, pid: %d, ppid: %d, cnt: %d\n, getpid(), getppid(), cnt--);sleep(1);}
}
const int n 10;int main()
{pid_t id fork();if (id 0){// childWorker();exit(1);}else{// sleep(10);// fatherint status 0;pid_t rid waitpid(id, status, 0);printf(wait after\n);if (rid id){// 我们不能对status整体使用printf(wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n, getpid(), rid, status0x7F, (status8)0xFF);if (WIFEXITED(status)){printf(child process normal quit, exit code : %d\n, WEXITSTATUS(status));}else{printf(child process quit except!\n);}}}return 0;
}3.获取子进程status
wait和waitpid都有一个status参数该参数是一个输出型参数由操作系统填充。如果传递NULL表示不关心子进程的退出状态信息。否则操作系统会根据该参数将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16比特位。 五、进程程序替换 添加一个环境变量 1.实现一个简单的进程程序替换 #include stdio.h
#include stdlib.h
#include unistd.h
#include sys/types.h
#include sys/wait.hint main()
{printf(-------------------------------------------\n);execl(/usr/bin/ls,-l,-a,NULL);printf(-------------------------------------------\n);return 0;
} 我们查看运行结果发现进程程序替换后还有一个printf没有执行为啥呢我们下面来讲解一下进程程序替换的原理。 2.进程程序替换原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。 我们就可以解释 进程程序替换后还有一个printf没有执行为啥呢 3.进程程序替换的函数 六种进程程序替换函数 程序替换的本质 1、execl 进程程序替换函数 2、execlp 进程程序替换函数 3、execle 进程程序替换函数 4、execv 进程程序替换函数 5、execvp 进程程序替换函数 6、execve 进程程序替换函数 根据上面的规律我们可以总结出如下 这些函数原型看起来很容易混,但只要掌握了规律就很好记:
v(vector) : 参数用数组。p(path) : 有p自动搜索环境变量PATH。e(env) : 表示自己维护环境变量。l(list) : 表示参数采用列表。
#include unistd.h
int main()
{char *const argv[] {ps, -ef, NULL};char *const envp[] {PATH/bin:/usr/bin, TERMconsole, NULL};// 带l的表示list表示参数采用列表execl(/bin/ps, ps, -ef, NULL);// 带p的可以使用环境变量PATH无需写全路径execlp(ps, ps, -ef, NULL);// 带e的需要自己组装环境变量execle(ps, ps, -ef, NULL, envp);// 带v的表示vector表示参数采用数组execv(/bin/ps, argv);// 带p的可以使用环境变量PATH无需写全路径execvp(ps, argv);// 带e的需要自己组装环境变量execve(/bin/ps, argv, envp);exit(0);
} 4.进程程序替换函数应用场景 例如我们执行一个python的文件 test.py
print(hello python)
code.c
#include stdio.h
#include stdlib.h
#include unistd.h
#include sys/types.h
#include sys/wait.hchar* const argv[] {ls,-l,-a,NULL
};int main()
{pid_t id fork();if(id 0){// childprintf(I am a child , my PID:%d\n,getpid());execl(/usr/bin/python3,python3,test.py,NULL);// execl(/usr/bin/ls,ls,-l,-a,NULL);// execlp(ls,ls,-l,-a,NULL);// execv(/usr/bin/ls,argv);//execvp(ls,argv);printf(-------------------------------------\n);exit(0);} else {pid_t rid waitpid(-1,NULL,0);if (rid 0){printf(wait succes!! PID:%d\n,rid);}}return 0;
}