Linux--进程控制

🚀每日鸡汤:

  • 不要在该奋斗的年纪选择安逸。

目录

🏆一、进程终止

👓Ⅰ echo $?

👓Ⅱ退出码的含义

👓Ⅲ exit && _exit

①exit

 ②_exit

🏆二、进程等待

👓Ⅰwait方法

👓Ⅱwaitpid方法

👓Ⅲ阻塞和非阻塞

🏆三、进程程序替换

👓Ⅰ替换原理

👓Ⅱ替换函数

👓Ⅲ 函数解释

👓Ⅳ演示

①execl

 ② execlp

③ execv

 ④ execvp

 ⑤ execle

🏆Ⅴ 深入探讨

🏆一、进程终止

我们知道,当代码结束的时候,我们通常要return,那么为什么return代表了进程结束,有没有其他进程退出的场景呢?还有为什么我们要return 0,而不是其他数值呢?如果代码终止异常,我们如何知道它出了哪些问题呢?基于这些问题,我们有必要掌握进程终止的相关知识。

👓Ⅰ echo $?

首先我们要明白进程退出的场景有三种:

  • 代码运行完毕,结果正确。
  • 代码运行完毕,结果不正确。
  • 代码异常终止

那么对于进程结束的结果,我们如何得知结果是否正确还是它是异常终止呢?

我先写一段代码感受一下:

 

 如果结果是5050,那么返回1,如果不是则返回0.

echo $?用于查看最近一个进程在命令行中执行结束时对应的退出码

我们发现使用echo $?查看mytest进程结束的退出码的确得到了退出码1,那么我们再次执行的时候为什么会是0呢?

因为echo $?打印的是在命令行执行的最近一个进程的退出码,虽然第一个echo $?的确得到mytest可执行程序的退出码,而第二个echo $?得到的是第一个echo $?的退出码,也就是说echo $?也算在命令行执行的进程,所以会出现这样的结果。那么这些退出码代表什么含义呢?

👓Ⅱ退出码的含义

一般而言,退出码0表示成功,非0表示错误。我们可以用strerror函数来查看退出码具体都有哪些含义。

 

我们看到0表示成功1表示操作没有权限2表示没有对应的文件或目录3表示没有对应的进程......

 

 至此我们解决了为什么我们在写完一个程序后要return 0,因为它代表了程序成功执行退出。知道了这些,我们对于进程终止的情况进行进一步解析。

 只有代码执行完毕,退出码才有意义,正确与否决定退出码是0还是非0;没执行结束的,程序异常退出的退出码是没有意义的。

我们这里还有一个问题,进程终止除了return还有其他方式吗?有的,就是exit_exit

👓Ⅲ exit && _exit

①exit

exit--终止进程的一个函数。

 ②_exit

_exit也是用于终止进程。

 这里看来好像和exit一致,有什么不同呢?

man手册查询的结果来看:

exit();  //属于库函数(C 3号手册)

_exit(); //系统调用(系统提供,2号手册)

那么具体有什么区别呢?

我们看到,调用exit函数,打印了出来要输出的内容,而调用_exit函数,则没有打印。这是因为,我们没有在这里printf之后加上换行符,导致进程没有刷新缓冲区。而exit终止进程会自动刷新缓冲区,_exit终止进程不会自动刷新缓冲区。

实际从本质上讲,exit是对_exit函数进行了封装,它的本质也是调用_exit函数。

 

 进程终止有两种方式,一种是return退出,另一种就是调用exit()函数_exit()函数,它们在退出进程后,我们都可以通过echo $?得到在命令行执行的最近一次进程的退出码。

🏆二、进程等待

之前我们提到过如果子进程退出而父进程仍在执行,那么子进程就会进入Z状态(僵尸状态),等待父进程回收。进程等待就是用于解决僵尸进程的问题。通过回收子进程资源,获取子进程退出信息。

进程等待的方法有两种,wait方法waitpid方法

👓Ⅰwait方法

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int* status);

返回值:等待成功返回子进程pid,失败返回-1.

参数:输出型参数,获取子进程退出状态,如果不关心子进程退出结果可以设置为NULL.

那么wait方法具体如何使用呢?

