当前位置: 首页 > news >正文

Linux 进程创建与控制详解

目录

1. 什么是进程?—— 深入理解进程模型

a. 唯一的进程ID (PID)

b. 独立的虚拟地址空间

c. 进程控制块 (PCB - Process Control Block)

d. 文件描述符表

2. 经典的 fork() - exec() - wait() 模式

a. fork(): 克隆一个新进程

b. exec() 家族: 变身为一个新程序

c. wait() 和 waitpid(): 为子进程“收尸”

3. 进程的终止: exit() 和 _exit()

总结


1. 什么是进程?—— 深入理解进程模型

在 Linux 中,一个进程(Process)并不仅仅是“一个正在运行的程序”,它是一个更为丰富和具体的概念。操作系统为了管理和隔离这些运行中的程序,为每一个进程都创建了一个完整、独立的运行环境。这个环境主要由以下几个部分构成:

a. 唯一的进程ID (PID)

每个进程都有一个独一无二的非负整数标识符,称为 PID。同时,每个进程(除了系统启动的第一个进程 init)都有一个父进程,其 PID 被称为 PPID (Parent Process ID)。这个父子关系构成了系统中的进程树。

b. 独立的虚拟地址空间

这是进程隔离的核心机制。每个进程都“认为”自己独占了整个系统的内存。这个地址空间通常被划分为几个区域:

  • 文本段 (.text): 存放程序的可执行代码,只读。

  • 数据段 (.data): 存放已初始化的全局变量和静态变量。

  • BSS 段 (.bss): 存放未初始化或初始化为零的全局变量和静态变量。

  • 堆 (Heap): 用于动态内存分配(例如,通过 malloc()),从低地址向高地址增长。

  • 栈 (Stack): 存放局部变量、函数参数和返回地址,从高地址向低地址增长。

由于每个进程都有自己的虚拟地址空间,一个进程的内存操作(如指针访问)无法直接影响到另一个进程,从而保证了系统的稳定性。

c. 进程控制块 (PCB - Process Control Block)

这是操作系统内核中用于描述和管理进程的一个数据结构(在 Linux 中是 task_struct)。它包含了关于进程的所有关键信息,可以看作是进程的“身份证”:

  • 进程状态(运行、睡眠、僵尸等)

  • PID 和 PPID

  • CPU 寄存器的值(如程序计数器、栈指针)

  • 虚拟内存映射信息

  • 文件描述符表

  • 用户和组 ID

当内核需要进行进程切换时,它会保存当前进程的 PCB 信息,并加载下一个要运行进程的 PCB 信息。

d. 文件描述符表

每个进程都有一张表,用于记录它打开的文件。表中的每一项是一个整数(文件描述符),指向一个内核中代表打开文件的对象。这张表的前三项通常被预留给:

  • 0: 标准输入 (stdin)

  • 1: 标准输出 (stdout)

  • 2: 标准错误 (stderr)

2. 经典的 fork() - exec() - wait() 模式

这是 UNIX/Linux 系统中创建和管理新任务的基石。几乎所有的程序启动,包括你在 Shell 中执行的每一条命令,都遵循这个模式。

a. fork(): 克隆一个新进程

fork() 是一个非常独特的系统调用,它的作用是创建一个新进程(子进程),这个子进程几乎是调用 fork() 的进程(父进程)的完整副本。

  • 返回值是关键fork() 被调用一次,但会返回两次。

    • 父进程中,fork() 返回新创建的子进程的 PID

    • 子进程中,fork() 返回 0

    • 如果创建失败,fork() 在父进程中返回 -1。 通过检查返回值,程序可以确定当前代码是在父进程中执行还是在子进程中执行。

  • 写时复制 (Copy-on-Write, COW)fork() 之后,内核并不会立即复制父进程的整个物理内存空间给子进程,因为这非常低效。相反,父子进程暂时共享相同的物理内存页。只有当其中一个进程尝试写入某个内存页时,内核才会为该进程复制那个页,让它拥有自己的副本。这种优化策略使得 fork() 的执行速度非常快。

  • 继承了什么?:子进程继承了父进程的虚拟地址空间、文件描述符表、用户和组 ID 等大部分内容。但 PID、PPID 和某些资源(如内存锁)是独有的。

【代码示例:fork() 的使用】

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> // 引入 wait() 所需的头文件int main() {pid_t pid = fork();if (pid < 0) {// fork 失败fprintf(stderr, "Fork Failed\n");return 1;} else if (pid == 0) {// 这里是子进程的代码printf("I am the child process.\n");printf("My PID is %d, my parent's PID is %d.\n", getpid(), getppid());printf("Child process is finishing.\n");} else {// 这里是父进程的代码printf("I am the parent process.\n");printf("My PID is %d, I created a child with PID %d.\n", getpid(), pid);// 等待子进程结束,进行回收,防止产生僵尸进程wait(NULL); printf("Parent process finished waiting and is finishing.\n");}return 0;
}

b. exec() 家族: 变身为一个新程序

fork() 只是创建了父进程的一个副本,如果想让子进程执行一个全新的程序(比如 /bin/ls),就需要 exec() 函数族。

  • 核心作用exec() 会用一个全新的程序镜像替换当前进程的内存空间(包括代码、数据和堆栈)。一旦调用成功,原有的程序代码就完全被覆盖了,exec() 调用之后的代码将永远不会被执行。

  • 没有新进程exec() 不会创建新进程。它只是在当前进程(通常是 fork() 出来的子进程)的上下文中加载并运行一个新程序。进程的 PID 保持不变。

  • 函数族exec() 有多个变体,命名规则可以帮助区分它们:

    • l (execl, execlp): 参数以列表(list)形式逐个列出,以 NULL 结尾。

    • v (execv, execvp): 参数以字符串指针数组(vector)形式传递。

    • p (execlp, execvp): 会在系统的 PATH 环境变量指定的目录中搜索要执行的程序。

    • e (execle, execve): 允许你手动传入一个环境变量数组。

