一、守护进程(Daemon Process)

1.1. 概念

守护进程(Daemon Process)是一种在后台持续运行的进程,通常在系统启动时自动启动,直到系统关闭才结束。它不与任何控制终端关联,独立于用户登录和注销操作,主要用于执行一些需要长期运行且不受用户交互影响的系统任务。如日志记录、定时任务等。

1.2. 特点
  • 后台运行:守护进程在后台默默工作,不会占用终端,用户可以在终端上进行其他操作,不会受到守护进程的干扰。
  • 生命周期长:守护进程通常在系统启动时就开始运行,直到系统关闭才会结束,为系统提供持续的服务。
  • 独立于控制终端:守护进程不与任何控制终端相关联,意味着即使控制终端关闭,守护进程也不会受到影响,继续正常运行。
  • 权限管理严格:守护进程通常需要特定的权限来执行系统级任务,因此在创建和运行过程中需要进行严格的权限管理。
1.3. 守护进程的命名

守护进程的名称通常以“d”结尾,例如sshd、xinetd、crond等。这种命名约定有助于区分守护进程和普通进程。

1.4. 创建守护进程的步骤
  • 创建子进程,父进程退出:通过fork()函数创建子进程,然后父进程使用exit()函数退出。这样,子进程将变成一个孤儿进程,被init进程(PID为1的进程)收养。这一步实现了子进程与父进程的脱离,使得子进程可以在后台运行。
  • 在子进程中创建新的会话:使用setsid()函数创建一个新的会话,并设置进程的会话ID。这一步使得子进程成为新会话的会话领导者,并摆脱原会话、进程组和控制终端的控制。
  • 更改当前工作目录:通常将守护进程的当前工作目录更改为根目录“/”,以避免因文件系统卸载而导致的问题。
  • 重设文件权限掩码:将文件权限掩码设置为0,以确保守护进程具有最大的文件操作权限。
  • 关闭所有打开的文件描述符:关闭从父进程继承的所有打开文件,以避免资源泄漏和文件系统无法卸载的问题。通常将标准输入、标准输出和标准错误重定向到/dev/null,使得守护进程的输出无处显示,也无处从交互式用户那里接收输入。
1.5. 守护进程的实例

以下是一个简单的守护进程创建实例:

代码语言:javascript

AI代码解释

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

int main() {
    pid_t pid;
    int i;

    // 第一步:创建子进程,父进程退出
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    } else if (pid > 0) {
        exit(0); // 父进程退出
    }

    // 第二步:在子进程中创建新的会话
    if (setsid() < 0) {
        perror("setsid");
        exit(1);
    }

    // 第三步:更改当前工作目录
    if (chdir("/") < 0) {
        perror("chdir");
        exit(1);
    }

    // 第四步:重设文件权限掩码
    umask(0);

    // 第五步:关闭所有打开的文件描述符
    for (i = 0; i < sysconf(_SC_OPEN_MAX); i++) {
        close(i);
    }

    // 重定向标准输入、标准输出和标准错误到/dev/null
    open("/dev/null", O_RDWR);
    dup(0);
    dup(0);

    // 守护进程的主体工作
    while (1) {
        sleep(10); // 模拟守护进程的工作,每隔10秒执行一次任务
        // 在这里添加守护进程需要执行的任务代码
        printf("守护进程运行中...\n"); // 注意:这里的输出实际上会被重定向到/dev/null,因此不会在终端上显示
    }

    exit(0);
}

printf("守护进程运行中...\n");语句实际上并不会在终端上显示输出,因为守护进程的标准输出已经被重定向到/dev/null。在实际应用中,守护进程通常会执行一些后台任务,如监听网络请求、处理系统日志等。

1.6. 守护进程的管理

在Linux系统中,可以使用ps、top等命令来查看正在运行的守护进程。如果需要终止某个守护进程,可以使用kill命令向该进程发送SIGKILL或SIGTERM信号。此外,还可以使用nohup命令或&符号将命令放入后台运行,并使其具有一定的守护进程特性(尽管这并不是真正的守护进程)。

1.7. 影响与处理

