Linux 入门八:Linux 多进程

发布于:2025-04-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、概述

1.1 什么是进程?

在 Linux 系统中,进程是程序的一次动态执行过程。程序是静态的可执行文件,而进程是程序运行时的实例,系统会为其分配内存、CPU 时间片等资源。例如,输入 ls 命令时,系统创建进程执行 ls 程序来显示文件列表。进程是资源分配的基本单位,理解进程对掌握 Linux 系统运行机制至关重要。

1.2 查看进程

在 Linux 中,可使用 ps 命令查看系统中当前运行的进程。下面是一些常用的 ps 命令参数组合:

  • ps -ef
    • 功能:以全格式显示所有进程的详细信息。
    • 步骤
      1. 打开终端。
      1. 输入 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。
创建进程的步骤
  1. 包含必要的头文件:#include <unistd.h> 和 #include <stdio.h>。
  1. 调用 fork 函数创建子进程。
  1. 根据 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;

}
编译和运行步骤
  1. 把上述代码保存为 fork_example.c。
  1. 打开终端,进入代码所在目录。
  1. 使用 gcc 编译代码:gcc fork_example.c -o fork_example。
  1. 运行编译后的可执行文件:./fork_example。

三、僵尸进程

3.1 形成条件

僵尸进程的形成需要满足以下三个条件:

  1. 子进程优先于父进程结束。
  1. 父进程不结束。
  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 形成条件

孤儿进程的形成需要满足以下两个条件:

  1. 父进程优先于子进程结束。
  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;

}

关闭标准输入、标准输出和标准错误输出的文件描述符,防止守护进程与控制终端交互。

编译和运行步骤

  1. 把上述代码保存为 daemon_example.c。
  1. 打开终端,进入代码所在目录。
  1. 使用 gcc 编译代码:gcc daemon_example.c -o daemon_example。
  1. 运行编译后的可执行文件:./daemon_example。此时,守护进程会在后台持续运行。

通过以上步骤,你可以逐步掌握 Linux 多进程的相关知识,包括进程的创建、僵尸进程和孤儿进程的处理,以及守护进程的实现。在实际应用中,多进程编程可以提高程序的并发性能,充分利用多核 CPU 的资源。


网站公告

今日签到

点亮在社区的每一天
去签到