目录

Linux网络编程守护进程

Linux网络编程——守护进程


一、前言

在我们上一篇TCP网络通信文章中,我们使用了守护线程,但是当时没有做过多的了解,下面再本文中,我们再做详细介绍。

二、守护进程简单概念

在我们所使用的系统中,一般以服务器的方式对外提供服务的服务器(服务器环境),都是以守护进程的方式在系统中工作的。这有着很多的优点:

  1. 后台运行 :作为守护进程运行的服务不会干扰用户的交互式会话,并且可以在用户注销后继续运行。
  2. 系统启动时自动运行 :很多重要的服务被配置为在系统启动时自动作为守护进程启动,确保了服务的高可用性。
  3. 资源管理 :守护进程可以更高效地管理系统资源,比如网络端口、文件描述符等。
  4. 监控与维护 :通过专门的工具和服务管理器(如Systemd、Supervisord),可以方便地对守护进程进行监控、重启和日志记录。

下面是常见的以守护进程方式运行的服务

  • Web服务器 :例如Apache HTTP Server或Nginx,它们监听HTTP请求并响应客户端。
  • 数据库服务器 :如MySQL、PostgreSQL等,处理数据库查询和事务。
  • 邮件服务器 :例如Postfix或Sendmail,负责发送和接收电子邮件。
  • FTP服务器 :用于文件传输服务。
  • 打印服务 :CUPS(Common UNIX Printing System)等服务允许用户从远程计算机发送打印任务。

在我们使用Linux服务器时,大多都会使用一些终端软件通过 ssh 远程连接服务器使用,比如Xshell,这就是因为在Linux服务器中通常默认运行着 ssh服务器的守护进程。

当我们尝试通过SSH客户端(例如Xshell)连接到Linux服务器时,实际上是与运行在服务器上的 sshd 守护进程进行通信。这个守护进程负责处理登录请求,验证用户凭据,并为成功认证的用户提供访问权限。

https://i-blog.csdnimg.cn/direct/22b3bceaeca94abc8955efef0c8cd3a8.png

守护进程,一旦启动之后,除非用户手动关闭,否则不会被关闭,会一直运行。

三、有关进程的属性标识符

1、进程组ID PGID

使用 ps ajx |head -1 查看进程的相关头栏 https://i-blog.csdnimg.cn/direct/7e656fa345384bd4bba7c18dd6cbffed.png

这里面的项目我们在之前的进程控制一节已经了解到了,这个 PGID 是什么呢? 进程组ID,Progress Group ID

什么是进程组呢?

它是我们为了做某些操作而创建的一系列进程,它允许将多个相关联的进程组织在一起以便于管理和控制。进程组由一个或多个进程组成,并且每个进程组都有一个唯一的进程组ID(PGID)。进程组的主要用途之一是实现作业控制,特别是在Unix/Linux系统中。

  • 进程组 :一组相关进程的集合,通常用于对这些进程进行统一管理。例如,在shell环境中执行一个管道命令时,所有参与该命令的进程会被分配到同一个进程组中。
  • 进程组ID (PGID) :每个进程组都有一个唯一的标识符,称为进程组ID。这个ID通常是该组中第一个创建的进程(即组长进程)的PID(进程ID)。
  • 组长进程 :进程组中的第一个进程被称为组长进程。组长进程的PID就是该进程组的PGID。组长进程可以终止,但它一旦终止,就不会有新的进程成为组长进程。

我们先用 sleep 1000 | sleep 2000 | sleep 3000 &

(&表示将命令放在后台)创建一个后台进程,再查看进程的信息

https://i-blog.csdnimg.cn/direct/48b1613abbf94e0fa29cbd102fcb254a.png

可以看到系统中已经存在了四个进程(3个 sleep 进程,一个 grep 进程)并且三个 sleep 进程具有相同的 PGID ,即这三个进程是属于同一个进程组的,并且创建一个进程组,其第一个创建的进程就是进程组的组长, PGID 即为组长的 PID.

jobs 命令 : 主要用于显示当前 shell 会话中正在运行的或已停止的作业(即后台进程)。

2、作业编号 Job ID

作业编号Job ID ):这是shell为每个后台任务分配的一个内部编号。它仅仅在当前shell会话中有意义,并且用于方便用户通过 jobs、fg、bg 等命令来管理和控制这些任务。例如,可以在shell中使用 fg %1 将第一个后台作业带到前台运行。这个编号是由shell根据启动的后台任务顺序自动分配的,并不是全局唯一的,也不与系统中的任何其他标识符直接关联。

上图中, [1] 4527 表示后台作业的ID为4257.

当你执行 sleep 1000 | sleep 2000 | sleep 3000 & 这条命令时,shell创建了一个新的进程组来包含这三个通过管道连接的 sleep 命令。shell为这个命令序列分配了作业编号[1],而其中的第一个 sleep 1000 进程成为了这个新进程组的组长进程,其PID被用作PGID。在这个例子中,作业编号4257实际上是指最后一个sleep 3000进程的PID.

