fork 函数
fork 函数是 Linux 中一个非常重要的函数,它的作用是从已存在的进程中创建一个新进程。这个新进程就是当前进程的子进程。
fork() 函数使用方法:它在头文件 #include <unistd.h> 中,函数原型为
pid_t fork(void);
用一个 pid_t 类型的变量来接收 fork() 函数的返回值。当创建进程成功时,fork() 函数会给子进程返回 0,给父进程返回新创建的子进程的 id;当创建失败时,fork() 函数会返回 -1(给父进程返回,因为子进程根本没创建出来)
当创建子进程成功后,操作系统会做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。但它们都运行到相同的地方后,每个进程都会可以开始它们自己的旅程。可以使用下面的代码测试(Linux系统):
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t pid = fork();
printf("Before: pid is %d\n", getpid());
if (pid == -1) perror("fork()"), exit(1);
if (pid == 0)
{
printf("child:pid is %d, fork return %d\n", getpid(), pid);
}
else
{
printf("After:pid is %d, fork return %d\n", getpid(), pid);
}
sleep(1);
return 0;
}
运行结果:
fork()函数 内核示意图
fork() 函数的常规用法及错误原因
常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
错误原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
C/C++中捕捉错误的方式(代码运行完毕,结果错误)
errno 是C语言调用C函数时的错误码,当调用函数出错时,它会在内部给出错误码(数字),但这个数字往往我们不知道是什么错误,那我们就要用 strerror 函数来解析这个错误码。strerror 函数可以将错误码以字符串的形式描述起来。
代码异常终止
当代码异常终止时,一般操作系统会给这个进程发信号,让这个进程以某种错误而终止,其表现就是这个进程被操作系统杀掉!它的内部逻辑类似于手动调用 kill 指令。
kill -n id
n 为选项,表示各种信号;id 为被发送信号进程的 id
使用如下指令可以查看 Linux 中全部的信号
kill -l
进程终止
进程退出有三种场景:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
这三种情况可以用2个数字组合完全覆盖!
当进程正常终止时:可以通过 echo $? 查看进程退出码。
进程正常终止有三种情况:1. 从main返回;2. 调用exit;3. 调用_exit
异常退出:ctrl + c,信号终止
exit() 函数
#include <unistd.h>
void exit(int status);
status 定义了进程的终止状态,也就是进程的结果正确还是不正确。status 是一个整形值,父进程可以通过 wait函数来获取该值,得到子进程的最终执行结果!
_exit() 函数
#include <unistd.h>
void _exit(int status);
_exit() 函数与 exit() 函数的作用可以说是一模一样,只是在细节上有所差异:在终止进程时,exit() 函数会自动刷新缓冲区,而_exit() 函数不会!
其实相当于 exit()函数 是先调用了 _exit() 函数,如图
由此可以得出一个结论:我们所认识的缓冲区,并不在操作系统内部!否则 exit() 和 exit() 都应该能刷新缓冲区。
进程等待
什么是进程等待?
通过 wait/waitpid 的方式,让父进程对子进程进行资源回收的等待过程。那为什么要进行等待呢?
第一,可以解决子进程僵尸问题带来的内存泄漏问题(进程僵尸只有父进程回收才能解决,且不能被杀掉,所以这是目前必须使用进程等待解决的问题);第二,父进程创建子进程的目的,就是让子进程来完成任务,而父进程需要知道子进程任务到底完成的如何,就必须通过等待的方式来获取子进程退出的信息(两个数字:退出码和信号)这个退出信息也许并不是必须的,但是系统需要提供这样的基础功能!
如何进行等待
在 Xshell 终端中输入如下指令,查看 waitpid 和 wait 的使用方式
wiat 和 waitpid 方法
man waitpid
wait 函数:
返回值:成功返回被等待进程 pid,失败则返回 -1。
参数:输出型参数,获取子进程退出状态,若不关心则可以设置成为NULL
waitpid 函数
返回值:当正常返回的时候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。
那么父进程是如何得知子进程的退出信息呢?
子进程在退出的时候,要修改状态 Z,并将自己的退出信息和退出码写入 pcd 中,父进程通过读取子进程的 pcd 获取这些信息。注意:不能使用全局变量获取子进程的信息,因为进程之间具有独立性,各有各的进程地址空间,子进程对数据的修改不改变父进程中的数据!
获取子进程的 status
当需要获取子进程的 status时,通常采用位图的思想,使用位运算!
如图,经过位运算得到需要的数字:
是否收到信号的判定方法:exit sig 是否等于0,等于0说明没收到信号,不等于0说明收到了异常的信号;当一个进程异常了(收到信号),exit code 就变得没有意义了。
进程的阻塞等待和非阻塞等待
设 wait/waitpid 的返回值为 rid
- rid > 0 :等待成功
- rid == 0:等待成功,但对方(子进程)还没有退出
- rid < 0:等待失败
对于 waitpid 方法而言,它的第三个参数 options 可以控制父进程是阻塞等待还是非阻塞等待。
阻塞等待:子进程不退出,父进程就一直等待子进程,waitpid 不返回。这种情况在计算机中叫 宕(dang)机或应用夯(hang)住了。这种等待的缺点就是父进程在等待的过程中,什么事都做不了!
进程的阻塞式等待代码
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0) { //child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else {
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
非阻塞等待:如果子进程的退出条件不满足,wait/waitpid 不会阻塞,而是立即返回!所以非阻塞等待一般要重复读多次调用,这种一般叫做:非阻塞轮询方案进行进程等待 这样做的好处是:在子进程没有退出的情况下,父进程可以在等待的过程中,顺便做一些占用时间比较少的事情。
进程的非阻塞式等待代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0) { //child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(1);
}
else {
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if (ret == 0) {
printf("child is running\n");
}
sleep(1);
} while (ret == 0);
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}