中企动力邮箱官网,网站建设优化开发公司哪家好,谷歌seo课程,wordpress的插件在哪目录 一、进程创建
写时拷贝
二、进程终止
echo $?
如何终止进程
_exit与exit
三、进程等待
进程等待的必要性
进程等待的操作
wait
waitpid
status
异常退出情况
status相关宏
options
四、进程程序替换
1、关于进程程序替换
2、如何进行进程程序替换
程序…目录 一、进程创建
写时拷贝
二、进程终止
echo $?
如何终止进程
_exit与exit
三、进程等待
进程等待的必要性
进程等待的操作
wait
waitpid
status
异常退出情况
status相关宏
options
四、进程程序替换
1、关于进程程序替换
2、如何进行进程程序替换
程序替换函数
execl
execv
execlp
execvp
Makefile(补充)
命令行参数(补充)
execle
execvpe
execve
简易的shell编写 一、进程创建
当一个进程调用fork后就会创建出一个新进程新进程是子进程原来的进程是父进程但是却可能执行不同的代码 运行结果 上面的结果可以充分证明父子进程执行不同代码的事实id值0的就是子进程id值0(即返回子进程的pid)的是父进程
结合地址空间的知识一个父进程一个子进程各自都有地址空间都有页表所以用的虚拟地址相同而物理内存其实是被映射到不同区域的所以同一个变量id会有两个不同的值所以父子进程会执行不同的代码 写时拷贝
父子进程的代码通常是共享的父子进程不写入时数据也是共享的而当任意一方试图写入的时候便以写时拷贝的方式再拷贝一份出来。
下面就是OS为何选择写时拷贝的原因
1、OS在代码执行前无法预知哪些空间会被访问
2、在用的时候再分配是高效使用内存的表现是一种延时申请的技术
正是有写时拷贝技术的存在所以父子进程得以彻底的分离保证了进程的独立性 二、进程终止
首先在进程终止时操作系统会释放进程申请的相关内核数据结构(PCB结构体、页表...)、对应的数据和代码
其实就是操作系统会释放系统资源
进程终止有如下三种方式
1、代码执行完结果正确
2、代码执行完结果不正确
3、代码没有执行完程序崩溃涉及信号后面会说到 我们平常所写的main函数最后一行都是return 0这个0是什么这个返回值的意义是什么必须要是0吗其他值比如5可以吗
其实这里的0是进程退出码返回值如果是0就说明是成功运行且结果正确而如果是非0则说明运行的结果是不正确的
而这里的非0值是有无数个的不同的非0值就可以表示不同的错误原因这样就可以在我们的程序运行结束后如果结果不正确就能清楚的知道错误的原因
main函数返回值的意义是返回给上一级进程用于判断进程执行结果对还是不对
下面的代码可以看出来return 5也是可以的不一定是0 echo $?
echo $?可以获得最近一个进程执行完毕的退出码 运行结果 可以看到第一次执行echo $?时打印出来的就是我们main函数中写的退出码5
第二次执行echo $?时打印出来的退出码是0是因为当第二次执行echo $?时最近一次执行完毕的进程就是上一次的echo $?而上一次的echo $?执行成功所以退出码是0 下面举个退出码验证结果的例子 我们使用退出码验证结果是否正确从1加到3结果应该是6所以判断一下如果结果不等于6则改变退出码ret变为1否则就是正常返回0 可以看到正常运行结束echo $?查看退出码为0说明结果正确
下面改变一些将for循环的判断语句变化一下 这时运行完再观察退出码 这时的退出码就不是0了变为了1说明运行结果不正确 下面可以看看正常情况下非0值表示的什么意思
其中strerror就是将一个整数转化成字符串描述所以可以查看每一个非0值表示的意义 结果为 每一个非0值后面就表示它所代表的错误信息
下面举个例子
我们平时ls打印文件信息如果ls后面的文件不存在就会报这样的错误提示 而我们刚刚打印出来的退出码的含义其中2后面对应的错误信息就是这个所以我们执行一下echo $?打印一下最近一次进程的退出码即ls test.c这个进程的退出码 通过结果可以看到退出码就是2
还需要注意的是如果进程终止是第三个情况程序崩溃而终止这时的退出码无意义这种情况一般都是退出码对应的return语句没有被执行 如何终止进程
①main函数中return 在main函数中直接return运行结果为 执行到return语句前面就终止了并且查看退出码也是5结果正确 ②exit
需要包头文件stdlib.h 运行结果即退出码如下 可以看到与return的结果相同 return和exit的区别
①return是语句而exit是函数
②exit在任何地方调用都表示直接终止进程
而return在普通函数中表示函数调用结束在main函数中才代表进程退出
如下所示 运行后查看退出码 可以发现退出码并不是调用的add函数中的return值而是main函数中的return值
下面看exit的情况 运行后查看退出吗 可以发现在调用add函数中最后的exit(10)而退出码也是10说明exit在任何地方调用都表示直接终止进程 _exit与exit
正常情况下区别不大下面举个缓冲区的例子 正常情况下由于我们的hello后面没有\n换行所以程序是将hello先放到缓冲区中去执行后面的sleep(2)和exit(5)这时看下面的结果 exit会在终止进程前从缓冲区中读出hello然后再终止
下面观察_exit的情况
结果为 我们可以发现如果是_exit终止进程前并没有打印出缓冲区的内容
所以exit与_exit的显著区别就是
exit终止进程前会执行用户清理函数打印缓冲区的内容关闭流...
_exit则直接终止进程
所以上面的代码exit等看到打印的结果而_exit则看不到结果 三、进程等待
进程等待的必要性
父进程创建出子进程是要子进程完成任务的那么子进程是否完成又该如何处理是进程等待需要做的
如果子进程结束父进程不管子进程就可能会造成僵尸进程造成内存泄漏的问题
因此通过进程等待可以回收子进程资源获取子进程退出信息 进程等待的操作
wait 首先运行fork函数创建出子进程并且子进程执行三次就退出而父进程继续执行
还没运行可执行程序proc时用ps指令查看进程发现只有grep进程因为我们在执行这个命令 然后复制SSH渠道变为两个窗口方便我们观察进程状态 右边窗口执行左边观察 可以发现在刚开始运行时子进程还没有终止时进程状态是S而子进程运行三次终止后只剩父进程执行时再观察进程信息可以看到子进程的进程状态变为了Z即僵尸状态后面也有defunct表示它是无效的 接着man 2 wait了解一下wait wait的返回值成功就返回子进程的pid失败了就返回-1
下面改变父进程的else中的语句 先执行一次父进程的printf语句然后sleep7秒由于子进程执行每次循环都sleep1秒执行三次就退出所以三秒后子进程就是僵尸进程
我们设置父进程sleep7秒再执行下面的代码前三秒父子进程都正常三秒后子进程变成僵尸进程7秒后父进程执行wait就可以清楚观察到子进程变成僵尸进程后父进程执行wait所造成的结果是什么
结果如下 一共用ps指令查看了三次第一次是子进程还没有执行完三次父子进程正常运行
第二次是子进程执行完三次已经退出而父进程还在sleep没有执行wait的进程状态
第三次是父进程执行完wait的进程状态
上面左边窗口的图放大 可以看到第一次pid为25490的子进程的进程状态是S父子进程正常运行
第二次pid为25490的子进程的进程状态为Z子进程已经退出进程变为僵尸进程父进程正常运行
第三次父进程执行完wait后可以发现pid为25490的子进程已经被父进程回收所以进程列表看不见pid为25490的子进程了
上面就是执行完wait后的情况所以之后编写多进程都会使用fork wait/waitpid waitpid
同样用man看一下waitpid的信息 同样包含在头文件sys/types.h和sys/wait.h中
注意看上面的第二个框中的内容waitpid有三个参数
第一个参数pid
当 pid -1时等待任一子进程与wait等效
当pid 0时等待其进程id与pid相等的子进程
第三个参数options
默认为0表示阻塞等待
第二个参数status
status是一个输出型参数我们通过waitpid获得子进程的退出结果想用status来标识子进程退出的结果是什么如果不关心可以为NULL
所以使用waitpid时waitpid(pid, NULL, 0) 与 wait(NULL)等价 waitpid的返回值pid_t
大于0表示等待子进程成功子进程退出
小于0表示等待子进程失败
等于0表示等待子进程成功子进程未退出 下面将使用wait时的代码做以改变将wait改为waitpid 其中框住的部分第三个参数为0是父进程默认在阻塞状态去等待子进程状态变化即等待子进程退出
和上面处理一样两个窗口方便观察进程状态 左边窗口观察结果 与wait一样刚开始父子进程正常执行时都是S子进程执行完毕退出状态变为Z即僵尸进程然后父进程执行waitpid回收子进程资源僵尸进程被回收
用waitpid也完成了进程等待这就是回收僵尸进程的两种方法wait、waitpid status
下面详细说下status就status的构成来说status不是按照整数来整体使用的而是按照比特位的方式将32位进行划分我们只学习低16位
而这低16位中次低的8位表示的是进程的退出码所以如果想打印出子进程退出码需要将status的先右移8位将次低的8位移到低位的8位然后按位与0xFF就可以得到这低16位中次低8位的值即进程的退出码 可以看到子进程中我们设置的退出码是10这时运行代码 可以得到子进程的退出码
所以父进程就可以通过子进程的退出码是0还是非0来确定子进程运行有没有成功结果是否正确如果失败错误码是什么表示什么原因失败的 进程终止的情况三是进程异常退出或是崩溃其实本质就是操作系统杀掉了进程
操作系统是通过发送信号的方式杀掉进程
而我们上面说到进程退出码是status的次低8位比特位而终止信号则是最低7个比特位倒数第八个比特位是core dump标志(gdb调试崩溃信息时在信号部分详解)
信号有以下这些通过kill -l查看 我们只需要关注前31个普通信号其中第9个我们曾经还用于终止进程
所以这时我们还可以看到子进程收到的信号编号在代码中再加以改进 代码中加了红框框起来的由于最低7位是终止信号所以status与0x7F按位与得到的就是最低7位的值0x7F即0000......0111 1111四个二进制位表示一个16进制位后七位即7F
打印结果为 子进程收到的信号编号为0说明进程是正常跑完的
子进程的退出码是10说明结果是正确的 异常退出情况
接下来尝试一下异常情况子进程直接崩溃即一个数除0时观察子进程的信号编号情况 观察下面结果 编译时给了一个警告warning分母不能为0但不是报错依然可以运行在子进程循环第一次的时候就终止程序了程序崩溃可以看到报的信号编号为8
这时子进程收到的信号编号为8不为0表示不正常进程崩溃了这时的退出码0无意义
而信号编号8则可以看上面的kill -l打印的信号列表即SIGFPE表示浮点数错误 下面试一下野指针的情况 指针pa指向空再改变pa的值即野指针问题
结果为 子进程收到的信号编号为11不为0同样表示不正常进程崩溃了这时的退出码0同样无意义
信号编号11则可以看上面的kill -l打印的信号列表即SIGSEGV即段错误 程序异常不仅仅是内部代码有问题也可能是外力造成的 我们将子进程while循环的num不--就会导致子进程死循环这时父进程等不到子进程结束于是使用外力kill直接杀掉进程如下 子进程pid是28223所以直接输入kill -9 28223相当于直接杀死进程这时的信号编号是9即我们经常用到的SIGKILL
而kill杀掉了进程子进程代码无法确定是否运行完毕所以退出码同样没有任何意义 status相关宏
我们其实不用每次都是使用这种按位与或者右移再按位与的方法对status进行二进制处理系统中给我们提供了宏
WIFEXITED(status)若正常终止子进程则为真检测是否正常退出
WEXITSTATUS(status)若WIFEXITED为真则提取子进程退出码查看子进程退出码
所以上面代码中可以不用进行二进制处理可以用以下方式 使用宏处理运行结果为 可以正确得到子进程退出码
若是异常退出则为假返回值为0 经过上面知识的铺垫我们就可以理解父进程通过wait/waitpid拿到子进程的退出结果而不用全局变量是因为进程是具有独立性的改变数据时会发生写时拷贝父进程是无法拿到的并且还有上面说到的子进程的信号编号通过全局变量也是无法做到的
并且还有一个问题进程是具有独立性的那子进程退出了子进程的退出码也是子进程的数据父进程为什么可以拿到其中的wait/waitpid是怎么做到的
其实很简单在子进程变为僵尸进程后父进程回收子进程资源其中子进程的PCB结构体是保留下来的而这个PCB结构体中是保留了子进程的退出结果信息的而wait/waitpid就是系统调用接口相当于操作系统所以是有权限调用PCB结构体中的信息的所以本质其实就是父进程通过wait/waitpid系统调用接口读取了子进程的PCB结构体里的信息因此父进程可以拿到子进程的退出码就可以很好地解释了 options
options是waitpid的第三个参数默认为0代表阻塞等待
WNOHANG选项代表父进程非阻塞等待
这里的WNOHANG其实就是Wait No HANG(夯住了)
夯住了本质就是这个进程没有被调度也就是要么是在阻塞队列中要么是等待被调度
那么WNOHANG(Wait No HANG)就是不被夯住也就是非阻塞等待
之前的阻塞等待时子进程如果没有运行结束父进程是会一直等待子进程运行结束的而现在的非阻塞等待改变代码如下 进行非阻塞等待运行结果如下 观察结果可知非阻塞等待中在子进程还没有退出时父进程不像阻塞等待那样什么都不干父进程是可以边执行任务边等待子进程的 四、进程程序替换
1、关于进程程序替换
前面说到fork()后父子会各自执行父进程代码的部分父子代码共享数据写时拷贝那么子进程想执行一个全新的程序时就要引入进程程序替换的概念了
程序替换是通过特定的的接口加载磁盘上全新的程序(代码数据)进而加载到调用程序的地址空间中从而让子进程执行其他程序
而进程替换这个过程原本的PCB结构体、地址空间等内核数据结构都不发生改变只是将新的磁盘上的程序加载到内存中并和当前进程的页表建立映射关系即可所以进程替换并没有创建新的子进程
而程序替换的原因就是是和应用场景有关的有时候我们必须要程序替换
exec*函数的本质就是如何加载程序的函数 2、如何进行进程程序替换
首先看下面的简单例子 这是一个很简单的程序运行结果 程序替换函数
execl
接下来调用execl函数先用man查看一下execl execl的参数解释一下
第一个参数path是路径目标文件名
第二个参数arg及后面的...我们在Linux命令行上怎么填就在这里怎么填最后一个必须是NULL表示参数传递完毕
参数最后的...表示可变参数列表即可以传入多个不定个数的参数
具体见下面例子以ls举例 使用which查看ls路径是/usr/bin/ls所以在代码做以修改 execl第一个参数传入ls的路径第二个参数及后面的参数按命令行的写法顺序写入即ls--colorauto-a-l最后以NULL结尾其中--colorauto是ls打印时所带的颜色
然后运行程序再运行ls -a -l对比 可以发现结果是一样的
另外在使用函数execl之前打印了 进程开始 和 进程结束 但是执行execl后没有打印进程结束了
这是因为execl是程序替换在调用该函数成功后会将当前进程的所有代码和数据都进行替换其中包括已经执行的和没有执行的
所以一旦调用成功后续代码都不会执行
根据这个性质我们可以得到execl不需要的返回值的理由如果有返回值调用成功的时候会将当前进程所有代码都替换包括返回值这时的返回值无意义
如果调用失败我们只需要在execl下面写exit即可这样成功了不会执行exit失败才执行
如下面的例子path里传入一个完全不对的路径在下面加上exit(6) 这时编译再运行 这时调用失败就不会再执行下面的代码了并且查看退出码是我们设置的6 上面进程替换的例子是不创建子进程时的例子下面都是在创建子进程的前提下的例子
而我们创建子进程的原因
创建子进程是为了不会影响父进程我们需要父进程执行读取数据、解析数据、指派进程执行的功能如果在进程替换时不创建子进程那么替换的就是父进程所以需要创建子进程 execv execl函数以l结尾可以看做list我们需要将参数一个一个传入
而execv函数以v结尾可以看做vector我们则需要将所用参数传入一个指针数组argv中然后将argv当做参数传入execv函数中
所以execl与execv只是在传参方式上有区别
下面是execv的例子 执行结果 所以execl与execv的功能是一样的 execlp 通过观察execlp和execl的第一个参数一个是path一个参数是file
这个execlp函数结尾是p可以看做它会自己在环境变量PATH中进行查找不需要告诉它要执行的程序的路径
所以将上面的程序做以改变 运行结果 同样执行成功
这里需要注意看下图 这里两个ls并不是两次无意义的重复
第一个ls是找到程序
第二个ls及之后的内容表示找到程序后要执行什么选项 execvp
同样通过man查看 这里的execvp与execlp都是以p结尾所以也不需要带路径
并且第二个参数与execv相同所以就不用多说直接看用法 运行结果为 同样执行成功 Makefile(补充)
如果我们当前文件夹中有两个文件Makefile如何书写可以做到make时生成两个可执行如下所示 我们当前有两个文件exec.c和test.cMakefile改变如下 这时当Makefile从上往下被扫描时需要生成的目标文件第一个遇到的就是all而all依赖的是exec和test所以就会往下扫描将exec与test的可执行程序执行完all的依赖条件具备后想执行all的依赖方法发现all没有依赖方法所以Makefile就结束了在clean中也删除两个可执行文件
以后如果还想make生成更多的可执行文件只需在all后面空格为间隔继续跟即可
这时执行make就可以生成两个可执行程序 make clean也可以清理两个可执行程序 上面创建的test.c代码如下
命令行参数(补充) main函数后面的参数argc、argv叫做命令行参数因为我们生成的可执行程序是test假设执行的操作是test -a或test -b
所以这里的第一个参数argc是2表示test是一个-a或-b是第二个
argv[0]就是testargv[1]就是-a或-b
因此第一个if语句就是与argc有关判断是否为2为2再往下执行不为2直接exit退出进程
下面的else if语句则检测输入的选项是否是-a或-b或其他 我们的test.c程序写好了那么如何在exec.c中让子进程执行我自己写的C/C代码例如让exec的子进程执行test.c
里面的/home/fcy/lesson3/test是绝对路径的表示也可以使用相对路径表示即./test 使用execl第一参数传入可执行程序test的路径第二个参数及后面的参数按命令行的形式填入即test -a这时结果为 成功在exec可执行程序中执行我们自己写的代码test.c的可执行程序
如果改变为 则结果为 而如果出现test中并未提及的选项比如-d如下 则会进入else打印失败 execle 这里的execle比上面说到过的execl多了一个e这里的e可以看做环境变量
所以使用execle就可以向目标进程传递环境变量
我们想要在exec的子进程中调用test所以在test中实现我们自己的环境变量MY_VAR之后由exec的子进程调用即可
由execle的第三个参数类型可以看出传入的环境变量类型也是一个指针数组 所以在exec.c中自己创建一个环境变量MY_VAR在指针数组env中接着在子进程中调用execle函数最后一个参数传入env第一个参数中的路径是可执行程序test
下面是test.c中代码 执行结果为 得到了我们自己创建的环境变量MY_VAR
此时exec里面的环境变量MY_VAR就传递给了test
所以到这里就可以明白之前说到过的环境变量可以被子进程继承具有全局属性的原因main函数的命令行参数三个分别是int argc, char* argv[], char* env[]其中第三个参数env就是环境变量所以在子进程执行execle时最后一个参数传入env就可以继承父进程的环境变量 execvpe
还有最后一个execvpe没有说到 经过上面的例子可以非常清楚看到execvpe就只是比execvp多了一个环境变量参数这个参数的用法具体参照execle就不具体说明了 execve
首先通过man查看execve如下 而上面的六种exec*函数通过man查看 可以发现execve是2号即系统调用而上面的六个函数都是3号是系统提供的基本封装
也可以说上面六个的函数底层就是execve传入数据根据不同的情况传入这几种不同的函数中然后经过处理变为execve的三个参数最终调用的都是execve
之所以提供这些封装就是因为我们遇到的场景不一样为了满足这些不同的调用场景
最后再看execve的参数 经过前面几种函数的学习可以很容易明白含义
第一个参数就是传入详细的路径
第二个参数即将所有参数放入指针数组中然后以指针数组的形式传入
第三个参数即传入环境变量
进程程序替换的exec*函数涉及的知识结束 简易的shell编写
具体实现的细节都在代码中详细注释了 1 #include stdio.h 2 #include stdlib.h3 #include unistd.h4 #include sys/wait.h5 #include string.h6 #include sys/types.h7 8 #define N 309 #define SIZE 2010 //保存完整的命令行字符串11 char my_cmd[N];12 //保存打散后的命令行字符串 13 char* _argv[SIZE];14 15 int main()16 {17 //命令行解释器不退出执行死循环while(1)18 while(1)19 {20 //1. 打印想Linux命令一样的提示信息21 printf([rootlocal myshell]# );22 //由于printf后面没有\n所以内容都在缓冲区23 //fflush(stdout)就是取出缓冲区的内容24 fflush(stdout);25 //将my_cmd内容初始化为\026 memset(my_cmd,\0,sizeof my_cmd);27 //2. 获取用户键盘输入的指令和选项28 if(fgets(my_cmd, sizeof my_cmd, stdin) NULL)29 {30 //如果输入错误重新输入31 continue;32 }33 //由于用户输入完指令和选项会按回车即\n多换一行34 //所以将用户输入的最后一个字符\n替换成\035 //即ls -a -l\n\0 变为 ls -a -l\0\036 my_cmd[strlen(my_cmd)-1] \0;37 //3. 命令行字符串解析38 //即ls -a -l - ls -a -l39 //strtok用于打散空格隔开的字符串40 //第一次调用要传入原始字符串41 _argv[0] strtok(my_cmd, );42 int num 1;43 //给ls执行后的结果带上颜色44 if(strcmp(_argv[0], ls) 0)45 {46 _argv[num] --colorauto;47 }48 //循环直到字符串遍历完毕49 //第二次调用strtok如果还是解析原字符串传入NULL50 while(_argv[num] strtok(NULL, ));51 //4. 如果是切换路径的命令cd就不能在子进程中执行52 //因为子进程被替换后会exit退出再次回到循环最开始的地方53 //路径并没有发生改变所以这种需要父进程执行的就是内置命令54 //内置命令的本质就是shell的一个函数调用55 if(strcmp(_argv[0], cd) 0)56 { 57 //如果cd后面有命令则调用函数chdir改变路径然后continue58 if(_argv[1] ! NULL)59 {60 chdir(_argv[1]);61 }62 continue;63 }64 //5. fork()创建子进程65 pid_t id fork();66 if(id 0)67 {68 //子进程69 printf(子进程执行下面的功能\n);70 execvp(_argv[0],_argv);71 exit(1);72 }73 //父进程74 int status 0;75 pid_t ret waitpid(id, status, 0);//阻塞等待76 if(ret 0)77 printf(进程退出码%d\n,WEXITSTATUS(status));78 79 }80 return 0;81 }
实际执行的和我们命令行上的操作基本类似底层使我们自己刚刚模拟实现的简易的shell能够执行简单的指令如下