南坪网站建设,做网站开发哪里好,wordpress 输出标签id,建筑公司招聘信息目录前言一、进程创建1.1 fork函数初识1.2 写时拷贝1.3 fork常规用法1.4 fork调用失败的原因二、进程终止2.1 进程终止时#xff0c;操作系统做了什么#xff1f;#xff1f;2.2 进程终止的常见方式有哪些#xff1f;#xff1f;2.3 如何用代码终止一个进程三、进程等待3.…
目录前言一、进程创建1.1 fork函数初识1.2 写时拷贝1.3 fork常规用法1.4 fork调用失败的原因二、进程终止2.1 进程终止时操作系统做了什么2.2 进程终止的常见方式有哪些2.3 如何用代码终止一个进程三、进程等待3.1 进程等待的必要性3.2 进程等待的方法wait方法waitpid方法补充知识四、waitpid进一步讲解五、进程替换5.1 概念及原理5.2 操作5.2.1 不创建子进程execl5.2.2 创建子进程execvexeclpexecvpexecle 执行其他的C二进制程序执行其他语言的程序5.3 execve六、自己实现一个简易的shell程序6.1 函数介绍6.2 myshell.c完整代码总结前言
前面的文章都是关于进程概念的学习今天我们就要进入一个新的章节关于进程控制的解析这其中我会穿插着前面已经讲过的知识在以前知识的基础上学习新的知识如果其中有哪里不懂的地方大家可以去看一看我前面的文章下面我们一起来学习新的知识吧。
一、进程创建
1.1 fork函数初识
在Linux中fork函数是非常重要的函数它从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程。 进程调用fork当控制转移到内核中的fork代码后内核做: 1.分配新的内存块和内存数据结构给子进程 2.将父进程部分数据结构内容拷贝至子进程 3.添加子进程到系统进程列表中 4.fork返回开始调度器调度 fork之后父子进程代码的共享情况 1.2 写时拷贝
通常父子代码共享父子在不写入时数据也是共享的当任意一方试图写入便以写时拷贝的方式变为各自一份副本,具体可见下图 写时拷贝的好处: 因为有写时拷贝技术的存在所以父子进程得以彻底分离!完成了进程独立性的技术保证! 写时拷贝是一种延时申请技术可以提高整机内存的使用率
1.3 fork常规用法
1.一个父进程希望复制自己使父子进程同时执行不同的代码段。 2.一个进程要执行不同的程序
1.4 fork调用失败的原因
1.系统中有太多的进程 2.实际用户的进程数超过了限制(我们平时在写代码的时候进程数其实是有限的)
二、进程终止
2.1 进程终止时操作系统做了什么
当然是释放进程申请的相关内核数据和对应的数据和代码本质就是释放系统资源。
2.2 进程终止的常见方式有哪些
a.代码跑完结果正确 b.代码跑完结果不正确 c.代码没有跑完程序崩溃了(这部分涉及到了信号部分我们先说一点点后面再详细的说) 这里我们主要分析a、b两条内容我先提一个问题: 大家平时在写C/C程序的时候一定都会写main函数main函数最后都会有return 0那么这里的return 0的含义是什么呢为什么总是0main函数返回的意义是什么 在做下面的实验前我再补充一个小的知识点 echo $?:获取最近一个进程执行完毕的退出码。 现在相信大家已经知道main函数最后的return 0是什么含义了每一个退出码都有着不同的含义我们通过下面的这个函数把C语言中的退出码是什么含义打印一下看看。 我们今天学习完退出码的概念大家以后写代码的时候要可以将其应用起来大家一起看下面的这个例子
int main()
{FILE* fp fopen();if(fp NULL)return 1;return 0;
}当我们需要打开一个文件的时候就可以使用上述的if判断语句来判断我们的这个文件是否被成功打开当程序的退出码为1的时候就是不允许操作即这个文件打开失败这样就可以更快地排查错误。 a、b两条内容分析完后我们还要谈一谈c的内容代码没有跑完程序崩溃了。
2.3 如何用代码终止一个进程
什么是一个正确的终止 1.return main函数内的return语句就是终止进程的return 退出码 只有在main函数返回进程退出 调用一些其他的函数return则是退出当前的函数并返回一个值。 2.exitC语言层面的函数 通过上面的情况再与return进行比较我们可以发现: return只有在main函数中有结束进程的作用exit在代码的任何位置调用都表示直接终止进程!!! 3._exit系统层面的函数 上面图片中的_exit与_Exit的作用类似我们只用_exit来举例子让大家看一下与_exit与exit有什么不同之处
从图中大家可以看到_exit()与exit()的区别在平时写代码的时候还是建议大家使用exit()函数。
三、进程等待
在讲述进程等待前我们先来试想两种场景 1.子进程退出父进程不管子进程子进程就要处于僵尸状态一直处于僵尸状态不释放资源就会导致内存泄漏 2.父进程创建了子进程是要让子进程完成一些任务的那么子进程把任务完成得怎么样?父进程需不需要关心如果需要怎么得知子进程的情况如果不需要又要怎么处理呢? 子进程把任务完成的情况也是分为三种情况: a.代码跑完结果正确 b.代码跑完结果不正确 c.代码没有爬完程序崩溃了
3.1 进程等待的必要性
1.子进程退出父进程如果不管不顾就可能造成僵尸进程的问题进而造成内存泄漏。另外进程一旦变成僵尸状态那就刀枪不入即使是kill -9也无能为力因为谁也没有办法杀去一个已经死去的进程。 2.最后父进程派给子进程的任务完成的如何我们其实是需要知道的比如子进程运行完成后结果对不对或者最后是否正常退出都是我们需要考虑的问题 父进程通过进程等待的方式回收子进程资源获取子进程退出信息
3.2 进程等待的方法
我们先来看一下不让父进程去回收子进程的资源的情景来进行一下对比 在上面的代码中我们使用fork()函数创建了一个子进程然后让其循环五次打印同一行代码循环结束后结束进程而父进程则一直进行循环这样当子进程结束后父进程还在死循环无法收集子进程的资源那么子进程就将成为僵尸进程。下面我们来看结果
这样就导致了僵尸进程的产生那么如何解决这个问题呢?
wait方法
主要验证回收僵尸进程的问题 wait接口我们可以通过man 2 wait来查询关于wait的信息 当然其中也有waitpid的信息。 下面我们先来简单认识一下wait() 下面我们来看一下实验代码以及运行结果 实验一:我们先来看一下如果父进程属于睡眠状态不调用wait函数子进程是不是还是变为僵尸进程 运行结果分析 实验二:当父进程不处于睡眠状态时又是一种怎样的情景 运行结果分析
父进程:wait时父进程处于阻塞式等待,父进程处于阻塞态等子进程退出。当子进程退出时操作系统再将父进程唤醒放到运行队列里由父进程调用wait函数再返回。 父进程处于阻塞式的等待意味着: 子进程不退出父进程也不退出
waitpid方法 进程异常退出或者崩溃本质是操作系统杀掉了你的进程 操作系统如何杀掉的呢本质是通过发送信号的方式 下面我们来看一下实验代码以及运行结果 实验一:子进程正常结束 运行结果 实验二:子进程非正常结束(1) 注: 当进程非正常结束时进程的退出码就没有任何意义主要观察进程收到的信号编号 运行结果 关于信号的信息我们可以通过kill -l(小写的l)来查看 其中1~31为普通信号没有0号信号 实验二:子进程非正常结束(2) 运行结果 实验二:子进程非正常结束(3) 程序异常不光光是内部的代码有问题也可能是外力杀掉了子进程(子进程代码有没有跑完并不确定) 上面的代码我们让子进程死循环那么这个进程就会一直运行 此时我们在另一个窗口将该进程杀掉 最后父进程收到的信号编号就是9号信号 总结: 1.如果子进程已经退出调用wait/waitpid时wait/waitpid会立即返回并且释放资源获得子进程退出信息 2.如果在任意时刻调用wait/waitpid子进程存在企鹅撑场运行则进程可能阻塞 3.如果不存在该子进程则立即出错返回 4.往后我们编写多进程基本的写法就是fork wait/waitpid
补充知识
1.父进程通过wait/waitpid可以拿到子进程的退出结果为什么要用wait/waitpid函数呢?可不可以使用全局变量 答:不可以 因为我们在前面就说过进程具有独立性那么数据就要发生写时拷贝父进程无法拿到子进程内的数据更何况是信号了 2.既然进程是具有独立性的那进程退出码也应该是子进程的数据父进程凭什么能拿到呢wait/waitpid究竟做了什么事情? 答:wait/waitpid的本质其实是读取子进程的task_struct结构。 3.这又和task_struct结构有啥关系呢 其实最后进程退出的时候一个进程的退出码和退出信号会被写到task_struct中(task_struct结构中有exit_pid,exit_signal两个变量)僵尸进程也是一样的子进程退出那么他的代码和数据就不会被运行和应用就可以释放相关的数据但是至少他会保留该进程的PCB信息task_struct里面保留了任何进程退出时的退出结果信息 wait/waitpid读取task_struct结构后再将exit_pid,exit_signal(两个字段)进行位操作设置到传入的status变量中然后再返回出来就可以得到子进程的信号编号和退出码 4.wait/waitpid有这个权利么 答:肯定有这个权利 wait/waitpid属于系统调用函数本质是操作系统去拿取task_struct中的数据而task_struct是内核数据结构对象所以操作系统可以对其进行读取也就没有任何问题了。
四、waitpid进一步讲解 options中的WNOHANG选项。 WNOHANG选项代表父进程非阻塞等待 非阻塞等待:我们的父进程通过调用waitpid来进行等待如果子进程没有退出我们waitpid这个系统调用会立刻返回。 了解了上面的宏定义后我们先来使用一下顺便再结合而代码详细介绍一下我们上面所讲的内容 下面我们来看一下waitpid的伪代码 阻塞等待我们已经讲述完毕后面就是非阻塞等待 非阻塞等待我们还是通过代码来演示 其实非阻塞等待的最大意义在于: 当子进程还在处理自己事情的时候父进程不需要进入阻塞状态父进程依然可以去做一些自己的事情当子进程处理完自己的事情以后父进程再去回收子进程的资源和数据。 运行结果: 通过上面的运行结果我们可以看到当子进程还在运行的时候父进程可以腾出来时间来处理自己的任务这就是非阻塞等待的好处。 如果未来编写网络代码的话大部分都是IO类别会不断面临阻塞和非阻塞的接口
五、进程替换
5.1 概念及原理
fork()之后父子代码是共享的数据写时拷贝各自一份父子进程各自执行父进程代码的一部分但是如果子进程就想执行一个全新的程序呢 进程的程序替换可以完成这个功能程序替换是通过特定的接口加载磁盘上的一个权限的程序(代码和数据)加载到调用进程的地址空间中阿里让子进程执行其他的程序 可能上面说的内容比较抽象大家不好理解那么我们下面就通过画图的方式来剖析一下进程替换的原理 5.2 操作
关于进程替换的函数有以下六个看似很复杂其实还是比较好理解的 下面我们先来进行一下基本的演示 1.不创建子进程 2.创建子进程 从这两个方面介绍同时只使用最简单的exec函数先看一看进行进程替换后会发生什么事情 后面我们会详细展开其他函数的用法
5.2.1 不创建子进程
execl
第一个我们要介绍的函数就是execl 其实该函数的可变参数列表可以参考我们平时最常用的命令行参数 下面我们就以平时用的最多的ls命令进行举例先来看一下ls命令 红框内就是ls的路径那么我们下面就来用ls命令进行进程替换先看代码 在代码中我在main函数的开始和结尾都加上了一句打印这样就可以看到进程进行的情况然后中间去调用函数进行进程替换。 代码运行结果 通过代码运行的结果我们可以看到一些问题 程序刚开始运行的时候打印了第一句代码后面进行了程序替换执行了ls的命令同时也将其对应的结果打印了出来但是最后面的一行代码并没有被打印出来这个就是程序替换的特性 针对代码结果进行分析 1.为什么最后一句代码不打印呢 execl是程序替换调用该函数成功之后会将当前进程的所有代码和数据都进行替换包括已经执行的和没有执行的 简单来说就是执行execl之后execl之前和之后的代码都会被替换掉只不过之前的代码运气比较好在替换之前就已经执行完了所以才有了最开始的打印结果。而后面所有的代码全都不会执行 2.execl的返回值 我们看手册的介绍可以得知如果调用失败会返回-1但是调用成功并不会返回值这又是为什么呢 为什么调用成功没有返回值呢 这个其实也很好理解因为execl根本不需要进行函数返回值判定如果替换成功execl会将main函数中包含execl本身的代码全部都替换掉前面用于接受返回值的数据也都已经被替换了所以也就不需要有返回值了。 如果想让我们打印的字体也颜色的话就可以看ls的内容再加一些代码 讲解后面知识之前我们先来回忆一下之前的内容 加载新程序之前父子的数据和代码的关系: 代码共享数据写时拷贝 当子进程加载新程序的时候就是一种写入那么这个时候代码要不要进程写时拷贝呢 一定会进行写时拷贝父子进程的代码必须要分离 所以当我们调用execl函数的时候父子进程在代码和数据上就彻底分开了。
5.2.2 创建子进程
为什么要创建子进程呢? 为了不影响父进程我们想让父进程聚焦在读取数据分析数据指派进程执行代码的功能如果不创建那么我们替换的进程只能是父进程如果创建了替换的进程就是父进程从而不会影响父进程 在上面我们已经看了execl的使用所以这里就不多说了关于exec系列还有几个函数我们一起来看一下
execv 实验代码及结果
execlp 实验代码及结果
execvp 实验代码及结果 以上的代码基本都没有什么问题那么我们来探究一个新的问题 1.如何执行其他我自己写的C、C二进制程序 2.如何执行其他语言的程序
execle 执行其他的C二进制程序 Makefile操作延伸 在Makefile中替换单词 例如:将Makefile中所有的exec替换为myshell 1.先按Esc 2.输入 :(英文冒号) 3.%s/exec/myshell/g 回车
exec.c代码 mycmd.c代码 经过我们上面的代码成功地实现了一个程序调用另一个程序 回顾 总结: 环境变量具有全局属性可以被子进程继承下去通过execle就可以将父进程的环境变量传给子进程
执行其他语言的程序
我们这里就简单的演示一下执行python语言的程序和执行shell程序 python代码只输出一下能够看到我们调用了即可 shell程序代码 运行python: python test.py 运行test.sh: bash test.sh 如果我们使用shmod x test.py也可以执行python程序这句话的意思就是给test.py加上可执行权限就可以将test.py变为可执行程序那么也就能直接执行该程序了解了这些以后下面开始进行一下简单的调用 1.调用python程序 1.调用shell程序 以上就是调用其他程序的全部内容。大家只需要了解就可以了
5.3 execve 其实在程序替换中操作系统只提供了这一个接口这个才是真正的系统调用接口。前面讲的函数是系统提供了基础封装(C语言做的封装)从而用来满足上层的不同的调用场景上面的函数接受的数据经过整合后再传给execve函数 execve没有带p所以需要带全路径
六、自己实现一个简易的shell程序
shell执行的命令通常有两种 1.第三方提供的对应的在磁盘中有具体二进制文件的可执行程序(由子进程执行) 2.shell内部自己实现的方法由自己(父进程)来执行(shell代表的是用户) 有些命令就是要影响shell本身比如cdexport 我们在自己写shell的时候需要创建子进程去完成命令为什么呢? 因为我们执行的一些命令可能会出现一些错误子进程执行的话只会影响子进程的运行不会影响到父进程。
6.1 函数介绍
fgets函数介绍 strtok函数介绍 chdir函数介绍 父进程设置的环境变量传递到子进程的模拟实现 我们在父进程可能会设置环境变量环境变量是具有全局性的所以我们设置的环境变量也是会传到子进程的下面我们自己来模拟一下其过程 我们再写一个测试程序看看子进程是否能得到我们在父进程设置的环境变量 运行我们自己写的shell并设置环境变量并且调用mytest程序 总结: 程序替换会替换代码和数据但是与系统有关的数据比如环境变量其相关的数据就不会被替换同时也体现出来了环境变量具有全局属性进程替换一定和应用场景有关我们有时候必须让子进程执行新的程序 补充: shell的环境变量是从哪里来的? 环境变量是写在配置文件中的shell启动的时候通过读取配置文件获得的起始环境变量。 linux中的.bash_profile 、.bashrc 、/etc/bashrc等文件中就有环境变量的信息
6.2 myshell.c完整代码 总结
以上就是进程控制全部的内容了可以看到其中涉及到的知识点还是非常的多的有的也不是很好理解所以我们要多去看多去总结同时自己也要将这些函数全都用一下这样才可以加深印象如果文章中有错误的话希望大家评论指出我后面一定会改正最后如果你感觉我的文章对你有用的话就给波三连吧谢谢大家