Job IDPGID 这两个编号服务于不同的目的:一个是shell层面的任务管理,另一个是操作系统级别的进程组管理。

3、会话ID SID

会话(Session) 是一组进程的集合,通常与一个用户登录到系统相关联。会话ID(SID, Session ID)是用于唯一标识每个会话的一个数值。理解会话、会话ID以及它们如何影响进程和作业控制对于有效地管理系统资源至关重要。

例如,我们在使用终端软件并使用ssh连接Linux服务器之后,就会创建一个会话。一个会话, 可以有多个进程组, 必须有且只有一个 前台进程组 和 0个或多个 后台进程组。 反过来, 当我们登录Linux服务器时 会创建一系列的进程组, 这些进程组构建成了一个会话.

而, Windows也是一样的, 当我们登录Windows用户 就会由Windows启动左面环境并创建一个会话. 你可以在这个会话中启动任何软件, 并且 一般启动的软件都是属于这个会话的. 而且, 有时候认为Windows很卡了, 可能就会重启或者注销一下. 注销操作其实就是关闭此次的会话, 并关闭当前会话的进程.

会话的基本概念

  • 会话 :一个会话可以包含多个进程组,通常始于一次用户登录。当用户登录时,shell作为该会话的首进程启动,并可能创建额外的进程组来执行不同的任务或命令。
  • 会话首进程 :也称为会话领导者,通常是启动会话的第一个进程(例如,在终端登录时启动的shell)。它负责创建新的会话并拥有与该会话相同的SID。
  • 会话ID (SID) :会话ID是一个唯一的标识符,分配给每个会话。在一个会话内的所有进程共享相同的SID值,这个值通常是会话首进程的PID(进程ID)。

会话与进程组的关系

  • 每个会话可以包含一个或多个进程组。
  • 每个进程组只能属于一个会话。
  • 会话中的进程组可以通过 setsid() 系统调用来创建新的会话和进程组。

那么为什么必须有一个前台进程组呢?

  • 前台进程组的概念是作业控制机制的一部分,它确保用户与系统之间的交互能够有序进行。前台进程组主要用于处理用户输入和输出。当一个进程组被指定为前台进程组时,它可以读取标准输入,这使得用户可以直接与应用程序互动,比如在命令行中运行文本编辑器如 vim
  • 前台进程组对来自控制终端的某些信号有特殊的响应能力。例如,如果用户按下 Ctrl+C 组合键,控制终端会发送一个 SIGINT 信号给前台进程组中的所有成员,用于中断当前操作。
  • 在同一时间点,一个控制终端只能关联一个前台进程组。这样可以避免多个进程组同时尝试读取用户的输入或向终端输出内容,从而防止混乱。后台进程组如果有尝试读取标准输入或者输出到控制终端的操作,则可能会被挂起,直到它们成为前台进程组。
  • 通过将直接与用户交互的任务置于前台,操作系统可以确保这些任务得到优先处理,提供更加流畅的用户体验。而其他不需要即时用户输入的任务可以在后台运行,不影响前台任务的执行。

下面举个例子

https://i-blog.csdnimg.cn/direct/fa43243224a8443cbe8a3924b202daa3.png

  1. 我们先使用 sleep 1000 | sleep 2000 | sleep 3000 &

    创建了后台进程,通过查看我们发现这些进程的

    SID

    都是

    14181

  2. 然后再查看

    14181

    ,发现其是

    bash

    PID

    ,并且可以看到

    bash

    自成一族且自己为自己进程组的组长。后续 我们通过命令行解释器 启动进程或者任务, 那么启动的这些进程或任务 也都属于 bash

    这个会话。

  3. 我们再用

    jobs

    命令查看后台进程,并使用

    fg 1

    命令将它拿到前台。

  4. 后面我们再尝试输入命令就无法执行了。

  5. 这是因为由于一个会话只能有一个前台进程组(有且只能有一个进程组处于前台), 此时的前台进程组成了它, 而不是 bash

    . 没有了命令行解释器, 就没有办法使用命令

四、守护进程的理解

根据上述,在Linux中,当在命令行(即shell)中启动一个进程时,实际上可以认为是在当前会话中启动了一个或多个进程,并且根据具体情况,这些进程可能属于同一个进程组或者不同的进程组。并且, 此会话中的进程 执行 fork() 创建的子进程, 一般而言 也都属于这个会话.

  1. **如果你运行的是单个命令,如

    ls,shell 会创建一个子进程来执行这个命令。这个子进程形成一个新的进程组,其 PGID 通常是这个新进程的 PID 。一旦命令执行完毕,该进程组通常就会终止。**

  2. **如果你运行的是复合命令,例如使用管道将多个命令连接在一起,像

    commond1 | commond2 ,

    shell

    会为这些命令创建一个单独的进程组。所有由管道连接的命令都是这个进程组的一部分,并共享相同的

    PGID 。**

  3. **当你在命令后加上 &

    来将其放入后台执行时,

    shell 同样会为它创建一个新的进程组。这样做的好处之一是允许

    shell 继续接受新的命令输入,同时也便于对这组后台进程进行管理和控制(如暂停、恢复或终止它们)。**

