深入理解Linux下的任务管理,守护进程

发布时间:2023年12月21日

导言

在Linux操作系统中,任务管理和守护进程是系统运维和开发中非常重要的方面。本篇博客将深入探讨Linux下任务管理和守护进程的概念、使用方法以及一些实际的例子。

1. 任务管理

1.1 进程的基本概念

在Linux中,每个运行的程序都是一个进程。每个进程都有一个唯一的进程标识符(PID),并且可以属于一个父进程。通过ps命令可以查看当前系统中运行的进程。

ps 命令字段含义

ps 命令用于查看系统中的进程信息。以下是常见的字段及其含义:

ps aux
$ ps aux
USER       PID  %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  12345  6789 ?        Ss   Dec01   0:05 /sbin/init
john        22  1.0  0.5 123456 54321 pts/0    R+   10:30   0:10 ./my_program
字段名称含义
USER (UID):进程属于哪个用户。
PID:进程ID,唯一标识一个进程。
USER (UID):进程属于哪个用户。
%CPU:进程占用的CPU使用率。
%MEM:进程占用的内存使用率。
RSS (resident set size):进程占用的物理内存大小(以KB为单位)。
TTY:进程关联的终端。
STAT (process status):进程状态,如R(运行)、S(睡眠)、Z(僵尸)等。
TIME:进程累计占用CPU的时间。
COMMAND:启动进程的命令。
ps axj
$ ps axj
 PID  PPID  PGID   SID TTY      STAT   TIME COMMAND
  1     0     1     1 ?        Ss     0:03 /usr/lib/systemd/systemd
  2     0     0     0 ?        S      0:00 [kthreadd]
  3     2     0     0 ?        I<     0:00 [rcu_gp]
  ...

对比ps aux不同字段含义

字段名称含义
PPID (Parent Process ID):父进程ID,标识创建当前进程的父进程。
PGID (Process Group ID):进程组ID,标识进程所属的进程组。
SID (Session ID):会话ID,标识进程所属的会话。
TTY (Controlling Terminal):控制终端,表示与进程关联的终端设备。

1.2 进程组,作业,会话概念解释

我们以一个示例演示

首先创建四个进程,process_1,porcess_2,process_3,porcess_4
在这里插入图片描述

1. 进程组(Process Group)

概念:
进程组是一个或多个相关进程的集合,它们共享同一个进程组ID(PGID)。进程组的一个典型应用是将一组相关的进程组织在一起,以便能够方便地对它们进行管理。

process_1,porcess_2放在后台运行
process_3,porcess_4放在前台运行
开启另一终端(shell),用命令行ps axj | head -1 && ps axj | grep process | grep -v grep查看前台进程和后台进程

在这里插入图片描述

此时杀掉进程也是按照进程组进行的

在这里插入图片描述
当需要杀掉一个或多个进程时,通常会按照进程组进行操作。可以使用kill命令发送信号给进程和进程组。通过kill -n -pgid可以将信号n发送到进程组pgid中的所有进程。也可以使用kill -n pid将信号n发送到指定pid进程。

需要注意的是,如果一个进程启动了子进程,只杀死父进程,子进程仍在运行,因此仍消耗资源。为了防止这些“僵尸进程”,应确保在杀死父进程之前,先杀死其所有的子进程。

2. 作业(Job)

概念:
作业是一个或多个相关联的进程组的集合。通常,作业表示由一个用户发起的命令或脚本。

2.1 jobs命令

jobs 命令用于显示当前 shell 中正在运行的作业(jobs)列表。在一个 shell 会话中,你可能会同时运行多个作业,其中一些可能在后台运行。
在这里插入图片描述

状态标记 ([], -, +): 在方括号中的数字是作业的编号。方括号外的符号表示作业的状态。
  • []: 代表当前正在前台运行的作业。
  • -: 代表当前正在后台运行的作业,并且有一个或多个作业处于停止状态。
  • +: 代表最近放入后台的作业。
作业编号 (Job ID): 方括号中的数字是作业的编号。你可以使用这个编号来操作或引用特定的作业。
作业状态 (Job Status): 表示作业的当前状态。
  • Running: 作业正在前台或后台运行。
  • Stopped: 作业已被停止,通常是通过 Ctrl+Z 发送 SIGTSTP 信号。
    命令 (Command): 是启动作业的命令及其参数。
