2867 字
14 分钟

Linux进程关系深度解析:从父子进程到守护进程

Linux进程关系深度解析:从父子进程到守护进程#

引言:进程的家族树#

在Linux系统中,进程就像一个有组织的大家族。每个进程都有它的”父母”、“子女”,甚至可能成为”孤儿”或”僵尸”。理解这些关系对于编写健壮的系统程序至关重要。今天,我们就来深入探索这个有趣的进程家族世界!

1. 进程创建的基础:fork()函数#

在深入了解各种进程关系之前,我们先来认识一下进程创建的”魔法棒”——fork()函数。

fork()的工作原理#

fork()函数是Linux系统中创建新进程的核心函数。它的工作方式非常神奇:

  • 复制当前进程:创建一个与当前进程几乎完全相同的副本
  • 返回两个值:在父进程中返回子进程的PID,在子进程中返回0
  • 共享代码段:父子进程共享相同的代码,但拥有独立的数据空间
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 创建新进程
if (pid < 0) {
perror("fork失败");
return -1;
} else if (pid == 0) {
// 这里是子进程的代码
printf("我是子进程,我的PID是:%d\n", getpid());
} else {
// 这里是父进程的代码
printf("我是父进程,我的PID是:%d,我的孩子是:%d\n", getpid(), pid);
}
return 0;
}

2. 正常的父子进程关系#

代码示例:健康的父子进程#