**而 一个网络服务通常是不能被其他会话影响的, 所以 一个网络服务通常被设置脱离其他会话, 自己形成一个新的会话. 这样, 除非用户手动关闭, 否则此进程就不会被关闭.就像

sshd 自己创建一个会话, 自己就是会话首进程, 自己是一个进程组.这样的进程, 就被称为 守护进程 或 精灵进程**

1、如何实现守护进程

sesid()

让进程成为守护进程很简单, 只需要执行 setsid() 就可以让当前进程创建一个独立的会话, 成为守护进程

**不过,

setsid() 有一个非常重要的执行条件就是 执行进程不能是进程组组长**

进程组组长是进程组的管理者. 如果是一个进程组组长要创建新会话, 那么进程组的其他成员该怎么办呢?

**进程组的组长不能调用

setsid() , 那么一个进程该如何调用呢?其实很简单,

fork() 之后, 再让子进程调用就可以了, 因为此时的子进程是进程组的第二个进程.**

所以, 创建守护进程的方式就是:

if(fork() > 0)
    exit(0);
setsid();

此时, 就是子进程执行的 setsid() 也就可以成功设置守护进程, 不过需要注意的是 服务器的功能实现都要让子进程执行.这是用此方法设置一个守护进程必须要做的事情.

其次就是一些可做可不做的事情:

  • 忽略 SIGPIPE 信号:在使用管道时, 如果读端关闭, 写端会被终止也被关闭. 终止信号就是 SIGPIPE,都是流式通信, TCP服务器也是这样的. 所以 TCP服务器可以设置忽略 SIGPIPE 信号
  • 改变进程工作路径
  • 关闭0、1、2文件描述符 或 将其重定向到 /dev/null, 0为标准输入, 1为标准输出, 2为标准错误因为, 当一个进程成为守护进程之后 就脱离了终端会话. 与 标准输入、输出、错误 不再有关系了.所以, 可能会关闭0、1、2. 但是很少这么做.更多的是将三个文件描述符重定向到 /dev/null 这个文件中. 此文件是Linux中的 数据垃圾桶, 向此文件中写入的内容 都会被丢弃.

那么就可以用一个函数实现守护进程:

// 守护进程接口 daemonize.hpp
//这段代码定义了一个 daemonize 函数,
//用于将当前进程转变为守护进程(Daemon)。
//守护进程是一种在后台运行的特殊进程,通常用于执行系统服务。



void daemonize() {
    int fd = 0;

    // 1. 忽略SIGPIPE
    //当进程尝试写入一个已经关闭或不可写的管道或套接字时,操作系统会发送 SIGPIPE 信号,
    //默认情况下会导致进程终止。通过忽略这个信号,可以让程序在遇到这种情况时继续运行而不是崩溃。
    signal(SIGPIPE, SIG_IGN);
    // 2. 改变工作路径
    //chdir(const char *__path);
    // 3. 不要成为进程组组长
    //确保子进程不是进程组组长,以便后续调用 setsid() 成功
    //fork() 创建一个新的子进程。父进程会得到子进程的 PID 并立即退出(exit(0))
    //这样就保证了只有子进程继续运行,并且它不会是进程组组长。
    if (fork() > 0) {
        exit(0);
    }
    // 4. 创建独立会话
    //创建一个新的会话,使该进程成为新会话的会话首进程、新进程组的组长,并且脱离控制终端。
    //setsid() 调用后,进程不再与任何终端相关联,成为一个独立的会话和进程组的领导者。
    setsid();
    // 重定向文件描述符0 1 2
    //将标准输入(0)、标准输出(1)和标准错误(2)重定向到 /dev/null,以防止它们占用不必要的资源。
    //最后检查 fd 是否大于 STDERR_FILENO(即 2),如果是,则关闭多余的文件描述符 fd,因为它已经被复制到标准输入、输出和错误中了。
    if ((fd = open("/dev/null", O_RDWR)) != -1) { // 执行成功fd大概率为3
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);

        // dup2三个标准流之后, fd就没有用了
        //原本打开 /dev/null 得到的文件描述符 fd 已经没有用了,因为它已经被复制到了标准输入、输出和错误的文件描述符上。(上面的重定向)
        if (fd > STDERR_FILENO) {
            close(fd);
        }
    }
}

2、daemon

https://i-blog.csdnimg.cn/direct/120aa6feda2443cc8338ff4f6778c73c.png

系统提供的另一个接口: daemon() 也可以实现守护进程,它可以一键完成 fork()、setsid() 及重定向文件描述符的操作。不过这个使用起来不太灵活。

3、nohup

它是一个系统命令, 可以设置进程为不挂起状态.

也就让进程成为了守护进程.


感谢指正!