一、概述
1.1 什么是进程?
在 Linux 系统中,进程是程序的一次动态执行过程。程序是静态的可执行文件,而进程是程序运行时的实例,系统会为其分配内存、CPU 时间片等资源。例如,输入 ls 命令时,系统创建进程执行 ls 程序来显示文件列表。进程是资源分配的基本单位,理解进程对掌握 Linux 系统运行机制至关重要。
1.2 查看进程
在 Linux 中,可使用 ps 命令查看系统中当前运行的进程。下面是一些常用的 ps 命令参数组合:
- ps -ef:
-
- 功能:以全格式显示所有进程的详细信息。
-
- 步骤:
-
-
- 打开终端。
-
-
-
- 输入 ps -ef 并回车。
-
-
- 示例输出:
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 00:00 ? 00:00:01 /sbin/init splash
root 2 0 0 00:00 ? 00:00:00 [kthreadd]
- 参数解释:
-
- UID:进程所有者的用户 ID。
-
- PID:进程的 ID 号。
-
- PPID:父进程的 ID 号。
-
- C:CPU 占用率。
-
- STIME:进程启动时间。
-
- TTY:进程关联的终端。
-
- TIME:进程使用的 CPU 时间。
-
- CMD:启动进程的命令。
在 Linux 中,除了 ps -ef,还可使用以下命令查看进程:
- ps aux:
- 功能:显示所有进程的详细资源使用情况(如内存、CPU 占用率)。
- 示例输出:
TypeScript
取消自动换行复制
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 24176 4360 ? Ss 00:00 0:01 /sbin/init splash
- %CPU:CPU 占用百分比。
- %MEM:内存占用百分比。
- STAT:进程状态(如 S 表示睡眠,R 表示运行)。
- top 命令:
- 功能:动态实时显示进程资源占用情况,类似 Windows 任务管理器。
- 操作:输入 top 后,可按 q 退出。
二、进程的创建
2.1 fork 函数
在 C 语言里,fork 函数是创建新进程的关键函数。它的作用是复制当前进程,生成一个子进程,原进程则成为父进程。fork 函数的原型如下:
#include <unistd.h>
pid_t fork(void);
- 返回值:
-
- 在父进程中,fork 函数返回子进程的 PID(一个正整数)。
-
- 在子进程中,fork 函数返回 0。
-
- 若 fork 失败,返回 -1。
创建进程的步骤
- 包含必要的头文件:#include <unistd.h> 和 #include <stdio.h>。
- 调用 fork 函数创建子进程。
- 根据 fork 的返回值判断当前是父进程还是子进程,并执行相应的代码。
示例代码
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
} else if (pid == 0) {
// 子进程
printf("我是子进程,我的 PID 是 %d,父进程的 PID 是 %d\n", getpid(), getppid());
} else {
// 父进程
printf("我是父进程,我的 PID 是 %d,子进程的 PID 是 %d\n", getpid(), pid);
}
return 0;
}
编译和运行步骤
- 把上述代码保存为 fork_example.c。
- 打开终端,进入代码所在目录。
- 使用 gcc 编译代码:gcc fork_example.c -o fork_example。
- 运行编译后的可执行文件:./fork_example。
三、僵尸进程
3.1 形成条件
僵尸进程的形成需要满足以下三个条件:
- 子进程优先于父进程结束。
- 父进程不结束。
- 父进程不调用 wait 函数。
当子进程结束时,它会向父进程发送一个 SIGCHLD 信号,但如果父进程没有调用 wait 或 waitpid 函数来回收子进程的资源,子进程就会变成僵尸进程。
3.2 如何避免僵尸进程
方法一:父进程调用 wait 函数
wait 函数的作用是等待任意一个子进程结束,并回收其资源。其原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- 参数:status 用于存储子进程的退出状态。
- 返回值:返回结束的子进程的 PID。
示例代码
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
} else if (pid == 0) {
// 子进程
printf("子进程开始执行,PID 是 %d\n", getpid());
sleep(2);
printf("子进程结束\n");
} else {
// 父进程
int status;
pid_t child_pid = wait(&status);
printf("父进程回收了 PID 为 %d 的子进程\n", child_pid);
}
return 0;
}
方法二:使用 signal 函数处理 SIGCHLD 信号
可通过 signal 函数捕获 SIGCHLD 信号,并在信号处理函数中调用 wait 或 waitpid 函数。
示例代码
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void sigchld_handler(int signo) {
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("回收了 PID 为 %d 的子进程\n", pid);
}
}
int main() {
signal(SIGCHLD, sigchld_handler);
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
} else if (pid == 0) {
// 子进程
printf("子进程开始执行,PID 是 %d\n", getpid());
sleep(2);
printf("子进程结束\n");
} else {
// 父进程
printf("父进程继续执行\n");
sleep(5);
}
return 0;
}
四、孤儿进程
4.1 形成条件
孤儿进程的形成需要满足以下两个条件:
- 父进程优先于子进程结束。
- 子进程未结束。
当父进程结束后,子进程就会变成孤儿进程,此时它会被进程 ID 为 1 的 init 进程接管。
4.2 被进程 ID 为 1 的进程接管
init 进程会负责回收孤儿进程的资源,确保系统资源不会被浪费。
示例代码
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
} else if (pid == 0) {
// 子进程
printf("子进程开始执行,父进程 PID 是 %d\n", getppid());
sleep(5);
printf("子进程继续执行,父进程 PID 是 %d\n", getppid());
} else {
// 父进程
printf("父进程结束\n");
}
return 0;
}
在这个示例中,父进程会先结束,子进程在睡眠 5 秒后,会发现自己的父进程 ID 变成了 1。
五、守护进程(后台进程)
5.1 实现过程
守护进程是一种在后台持续运行的进程,通常在系统启动时就开始运行,并且不受用户登录和注销的影响。以下是创建守护进程的详细步骤:
步骤 1:创建子进程,父进程退出
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程继续执行
// 后续步骤...
return 0;
}
步骤 2:在子进程中创建新会话
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程创建新会话
pid_t sid = setsid();
if (sid < 0) {
perror("setsid 失败");
exit(EXIT_FAILURE);
}
// 后续步骤...
return 0;
}
setsid 函数的作用是创建一个新的会话,使子进程成为新会话的首进程,并且脱离原有的控制终端。
步骤 3:改变工作目录
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程创建新会话
pid_t sid = setsid();
if (sid < 0) {
perror("setsid 失败");
exit(EXIT_FAILURE);
}
// 改变工作目录
if (chdir("/") < 0) {
perror("chdir 失败");
exit(EXIT_FAILURE);
}
// 后续步骤...
return 0;
}
chdir 函数用于将工作目录切换到根目录,避免工作目录被卸载导致进程无法正常工作。
步骤 4:设置文件权限掩码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程创建新会话
pid_t sid = setsid();
if (sid < 0) {
perror("setsid 失败");
exit(EXIT_FAILURE);
}
// 改变工作目录
if (chdir("/") < 0) {
perror("chdir 失败");
exit(EXIT_FAILURE);
}
// 设置文件权限掩码
umask(0);
// 后续步骤...
return 0;
}
umask 函数用于设置文件权限掩码,确保守护进程创建的文件具有预期的权限。
步骤 5:关闭不需要的文件描述符
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程创建新会话
pid_t sid = setsid();
if (sid < 0) {
perror("setsid 失败");
exit(EXIT_FAILURE);
}
// 改变工作目录
if (chdir("/") < 0) {
perror("chdir 失败");
exit(EXIT_FAILURE);
}
// 设置文件权限掩码
umask(0);
// 关闭不需要的文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 守护进程的主循环
while (1) {
// 执行守护进程的任务
sleep(1);
}
return 0;
}
关闭标准输入、标准输出和标准错误输出的文件描述符,防止守护进程与控制终端交互。
编译和运行步骤
- 把上述代码保存为 daemon_example.c。
- 打开终端,进入代码所在目录。
- 使用 gcc 编译代码:gcc daemon_example.c -o daemon_example。
- 运行编译后的可执行文件:./daemon_example。此时,守护进程会在后台持续运行。
通过以上步骤,你可以逐步掌握 Linux 多进程的相关知识,包括进程的创建、僵尸进程和孤儿进程的处理,以及守护进程的实现。在实际应用中,多进程编程可以提高程序的并发性能,充分利用多核 CPU 的资源。