🏷️ 进程创建
📌 fork 函数
fork
函数的作用是用来创建一个子进程的,它的头文件是:include <unistd.h>
,它没有参数,返回值是:pid_t
。
#include <unistd.h>
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1
fork
函数返回的时候,如果是父进程 它就返回子进程的pid
,如果是子进程 它就返回0
。
fork
有两个 返回值。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
-
分配新的内存块和内核数据结构给子进程
(就是创建子进程的 pcb, 地址空间,页表 构建映射关系) -
将父进程部分数据结构内容拷贝至子进程
(就是用父进程对应的一些字段来初始化子进程,所以页表才会指向同一个地址 ) -
添加子进程到系统进程列表当中
-
fork返回,开始调度器调度
📌 fork 函数使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // 包含fork()和sleep()的定义
int main(void)
{
pid_t pid; // 声明一个pid_t类型的变量来存储fork()的返回值
// 打印当前(父)进程的PID
printf("Before: pid is %d\n", getpid());
// 调用fork()创建子进程
if ((pid = fork()) == -1)
{
// 如果fork()失败,则打印错误信息并退出程序
perror("fork()");
exit(1);
}
// 注意:从这一点开始,代码将同时在父进程和子进程中执行
// 打印当前进程的PID和fork()的返回值
// 在父进程中,这将显示父进程的PID和子进程的PID
// 在子进程中,这将显示子进程的PID和0(因为fork()在子进程中返回0)
printf("After: pid is %d, fork return %d\n", getpid(), pid);
// 父进程将执行sleep(1),即暂停执行1秒
// 子进程也会执行到这里,但由于没有更多的代码要执行(并且没有等待父进程的sleep完成),
// 子进程通常会立即退出。然而,由于输出缓冲区的存在,子进程的输出可能会稍后出现,
// 或者在某些情况下可能与父进程的输出混合。
sleep(1);
// 注意:这里没有针对子进程的特定退出代码,因为当子进程执行到这里时,
// 它已经完成了main函数中的所有代码,并将正常退出。父进程也将继续执行到这里,
// 但由于sleep(1)之后没有更多的代码,父进程也将正常退出。
return 0; // 父进程和子进程都将返回0,但它们的返回状态对彼此是独立的。
}
所以运行的结果如下:
root@localhost linux# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after
消息有43677打印的。注意到进程43677没有打印before,为什么呢?
因为fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器
决定。
📌 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副
本。具体见下图:
❓ 当父进程形成子进程之后,子进程写入,会发生写时拷贝 重新申请空间,进行拷贝,修改页表,我们的问题是:写时拷贝发生在什么时候?
父进程在创建子进程的时候,首先会将自己的读写权限修改为==只读==,然后才创建子进程
但是用户并不知道上面的过程,用户可能在未来的某个时刻对某一批数据进行写入,一旦你开始写入,页表转化会因为权限问题而出错,这个时候操作系统介入, 操作系统会先进行一个判断,如下:
- 如果你要写入的数据是在代码段(代码段的权限为:只读),那就是真的出错了。
- 如果你要写入的数据是在数据段(数据段的权限为:读写),那不是真正的出错,触发我们进行重新申请内存拷贝内容的策略机制
通过这种触发异常的方式让操作系统进行写时拷贝,写时拷贝完成之后再把对应的页表映射条目改成读写,然后就可以进行正常的访问了,没有写入的依旧是只读的
❓ 当父进程形成子进程之后,子进程写入,会重新申请空间,那为什么要进行拷贝呢(为啥要把原始的数据在拷贝一份呢)?
如果你只是想要修改某个变量的名字,那我们可以拷贝原有的代码,在原有代码的基础上进行修改而不用从头开始
写代码。
📌 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子
进程来处理请求。 - 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
📌 fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
🏷️ 进程终止
📌 问题引入?
我们思考一个问题,我们写 c/c++ 程序的时候,为什么要写一个return 0
? 不写行不行?如果 return 1,return 2行不行, 这个 0
是给谁返回的呢?
如果我们有以下程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#define N 10
// 下面是一个返回值为 void 参数为空的函数指针
typedef void (*callback_t)();
void worker()
{
int cnt = 10;
while(cnt--)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(),cnt);
sleep(1);
}
}
void creatSubProces(int n, callback_t cb)
{
sleep(1);
for (int i = 0; i < N; i++)
{
sleep(1);
printf("creat child process success: %d\n", i);
pid_t pid = fork();
if (pid == 0)
{
// child 说明这是一个子进程
cb();
exit(0);
}
}
}
int main()
{
creatSubProces(N,worker);
// for 循环跑完之后,在后续的代码处,就只有父进程了
sleep(100);
return 0;
}
我们看到上面main
函数的返回值是0
,如果我们把它改成10
,我们看看会怎样?
return 10 // return 0 被我们改成了 return 10
我们发现,上面的程序依然能够成功的运行,在liunx 下 运行下面的命令,查看
echo $?
我们会发现输入上面的命名后屏幕上输出的是 10 。
📌进程退出场景
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止
所以当我们运行一个程序的时候,结果有上面三种可能,当程序运行完成之后,我们要知道它的状态是怎样的,结果到底是正确的还是不正确的?
如果你创建了一个子进程,那父进程也会想要知道子进程运行的结果如何,那我们如何知道这个结果呢?所以就有了main
函数的返回值,main
函数的返回值又叫进程的退出码 ,0
通常表示 success
, 非 0
通常表示的是failed
, 同时,不同的非 0 数字也可以用来表示不同的出错原因,C语言也内置了一些错误码系统也为我们提供了相应的接口:strerror
上面提到的$?
其实就是保存的最近一个子进程退出时保存的退出码
根据不同的现象返回不同的退出码
📎 错误码和退出码的关系
[!NOTE] 错误码
错误码通常是衡量一个库函数或者是一个系统调用一个函数的调用情况
[!NOTE] 退出码
退出码通常是一个进程退出的时候,他的退出结果
他们两个虽然概念上有不同,但是目的都是想要弄明白出错的原因,错误码是想弄明白函数出错的原因,退出码是想弄明白进程出错的原因。
如果一个进程是因为代码异常而终止的,那这个进程有没有跑完,它的退出码还有意义吗?
答, 一旦你的进程出问题了,你的代码一般是没跑完的。其实当我们的代码出问题了,它的退出码是没有意义的。举一个形象的例子:张三考试作弊得了 100 分,但是被发现了。学校会这样做吗:通报批评张三,然后表杨他考了 100 分。显然不会,学校只会通报批评,同样,进程异常了,我们也不会去关心它的退出码了(类比我们不会去表杨张三考了 100 分。)
什么情况下是代码异常,代码异常的本质是什么?
📌 异常问题—引入一下
我们知道想要终止一个进程可以使用快捷键ctrl c
, 也可以使用命令kill -9
来杀掉它
你可以随意运行一个正常的无错误的代码,然后以 kill -11 (该进程的 pid)
来杀掉他,-11
表示 的该进程有段错误,然后这个进程就被终止了,并显示有段错误(这就是欲加之罪)
进程出异常 本质上是进程受到了对应的信号,自己终止了!!
进程终止是因为收到了信号,所以我们确定进程是否受异常,我们需要做的事就是检测有没有收到信号即可。