这里之前讲过,不再赘述:
【Linux】进程入门详解## fork()函数返回值
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
为什么不拷贝 只读数据?
所有的数据,并不是父和子都会写入,如果是只读的数据,写时拷贝不会拷贝,避免内存与系统资源的浪费
fork的时候,创建子进程的数据结构,如果还要将只读数据拷贝一份,会导致fork的效率降低。而且fork()本身就是把向系统要更多的资源,要的资源越多,fork就越容易失败。
系统提供了两个系统接口来供用户使用。
函数声明
//需要包含的头文件 #include<sys/types.h> #include<sys/wait.h> //函数声明 pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
我们写一个例子来验证一下wait函数:
同时我们使用监视脚本来对父与子进程状态进行跟踪:
//监控脚本,每一秒刷新一次进程状态 while : ; do echo "######################";ps ajx | grep proc | grep -v grep; echo "########################";sleep 1;done
我们看一下该程序运行时的进程转态:
很容易发现,在5->10秒,子进程是僵尸状态,在10秒开始时,父进程苏醒,wait函数回收子进程,所以在10到13 秒,只有父进程在运行。
相比较于 wait ,waitpid像是它的plus 版本,给用户提供更加个性化的选择:
函数声明:
pid_ t waitpid(pid_t pid, int *status, int options);
所以,对于之前的例子,我们是可以用waitpid 来代替wait的。
wait 与 waitpid 都有一个status 参数。
该参数是一个输出型参数,由操作系统填充
已知在32位操作系统下, status 是一个整形数字,有32个bit位。
我们画一个示意图:
其中,次第八位 存储的是 子进程退出时的退出码,即exit(n)
我们可以来验证一下:
运行结果:
这里有几个问题:
是否可以通过设置全局变量,告知父进程的退出码?
绝对不行,写时拷贝
我们通过waitpid 拿到的status 的值,是从哪里得到的,子进程已经结束了啊?
子进程时僵尸状态,子进程的数据结构没有消失,task_struct 会被填入其退出码,所以waitpid从子进程task_struct中拿退出码。
如果进程异常终止了,那么 在 status 的最低七位存储 终止信号,而空出的那第八位,存储core dump 标志。
所以我们在检测的时候,要先看最低七位是不是0-,如果是0,那么说明正常终止,此时我们再去查看其退出码,即次第八位。 如果非0,那么就说明 被异常终止。
同样,我们也可来测试一下这种情况;
我们通过写一个野指针的解引用来引发异常。
运行结果:
这里我们还有一个core dump 标志没有讲解,这是因为 这一块内容比较多,之后再介绍。
我们编写一个完整的判断逻辑:
但是有一个问题,我们每次想取得退出码和错误码都要按位与吗?并不是,系统提供了一堆的宏可以使用。
这里我们只介绍两个;
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
由此 ,代码可以简化为:
我们目前调用的函数,全部都是阻塞函数。调用->执行->返回->结束 。执行时 调用方一直在等待,没有做其他事情。(但执行流)
非阻塞轮询方案,更加高效。(如下图)
这种方案显然会有三种返回情况:
这就对对应了waitpid 的第三个参数options:
options:
也就是说,如果我们设置WNOHANG参数,那么就会对进程采用非阻塞轮询方案。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
Linux 提供了六组系统接口:
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[]);
我们循序渐进,先介绍第一个函数:
int execl(const char *path, const char *arg, ...);
其中:
我们直接实验一下:
这段代码等效于命令:
ls -a -l -i
但是,细心的同学发现,我们的最后一条语句没有打印出来。这是因为代码被替换为ls了。执行完ls后不会再回源程序了。
也就是说,exec函数,不需要考虑返回值,只要返回,一定是这个函数调用失败了。
当然,我们也可以通过父进程来创建子进程来执行程序替换,此时的程序替换是不会影响父进程的。
运行结果:
这个函数与execl 基本相同,l 代表 list ,v代表 vector,也就说,只是传参的方式不同,如下图:
int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...);
比较二者,多的p 表示path,指有p自动搜索环境变量PATH.
这个函数很显然了,不再赘述
函数声明:
int execvpe(const char* file, char* const argv[],char* const envp[])
其中多的e 是env ,即环境变量。
我们可以传入默认或者自定义的环境变量给目标的可执行程序。
实际上,只有execve是真正的系统调用,其它五个函数最终都调用 execve
那么 execve 也类似:
除了传自定义的本地变量,我们还可以传环境变量:
但是显然 ,此时我们是找不到MYENV的,因为它还没有写入环境变量:
写入后:
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
程序替换通常有两种应用场景:
进程的基本内容基本掌握后,我们现在已经有能力模拟实现一下我们的命令行编辑器shell了。
这里我们要注意 cd 是内置命令,要单独处理。