Linux 入门八:Linux 多进程

一、概述

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 的资源。

相关推荐
我命由我1234541 分钟前
35.Java线程池(线程池概述、线程池的架构、线程池的种类与创建、线程池的底层原理、线程池的工作流程、线程池的拒绝策略、自定义线程池)
java·服务器·开发语言·jvm·后端·架构·java-ee
old_iron4 小时前
vim定位有问题的脚本/插件的一般方法
linux·编辑器·vim
爱知菜6 小时前
Windows安装Docker Desktop(WSL2模式)和Docker Pull网络问题解决
运维·docker·容器
做测试的小薄6 小时前
Nginx 命令大全:Linux 与 Windows 系统的全面解析
linux·自动化测试·windows·nginx·环境部署
影龙帝皖7 小时前
Linux网络之局域网yum仓库与apt的实现
linux·服务器·网络
月下雨(Moonlit Rain)7 小时前
Docker
运维·docker·容器
碎忆7 小时前
在VMware中安装虚拟机Ubuntu
linux·ubuntu
农民小飞侠7 小时前
ubuntu 安装pyllama教程
linux·python·ubuntu
打工人你好8 小时前
UNIX域套接字(Unix Domain Sockets, UDS) 的两种接口
服务器·unix
技术小甜甜8 小时前
[Dify] 使用 Docker 本地部署 Dify 并集成 Ollama 模型的详细指南
运维·docker·容器·dify