/**
* 父子进程示例:父进程等待子进程结束并回收资源
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
perror("进程创建失败");
return -1;
} else if (pid == 0) {
// 子进程执行的任务
int counter = 0;
while (counter < 5) {
printf("子进程[%d]正在工作:第%d次循环\n", getpid(), counter);
sleep(1); // 模拟工作耗时
counter++;
}
printf("子进程任务完成!\n");
return 42; // 子进程退出码
} else {
// 父进程的行为
printf("父进程[%d]等待子进程结束...\n", getpid());
int status;
wait(&status); // 等待子进程结束
if (WIFEXITED(status)) {
printf("子进程正常结束,退出码:%d\n", WEXITSTATUS(status));
}
printf("父进程继续执行后续任务\n");
}
return 0;
}

运行结果分析#

当你运行这个程序时,你会看到:

父进程[1234]等待子进程结束...
子进程[1235]正在工作:第0次循环
子进程[1235]正在工作:第1次循环
子进程[1235]正在工作:第2次循环
子进程[1235]正在工作:第3次循环
子进程[1235]正在工作:第4次循环
子进程任务完成!
子进程正常结束,退出码:42
父进程继续执行后续任务

关键知识点#

  1. wait()函数的作用:父进程调用wait()会阻塞,直到子进程结束
  2. 资源回收:wait()不仅等待,还负责回收子进程的系统资源
  3. 退出状态:可以通过WEXITSTATUS获取子进程的退出码

3. 孤儿进程:被遗弃的孩子#

什么是孤儿进程?#

孤儿进程是指父进程先于子进程结束,子进程失去父进程的情况。在Linux中,这些”孤儿”会被init进程(PID为1的系统进程)收养。

代码示例:孤儿进程的产生#

/**
* 孤儿进程示例:父进程提前结束,子进程成为孤儿
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("进程创建失败");
return -1;
} else if (pid == 0) {
// 子进程:持续运行并显示父进程ID的变化
int seconds = 0;
while (seconds < 10) {
printf("当前时间:%d秒,我的PID:%d,父进程PID:%d\n",
seconds, getpid(), getppid());
sleep(1);
seconds++;
}
printf("子进程结束\n");
} else {
// 父进程:运行3秒后结束
printf("父进程[%d]开始运行,将在3秒后结束\n", getpid());
sleep(3);
printf("父进程结束\n");
}
return 0;
}

运行现象观察#

运行这个程序,你会观察到有趣的现象:

父进程[1234]开始运行,将在3秒后结束
当前时间:0秒,我的PID:1235,父进程PID:1234
当前时间:1秒,我的PID:1235,父进程PID:1234
当前时间:2秒,我的PID:1235,父进程PID:1234
父进程结束
当前时间:3秒,我的PID:1235,父进程PID:1 ← 父进程变成init!
当前时间:4秒,我的PID:1235,父进程PID:1
...

孤儿进程的特点#

  1. 自动被收养:系统自动将孤儿进程的父进程设置为init进程
  2. 无害性:孤儿进程不会造成系统问题,init会负责回收它们
  3. 常见场景:在服务器程序中,有时会故意创建孤儿进程

4. 僵尸进程:未安息的亡魂#

什么是僵尸进程?#

僵尸进程是已经结束执行,但其退出状态还没有被父进程读取(回收)的进程。这些进程虽然不再运行,但仍然占用着系统资源。

代码示例:制造僵尸进程#

/**
* 僵尸进程示例:父进程不回收子进程资源
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
// 创建多个子进程
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid < 0) {
perror("进程创建失败");
return -1;
} else if (pid == 0) {
// 子进程立即结束
printf("子进程[%d]诞生并立即死亡\n", getpid());
_exit(0); // 立即退出,不清理缓冲区
}
}
// 父进程不调用wait(),直接进入长时间睡眠
printf("父进程[%d]创建了3个子进程,但不回收它们\n", getpid());
printf("现在可以使用命令查看僵尸进程:ps aux | grep Z\n");
sleep(30); // 给足够时间观察僵尸进程
printf("父进程结束\n");
return 0;
}

如何检测僵尸进程#

运行上述程序后,在另一个终端中执行:

Terminal window
ps aux | grep Z

你会看到类似这样的输出:

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
user 1235 0.0 0.0 0 0 pts/0 Z 14:30 0:00 [zombie] <defunct>
user 1236 0.0 0.0 0 0 pts/0 Z 14:30 0:00 [zombie] <defunct>
user 1237 0.0 0.0 0 0 pts/0 Z 14:30 0:00 [zombie] <defunct>

僵尸进程的危害和解决方法#

危害

  • 占用进程ID资源
  • 占用系统进程表项
  • 大量僵尸进程可能导致无法创建新进程

解决方法

  1. 父进程调用wait()或waitpid()回收子进程
  2. 如果父进程不回收,可以杀死父进程(孤儿进程会被init回收)
  3. 使用信号处理SIGCHLD

5. 守护进程:后台的守护者#

什么是守护进程?#

守护进程是在后台运行的特殊进程,它没有控制终端,不与用户直接交互,通常用于提供系统服务。

创建守护进程的标准步骤#

  1. fork()并退出父进程:使子进程成为init的子进程
  2. setsid()创建新会话:脱离终端控制
  3. 改变工作目录:通常改为根目录
  4. 重设文件权限掩码:确保文件创建权限
  5. 关闭文件描述符:释放不需要的资源
  6. 处理信号:配置适当的信号处理

代码示例:时间记录守护进程#

/**
* 守护进程示例:每分钟记录系统时间到文件
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <stdlib.h>
#define LOG_FILE "/tmp/system_time.log"
// 获取当前时间的字符串表示
void get_current_time(char* buffer, size_t size) {
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
strftime(buffer, size, "%Y-%m-%d %H:%M:%S", tm_info);
}
// 完整的守护进程初始化
void init_daemon() {
pid_t pid = fork();
if (pid < 0) {
perror("第一次fork失败");
exit(EXIT_FAILURE);
} else if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程继续
// 创建新会话,脱离终端控制
if (setsid() < 0) {
perror("setsid失败");
exit(EXIT_FAILURE);
}
// 第二次fork,确保不是会话首进程
pid = fork();
if (pid < 0) {
perror("第二次fork失败");
exit(EXIT_FAILURE);
} else if (pid > 0) {
exit(EXIT_SUCCESS);
}
// 改变工作目录到根目录
if (chdir("/") < 0) {
perror("chdir失败");
exit(EXIT_FAILURE);
}
// 重设文件权限掩码
umask(0);
// 关闭所有打开的文件描述符
for (int fd = sysconf(_SC_OPEN_MAX); fd >= 0; fd--) {
close(fd);
}
// 重定向标准输入输出错误
freopen("/dev/null", "r", stdin);
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
}
int main() {
// 初始化守护进程
init_daemon();
// 守护进程的主循环
while (1) {
FILE* log_file = fopen(LOG_FILE, "a");
if (log_file != NULL) {
char time_str[64];
get_current_time(time_str, sizeof(time_str));
fprintf(log_file, "[守护进程] 系统时间:%s\n", time_str);
fclose(log_file);
}
// 每分钟记录一次
sleep(60);
}
return 0;
}

守护进程的管理#

启动守护进程

Terminal window
./time_daemon &

查看守护进程

Terminal window
ps aux | grep time_daemon

查看日志内容

Terminal window
tail -f /tmp/system_time.log

停止守护进程

Terminal window
pkill time_daemon

6. 综合比较与最佳实践#

四种进程关系的对比#

进程类型特点资源占用处理方式
正常子进程父进程等待回收临时占用wait()回收
孤儿进程被init收养正常占用自动处理
僵尸进程已结束未回收占用PID需要手动回收
守护进程后台运行持续占用信号控制

最佳实践指南#

  1. 总是回收子进程:使用wait()或waitpid()避免僵尸进程
  2. 处理SIGCHLD信号:异步回收子进程
  3. 守护进程要彻底:完全脱离终端,正确设置权限
  4. 使用进程监控:对于重要守护进程,使用监控工具
  5. 日志记录:守护进程应该记录运行状态

信号处理示例#

#include <signal.h>
#include <sys/wait.h>
void sigchld_handler(int sig) {
// 非阻塞方式回收所有已结束的子进程
while (waitpid(-1, NULL, WNOHANG) > 0) {
// 子进程回收成功
}
}
int main() {
// 设置SIGCHLD信号处理
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
// 程序主逻辑...
return 0;
}

7. 实战练习#

练习1:编写一个安全的进程创建函数#

/**
* 安全的进程创建函数:自动处理僵尸进程
*/
pid_t safe_fork() {
pid_t pid = fork();
if (pid < 0) {
perror("safe_fork失败");
return -1;
} else if (pid == 0) {
// 子进程:设置信号处理忽略SIGCHLD
signal(SIGCHLD, SIG_IGN);
return 0;
} else {
// 父进程:返回子进程PID
return pid;
}
}