守护进程可以提高系统的稳定性和可靠性,确保一些重要的服务始终处于运行状态。在开发守护进程时,需要注意资源管理和错误处理,避免守护进程出现异常导致系统不稳定。

二、僵尸进程(Zombie Process)

2.1. 僵尸进程的定义

在Linux系统中,僵尸进程是一种特殊的进程状态。当一个子进程已经完成执行(即已经终止),但其父进程尚未通过wait()或waitpid()系统调用来回收其资源和状态信息时,这个子进程就处于僵尸状态,被称为僵尸进程。

2.2. 僵尸进程的特点
  • 进程状态:僵尸进程已经终止,不再执行任何任务,且所有资源(如CPU、内存等)都已释放。然而,它的进程描述符(PCB)仍然保留在系统中。
  • 占用系统资源:尽管僵尸进程本身不占用CPU和内存等资源,但它仍然占用进程表中的一个条目,以及保留一定的信息(如进程号PID、退出状态等)。这些信息直到父进程调用wait()或waitpid()时才被释放。
  • 无法被直接杀死:僵尸进程已经处于死亡状态,因此无法通过常规的信号(如SIGKILL)将其杀死。只能通过杀死其父进程或等待其父进程主动回收来间接清理僵尸进程。
2.3. 僵尸进程的产生原因

僵尸进程的产生通常是因为父进程没有正确地回收子进程的资源。当子进程退出后,它会发送一个SIGCHLD信号给父进程,通知父进程它已经结束。如果父进程没有处理这个信号或者没有调用wait()系列函数来清理子进程的状态,子进程就会变成僵尸进程。

以下是一个简单的示例代码,演示了僵尸进程的产生:

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is exiting.\n", getpid());
        exit(0);
    } else {
        // 父进程
        printf("Parent process (PID: %d) is running.\n", getpid());
        // 父进程不调用 wait() 或 waitpid()
        sleep(30); 
        printf("Parent process is exiting.\n");
    }
    return 0;
}

子进程先退出,但父进程在一段时间内没有调用 wait()waitpid() 来回收子进程的资源,子进程就会变成僵尸进程。

2.4. 僵尸进程的影响
  • 系统资源消耗:虽然僵尸进程本身不占用大量资源,但大量僵尸进程会占用进程表中的条目,可能导致进程表耗尽,从而无法创建新的进程。
  • 系统性能问题:在资源有限的情况下,僵尸进程可能影响系统的管理和资源分配,进而导致系统性能下降。
2.5. 检测方法

可以使用以下命令来检测系统中的僵尸进程:

  • ps 命令:使用 ps -efps aux 命令可以查看系统中所有进程的信息。僵尸进程在输出中会显示为 <defunct> 状态。例如:

代码语言:javascript

AI代码解释

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      1234  0.0  0.0      0     0 ?        Z    10:00   0:00 [defunct]

其中,STAT 列显示为 Z 表示该进程是僵尸进程。

  • top 命令:运行 top 命令后,按下 Shift + Z 组合键,可以突出显示僵尸进程。僵尸进程会以特殊的颜色显示,方便用户识别。
2.5. 解决办法

为了避免僵尸进程的产生,父进程应该在子进程结束后及时回收其资源。可以采用以下几种方法:

①使用 wait() 函数wait() 函数会使父进程阻塞,直到有一个子进程结束。父进程调用 wait() 后,会获取子进程的退出状态,并释放子进程的相关资源。示例代码如下:

代码语言:javascript

AI代码解释

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

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is exiting.\n", getpid());
        exit(0);
    } else {
        // 父进程
        int status;
        pid_t wpid = wait(&status);
        if (wpid > 0) {
            printf("Parent process reaped child process (PID: %d).\n", wpid);
        }
    }
    return 0;
}

②使用 waitpid() 函数waitpid() 函数比 wait() 函数更加灵活,它可以指定要等待的子进程的 PID,也可以设置非阻塞模式。示例代码如下:

代码语言:javascript

