一. 进程创建
1.fork函数
在 linux 中 fork 函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程,而原进程为父进程。
例如:
调用 fork() 之前:
父进程
│
│ 执行
▼
fork()
调用成功以后:
fork()
│
┌──────┴──────┐
▼ ▼
父进程 子进程
PID=100 PID=101
父子进程会从 fork() 之后的位置继续执行。
**2.**fork函数返回值
子进程返回0
父进程返回的是子进程的pid。
为什么这样设计?
因为父进程可能有很多子进程:
父进程
/ | \
/ | \
子进程 子进程 子进程
101 102 103
父进程需要知道:我刚刚创建出来的这个子进程到底是谁?
所以父进程得到:101
也就是子进程 PID。
而子进程只需要知道:我是子进程。
所以直接返回:0
因此可以利用返回值让父子进程执行不同代码:
pid_t pid = fork();
if (pid == 0)
{
// 子进程
}
else if (pid > 0)
{
// 父进程
}
else
{
// fork失败
}
为什么一个 fork() 函数会有两个返回值?
这里最容易产生误解。
不是一个函数执行了一次,然后返回了两次。
而是fork() 调用一次,但是调用成功以后,系统中存在两个进程,并且父子进程都会从 fork() 返回的位置继续执行。
例如:
printf("A\n");
fork();
printf("B\n");
执行过程:
fork()之前
一个进程
│
▼
printf A
│
▼
fork()
│
┌────┴────┐
▼ ▼
父进程 子进程
│ │
▼ ▼
printf B printf B
所以最终:
A
B
B
关键理解
fork() 本身不是"返回两次"。
而是:
一个进程
↓ fork()
两个进程
↓
两个进程分别从 fork() 后面继续执行
所以看起来就像 fork() 有两个返回值。
为什么同一个 pid 既可以等于 0,又可以大于 0?
pid_t pid;
pid = fork();
printf("%d\n", pid);
pid 是每个进程自己的变量。
fork() 后,父子进程各自拥有自己的进程地址空间。
所以实际上变成:
父进程:
pid = 101
子进程:
pid = 0
虽然它们最开始是从同一份代码复制出来的,但是已经是两个独立的进程。
因此:
父进程看到的 pid = 101
子进程看到的 pid = 0
并不存在:同一个变量同时既是 0 又是 101
而是:父进程中的 pid 和子进程中的 pid 是两个不同进程里的变量。
fork之后谁先执行?
不确定。
例如:
pid = fork();
printf("hello\n");
可能:
父进程 hello
子进程 hello
也可能:
子进程 hello
父进程 hello
因为 fork() 返回之后,由 Linux 的进程调度器决定谁先获得 CPU。
所以:不要认为父进程一定先执行,也不要认为子进程一定先执行。
3.写时拷贝
fork() 后,父子进程虽然拥有各自独立的地址空间,但一开始并不会立即把所有数据真的复制一份。
可以简单理解成:
父进程
│
├──────────┐
│ │
▼ ▼
共同使用物理内存
如果父子进程只是读取:
printf("%d", x);
可以暂时共享。
但是如果某一个进程修改:
x = 100;
系统才真正复制对应的内存:
原来的内存
│
┌──────┴──────┐
▼ ▼
父进程 子进程
自己一份 自己一份
这就是写时拷贝
如果 fork() 一开始就把父进程所有内存全部复制:
父进程 1GB
↓ fork
子进程再复制1GB
非常浪费。
很多时候子进程根本不会修改这些数据。
所以 Linux 采用:先共享,谁写谁复制。
这样可以节省内存,提高效率。
**4.**fork常规用法
① 创建子进程处理任务
⼀个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客⼾端请求, 生成子进程来处理请求。
例如服务器:
父进程
等待客户端请求
│
fork()
/ \
▼ ▼
父进程 子进程
继续等待 处理请求
这就是很典型的服务器模型。
② fork + exec 执行另一个程序
⼀个进程要执行⼀个不同的程序。例如⼦进程从fork返回后,调用exec函数
例如:
父进程
│
▼
fork()
│
▼
子进程
│
▼
exec()
│
▼
运行另一个程序
这也是 Linux 中非常重要的进程创建方式。
**5.**fork 失败的原因
正常情况下:
pid = fork();
可能出现三种情况:
pid < 0 → fork失败
pid == 0 → 当前是子进程
pid > 0 → 当前是父进程,pid是子进程PID
fork() 失败常见原因:
- 系统进程数量达到限制
- 当前用户的进程数量达到限制
- 系统资源不足,例如内存不足
二.进程终止
进程终止,本质上就是进程退出,并释放它占用的系统资源。
**1.**进程退出场景
① 代码运行完毕,结果正确
例如:
int main()
{
printf("hello\n");
return 0;
}
正常运行:
hello
然后程序结束。
退出码:
0
表示正常。
② 代码运行完毕,但是结果不正确
例如程序自己检测到了错误:
int main()
{
printf("发生错误\n");
return 1;
}
程序虽然正常执行到了 return,但是告诉 Shell:我这次运行有问题。
所以:
退出码 = 1
③ 代码异常终止
例如:
Ctrl + C
会向程序发送:
SIGINT
程序收到信号后通常会终止。
所以:
正常终止 ≠ 异常终止
2.Linux 中进程正常退出的三种方式
最重要的三个:
① return
② exit()
③ _exit()
退出码
退出码就是:程序结束时给操作系统 / Shell 留下的一个数字,用来表示程序执行结果。
最常见:
0 → 成功
非0 → 通常表示出现问题
例如:
int main()
{
return 0;
}
执行:
./a.out
echo $?
得到:
0
为什么 echo $? 可以查看退出码?
这里的 $? 是 Shell 中一个特殊变量。
echo $?
意思就是:查看上一条命令的退出状态。
例如
./a.out
echo $?
如果程序:
return 0;
那么:
0
如果:
return 10;
那么:
10
五、为什么退出码通常用 0 表示成功?
这是 Linux/Unix 中约定俗成的规则:
0 → 成功
非0 → 失败/异常/其他状态
所以写程序的时候经常看到:
return 0;
意思就是:程序正常结束,没有错误。
而:
return 1;
通常表示:程序执行过程中出现了某种错误。
退出码不是"错误码"的严格同义词
退出码本质上是程序结束时提供给父进程/Shell 的状态值。
例如:
return 100;
不一定意味着:错误100
它只是:程序退出时返回了 100。
至于 100 具体代表什么,是程序自己定义的。
3._exit函数
作用:直接终止当前进程。
例如:
#include <unistd.h>
int main()
{
_exit(0);
}
程序直接退出。
为什么 _exit(-1) 最后是 255?
虽然:
_exit(int status);
参数是 int:
int = 32位
但是进程退出状态中,通常只有低 8 位用于传递退出码。
也就是:
0 ~ 255
_exit(-1)
-1 的低 8 位:
11111111
换成无符号数就是:
255
所以:
_exit(-1);
然后:
echo $?
得到:
255
记住退出码通常只看低 8 位,所以范围可以理解为 0~255。
exit() 和 _exit() 的区别
两者都会让进程终止,但是 exit() 在真正终止之前还会做一些清理工作。
_exit()
可以简单理解:
_exit()
↓
直接进入进程终止流程
exit()
可以理解成:
exit()
↓
执行清理函数
↓
刷新/关闭标准 I/O 流
↓
调用 _exit()
↓
进程终止
所以:exit() 最终也会调用 _exit(),但是在调用 _exit() 之前会做额外的清理工作。
为什么 printf("hello") + exit() 能看到 hello?
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello");
exit(0);
}
注意:
printf("hello");
这里没有:
\n
所以 hello 可能还留在 stdio 的输出缓冲区里,没有立即写到终端。
但是:
exit(0);
会处理标准 I/O 流,并刷新缓冲区。
所以最终能看到:
hello
为什么换成 _exit() 后 hello 消失了?
代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello");
_exit(0);
}
流程:
printf("hello")
↓
hello可能还在stdio缓冲区
↓
_exit(0)
↓
直接终止
↓
没有通过exit进行stdio缓冲区刷新
因此终端可能什么都看不到。
也就是说:
exit()
↓
刷新stdio缓冲区
↓
hello显示
而:
_exit()
↓
不负责这种stdio层面的清理
↓
hello可能没显示
return 为什么也能退出进程?
例如:
int main()
{
printf("hello\n");
return 0;
}
这里:
return 0;
会结束 main()。
而 main() 是程序的入口函数。
C 运行时环境会把:
return 0;
对应到进程退出状态。
可以简单记成:
main中的 return n
↓
相当于
exit(n)
所以return n 等同于 exit(n)。