【代码示例:结合 fork()execvp()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid < 0) {fprintf(stderr, "Fork Failed\n");return 1;} else if (pid == 0) {// 子进程printf("Child process is about to run 'ls -l'.\n");char *args[] = {"ls", "-l", NULL}; // execvp 的参数数组execvp(args[0], args);// 如果 execvp 成功,下面的代码永远不会执行perror("execvp failed"); // 如果 execvp 失败,它会返回,我们可以打印错误exit(1);} else {// 父进程printf("Parent process is waiting for the child to complete.\n");wait(NULL); // 等待子进程结束printf("Child process has finished.\n");}return 0;
}

c. wait()waitpid(): 为子进程“收尸”

进程管理中一个重要环节是资源回收。

  • 僵尸进程 (Zombie Process):如果一个子进程已经终止,但其父进程还没有调用 wait()waitpid() 来获取它的终止状态,那么这个子进程的进程控制块(PCB)会一直保留在内核中。这个已经死亡但 PCB 仍存在的进程就被称为“僵尸进程”。它不占用内存,但会占用一个 PID,如果大量出现会耗尽系统的 PID 资源。

  • wait():

    • 阻塞父进程,直到它的任何一个子进程终止。

    • 返回终止的子进程的 PID。

    • 通过一个整数指针参数,可以获取子进程的退出状态。

  • waitpid():

    • 功能更强大,是 wait() 的超集。

    • 可以等待一个指定的子进程。

    • 可以通过选项参数(如 WNOHANG)实现非阻塞等待。

通过调用 wait()waitpid(),父进程完成了对子进程的“收尸”(reaping),内核可以安全地释放子进程的 PCB,从而避免了僵尸进程的产生。

3. 进程的终止: exit()_exit()

一个进程可以通过多种方式终止,最常见的是调用 exit() 函数。

  • 退出状态码:每个进程终止时都会向其父进程返回一个 0 到 255 之间的整数,称为退出状态码。按照惯例,0 表示成功,非 0 表示发生了某种错误。父进程通过 wait() 可以获取这个状态码。

  • exit() vs _exit():

    • exit() 是 C 标准库函数。在终止进程前,它会执行一系列清理工作:

      1. 调用 atexit() 注册的函数。

      2. 刷新并关闭所有标准 I/O 库的流 (stream)(比如 stdout 的缓冲区)。

      3. 最后调用 _exit() 系统调用来终止进程。

    • _exit() 是一个系统调用。它会立即终止进程,不会进行任何清理工作。

为什么需要 _exit()?fork() 之后,子进程中通常建议使用 _exit() 而不是 exit()。因为子进程复制了父进程的 I/O 缓冲区,如果调用 exit(),父子进程可能会重复刷新和关闭相同的流,导致不可预料的行为。而在 exec() 之后,由于整个内存空间都被替换了,这个问题就不存在了。

总结

Linux 的进程创建与控制是一个优雅而强大的模型:

  1. fork() 负责“生”,创建一个与父进程几乎一样的子进程。

  2. exec() 负责“养”,让子进程变身为一个全新的程序,去执行新的任务。

  3. wait() 负责“葬”,父进程在子进程完成后回收其资源,维持系统整洁。

  4. exit() 负责“死”,进程完成任务后正常退出,并向父进程报告结果。

理解了这个生命周期,你就掌握了 Linux 多任务编程的基石。

http://www.dtcms.com/a/423488.html

相关文章:

  • 万网x3主机l系统放两个网站手机制作ppt
  • XML语言解析
  • AJAX XML:深入解析与实际应用
  • 十大网站在线观看深圳互联网推广公司
  • 价值流智能时代:DevOps平台如何成为企业高效交付的核心引擎?
  • Vue Router 动态路由完全指南:灵活掌控前端路由
  • 电子商务网站域名注册方法wordpress 模板语言包
  • 网站空间和服务器有什么区别阜宁网站制作价格
  • 【每日一问】X电容和Y电容有什么区别?
  • AI 播客:从体验到原理,知识获取的新姿势
  • 异构计算实战:CPU/GPU/TPU在创意工作流中的调度策略
  • 打破“形似”桎梏,OmniHuman-1.5让数字人“由内而外”活起来。
  • 语言理解-阿里木江【基础课笔记】
  • 邮件系统建设篇:Coremail与Exchange并行方案介绍
  • 解码数据结构队列
  • 典型的四大综合门户网站wordpress excel导入
  • 六边形架构实现:领域驱动设计 + 端口适配器模式
  • 六安网站建设定制全国最大的源码平台
  • Qt Linux交叉编译字节数目不一样
  • 概率统计中的数学语言与术语1
  • 微服务项目->在线oj系统(Java-Spring)--增删改
  • 空间设计网站yahoo搜索引擎
  • 网站建设合同英文软件外包公司名单
  • Java基础(①Tomcat + Servlet + JSP)
  • 连云港百度推广总代理上海谷歌seo公司
  • ssl外贸网站网站空间托管
  • k8s kubelet 10250监控端口访问配置
  • 十二、伪分布式配置
  • VScode通过跳板机连接内网服务器
  • wordpress小说下载站建设银行网站如何下载u盾