AI代码解释

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

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is exiting.\n", getpid());
        exit(0);
    } else {
        // 父进程
        int status;
        pid_t wpid;
        do {
            wpid = waitpid(pid, &status, WNOHANG);
            if (wpid == 0) {
                // 子进程还未结束,父进程可以继续执行其他任务
                sleep(1);
            }
        } while (wpid == 0);

        if (wpid > 0) {
            printf("Parent process reaped child process (PID: %d).\n", wpid);
        }
    }
    return 0;
}

③信号处理:父进程可以通过捕获 SIGCHLD 信号,并在信号处理函数中调用 wait()waitpid() 来回收子进程的资源。示例代码如下:

代码语言:javascript

AI代码解释

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

void sigchld_handler(int signum) {
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        printf("Reaped child process (PID: %d).\n", pid);
    }
}

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

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is exiting.\n", getpid());
        exit(0);
    } else {
        // 父进程
        while (1) {
            sleep(1);
        }
    }
    return 0;
}

三、孤儿进程(Orphan Process)

3.1. 定义

在 Linux 系统里,当一个子进程的父进程提前退出时,这个子进程就会变成孤儿进程。由于父进程已经不存在,孤儿进程会被 init 进程(进程 ID 为 1)收养,成为 init 进程的子进程。

3.2. 产生原因

孤儿进程通常是由于父进程在创建子进程后,未等待子进程结束就提前退出而产生的。这种情况可能出现在多种场景中,例如:

  • 父进程完成了自身的任务后正常退出,而子进程仍在执行某些耗时的操作。
  • 父进程因发生异常或错误而意外终止,导致子进程失去了父进程的管理。

以下是一个简单的 代码示例,用于演示孤儿进程的产生:

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is running, parent PID: %d\n", getpid(), getppid());
        sleep(10); // 子进程休眠 10 秒
        printf("Child process (PID: %d) is still running, new parent PID: %d\n", getpid(), getppid());
    } else {
        // 父进程
        printf("Parent process (PID: %d) is exiting.\n", getpid());
        exit(0);
    }
    return 0;
}

父进程先退出,子进程在一段时间后才会继续执行后续代码。在父进程退出后,子进程的父进程 ID 会变为 1,即被 init 进程收养。

3.3. 特点
  • init 进程收养:孤儿进程会自动被 init 进程接管,init 进程会成为其新的父进程。init 进程会周期性地调用 wait() 系统调用来回收孤儿进程的资源,确保不会产生僵尸进程。
  • 继续执行:孤儿进程并不会因为父进程的退出而终止,它会继续执行自己的任务,直到完成或被其他因素终止。
  • 与控制终端失去关联:如果父进程与控制终端有关联,当父进程退出后,孤儿进程会与该控制终端失去关联,成为一个后台进程。
3.4. 影响
  • 正常情况下无危害:在大多数情况下,孤儿进程本身不会对系统造成危害。由于 init 进程会负责回收孤儿进程的资源,所以不会出现资源泄漏的问题。
  • 可能影响系统资源:如果系统中存在大量的孤儿进程,可能会消耗一定的系统资源,如进程表项和内存等。不过,这种情况通常比较少见,因为 init 进程会及时回收孤儿进程的资源。
3.5. 处理方式
  • 无需特殊处理:一般来说,不需要对孤儿进程进行特殊的处理。init 进程会自动管理和回收孤儿进程的资源,确保系统的正常运行。
  • 监控与调试:在开发和调试过程中,可以使用系统工具(如 pstop 等)来监控孤儿进程的状态。例如,使用 ps -ef 命令可以查看系统中所有进程的信息,通过观察进程的父进程 ID 是否为 1 来判断是否为孤儿进程。
3.6. 实际应用场景
  • 守护进程的创建:在创建守护进程时,通常会先创建一个子进程,然后让父进程退出,使子进程成为孤儿进程。接着,子进程再进行一系列操作(如创建新会话、更改工作目录等),最终成为一个守护进程,在后台持续运行。
  • 并行任务处理:父进程可以创建多个子进程来并行处理任务,当父进程完成自己的任务后退出,子进程可以继续独立地完成剩余的任务,提高系统的处理效率。

孤儿进程是嵌入式 Linux 系统中一种正常的进程状态,了解其产生原因、特点和处理方式,有助于开发者更好地进行进程管理和系统开发。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