fork() 是 Unix 和 Linux 系统中一个非常重要的系统调用,用于创建一个新的进程。新进程被称为子进程,而调用 fork() 的进程被称为父进程。fork() 的设计使得子进程几乎完全复制了父进程的状态,这使得它可以独立地执行任务。
基本概念
父进程和子进程
- 父进程:调用
fork()的进程。 - 子进程:由
fork()创建的新进程。
返回值
- 在 父进程 中,
fork()返回子进程的进程 ID(PID)。 - 在 子进程 中,
fork()返回0。 - 如果
fork()失败,返回 -1,并设置errno以指示错误类型。
工作原理
- 子进程复制父进程的资源
- 子进程几乎完全复制了父进程的所有资源,包括但不限于:
- 文件描述符
- 环境变量
- 信号处理函数
- 当前工作目录
- 用户和组 ID
- 但是,子进程和父进程有不同的进程 ID(PID)和父进程 ID(PPID)。
- 子进程几乎完全复制了父进程的所有资源,包括但不限于:
- 父进程
fork后的运行fork函数不是阻塞函数,调用fork后继续运行后续代码- 也可以通过
wait()或waitpid()用于父进程等待子进程结束。这有助于避免僵尸进程(zombie process)。
- 内存管理
- 在现代操作系统中,
fork()使用 COW 技术来优化内存使用。这意味着在fork()刚刚调用后,父进程和子进程共享相同的物理内存页。只有当某个进程尝试修改这些页时,操作系统才会为该进程创建新的内存页副本。
- 在现代操作系统中,
代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid;
// 调用 fork() 创建子进程
pid = fork();
if (pid < 0) {
// fork() 失败
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
printf("This is the child process, PID: %d\n", getpid());
// 子进程可以在这里执行自己的任务
execlp("/bin/ls", "ls", NULL); // 执行 ls 命令
} else {
// 父进程
printf("This is the parent process, PID: %d, Child PID: %d\n", getpid(), pid);
// 父进程可以在这里等待子进程结束
wait(NULL);
printf("Child process has finished.\n");
}
return 0;
}应用场景
- 并发处理:
fork()常用于创建多个子进程来并行处理任务。 - 守护进程:创建守护进程时,通常会使用
fork()来创建一个子进程,然后父进程退出,使子进程成为孤儿进程,由 init 进程接管。 - 命令执行:在 shell 中执行命令时,通常会使用
fork()创建子进程,然后在子进程中调用exec()系列函数来执行新的程序。
并发处理
Redis AOF 重写机制。
守护进程
实际应用场景
- Web 服务器:
- 如 Apache 和 Nginx,这些服务器通常以守护进程的形式运行,处理来自客户端的请求。
- 日志记录服务:
- 如
rsyslog和syslog-ng,这些服务在后台运行,收集和记录系统日志。
- 如
- 定时任务:
- 如
cron,它在后台运行,定期执行预定的任务。
- 如
- 网络服务:
- 如
sshd(SSH 服务器),它在后台监听网络连接请求,并处理登录会话。
- 如
创建守护进程的流程
- 调用
fork()创建子进程:- 子进程将成为守护进程,父进程通常会退出。
- 调用
setsid()创建新的会话:- 这会使子进程成为新的会话的领导者,并脱离控制终端。
- 改变工作目录:
- 通常将工作目录改为根目录(
/),以防止守护进程占用当前目录的挂载点。
- 通常将工作目录改为根目录(
- 重设文件权限掩码:
- 通常将文件权限掩码设置为0,以便守护进程创建的文件具有默认权限。
- 关闭不需要的文件描述符:
- 关闭标准输入、输出和错误描述符,以防止守护进程占用这些文件描述符。
- 打开日志文件:
- 如果需要,可以打开日志文件来记录守护进程的活动。
实例代码
该守护进程每5秒向一个日志文件写入一条消息:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
void daemonize() {
pid_t pid, sid;
// 第一次调用 fork(),创建子进程
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程继续执行
umask(0); // 重设文件权限掩码
// 创建新的会话,使子进程成为新的会话的领导者,并脱离控制终端。
sid = setsid();
if (sid < 0) {
exit(EXIT_FAILURE);
}
// 改变工作目录
if ((chdir("/")) < 0) {
exit(EXIT_FAILURE);
}
// 关闭标准输入、输出和错误描述符,防止守护进程占用这些文件描述符。
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 打开日志文件
openlog("mydaemon", LOG_PID, LOG_DAEMON);
}
int main() {
daemonize();
while (1) {
syslog(LOG_NOTICE, "Daemon is running at %d", time(NULL));
sleep(5);
}
closelog();
return 0;
}注意事项
- 资源消耗:虽然
fork()使用 COW 技术来优化内存使用,但创建大量子进程仍然会消耗较多的系统资源。 - 信号处理:子进程继承了父进程的信号处理函数,但需要根据具体需求进行适当的调整。
- 文件描述符:子进程继承了父进程的文件描述符,如果不需要,应关闭不必要的文件描述符。