$ jobs
[1]+  Running                 command1 &
[2]-  Stopped                 command2
[3]+  Running                 command3

在上面的例子中:

command1 正在后台运行,并且是最近放入后台的作业。
command2 已经停止。
command3 正在后台运行。
2.2操作作业

如果你想操作这些作业,你可以使用 fg(将作业放到前台运行)或 bg(将作业放到后台运行)命令,以及 kill 命令来终止作业。例如:

$ fg %1   # 将作业1放到前台运行
$ bg %2   # 将作业2放到后台运行
$ kill %3 # 终止作业3
任务的挂起与恢复

使用Ctrl+Z可以将当前前台任务挂起,然后可以使用bg将其放入后台运行,或使用fg将其恢复到前台。

# 挂起当前任务
Ctrl+Z

在这里插入图片描述

# 将任务恢复到前台
fg %1

在这里插入图片描述

3. 会话(Session)

概念:
会话是一个或多个相关联的作业的集合。会话是由一个终端发起的一组作业的集合,可以包括多个进程组。

例子: 当用户登录到系统时,通常会创建一个新的会话。用户在终端中执行的所有作业都属于同一个会话。

# 创建新会话
ssh user@hostname

在这个例子中,用户通过SSH连接到远程主机,创建了一个新的会话。在这个会话中,用户可以启动不同的作业,每个作业可能包含一个或多个进程组。

小结

  • 进程组是相关进程的集合,共享同一个进程组ID。
  • 作业是一个或多个相关联的进程组的集合,通常表示一个用户发起的命令或脚本。
  • 会话是一个或多个相关联的作业的集合,由一个终端发起。

2. 守护进程

2.1 什么是守护进程

守护进程(Daemon)是在后台运行的系统进程,它们独立于用户会话并且没有直接的控制终端。它们通常在系统启动时启动,并在系统关闭时终止。守护进程通常用于执行系统级任务,如日志记录、网络服务等。

2.2 查看守护进程

ps aux | grep [守护进程名称]

过滤出数据库守护进程

在这里插入图片描述

字段名称含义
USER (UID):进程属于哪个用户。
PID:进程ID,唯一标识一个进程。
USER (UID):进程属于哪个用户。
%CPU:进程占用的CPU使用率。
%MEM:进程占用的内存使用率。
RSS (resident set size):进程占用的物理内存大小(以KB为单位)。
TTY:进程关联的终端。
STAT (process status):进程状态,如R(运行)、S(睡眠)、Z(僵尸)等。
TIME:进程累计占用CPU的时间。
COMMAND:启动进程的命令。

2.2 创建守护进程

一个守护进程通常通过编写一个程序来实现。以下是创建一个简单的守护进程的一般步骤:

  1. Fork 出一个子进程并结束父进程,使得子进程成为孤儿进程。
  2. 在子进程中调用 setsid(),创建一个新的会话,并成为该会话的组长,脱离终端控制。
  3. 再次 fork 一个子进程并结束父进程,以确保守护进程不是会话组长,这样它就不会重新获取控制终端。
  4. 修改工作目录,关闭文件描述符等,使得守护进程更独立和安全。
  5. 重定向标准输入、输出和错误流到/dev/null。

在这里插入图片描述

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

int main()
{
    // 1、设置文件掩码为0
    umask(0);

    // 2、fork后终止父进程,子进程创建新会话
    if (fork() > 0) {
        // father
        exit(0);
    }
    setsid();

    // 3、忽略SIGCHLD信号
    signal(SIGCHLD, SIG_IGN);

    // 4、再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联
    // (不是必须的,防御性编程)
    if (fork() > 0) {
        // father
        exit(0);
    }

    // 5、更改工作目录为根目录(可选的选项)
    chdir("/");

    // 6、将标准输入、标准输出、标准错误重定向到/dev/null(可选的选项)
    close(0);
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, 1);
    dup2(fd, 2);

    while (1)
        ;
    return 0;
}


在这里插入图片描述

