首页 云计算

从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二)

分类:云计算
字数: (4549)
阅读: (7352)
内容摘要:从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二),

在《操作系统真象还原》第九章第二部分中,我们深入探讨了进程管理的细节。对于后端工程师而言,理解操作系统的进程管理机制,能帮助我们更好地理解和优化高并发、高负载的系统。《操作系统真象还原》对进程的创建、调度、切换等关键概念进行了细致的讲解,本文将结合实际应用场景,进一步剖析其原理,并提供相应的代码示例和实战经验。

问题场景重现:僵尸进程与孤儿进程

在实际开发中,我们经常会遇到僵尸进程和孤儿进程的问题。这两种进程状态如果不加以处理,可能会导致系统资源的浪费,甚至引发系统崩溃。

  • 僵尸进程 (Zombie Process): 子进程已经执行完毕,但父进程没有调用 wait()waitpid() 系统调用来回收子进程的资源,导致子进程的状态仍然保存在系统中。当进程数量过多,会占用进程表项资源。
  • 孤儿进程 (Orphan Process): 父进程先于子进程结束,子进程变为孤儿进程,会被 init 进程(进程 ID 为 1)收养。

为了更好地理解这些概念,我们可以通过编写简单的 C 代码来模拟这两种情况。

从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二)

底层原理深度剖析

操作系统通过进程控制块 (PCB) 来管理进程,PCB 包含了进程的所有信息,如进程 ID (PID)、进程状态、程序计数器、寄存器值等。在进程创建时,操作系统会分配一个 PCB,并在进程结束时回收 PCB。但是,如果父进程没有及时回收子进程的 PCB,就会导致僵尸进程的产生。

操作系统通过信号机制来通知进程状态的变化,例如,当子进程结束时,会向父进程发送 SIGCHLD 信号。父进程可以通过注册信号处理函数来捕获该信号,并在信号处理函数中调用 wait()waitpid() 来回收子进程的资源。

从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二)

对于孤儿进程,init 进程会定期调用 wait()waitpid() 来回收这些进程的资源,避免资源泄漏。init 进程是所有进程的祖先,它负责管理系统的初始化和进程管理。

代码/配置解决方案

示例代码:避免僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

void sigchld_handler(int signo) { // 信号处理函数
  int status;
  waitpid(-1, &status, WNOHANG); // 回收任意子进程,非阻塞方式
  printf("Child process terminated.\n");
}

int main() {
  signal(SIGCHLD, sigchld_handler); // 注册信号处理函数

  pid_t pid = fork();

  if (pid == 0) {
    // 子进程
    printf("Child process: PID = %d\n", getpid());
    sleep(5); // 模拟子进程执行任务
    exit(0);
  } else if (pid > 0) {
    // 父进程
    printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
    sleep(60); // 父进程等待一段时间
    printf("Parent process exiting.\n");
  } else {
    perror("Fork failed");
    return 1;
  }

  return 0;
}

这段代码演示了如何使用信号处理函数来避免僵尸进程。父进程注册了 SIGCHLD 信号的处理函数,当子进程结束时,父进程会收到该信号,并在信号处理函数中调用 waitpid() 来回收子进程的资源。WNOHANG 标志表示非阻塞方式,即使没有子进程结束,waitpid() 也会立即返回,避免阻塞父进程。

从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二)

示例代码:创建守护进程(处理孤儿进程)

在实际应用中,很多后台服务程序都需要以守护进程的方式运行,以避免孤儿进程的产生。以下是一个简单的创建守护进程的示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
  pid_t pid;

  // 创建子进程
  pid = fork();
  if (pid < 0) {
    perror("Fork failed");
    exit(EXIT_FAILURE);
  }

  if (pid > 0) {
    // 父进程退出
    exit(EXIT_SUCCESS);
  }

  // 子进程成为会话组长
  if (setsid() < 0) {
    perror("Setsid failed");
    exit(EXIT_FAILURE);
  }

  // 改变工作目录
  if (chdir("/") < 0) {
    perror("Chdir failed");
    exit(EXIT_FAILURE);
  }

  // 关闭标准输入、输出和错误
  close(STDIN_FILENO);
  close(STDOUT_FILENO);
  close(STDERR_FILENO);

  // 在这里编写守护进程的逻辑
  while (1) {
    // 模拟守护进程的工作
    sleep(10);
    // 可以添加日志记录等操作
  }

  return 0;
}

该代码首先创建一个子进程,然后父进程退出,使得子进程成为孤儿进程。接着,子进程调用 setsid() 创建一个新的会话,使其与控制终端分离,避免终端信号的影响。最后,关闭标准输入、输出和错误,并将工作目录改变为根目录。

从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二)

实战避坑经验总结

  1. 定期检查进程状态: 使用 ps 命令或 top 命令定期检查系统中的进程状态,及时发现僵尸进程,并采取相应的措施进行处理。
  2. 合理设计进程模型: 在设计系统架构时,应尽量避免创建过多的进程,并合理设计进程间的通信方式,以减少进程管理的开销。
  3. 使用进程池技术: 对于需要频繁创建和销毁进程的场景,可以使用进程池技术来提高效率,避免频繁的进程创建和销毁带来的性能损耗。可以类比线程池在 Java 或 Go 中的应用,减少资源分配和回收开销。
  4. 关注系统资源限制: 操作系统的进程数量受到系统资源的限制,例如最大进程数、最大文件描述符数等。在设计系统时,需要关注这些限制,避免超出限制导致系统崩溃。可以使用 ulimit 命令查看和修改这些限制。
  5. 使用监控工具: 使用专业的监控工具,如 Prometheus、Grafana 等,可以实时监控系统的进程状态、资源使用情况等,及时发现和解决问题。

通过理解《操作系统真象还原》第九章第二部分的内容,并结合实际应用场景,我们可以更好地掌握进程管理的技术,并将其应用到实际开发中,提高系统的稳定性和性能。例如,使用 Nginx 作为反向代理服务器时,需要合理配置 Nginx 的 worker 进程数量,以充分利用多核 CPU 的性能,并避免进程数量过多导致系统资源耗尽。同时,需要注意 Nginx 的事件处理机制,如 epoll,以提高并发连接数。

从零构建操作系统:实战《操作系统真象还原》第九章进程管理(二)

转载请注明出处: 加班到秃头

本文的链接地址: http://m.acea1.store/blog/930773.SHTML

本文最后 发布于2026-04-18 06:14:14,已经过了9天没有更新,若内容或图片 失效,请留言反馈

()
您可能对以下文章感兴趣
评论
  • 芝麻糊 2 天前
    关于守护进程的创建,还有一些细节需要注意,比如文件锁的使用,避免多个守护进程同时运行。