这段代码的逻辑是:父进程创建一个子进程,子进程执行10s,父进程休眠15s。10s后,子进程被exit(终止),父进程还在sleep,子进程处于Z状态等待回收,5s后,父进程回收Z状态子进程,子进程结束。

那么我们如何验证wait方法确实得到了子进程的返回pid呢?

 

我们可以看到确实等待成功了,得到了子进程的返回pid

那么它的参数int * status到底有什么作用呢?我们可以先来看waitpid方法之后一块谈论。

👓Ⅱwaitpid方法

pid_t waitpid(pid_t pid, int* status,int options);

 返回值:

            当正常返回的时候,waitpid得到子进程的ID

            如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0.

            如果调用中出错,则返回-1,这时errno会被设置成相似的值以指示错误所在。

参数:

            pid:

                  pid=-1,等待任一个子进程,与wait等效。

                  pid>0,得到所等待的子进程的ID

            status:

                  WIFEXITED(status):若为正常终止子进程返回的状态,则为真(查看进程是否正常退出)

                  WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

             options:

                           一般设为0代表阻塞等待

                           若设为WNOHANG,代表非阻塞等待,它的意义在于若pid指定的子进程没有结束,则waitpid()函数返回0,不予等待。若正常结束,则返回该子进程的ID

对于参数options,它涉及到进程阻塞等待和非阻塞等待,我们这里先不予讨论,后面详谈,一律设为0.

这里我写一段演示如何使用waitpid方法

 

 我们可以看到,确实等待成功,返回得到了子进程的ID,但是我们发现status的值并不是子进程的退出码10,而是一个非常奇怪的数字:2560.所以status肯定不是我们我们所设想的得到退出码。

我们再回到进程终止,一段代码运行结束,它会有三种情况:

1、代码运行完,结果正确。

2、代码运行完,结果不正确。

3、代码没跑完,出现异常。

那么status如果要能得到这些情况,是需要别出心裁的设计。Linux设计中,status有32个bit位,我们只关注16个bit位

它的第8位到第15位保存的是进程的退出码,而前七bit位保存的是终止信号---是否正常结束

 

 我们现在发现,我们的确得到了子进程的ID和它的退出码。如果子进程正常被等待回收,那么它的终止信号就是0如果终止信号不是0,那么它的退出码也就没有了意义。

我们这里可以看到,÷0是没有意义的,所以代码崩溃,终止信号就不再是0,而这些数字有什么含义呢?

 

8号信号:意义是浮点数错误,原因是我们÷0.

 

如果我们每次都要输入(status)&0x7F得到终止信号,(status>>8)&0xFF得到退出码未免有些麻烦,所以我们要想简便就可以使用WIFEXITED(status)WEXITSTATUS(status)

 WIFEXITED(status)是对终止信号进行检验,判断是否正常终止,正常终止则为ture,而WEXITSTATUS(status)则是得到退出码。

👓Ⅲ阻塞和非阻塞

我们有没有想过一个问题,如果子进程一直没有结束,那么我们父进程呆呆地等待子进程结束是否效率太低了?这时如果父进程还有其他任务没有完成,在等待的时间里我们是否可以优先完成其他任务,如果子进程结束了就再对子进程处理,这样效率是否会更高呢?事实上,我们的Linux设计者也考虑到了这点,所以waitpid的最后一个参数options就是来让我们选择是阻塞等待子进程,还是非阻塞轮询子进程

那么到底什么是阻塞等待,什么是非阻塞等待呢?

如果我们设置waitpid最后一个参数为0,那么就是阻塞等待模式,父进程会一直等待子进程结束再往后执行程序。

而设置waitpid最后一个参数为WNOHANGwaitpid在被调用时,在任何时候它都会立即返回,无论子进程是否结束,如果子进程未结束,那么就返回0,如果已经结束,就返回子进程ID,如果调用失败就返回-1.

而这样做的缺点是,如果子进程还在运行而父进程退出会导致无法回收的问题,所以我们要设置循环来进行轮询

 完整的调用:

什么时候会无法等待呢?比如我们传一个错误的ID

 

这时我们就会发现调用失败。

那么非阻塞有什么好处呢?非阻塞等待不会占用父进程全部的精力,可以在轮询期间,干干别的事情

 