练习2:简单的进程监控框架#

/**
* 进程监控框架:确保子进程异常退出时能够重启
*/
void monitor_process(void (*child_func)(void)) {
while (1) {
pid_t pid = fork();
if (pid < 0) {
perror("监控进程fork失败");
sleep(5); // 等待后重试
continue;
} else if (pid == 0) {
// 子进程执行任务
child_func();
exit(EXIT_SUCCESS);
} else {
// 父进程等待子进程结束
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子进程正常退出,码:%d\n", WEXITSTATUS(status));
} else {
printf("子进程异常退出,正在重启...\n");
}
sleep(1); // 等待后重启
}
}
}

结语#

通过本文的学习,你应该对Linux进程的各种关系有了深入的理解。从简单的父子进程到复杂的守护进程,每种进程关系都有其特定的用途和注意事项。

记住关键原则:总是回收子进程资源正确处理信号守护进程要彻底脱离终端。这些最佳实践将帮助你编写出更加健壮和可靠的系统程序。

现在,尝试动手实践这些示例代码,观察不同进程关系的实际表现,这将加深你对Linux进程管理的理解!


本文代码在Linux环境下测试通过,建议使用gcc编译: gcc -o program program.c

Linux进程关系深度解析:从父子进程到守护进程
https://demo-firefly.netlify.app/posts/process-relationships-tutorial/
作者
长琴
发布于
2024-06-24
许可协议
CC BY-NC-SA 4.0
最后更新于 2024-06-24,距今已过 515 天

部分内容可能已过时

评论区

目录

Loading ... - Loading ...
封面
Loading ...
Loading ...
0:00 / 0:00