目录
1. 进程创建
fork()函数初识
这里之前讲过,不再赘述:
【Linux】进程入门详解## fork()函数返回值
写实拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
为什么不拷贝 只读数据?
所有的数据,并不是父和子都会写入,如果是只读的数据,写时拷贝不会拷贝,避免内存与系统资源的浪费
fork的时候,创建子进程的数据结构,如果还要将只读数据拷贝一份,会导致fork的效率降低。而且fork()本身就是把向系统要更多的资源,要的资源越多,fork就越容易失败。
fork()常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
2. 进程终止
2.1 进程退出的场景
- 代码跑完,结果正确,退出码为0
- 代码跑完,结果不正确,逻辑问题,但是没有导致程序崩溃,退出码 非0
- 代码没跑完,程序崩溃。退出码此时无意义。
2.2 进程常见退出方法
- 正常终止(可以通过 echo $? 查看进程退出码):
- 从main返回
- 调用exit
- _exit
- 异常退出:
ctrl + c,信号终止
3. 进程等待
3.1 进程等待必要性
- 回收僵尸进程,解决内存泄漏
- 需要获取进程的运行结束状态
- 尽量父进程要晚于子进程退出,可以规范化进行资源回收
3.2 进程等待的方法
系统提供了两个系统接口来供用户使用。
3.2.1 wait
函数声明
//需要包含的头文件
#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 秒,只有父进程在运行。
3.2.2 waitpid
相比较于 wait ,waitpid像是它的plus 版本,给用户提供更加个性化的选择:
函数声明:
pid_ t waitpid(pid_t pid, int *status, int options);
- 返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; - 参数:
- pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程(等待指定的进程) - status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) - options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
所以,对于之前的例子,我们是可以用waitpid 来代替wait的。
3.3 获取子进程status
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非零,提取子进程退出码。(查看进程的退出码)
由此 ,代码可以简化为:
3.4 阻塞 与 非阻塞
我们目前调用的函数,全部都是阻塞函数。调用->执行->返回->结束 。执行时 调用方一直在等待,没有做其他事情。(但执行流)
非阻塞轮询方案,更加高效。(如下图)
这种方案显然会有三种返回情况:
- 失败:下次再检测
- 成功:已经返回
- 失败:真正失败
这就对对应了waitpid 的第三个参数options:
options:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
也就是说,如果我们设置WNOHANG参数,那么就会对进程采用非阻塞轮询方案。
- 到底如何理解 进程的“等”?
将当前的进程放入等待队列,并将进程状态设置为非R状态。
当条件允许,OS就会唤醒进程,将其由等待队列转到运行队列,转为R状态。
4. 进程替换
4.1 进程替换概念
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2 替换函数
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[]);
我们循序渐进,先介绍第一个函数:
4.2.1 execl
int execl(const char *path, const char *arg, ...);
其中:
- path: 你要执行那个程序
- arg: 你要执行的命令
- … 可变参数列表,命令行怎么执行,传入什么选项,你就可以在这里直接按照顺序填写参数
我们直接实验一下:
这段代码等效于命令:
ls -a -l -i
但是,细心的同学发现,我们的最后一条语句没有打印出来。这是因为代码被替换为ls了。执行完ls后不会再回源程序了。
也就是说,exec函数,不需要考虑返回值,只要返回,一定是这个函数调用失败了。
当然,我们也可以通过父进程来创建子进程来执行程序替换,此时的程序替换是不会影响父进程的。
运行结果:
4.2.2 execv
这个函数与execl 基本相同,l 代表 list ,v代表 vector,也就说,只是传参的方式不同,如下图:
4.2.3 execlp
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
比较二者,多的p 表示path,指有p自动搜索环境变量PATH.
4.2.4 execvp
这个函数很显然了,不再赘述
4.2.5 execvpe
函数声明:
int execvpe(const char* file, char* const argv[],char* const envp[])
其中多的e 是env ,即环境变量。
我们可以传入默认或者自定义的环境变量给目标的可执行程序。
4.2.6 execve
实际上,只有execve是真正的系统调用,其它五个函数最终都调用 execve
那么 execve 也类似:
除了传自定义的本地变量,我们还可以传环境变量:
但是显然 ,此时我们是找不到MYENV的,因为它还没有写入环境变量:
写入后:
4.2.7 规律总结
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
4.3 为什么要程序替换
程序替换通常有两种应用场景:
- 子进程 执行父进程的一部分代码
- 子进程自身新的程序的需求
5. shell 的模拟实现
进程的基本内容基本掌握后,我们现在已经有能力模拟实现一下我们的命令行编辑器shell了。
这里我们要注意 cd 是内置命令,要单独处理。