事实上,阻塞非阻塞之间没有好坏之分,只是有不同的应用场景,如果父进程没有别的任务,那么阻塞状态就可以了,如果有别的任务要执行,那么 非阻塞可以在等待的同时执行其他任务

🏆三、进程程序替换

👓Ⅰ替换原理

fork创建子进程后执行的是和父进程相同的程序,如若要执行不同的代码分支,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未被改变。

👓Ⅱ替换函数

其实有六种以exec开头的函数,统称exec函数:

#include<unistd.h>

int execl(const char* path,const char *arg, ...);
int execlp(const char* file,const char *arg, ...);
int execle(const char* path,const char* arg, ... ,char *const envp[]);
int execv(const char* path,char *const argv[]);
int execvp(const char* file, char *const argv[]);
int execve(const char* path,char* const argv[],char* const envp[]);

👓Ⅲ 函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1.

命名理解

l(list):表示参数采用列表

v(vector):参数用数组

p(path):有p自动搜索环境变量PATH

e(env):表示自己维护环境变量

👓Ⅳ演示

①execl

execl为例,l是list,采取列举的方式执行。

 int  execl(const char *path,const char *arg,...);

const char *path:表示依赖路径,需要写具体的路径,以便查找。

const char *arg:如何执行?执行的方式,cmd 选项1 选项2 ...

...:可变参数列表。

我们看到了execl的示例,同时验证了进程替换的原理。因为这里运行结束这一行代码没有执行。

 

 进程程序替换只是替换了别的进程去执行,原来的代码和数据被覆盖。

 ② execlp

int execlp(const char *file, const char *arg, ...);

p:path。非常简单,就是我们不再需要写详细的依赖路径,OS自动在环境变量PATH,进行可执行程序的查找。

那么,这里有两个"ls",重复吗?

并不重复,含义不同,前者是告诉系统要执行谁,后者是告诉系统我想怎么执行。 

③ execv

int  execv(const char *path,char *const argv[]);

 v:vector:可以将所有的执行参数放入数组中统一传递,不用进行使用可变参数方案。

 ④ execvp

int execvp(const char*file,char *const argv[]);

以上都是执行系统命令,如果我想执行自己写的程序,那么应该怎么调用呢?

这里我们先补充一个makefile小tips

因为makefile默认只会从上到下生成一个可执行程序,如果我们想生成多个可执行程序:

添加all方法即可。可以看到我们一次生成两个可执行程序。

那么现在怎么一个程序创建子进程程序替换执行另一个程序呢?

 

 这里需要注意:因为我们写的程序没有在PATH环境变量里面,所以我们不使用exec*中带p的函数

我们可以做到在C程序文件里面执行C++程序

可以看到,程序替换非常强大,可以使用程序替换,调用任何后端语言对应的可执行程序。 

 ⑤ execle

int execle(const char *path,const char *arg,..., char *const envp[]);

e:自定义环境变量。

 

当我们自定义环境变量使用execle函数确实传给了子进程myexec,但是系统的环境变量没传进来。

 

 但是我们传environ没有传自己定义的环境变量时是不识别的,那么我们如何才能既打印出系统默认的环境变量,也能打印出自定义环境变量呢?

我们把自己的环境变量导入系统环境变量,这样再调用系统环境变量也能找到我们自己的环境变量。

 

🏆Ⅴ 深入探讨

我们再回顾一下main函数的参数。

进程程序替换本质是将我们的程序加载到内存中。具体如何加载?在Linux中就是exec*--加载器。

坦白讲,main函数也是函数,我们在程序替换时先将程序加载到内存,然后再执行main函数,而我们之前调用execle函数就会传参给main函数,arg选项传给main函数的前两个参数,envp环境变量传给main的env。

这样我们也就理解了为什么我们传自定义环境变量系统无法识别默认环境变量,而我们不传自定义环境变量系统就无法识别。什么都不传默认传系统默认环境变量。

事实上,只有execve是真正的系统调用,其他五个函数最终都调用execve所以execve在man手册第二节,也就是系统调用函数,而其余函数在man手册第三节,属于C库函数

 

 方便记忆:

 程序替换中execve是系统调用,其余的都是封装,为了方便我们,有了更多的选择性。

掌握了这些知识我们就可以自行写一个shell控制器了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值