东莞能做网站的公司,推广软件工具,兰州的网站建设,单页的网站怎么做#x1f496;作者#xff1a;小树苗渴望变成参天大树#x1f388; #x1f389;作者宣言#xff1a;认真写好每一篇博客#x1f4a4; #x1f38a;作者gitee:gitee✨ #x1f49e;作者专栏#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法#x1f384; 如 果 你 … 作者小树苗渴望变成参天大树 作者宣言认真写好每一篇博客 作者gitee:gitee✨ 作者专栏C语言,数据结构初阶,Linux,C 动态规划算法 如 果 你 喜 欢 作 者 的 文 章 就 给 作 者 点 点 关 注 吧 文章目录 前言一、进程创建1.1写时拷贝(数据)1.2 代码层面1.3 fork的常规用法1.4 fork调用失败的原因 二、进程终止三、进程等待3.1 为什么要进程等待3.2进程等待怎么做的3.3 进程的非阻塞轮询 四、进程程序替换4.1看看什么是进程程序替换4.2替换函数 五、总结 前言
今天博主又来更好新的文章了我们之前把进程的概念已经讲完了相信大家对进程应该了解的差不多了今天我们来讲进程的另一个知识点—进程空间这节的难度比地址空间还理解一些在系统编程这一环节进程概念算是比较难的一座大山我们翻过去后面可以缓缓了所以这节难度不是很大这节我讲分为四个部分给大家讲解进程控制
本节重点 1.进程创建 2.进程 终止 3.进程等待 4.进程替换
一、进程创建
相信这个大家之前已经知道了我们在学习fork的时候就已经知道了这个函数就是创建进程的不知道的看这篇博客fork看完这篇博客在来理解我们后所讲的内容会比较好理解。
1.1写时拷贝(数据) 我们拿出之前的代码回顾一下
#includestdio.h
#includeunistd.hint main()
{printf(我是一个父进程\n);pid_t idfork();if(id0){while(1){printf(我是一个子进程pid:%d,ppid:%d\n,getpid(),getppid());sleep(1);}}else {while(1){printf(我是一个父进程pid:%d,ppid:%d\n,getpid(),getppid());sleep(1);}}return 0;
} 现在大家已经知道为什么一个变量可以接收两个返回值为什么要返回两次了所以这方面就不跟大家多解释了。 现在的问题是我们fork创建子进程后os都干了那些事 之前给大家解释过为什么父子进程的代码是共享的数据一开始也是共享的当子进程修改数据就是发生写时拷贝当时只是说这样做为了防止空间不足时拷贝的有些数据子进程暂时用不上就会造成那块空间会严重被占用所以采取写时拷贝现在我们也学了地址空间也知道内存为了节省空间做出的贡献接下来在深刻带大家来理解一下fork的写时拷贝 我们fork之后我们的os里面多了一个进程将代码和数据加载到内存中代码和数据都是从磁盘中加载来的但是我们通过fork创建的子进程是没有经过加载的是父进程在运行的时候创建的所以这个子进程不存在从磁盘中加载数据和代码到内存上的也就是说子进程没有自己的代码和数据所以子进程只能使用父进程的代码和数据因为进程之间有独立性意味着子进程使用父进程的代码和数据不能影响父进程 代码 因为在代码区页表有权限的限制所以代码是是只读的那么共享代码是不影响父进程的代码的所以代码共享符合进程之间的独立性原则的 数据可能会被修改所以父子进程的数据要分离开 数据有两种分离方式 1.创建子进程的时候os拿一块空间给子进程把父进程的数据拷贝一份给子进程 这样做不好的是第一、你创建的时候就把数据拷贝给你你能保证你立马就使用嘛如果有的数据长时间不使用就造成那块空间资源的浪费第二、不是所有的数据都会被使用到这样就会造成数据在内存种会有两份造成空间浪费第三、就算你使用到数据了能保证就一定是修改数据嘛万一是读取呢以上种种原因都导致数据不能再创建的时候就给子进程拷贝一份 2.写时拷贝法 上面那种用法都不能保证再内存的数据再任何是时刻都是一直在使用就不能保证内存的百分之百的有效使用率整机效率就没有那么高所以不需要把不会访问的或者只会读取的数据拷贝过去但是有时候子进程修改数据又不得不拷那什么样的数据值得拷贝过去呢 答os也不知道哪些数据可能会被修改就算使用一些方式例如const这些把值得拷的数据拷过去也会又遗漏又回到最开始的问题你能保证拷过去的数据会立即使用嘛所以提前拷贝过去没有必要通过以上的说明我们的os采取了写时拷贝技术但子进程确实要修改数据的时候os在创建空间将父进程的数据拷贝过去在进行修改这样就解决考虑上面说的资源浪费以及使用率问题 总结:为什么要使用写时拷贝技术 1.用的使用在给你分配时一种高效使用内存的表现防止占用空间不使用情况 2.os也不知道哪些数据将来会被子进程进行修改或者访问的。 因为刚给大家讲解了地址空间的知识大家应该明白os要想办法提高内存有效使用率这就导致写时拷贝技术的优点之前只是简单的说明了一下现在应该更加的清楚了。 1.2 代码层面
上面说了代码是共享此时的问题是fork之后是修改的数据的代码部分是共享的还是所有的代码都是共享的为什么 程序在加载到内存的时候都会有对应的地址然后由cpu通过虚拟地址映射到物理内存开始执行代码一个程序cpu以此不能直接运行结束有时间片当运行一半的时候就会发生中断让其他程序运行那么cpu怎么知道下次运行从哪个位置开始继续运行呢不可能从头开始重新运行的所以cpu内部里面有对应的寄存器存放当前运行代码的地址方便下次找到地址。在linux上这个寄存器叫做EIP(pc指针)程序计数器记录当前指令的下一条指令的地址这些寄存器上面的数据在进程被拿下来的时候会被进程自己给带走下次运行在读取出来这就叫进程的上下文数据在优先级那一篇说过,创建子进程也是进程也是需要被cpu调度的需要自己修改EIP在创建子进程的时候子进程的EIP就认为自己的起始值是fork之后的代码了cpu调用哪个进程就读取哪个进程的上下文数据通过EIP继续运行程序。 所以fork之后的所有代码都是父子共享
1.3 fork的常规用法
一个父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程等待客户端请求生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从fork返回后调用exec函数
1.4 fork调用失败的原因
系统中有太多的进程实际用户的进程数超过了限制
二、进程终止
这个话题不难理解我会使用一些例子带大家去理解的进程终止从字面意思就是将进程杀掉接下来我将解决三个问题来解决进程终止这个话题 1.进程终止后操作系统做了哪些事 创建进程就是os给程序分配相应的资源创建内核数据结构进程终止就是释放进程申请的相关内核数据结构和对应的代码数据—本质就是释放系统资源 2.进程终止的常见方式 1.代码跑完结果正确 2.代码跑完结果不正确 3.代码没跑完程序崩溃了 先讲前两个大家有没有发现我们在写c/c代码的时候在main函数最后都喜欢写一个 return语句默认都写0返回的含义是什么不返回0可不可以 返回的含义是告诉父进程用来评判结果用的。 我们父进程创建子进程是希望子进程帮父进程做一些事情的事情完成的成功需要让父进程知道这样才有意义就好比领导让你做事他是关心这件事结果的。不总是返回0 如果返回0就表示结果正确非0有很多个每个非0都表示一个错误信息Linux默认的错误信息是134个为什么要有错误信息这样是方便父进程快速定位错误是什么就好比你考试没过你父亲肯定关心你为什么没过。(使用strerror查看错误信息对应的字符串) 使用echo $?查看最近依次进程的退出码 我们的ls指令也是一个进程通过这个例子大家应该知道return的含义大致是什么了吧。因为我们平时写的代码运行是我们自己的行为没有父进程关心所以没人关心你为什么错误导致你可以直接写返回0. 我们的退出码也是可以自己规定的我们之前写的通讯录打开文件文件打开失败就返回-1他又自己的含义。 第三种代码没跑完我们有时候在运行代码的时候程序崩溃了此时你看运行窗口退出代码不是0说明程序崩溃了因为此时代码没有运行到return语句就终止了 3.用代码演示一个进程终止 什么是一个正确的进程终止
在main函数内部使用return就是终止进程 在其他函数里面使用return不算进程终止只是作为返回值返回给调用他的函数exit函数这也是一个是进程终止的函数我们来看看这个函数怎么使用
#includestdio.h
#includeunistd.h
#includestring.h
#includestdlib.h
int main()
{printf(hello,world\n);printf(hello,world\n);printf(hello,world\n);exit(1);printf(hello,world\n);printf(hello,world\n);printf(hello,world\n);return 0;
}运行到exit时进程就退出了退出码也是1我们在来写一个函数看看 #includestdio.h
#includeunistd.h
#includestring.h
#includestdlib.h
int sum(int top)
{int i0;int sum0;for(i0;itop;i){sumi;}exit(2);return sum;
}
int main()
{printf(sum:%d,sum(100));return 0;
}我们讲sum函数返回之前加一个exit函数看看什么效果 通过上面的例子我们的return之后再main内部才会终止程序而exit再程序的任意位置都会终止程序 _exit函数这也是一个终止程序的接口。 通过头文件大家应该知道exit是库函数_exit是系统函数那这两个有什么区别呢一起来验证一下
#includestdio.h
#includeunistd.h
#includestring.h
#includestdlib.h
int sum(int top)
{int i0;int sum0;for(i0;itop;i){sumi;}_exit(2);return sum;
}
int main()
{printf(sum:%d\n,sum(100));return 0;
}这种用法是和exit是一样的效果 我们来看看这样的代码 不加\n会先刷新到缓冲区等缓冲区满了或者程序终止后刷新出来
int main()
{printf(hello,world);sleep(3);exit(3);return 0;
}int main()
{printf(hello,world);sleep(3);_exit(3);return 0;
}通过这两个例子对比我们从exit的退出前把缓冲区的数据刷新出来了再进行退出了来看下面这个图 通过上面的例子也间接说明的缓冲区是标准库在维护而不是系统如果是系统维护那么_exit也可以刷新出来。 至此我们的进程终止也就讲解完毕为什么要先说这个了因为在进程等待的讲解需要用到这个知识所以提前说下一节就是进程等待
三、进程等待
我们上一节讲过进程终止这节单出来讲一会旧讲完了就演示一下进程终止啥样子就行了那博主为什么要花时间讲解终止的情况呢讲解退出码的含义呢原因就是为进程等待做铺垫进程等待事一个非常重要的环节他可以让父进程知道子进程的退出信息计算机里面的进程有很多但是划分出来就是一些父子关系所以这节对我们很重要话不多说我们开始介绍进程等待。
3.1 为什么要进程等待
大家有没有看过我之前写过的一篇关于僵尸进程的博客就是子进程比父进程先退出然后父进程得不到子进程的反馈才会造成僵尸进程而僵尸进程连kill -9 都杀不掉我们只有使用进程等待才可以解决这个问题我们先来演示一下僵尸进程看代码
#includestdio.h
#includeunistd.h
#includestdlib.h
int main()
{pid_t idfork();//创建子进程if(id0){perror(fork);//子进程创建失败通过perror这个函数将错误信息打印出来return 1;}else if(id0){//子进程int cnt5;while(cnt){printf(i am child ,pid:%d ,ppid:%d ,cnt:%d\n,getpid(),getppid(),cnt);cnt--;sleep(1);}exit(0);//五秒后退出此时父进程还在运行}else {//父进程 int cnt10;while(cnt){printf(i am father ,pid:%d ,ppid:%d ,cnt:%d\n,getpid(),getppid(),cnt);cnt--;sleep(1);}}return 0;
} 上面的代码大家并不感到陌生这是我们以前经常写的代码此时的僵尸进程使用信号都杀不掉只有等整个程序结束他才退出有人说这也没关系最后在一起退出呗但是像服务器这种可能很长时间都不终止成为僵尸进程后他还是一个进程也是占用资源的这样就导致资源浪费所以我们要想办法在子进程先退出的情况下我们要进程获取他的资源将其释放。 wait() 我们要介绍这两种系统等待函数通过第一个函数来讲解进程等待的必要性通过第二种来讲解他事怎么做到的第一个和第二种里面的参数事一样的所以在介绍第一个的时候就不具体介绍里面的参数了可以给默认值null
wait的使用方法 我们的返回值是等待的子进程的进程号里面的参数先设置为空一会在waitpid介绍,来看代码 (1)单个子进程
#includestdio.h
#includeunistd.h
#includestdlib.h
#includesys/wait.h
int main()
{pid_t idfork();//创建子进程if(id0){perror(fork);//子进程创建失败通过perror这个函数将错误信息打印出来return 1;}else if(id0){//子进程int cnt5;while(cnt){printf(i am child ,pid:%d ,ppid:%d ,cnt:%d\n,getpid(),getppid(),cnt);cnt--;sleep(1);}exit(0);//五秒后退出此时父进程还在运行}else {//父进程 int cnt10;while(cnt){printf(i am father ,pid:%d ,ppid:%d ,cnt:%d\n,getpid(),getppid(),cnt);cnt--;sleep(1);}pid_t retwait(NULL);if(ret0)//当个子进程wait就是等待这个一个子进程等待成功返回的就是这个子进程的进程号,进程号是大于0通过打印来看看是不是子进程的pid{printf(wait sucessful,pid:%d\n,ret);}sleep(5);}return 0;
}我们可以看到在wait之后我们的僵尸进程被回收了这是因为父进程里面有循环所以僵尸进程不能立马被回收但是wait确实可以解决我们父进程还在运行的时候就可以干掉子进程僵尸的行为。 2多个子进程 我们的父进程创建子进程可以像创建多个分别干不同的是事那我们先创建多个进程一起退出的场景
#includestdio.h
#includeunistd.h
#includestdlib.h
#includesys/wait.hvoid runChild()
{int cnt 5;while(cnt){printf(I am Child Process, pid: %d, ppid:%d\n, getpid(), getppid());sleep(1);cnt--;}
}
int main()
{int i0;for(i0;i10;i){pid_t idfork();if(id0){runChild();//子进程做自己的事exit(0);}printf(create child process: %d success\n, id); // 这句话只有父进程才会执行}sleep(10);return 0;
}我们来看看这么多子进程要如何让进程等待呢wait是等待任意一个进程的。
#includeunistd.h
#includestdio.h#includestdlib.h#includesys/wait.hvoid runChild(){int cnt 5;while(cnt){printf(I am Child Process, pid: %d, ppid:%d\n, getpid(), getppid());sleep(1);cnt--;}}int main(){int i0;for(i0;i10;i){pid_t idfork();if(id0) { runChild();//子进程做自己的事exit(0); } printf(create child process: %d success\n, id); // 这句话只有父进程才会执行} for(i 0; i 10; i)//等待{// wait当任意一个子进程退出的时候wait回收子进程pid_t id wait(NULL);if(id 0){printf(wait %d success, childid:%d\n,i1,id);}}return 0;}我们发现在监视窗口都没有看到子进程的僵尸状态原因是刚进入僵尸状态就被等待回收了而最上面的单个进程有循环所有没有及时回收。 2任意一个进程都没有退出 我们将runChild的循环改成死循环这样就会造成进程阻塞父进程一直在等子进程退出所以阻塞不一定是等待硬件资源还有可能等待软件资源。 父进程默认在wait的时候子进程一直不退出然后调用这个系统接口的时候也就不返回这个在将原理的再说。 总结通过上面的案例展示我们的进程等待是必须的父进程通过进程等待避免了刀枪不入的僵尸进程回收子进程资源获得子进程退出信息
3.2进程等待怎么做的
我们之前说过创建子进程是父进程想要他帮自己做事情那事情做的咋样我父进程是不是要知道一下结果对不对或者是否正常退出得让父进程知道对吧所以接下来我就开始介绍另一个进程等待函数。
waitpid的使用方法
前面的wait只是为了讲解为什么要进程等待我们使用waitpid会更好他的功能更强大 他的返回值和wait是一样的都是返回等待子进程的进程号他有三个参数第二个和第三个默认传NULL和0 1第一个参数 pid Pid-1,等待任一个子进程。与wait等效。 Pid0.等待其进程ID与pid相等的子进程 2第二个参数 这是我们重点介绍的 这个参数是一个输出型参数希望通过这个参数将内部的数据带出来这个我们在c语言刷题的时候经常会遇到就好比两个数字的交换传地址是一样的道理我们这个参数是int类型的但是分成了号几个部分。
我们先来看看使用通过单个子进程简单演示一下好看结果 我们看到status是256一点头绪都没有我们的第二个参数就是获取子进程终止的信息大家还记得我们在进程终止的时候说过的三种退出状态吧 所以我们的status他的数字就可以代表子进程这三种退出状态的信息 我们的int有32位我们只使用低十六位0-7这八位表示进程是否正常退出8-15表示进程正常退出结果对不对也就是退出码我们进程异常退出都是接收到信号才会异常退出的比如除0空指针这都是有对应的信号我们来看看有哪些信号; 我们的Linux有64个信号而0-6位刚好可以表示这64个信号而刚好没有0信息所以0就表示正常退出第7位等我们到信号章节在继续介绍 在上面进程终止介绍到我们的退出码有130多个 既然这样那我们上面的status为什么出现256我们来分析一下 这个结果就是256我们使用位操作将低第八位和次第八位取出来 printf(wait sucessful,pid:%d,exit_signl:%d,exit_code:%d\n,ret,status0x7F,(status8)0xFF); 我们来修改一下退出码 让他异常一下 信号是8刚好是浮点数异常导致的当出现异常我们的退出码就默认是0 我们发现这样使用按位获取太麻烦了所以库里面给我们提供了宏函数。 printf(wait sucessful,pid:%d,exit_signl:%d,exit_code:%d\n,ret,WIFEXITED(status),WEXITSTATUS(status) WIFEXITED(status): 若为正常终止子进程返回的状态则为真。查看进程是否是正常退出 WEXITSTATUS(status): 若WIFEXITED非零提取子进程退出码。查看进程的退出码 效果就不给大家进行演示了。 进程等待失败的演示 这个很简单当我们的等待不是自己的子进程就会等待失败
原理 为什么父进程要获得子进程的任意数据要通过wait和waitpid去获得因为进程之间事独立的事不知道对方的存在的所以我们说有父子关系这种关系也事操作系统给的他两想获取数据必须通过操作系统的接口去获得这也侧面体现为什么要进程等待。
3.3 进程的非阻塞轮询
我们上面说过阻塞等待如果等待的时候子进程一直不退出就会造成阻塞等待这样父进程就会一直等着因为等待不一定再父进程的最后面他后面可能还有其他的事要做这样的阻塞等待就不好所以我们需要进程非阻塞等待博主接下来要将小故事 第一次数据库考试 小张是一个学霸小李是一个学渣但两个人是好朋友一个住在八层一个再楼下到考试的时候小李很慌张想要找小张复习但又不想爬楼这时候小李就打电话给小张能不能带我复习小张说没问题但是要等我复习完小李说好过了一分钟小张还没有下来小李又电话给小张小张说还没有就这样小李打了十几个电话小张才下来了打电话问小张这个过程就是等待查询父进程也不知道子进程有没有退出所以去检查挂掉电话就是返回的过程这就是非阻塞打了十几个就叫轮询 第二次数据结构考试 小李学聪明了但还是要找小张复习上次打了十几个电话浪费了好多电话费这次打电话就说你不要挂等你复习好下来了再挂此时一直没有挂那么就相当于等待没有返回此时就一直是等待状态也就是阻塞等待 第三次操作系统考试 小李心想上两次太浪费时间了我再下面等他的时候复习复习然后再问问他小张就这样小李打一个电话问一下挂掉自己复习四五分钟再打这样的过程就是非阻塞轮询做自己的事情第一次考试的非阻塞轮询没啥意义 总结非阻塞轮询加做自己的事情才是我们想要的结果
怎么做到 3第三个参数我们上面演示过waitpid的返回值大于就是等待成功小于0就是等待失败还有一个0这是等待成功了只是子进程没结束而已 options:传的是一个宏函数 WNOHANG: 若pid指定的子进程没有结束则waitpid()函数返回0不予以等待。若正常结束则返回该子进 程的ID。 我们来通过代码演示一下
#includestdio.h
#includeunistd.h
#includestdlib.h
#includesys/wait.h
int main()
{pid_t idfork();//创建子进程if(id0){perror(fork);//子进程创建失败通过perror这个函数将错误信息打印出来return 1;}else if(id0){//子进程int cnt5;while(cnt){printf(i am child ,pid:%d ,ppid:%d ,cnt:%d\n,getpid(),getppid(),cnt);cnt--;sleep(1);}exit(12);//五秒后退出此时父进程还在运行}else {//父进程 while(1)//轮询{int status0;pid_t retwaitpid(-1,status,WNOHANG);if(ret0)//当个子进程wait就是等待这个一个子进程等待成功返回的就是这个子进程的进程号,进程号是大于0通过打印来看看是不是子进程的pid{printf(wait sucessful,pid:%d,exit_signl:%d,exit_code:%d\n,ret,status0x7F,(status8)0xFF);break;}else if(ret0) {printf(wait failed\n);break;}else {//ret0,检查子进程退出状态printf(你好了没子进程还没有退出我在等等...\n);sleep(1);}}sleep(5);}return 0;
} 上面的代码我们只是实现了非阻塞轮询接下来就介绍怎么加做自己的事情
我们可以将父进程自己的放再else里面但是不太规范接下来写一个比较规范的
#includestdio.h
#includeunistd.h
#includestdlib.h
#includesys/wait.h
#define TASK_NUM 10typedef void(*task_t)();
task_t tasks[TASK_NUM];void task1()
{printf(这是一个执行打印日志的任务, pid: %d\n, getpid());
}void task2()
{printf(这是一个执行检测网络健康状态的一个任务, pid: %d\n, getpid());
}void task3()
{printf(这是一个进行绘制图形界面的任务, pid: %d\n, getpid());
}int AddTask(task_t t);// 任务的管理代码
void InitTask()
{int i0;for(i 0; i TASK_NUM; i) tasks[i] NULL;AddTask(task1);AddTask(task2);AddTask(task3);
}int AddTask(task_t t)
{int pos 0;for(; pos TASK_NUM; pos) {if(!tasks[pos]) break;}if(pos TASK_NUM) return -1;tasks[pos] t;return 0;
}void DelTask()
{}void CheckTask()
{}void UpdateTask()
{}void ExecuteTask()
{int i0;for(i 0; i TASK_NUM; i){if(!tasks[i]) continue;tasks[i]();}
}
int main()
{pid_t idfork();//创建子进程if(id0){perror(fork);//子进程创建失败通过perror这个函数将错误信息打印出来return 1;}else if(id0){//子进程int cnt5;while(cnt){printf(i am child ,pid:%d ,ppid:%d ,cnt:%d\n,getpid(),getppid(),cnt);cnt--;sleep(1);}exit(12);//五秒后退出此时父进程还在运行}else {InitTask();while(1)//轮询{int status0;pid_t retwaitpid(-1,status,WNOHANG);if(ret0)//当个子进程wait就是等待这个一个子进程等待成功返回的就是这个子进程的进程号,进程号是大于0通过打印来看看是不是子进程的pid{printf(wait sucessful,pid:%d,exit_signl:%d,exit_code:%d\n,ret,status0x7F,(status8)0xFF);break;}else if(ret0) {printf(wait failed\n);break;}else {printf(你好了没子进程还没有退出我在等等...\n);ExecuteTask();usleep(500000);}}sleep(5);}return 0;
} 我们的非阻塞轮询和做自己的事情哪个重要答案是非阻塞轮询做自己的事情是顺带的所以自己的事情一般都是轻量化的而且子进程退出成僵尸状态我晚一点回收也是可以的叫做延时回收非阻塞轮询就是等待回收子进程所以两者各推一步可以晚点回收但你做自己的事情不要太重简单点就行了。 最后再说一下创建多个子进程谁先调度不知道有调度器管理但是父进程肯定要最后一个终止这个大家记住就好了到这里我们的进程等待就讲解完毕了大家要看看的自己敲敲代码感受一下接下来博主就开始讲解进程的程序替换也是一个比较难理解的话题。
四、进程程序替换
这一节再理解难度不是很大但是内容非常多而且前后关联很大再将一些疑惑的时候都需要理解前面的一些知识才能很好的理解但是主要的大家还是可以理解的话不多说我们开始进入正文的讲解
4.1看看什么是进程程序替换
我们首先看看进程程序替换是什么给大家演示一下然后给大家讲解原理。 我们进行程序替换要使用到进程替换函数exec系列函数我先以其中一个做例子给大家演示execl
来看代码 (1)本身进程调用
int main()
{printf(before: I am a process, pid: %d, ppid:%d\n, getpid(), getppid());execl(/usr/bin/ls,ls,-a,-l,NULL);printf(after: I am a process, pid: %d, ppid:%d\n, getpid(), getppid());return 0;
}大家看到我们使用execl函数把我们ls -al 程序执行出来了 2子进程调用
#includestdio.h
#includeunistd.h
#includestdlib.h
#includesys/wait.h
int main()
{pid_t idfork();if(id0){printf(before: I am a process, pid: %d, ppid:%d\n, getpid(), getppid());execl(/usr/bin/ls,ls,-a,-l,NULL);printf(after: I am a process, pid: %d, ppid:%d\n, getpid(), getppid());exit(1);}pid_t retwaitpid(id,NULL,0);if(ret0) printf(wait success, father pid: %d, ret id: %d\n, getpid(), ret);sleep(1);return 0;
} 通过现象可以看到我们的进程再遇到execl函数之前的代码都可以运行但是到execl函数之后就换成了其他程序而这上面两个例子的特点就是execl后续的代码没执行出来。这是为什么呢我们来看原理其实很简单 通过上面的两个例子子进程被程序替换后不会影响父进程这是写实拷贝技术也可以说明进程之间是独立的之前说过写实拷贝是针对数据的那我们程序替换后代码应该也被修改了才能执行替换后的数据而代码是只读不可修改的啊这是怎么替换的答案是代码是不可以被修改的但是要看人操作系统是不允许用户去修改代码的他不信任任何人但是我们这次是调用系统调用函数就相当于操作系统自己去操作他想修改就修改所以写实拷贝再代码和数据层面都会体现 解决疑惑 我们创建子进程execl后的代码被替换那么说明exit函数也被替换掉了那父进程怎么还是可以去等待呢大家可以想想有没有这个exit函数对于子进程的退出效果是不是都是一样的都是再函数的结尾就结束就算没有exit函数子进程运行到最后也会自动退出符合进程终止的三种情况之一那么就可以被等待。所以大家不要太死板了。 我们看到第二个示例代码的打印结果我们再进行程序替换后有没有创建新的进程答案是没有的我们看到等待之后获取的进程好肯定是子进程终止后获得的但是进程替换是再子进程终止之前的操作而使用execl函数的子进程号和等待后获得的子进程号是一样的说明使用的同一个进程task_struct,大家还有一种猜想就是旧的子进程立马被销毁创建新进程刚好进程号和之前的子进程号是一样的这个是不存在的旧的子进程一旦被销毁等待立马检测到直接先把等待成功给打印出来再来执行新进程。但是结果去不是这样的。所以说明进程替换没有创建新进程。 补充
调用后的代码没有被执行的原因是被替换了只有exec函数调用失败才会执行后续的代码所以说明exec函数只有失败的返回值成功时候没有返回值返回也不知道给谁接收Linux中形成可执行程序的时候是有格式的ELF(可执行程序的表头)刚才的程序替换实际上一开始不会把ls程序的代码和数据立马替换表中有程序入口的地址先把ls的表头替换过去等调用成功再将数据和代码替换掉失败就表头地址替换失败一会介绍每个参数的含义的时候就知道为什么会失败。
4.2替换函数
我们的替换函数实际有7个每个含义都不一样但是都有相似之处来看文档 只讲解我圈中的五个其余两个类似
1execl
l(list) : 表示参数采用列表 大家看到一个非常熟悉的三个点这是一个可变参数我们的第一个参数是传你要替换程序的位置得让execl函数找到后面介绍的四个函数第一个参数都是这个意思为了找到后面传的是命令行参数的格式我们再命令行咋输入就是咋传参的告诉execl要怎么去执行这个程序 我们再介绍环境变量的博客中介绍到命令行输入的参数最后都是一串字符串字符串后面默认是NULL等会再介绍第三个函数的时候就更好理解了。 使用演示execl(“/usr/bin/ls”,“ls”,“-a”,“-l”,NULL); 注意usr前面的/不能丢 2execlp p(path) : 有p自动搜索环境变量PATH 通过参数我们也能看出path是路径file只需要传一个文件就行了。 这个函数和前面第一个函数几乎一模一样就在第一个参数上修改了一下第一函数给了要替换函数的具体地址而这个函数是通过PATH去找到PATH就是环境变量 我们使用PATH替我们去找就可以了 使用演示execlp(ls,ls,-a,-l,NULL); 3execv
v(vector) : 参数用数组 第一个参数还是传替换程序的具体路径因为没有加p,第二个参数传一个字符串指针数组 我们只需要把刚才再命令行输入的参数写在一个数据里面就行了用NULL结束就可以。 使用演示 char*const argv[]{ ls, -a, -l, NULL }; execv(/usr/bin/ls,argv); 大家应该看到我们为什么要传一个NULL了吧可以理解把他当作一个结束标志。 通过这个参数列表 int execv(const char *path, char *const argv[]);我们发现了一个非常熟悉的argv,这个不是和我们命令行参数传参一个道理吗我们替换的程序ls是一个程序是不是也要有函数的入口main也就可以接收命令行参数其实我们的bash是操作系统的子进程再bash上跑的都是bash的子进程那我们命令行运行程序的时候其实就类似于调用了exec函数来运行我们的程序所以exec起到的加载器的效果我们系统的内部就已经把我们的第一个参数和命令行参数传给对应的exec函数今天我们只是显示的把exec函数写出来了那我们可以传命令行参数说明环境变量也是可以传的一会再介绍第五个函数的时候再说。
4execvp 这个函数我就不做过多的解释了有vp,所以直接给大家看看使用演示char*const argv[]{ ls, -a, -l, NULL }; execvp(ls,argv);
5execle 再讲解这个函数之前我要先给大家演示跨语言调用。
1. 一次编译多个程序 我们的makefile暂时只能一次编译一个程序
mycommand:mycommand.cgcc -o $ $^
.PHONY:clean
clean:rm -f mycommand 如果我们想要同时编译形成两个可执行文件呢
otherfile:otherfile.cppg -o $ $^
mycommand:mycommand.cgcc -o $ $^
.PHONY:clean
clean:rm -f mycommand otherfile谁在前谁先被编译那我们怎么解决这种问题呢使用伪目标就可以了
.PHONY:all
all:otherfile mycommand otherfile:otherfile.cppg -o $ $^
mycommand:mycommand.cgcc -o $ $^
.PHONY:clean
clean:rm -f mycommand otherfile2. 跨语言调用 1c调用py
#!/usr/bin/python3print(hello python);execl(/usr/bin/python3, python3, test.py, NULL);(2)c调用shell脚本
#!/usr/bin/bash
function myfun()
{cnt1while [ $cnt -le 10 ]doecho hello $cntlet cntdone
}echo hello 1
echo hello 1
echo hello 1
echo hello 1
echo hello 1
echo hello 1
echo hello 1
ls -a -l
myfunexecl(/usr/bin/bash, bash, test.sh, NULL);(3)C调用cpp
//otherfile.cpp
#includeiostream
using namespace std;
int main(int argc, char *argv[])
{cout argv[0] begin running endl;cout 这是命令行参数: \n;for(int i0; argv[i]; i){cout i : argv[i] endl;}return 0;
}//mycommand.cchar*const argv[]{otherfile,-w,-z,NULL };execv(./otherfile,argv);我们发现命令行参数确实传下来了我们来看看环境变量会不会传下来
//otherfile.cpp
#include iostream
using namespace std;
int main(int argc, char *argv[], char *env[])
{cout argv[0] begin running endl;cout 这是命令行参数: \n;for(int i0; argv[i]; i){cout i : argv[i] endl;}cout 这是环境变量信息: \n;for(int i 0; env[i]; i){cout i : env[i] endl; }cout argv[0] stop running endl;return 0;
}//mycommand.cchar*const argv[]{otherfile,-w,-z,NULL };execv(./otherfile,argv);我们发现我没有传环境变量啊怎么也可以把环境变量打印出来呢 我们给bash设置一个特有的环境变量然后再父进程里面创建特有的环境变量看看替换后程序里面能不能获得环境变量
bash新增环境变量 也被替换程序获得到了父进程创建环境变量 结论环境变量是什么时候给进程的再进程地址空间的时候我们看到环境变量和命令行参数再虚拟地址上环境变量也是数据创建子进程的时候子进程也有自己的地址空间此时的环境变量就会有父进程传下去通过再爷父进程中添加特有的环境变量发现也替换的程序继承下去了**所以说明我们再进行程序替换的过程中环境变量是没有被替换的。** 介绍我们的第五个函数execle 最后一个参数就是传我们的环境变量的字符串指针数组我们可以传系统自带的
extern char** environ;//声明一下 execle(./otherfile,otherfile,-a,-b,NULL,environ);再父类里面自己写环境变量传进去 结论出来了采用的覆盖而不是追加而environ这个里面本来就有一开始的环境变量所以看不出来是覆盖的感觉。 总结一下我们一开始就说exec函数有七个但是其中六个再3号i手册里面还有一个execve再2号手册3号手册里面的是库函数而2号手册里面是系统函数所以这六个最终都是调用execve这个函数 至此我们的程序替换旧讲解完毕了。
五、总结
博主将进程控制放在一篇博客里面写了原因是前后关联性很大大家越阅读到后面越兴趣对学习效率也好里面的细节还是很多的理解难度我解决还没有进程地址空间难度大接一下的一篇博主就使用这节知识带大家写一个自定义shell程序达到和系统的差不多功能让大家可以更好的理解shell是怎么做到的此前大家一定要熟悉这篇的知识点我们下篇再见。