状态说明
Ss:会话领导。
Ss+:会话领导,且在前台运行。
S:睡眠状态
  1. 运行后发现该进程的TPGID为-1,TTY显示的是?,也就意味着该进程已经与终端去关联了。

  2. 该进程的PID与其PGID和SID是不同的,也就是说该进程既不是组长进程也不是会话首进程。

  3. 该进程的SID与bash进程的SID是不同的,即它们不属于同一个会话。

  4. 该进程的工作目录已经成功改为了根目录。
    在这里插入图片描述

  5. 该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null,在这里插入图片描述

2.3 示例:日志守护进程

在这里插入图片描述

  • 在本目录下创建一个日志守护进程,用于捕捉其他进程产生的错误
  • 再在本目录下写一段简单的c代码,这段c代码将产生野指针问题
  • 将这段代码运行后的错误信息通过守护进程输出到到守护进程创建的log文件中
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <sys/wait.h>
#include <fcntl.h>  
#include <signal.h>  
#include <string.h>
  
#define MAX_ERROR_LENGTH 256  
  
int main() {  
    pid_t pid;  
    int log_fd;  
    char error_buffer[MAX_ERROR_LENGTH];  
    char log_file[MAX_ERROR_LENGTH];  
  
    // 创建子进程  
    pid = fork();  
  
    // 子进程退出  
    if (pid == 0) {  
        exit(EXIT_SUCCESS);  
    }  
  
    // 父进程创建新会话  
    if (setsid() < 0) {  
        perror("setsid failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 打开日志文件  
    sprintf(log_file, "error_%d.log", getpid()); // 根据进程ID创建日志文件名  
    log_fd = open(log_file, O_CREAT | O_WRONLY | O_APPEND, S_IRUSR | S_IWUSR);  
    if (log_fd < 0) {  
        perror("open failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 重定向标准错误输出到日志文件  
    if (dup2(log_fd, STDERR_FILENO) < 0) {  
        perror("dup2 failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 关闭日志文件描述符,因为dup2已经复制了一个副本,所以不需要再使用它了。  
    close(log_fd);  
  
    // 捕捉其他进程产生的错误信息,并打印到日志文件中。  
    signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号,防止子进程在写日志文件时退出。  
    while (1) { // 循环等待错误信息打印。  
        // 这里通过wait()函数等待子进程结束,并获取其退出状态和错误信息。  
        int status;  
        wait(&status); // 等待子进程结束。  
        if (WIFEXITED(status)) { // 如果子进程正常退出,获取其退出状态。  
            printf("Child process exited with status %d.\n", WEXITSTATUS(status)); // 打印退出状态。  
        } else if (WIFSIGNALED(status)) { // 如果子进程被信号杀死,获取其被杀死的信号。  
            printf("Child process killed by signal %d.\n", WTERMSIG(status)); // 打印被杀死的信号。  
        } else { // 其他情况。  
            printf("Unknown exit status for child process.\n"); // 打印未知的退出状态。  
        }  
        // 清空错误信息缓冲区。  
        memset(error_buffer, 0, MAX_ERROR_LENGTH);  
        // 从子进程的stdout和stderr中读取错误信息。  
        read(STDIN_FILENO, error_buffer, MAX_ERROR_LENGTH - 1); // 从stdin中读取错误信息。  
        read(STDERR_FILENO, error_buffer + strlen(error_buffer), MAX_ERROR_LENGTH - strlen(error_buffer) - 1); // 从stderr中读取错误信息。  
        // 将错误信息写入日志文件中。  
        write(log_fd, error_buffer, strlen(error_buffer)); // 将错误信息写入日志文件。  
        fflush(stdout); // 将输出刷新到标准输出缓冲区,防止在程序退出后丢失输出信息。  
    }  
  
    return 0; // 程序正常退出,返回0表示成功结束进程。  
}

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

void cause_wild_pointer() {
    int *ptr = NULL;
    *ptr = 42;  // 这里会引发野指针问题
}

int main() {
    cause_wild_pointer();
    return 0;
}

在这里插入图片描述
这里可以看到守护进程完成了错误的捕捉(TODO

文章来源:https://blog.csdn.net/sun_0228/article/details/135126207
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。