进程的创建
fork()用于创建子进程。但fork创建的子进程获得的是父进程(即调用 fork()
的进程)的一份几乎完全相同的副本,包括父进程的代码、数据、堆、栈和数据结构等内容。当进程调用fork后,一旦控制转移到内核中的fork代码后,内核将会做以下三点:
1,分配新的内存块和内核数据结构给子进程
2,将父进程部分数据结构内容拷贝至子进程,这里是浅拷贝
3,添加子进程到系统进程列表当中
4,fork返回,开始调度器调度
这里要说明一下,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。这里父子进程执行的先后顺序完全由调度器决定。退出时父进程是最后退出的,因为父进程要回收子进程。这里注意,当系统有太多进程或实际用户的进程数超过了限制,fork调用就会创建失败。具体内部流程如下图:
修改之前,父子进程在指针指向方面完全相同,也就是C++中的浅拷贝。当内核调用fork创建子进程后,并不会立即复制父进程的所有内存页给子进程,而是让子进程与父进程共享相同的内存页。只有当父进程或子进程尝试修改某个共享的内存页时,操作系统才会为修改者复制那一页内存,从而确保每个进程都有自己独立的数据副本。这种机制就叫做写时拷贝。写时拷贝减少了不必要的资源复制,它可以降低系统的开销,提高资源的利用率。
通过以上图观察,在页表中还存储着一个权限的限制。平常的代码存储是按照虚拟地址进行存储的,所以当我们对代码进行操作时,这时会对页表中对应代码的权限进行访问,然后对其进行操作。比如对常量字符串进行修改时,内部就是页表对其权限的限制不可被修改。但注意,编译器在编译时出错,这只是编译器内部单纯对其做出的报错处理,与系统无关。总的来说系统防御的是运行报错,编译报错依靠编译器预防。
进程的退出
进程退出场景共分为三种:1,代码运行完毕,结果正确。2,代码运行完毕,结果不正确。3,代码异常终止。
首先,我们先来观察进程正常运行时的情况。进程的正常退出都是从main函数中退出的。在平常写C/C++代码中的大多情况下,我们都会在进程代码的main函数最后写上return 0。return 0其实表示进程返回0的退出码,以此退出码0来表示进程的正常退出,即进程退出码(一般情况下0表示进程退出成功,非0表示进程退出失败)。
进程退出码在Linux中使用 echo $? 指令可打印查看。
[zhujunhao@bogon code]$ cat code.cpp
#include <iostream>
using namespace std;
int main()
{
return 10; //进程退出码设置为10
}[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
[zhujunhao@bogon code]$ ./code.exe
[zhujunhao@bogon code]$ echo $?
10//当多次使用时,系统会记录最近一次进程执行完毕时的退出码
[zhujunhao@bogon code]$ echo $? //记录此时的echo进程,执行成功,退出码为0
0
[zhujunhao@bogon code]$ echo $?
0[zhujunhao@bogon code]$ ls -ruowvv
ls: invalid line width: vv
[zhujunhao@bogon code]$ echo $? //上一个ls进程运用错误,退出码为2
2
进程退出码在C/C++中不仅可使用return 0表示,也可使用接口 exit(退出码)或_exit 指定退出码表示。exit与_exit不同的是_exit在退出时不会清理专门的缓冲区,exit调用时会先刷新缓冲区并关闭流等,然后调用_exit退出进程,所以在exit和_exit之间,最好使用exit。
两者虽然都是用来表示进程正常退出,但不同的是 exit 退出进程是无论在哪个函数(main函数和其它普通函数)中运用都会直接将此进程退出。return退出必须在main函数中退出,其它普通函数只会当成返回值处理。在exit和return之间,return是一种更常见的退出进程方法。执行 return n 等同于执行exit(n),因为调用main的运行时,函数会将main的返回值当做 exit 的参数。
异常处理是通过系统内部的异常信号来实现。当进程代码抛不同异常时,内部就会发送不同的异常信号,然后通过异常信号返回指定的异常退出码,这里每一个异常退出码对应一个异常信息。我们通过使用 strerror(i) 接口查看第i个的错误码所对应的错误信息,此接口参数通过与接口 errno(返回此进程的退出码。正常退出返回0,异常退出返回指定的错误码) 连用,即strerror(errno)。如下,这里我们模拟实现系统中50个异常退出码对应的异常信号。
[zhujunhao@bogon code]$ cat code.cpp
#include <iostream>
#include <cstring>
#include <errno.h>
using namespace std;
int main()
{
for (int i = 0; i < 50; i++)
{
cout << i << " : " << strerror(i) << endl;
}
return errno; //返回进程退出码
}[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
[zhujunhao@bogon code]$ ./code.exe//以下是退出码对应的进程代码说明
0 : Success
1 : Operation not permitted
............
与此函数功能相同的还有接口 perror 直接输出错误信息。
错误码转换成错误描述除了以上使用语言和系统自带的方法转换外还有自定义的形式实现。
[zhujunhao@bogon code]$ cat code2.cpp
#include <iostream>
#include <string>
#include <unistd.h>
using namespace std;
enum {
A=0,
B,
C
};
const string ToDesc(int code)
{
switch(code)
{
case A:
return "A error";
case B:
return "B error";
case C:
return "C error";
default:
return "success";
}
}
int main()
{
int code = C;
cout << ToDesc(code) << endl;
return code;
}
[zhujunhao@bogon code]$ g++ code2.cpp -o code2.exe
[zhujunhao@bogon code]$ ./code2.exe
C error[zhujunhao@bogon code]$ echo $?
2 //自定义返回退出码为数值2
进程等待
父进程创建子进程是为了让子进程完成某个特定的任务。但若子进程创建后,父进程如果不管不顾,就可能造成 ‘僵尸进程’ 的问题,因为僵尸进程只是释放了一部分资源,另一部分资源需要父进程收到退出信息后才可释放,所以僵尸进程会造成内存泄漏问题。另外,进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如:子进程运行完成、结果对还是不对、 或者是否正常退出等。这些信息需要父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的方法有两种:wait方法和waitpid方法。它们都是用来获取子进程的信息并且也可以处理僵尸进程。如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
当调用wait时,如果没有运行完成的子进程,父进程会阻塞等待,直到有一个子进程结束,然后wait
函数返回。当调用waitpid接口时,父进程即可选择进入阻塞状态,又可选择进入非阻塞状态。
在Linux中,wait函数是用于等待子进程结束并获取子进程的终止状态的系统调用。它在父进程中使用,用于等待其子进程终止并获得子进程的退出状态。waitpid
跟wait
函数相比提供了更多的灵活性,允许父进程指定要等待的子进程以及控制等待的行为。
wait和waitpid的头文件
#include<sys/types.h>
#include<sys/wait.h>接口类型
pid_t wait(int* status);
pid_ t waitpid(pid_t pid, int* status, int options);
wait返回值:
如果有一个或多个子进程结束,
wait
函数会直接成功返回被等待的一个进程的pid,如果该进程没有子进程,wait
函数会失败,返回-1。waitpid返回值:
当正常返回的时,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,即不在阻塞等待,进入非阻塞等待。而调用中waitpid发现没有已退出的子进程可收集,返回值为0;
如果调用中出错,即没有子进程,则返回-1,这时errno会被设置成相应的值以指示错误所在;
status参数:此参数为输出型参数,用来获取子进程退出状态, 不关心则可以设置成为NULL。
pid参数:
Pid = -1,等待任一个子进程。与wait等效。
Pid > 0,等待专门子进程的pid。options参数:
options可设为两种状态,数值为0时表示阻塞等待,数值不为0时表示非阻塞等待。
这里我们先研究status。status指向的整数不能简单的当作整形来看待,这个整数是一个特殊的位掩码,用于存储子进程的退出状态信息或接收到的信号信息。这个位掩码具有自己的特定格式,它一共有32位比特位。前16位暂时我们先不用管,后面会具体解释,这里先只考虑status后16比特位,具体细节如下图:
当进程正常退出时,系统抛出信号0表示正常,退出状态返回退出码。当进程出现异常时,进程将会收到系统下的终止信号,被信号所杀。这里在系统处理进程时,系统会优先查看是否收到了终止信号字段,若后六位的终止信号字段是0,表示没有收到异常信号,这时会查看退出状态,若也是0,表明退出码正常。若终止信号字符是0,进程状态非0,表明代码正常跑完了,但是结果不正确,不正确的原因由退出码表明。若终止信号字段非0,退出状态就不用看了,表明代码运行的中间出了问题。系统就是通过这样的信号与退出码来标明进程的退出状态。
这里简单说明一下信号。在Linux中,使用 kill -l 指令可查看进程信号,信号数字对应的就是处理功能(功能与数子之间是宏关系,也就是说即可使用数字表示功能,也可使用具体的名字)。当我们使用 kill -9 来强制杀掉指定的pid进程时,其实就是向系统发送信号9所对应的功能来处理指定pid的进程。
下面我们观看当status为空时的情况:
[zhujunhao@bogon code]$ cat code.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
// Child
if (id == 0)
{
int cent = 5;
while (cent)
{
cout << "Child is Running, pid: " << getpid() << " ppid: " << getppid() << endl;
sleep(1);
cent--;
}
cout << "子进程准备退出,马上变僵尸进程" << endl;
exit(0);
}
// Father
pid_t fid = wait(NULL); //阻塞等待。这里不关心子进程退出状态
if (fid > 0)
{
cout << "Father pid is: " << getpid() << endl;
cout << "wait success, fid = wait(NULL): " << fid << endl;
}
return 0;
}
[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
[zhujunhao@bogon code]$ ./code.exe
Child is Running, pid: 19283 ppid: 19282
Child is Running, pid: 19283 ppid: 19282
Child is Running, pid: 19283 ppid: 19282
Child is Running, pid: 19283 ppid: 19282
Child is Running, pid: 19283 ppid: 19282
子进程准备退出,马上变僵尸进程
Father pid is: 19282
wait success, fid = wait(NULL): 19283 //返回子进程的pid
然后再来观察当status不为空时的情况。
[zhujunhao@bogon code]$ cat code.cpp
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
// Child
if (id == 0)
{
int cent = 5;
while (cent)
{
cout << "Child is Running, pid: " << getpid() << " ppid: " << getppid() << endl;
sleep(1);
cent--;
}
cout << "子进程准备退出,马上变僵尸进程" << endl;
exit(1); //子进程退出码设置为1
}
// Father
int status = 0;
pid_t fid = wait(&status); //这里也可设置为为waitpid("子进程的专属id", &status, 0);与wait(&status)等价
if (fid > 0)
{
cout << "Father pid is: " << getpid() << endl;
cout << "wait success, fid = wait(nullptr) = " << fid << " : status = " << status << endl;
}
return 0;
}[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
[zhujunhao@bogon code]$ ./code.exe
Child is Running, pid: 28938 ppid: 28937
Child is Running, pid: 28938 ppid: 28937
Child is Running, pid: 28938 ppid: 28937
Child is Running, pid: 28938 ppid: 28937
Child is Running, pid: 28938 ppid: 28937
子进程准备退出,马上变僵尸进程
Father pid is: 28937
wait success, fid = wait(nullptr) = 28938 : status = 256 //发现status子进程退出码不是1
上面说过,退出码是存放在专有的位字符中,存放退出码后,在后16位字段中二进制 0000 0001 0000 0000 == 十进制256。若我们只想提取出子进程的退出码,这里我们可设计一下,如以上代码中 (status>>8) & 0xFF 表示取出子进程的退出码,status & 0x7F 表示收到的信号编号。
由于位掩码可以表示很多种意思,所以系统专门给我们提供不同的宏来表示不同的功能。如:WIFEXITED(status) 表示若正常终止子进程返回的状态,则为真。可用来查看进程是否是正常退出。WEXITSTATUS(status) 表示若WIFEXITED非零,则提取子进程的退出码。
[zhujunhao@bogon code]$ cat code.cpp
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
// Child
if (id == 0)
{
cout << "Child is Running, pid: " << getpid() << " ppid: " << getppid() << endl;
sleep(1);
exit(1); //子进程退出码设置为1
}
// Father
int status = 0;
pid_t fid = waitpid(id, &status, 0);
if (fid > 0)
{
cout << "Father pid is: " << getpid() << endl;
cout << "waitpid success, fid = waitpid(id, &status, 0) = " << fid << " : status = " << status << endl;
cout << "子进程退出码(status>>8)&0xff: " << ((status>>8) & 0xff) << " 子进程信号编号: " << (status & 0x7f) << endl;
if (WIFEXITED(status))
{
cout << "子进程退出码WEXITSTATUS(status): " << WEXITSTATUS(status) << endl;
}
else
{
cout << "Chhild process error" << endl;
}
}
return 0;
}[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
[zhujunhao@bogon code]$ ./code.exe
Child is Running, pid: 31042 ppid: 31041
Father pid is: 31041
waitpid success, fid = waitpid(id, &status, 0) = 31042 : status = 256
子进程退出码(status>>8)&0xff: 1 子进程信号编号: 0
子进程退出码WEXITSTATUS(status): 1
可看出,由于信号编号为0,以上代码均正常。下面我们观看不正常的情况。这里的进程代码还是以上代码。如下图:
手动信号异常终止
代码异常终止
这里我们在代码的子进程中添加以下代码
int a = 10;
a /= 0; //除0错误操作
[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
//这里编译时会显示警告消息[zhujunhao@bogon code]$ ./code.cpp
bash: ./code.cpp: Permission denied
[zhujunhao@bogon code]$ ./code.exe
Child is Running, pid: 32519 ppid: 32518
Father pid is: 32518
waitpid success, fid = waitpid(id, &status, 0) = 32519 : status = 136
子进程退出码(status>>8)&0xff: 0 子进程信号编号: 8
Chhild process error
这里我们在原本代码的子进程中添加以下代码
int *p = NULL;
*p = 200; //空指针解引用错误操作
[zhujunhao@bogon code]$ g++ code.cpp -o code.exe
[zhujunhao@bogon code]$ ./code.exe
Child is Running, pid: 32791 ppid: 32790
Father pid is: 32790
waitpid success, fid = waitpid(id, &status, 0) = 32791 : status = 139
子进程退出码(status>>8)&0xff: 0 子进程信号编号: 11
Chhild process error
下面我们来说明一下waitpid第三个参数中的options。waitpid是进入阻塞状态还是进入非阻塞状态跟此参数有关。当options为0时,父进程进入阻塞状态。当options不为0时,父进程进入非阻塞状态。选择进入非阻塞状态通常将options参数设置 WNOHANG。此宏表示若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
在非阻塞模式下,父进程可以在不阻塞的情况下轮询 waitpid
来检查是否有子进程已经终止,因此需要这种方式需要使用循环的方式来不断轮询检查。这种方式允许父进程继续执行其他任务,同时仍然能够处理子进程的终止。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;#define NUM 5
typedef void(*fun_t)();
fun_t tasks[NUM];void printLog()
{
cout << "this is a log print task" << endl;
}void printNet()
{
cout << "this is a net task" << endl;
}void printNPC()
{
cout << "this is a flush NPC" << endl;
}void initTask()
{
tasks[0] = printLog;
tasks[1] = printNet;
tasks[2] = printNPC;
tasks[3] = NULL;
}void excuteTask()
{
for (int i = 0; tasks[i]; i++)
tasks[i](); // 回调机制
}int main()
{
initTask();
pid_t id = fork();
if (id == 0)
{
cout << "Child: pid = " << getpid() << " ppid = " << getppid() << endl;
sleep(5);
exit(10);
}
int status = 0;
while (true) // 非阻塞状态,不断循环访问子进程是否退出
{
pid_t fid = waitpid(id, &status, WNOHANG);
if (fid > 0)
{
cout << "waitpid success, fid = waitpid(id, &status, 0) = " << fid << " status = " << status << endl;
cout << "子进程退出码WEXITSTATUS(status): " << WEXITSTATUS(status) << " " << "子进程信号编号: " << (status & 0x7f) << endl;
break;
}
else if (fid == 0)
{
cout << "Child is running, Father do other thing" << endl;// 这里子进程在运行的时候父进程模拟实现运行自己的部分
cout << "#################### task begin ####################" << endl;
excuteTask();
cout << "#################### task end ####################" << endl;
}
else // 返回值-1的情况
{
perror("waitpid");
break;
}
sleep(1);
}
return 0;
}