Linux进程创建与终止
目录
一、进程创建
1. fork函数初识
2. fork函数返回值的意义
3. 写时拷贝(Copy-On-Write, COW)
4.fork的常规用法
5. fork调用失败的原因
二、进程终止
1. 终止的本质
2. main函数的return返回值
3. 进程退出状态的含义
4. 父进程为什么会得到子进程的退出码?
5. 异常退出与信号
6. 进程常见终止情况
7. exit 和 _exit 的区别
8. return与exit的区别
一、进程创建
1. fork函数初识
在Linux系统中,fork()
函数是创建进程的核心工具。它通过复制现有进程(父进程)来创建新进程(子进程)。
函数声明:
#include <unistd.h>
pid_t fork(void);
返回值:
在子进程中返回
0
。在父进程中返回子进程的PID(进程ID)。
如果出错,返回
-1
。
fork函数的工作原理:
进程调用
fork
时,当控制转移到内核中的fork代码后,内核会执行以下操作:
分配新的内存块和内核数据结构给子进程。
将父进程的部分数据结构内容拷贝至子进程。
将子进程添加到系统进程列表中。
fork
返回后,调度器开始调度。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
printf("Before: pid is %d\n", getpid()); // 打印当前进程ID
if ((pid = fork()) == -1)
{
perror("fork()"); // 出错处理
exit(1);
}
printf("After: pid is %d, fork return %d\n", getpid(), pid);
sleep(1); // 延迟1秒
return 0;
}
运行结果:
这里看到了三行输出,一行Before,两行After。进程61200先打印Before消息,然后它又打印After。另一个After消息是由61201打印的。我们注意到进程61201没有打印Before,为什么呢?如下图所示:
分析:
父进程(PID=61200)先打印
Before
消息,然后调用fork
创建子进程(PID=61201)。父进程和子进程各自打印
After
消息。子进程没有打印Before
消息,因为它是在fork
之后才创建的。所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
经典面试题:连续调用两次fork会创建多少进程?
fork(); fork();
答案:总共会创建3个子进程(2^n-1公式)
2. fork函数返回值的意义
-
子进程返回0:子进程需要知道自己是子进程,返回0是一个约定。
-
父进程返回子进程的PID:父进程需要管理子进程(如等待子进程),PID是唯一标识符。
PID管理示意图:
父进程 └── 子进程1(PID=100) └── 子进程2(PID=101)
3. 写时拷贝(Copy-On-Write, COW)
父子进程在fork
之后,虽然共享代码段和数据段,但在写入时会触发写时拷贝机制。即,当父子进程中的任意一方试图修改共享数据时,系统会为修改方创建一份独立的副本,以避免数据冲突。具体见下图:
内存管理机制:
初始时父子共享物理内存页
当任一进程尝试写入时触发缺页异常
内核复制该内存页供修改进程专用
示例验证:
int global_val = 100;
int main()
{
int local_val = 200;
pid_t pid = fork();
if (pid == 0)
{
global_val++;
local_val++;
printf("Child: %d, %d\n", global_val, local_val);
}
else
{
sleep(2); // 确保子进程先执行
printf("Parent: %d, %d\n", global_val, local_val);
}
return 0;
}
运行结果:
4.fork的常规用法
- 父进程复制自己,使父子进程同时执行不同代码段:例如,父进程等待客户端请求,生成子进程来处理请求。
- 子进程执行不同的程序:子进程从
fork
返回后,调用exec
函数执行新程序。
5. fork调用失败的原因
系统中已有太多进程。
实际用户的进程数超过了系统限制。
二、进程终止
1. 终止的本质
进程终止时,系统会执行以下操作:
-
释放代码和数据占用的空间。
-
释放内核数据结构(如
task_struct
)。
2. main
函数的return
返回值
-
main
函数的return
返回值表示进程的退出状态。 -
该值会传递给父进程(如Bash shell),父进程可以通过
echo $?
来获取这个退出状态。
3. 进程退出状态的含义
-
退出码(Exit Code):
-
0 表示成功。
-
非 0 表示失败,具体值可自定义,用于表示不同的失败原因。
-
-
退出信号(Exit Signal):
-
如果进程因接收到信号而终止,退出码可能没有意义。
-
退出信号可以通过特定方式(如
wait
函数)获取,以判断进程异常原因。
-
示例分析:
int main()
{
int *ptr = NULL;
*ptr = 10; // 访问空指针,触发段错误(SIGSEGV)
return 0;
}
运行时,进程会因段错误被操作系统终止,此时通过 echo $?
获取的值是 139
。这是因为 SIGSEGV
的信号编号是 11
,而退出状态码是 128 + 11 = 139
。
4. 父进程为什么会得到子进程的退出码?
父进程需要知道子进程的退出情况,以便:
-
判断子进程是否成功完成任务。
-
如果失败,可以通过退出码或信号得知失败原因,从而进行相应的处理(如重试、记录错误日志等)。
5. 异常退出与信号
-
异常退出原因:
-
进程做了非法操作,如访问非法内存、除以零等。
-
-
信号(Signal):
-
操作系统会发送一个信号(如
SIGSEGV
、SIGFPE
等)来终止进程。 -
退出信号可以用
wait
和相关宏(如WIFSIGNALED
、WTERMSIG
)来获取。
-
6. 进程常见终止情况
-
正常终止:
-
main函数返回:
return
语句的返回值表示进程的退出码。 -
调用
exit
函数: 可以在代码的任意位置调用exit
终止进程。 -
调用
_exit
系统调用: 直接终止进程,不执行清理操作。
-
-
异常终止:
-
收到操作系统发送的信号(如
SIGKILL
、SIGTERM
)。 -
例如,用户按下
Ctrl+C
会发送SIGINT
信号。
-
7. exit
和 _exit
的区别
exit
系列相关函数:
#include <unistd.h>
void exit(int status); // 调用库函数,执行清理操作后调用_exit
void _exit(int status); // 系统调用,直接终止进程
exit
:
调用时会执行清理操作,如调用用户注册的清理函数(通过
atexit
或on_exit
)、刷新流缓冲区等。最后调用
_exit
来终止进程。
_exit
:
系统调用,直接终止进程,不执行清理操作。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void cleanup()
{
printf("Cleanup function called!\n");
}
int main()
{
cleanup();
printf("hello");
exit(0); // exit会调用清理函数并刷新缓冲区
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void cleanup()
{
printf("Cleanup function called!\n");
}
int main()
{
cleanup();
printf("hello");
_exit(0); // _exit不会调用清理函数,也不会刷新缓冲区
}
8. return与exit的区别
-
return:
-
用于从
main
函数返回,等同于调用exit
。 -
返回值会被父进程获取。
-
-
exit:
-
显式终止进程,可以指定退出码。
-
会刷新缓冲区并执行清理操作。
-