Linux-进程间关系与守护进程

一、进程组

1.1 什么是进程组

        之前我们提到了进程的概念, 其实每一个进程除了有一个进程 ID(PID)之外 还属于一 个进程组。进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。 每一 个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是 一个正整数, 可以存放在 pid_t 数据类型中。

C++
$ ps 查看进程的信息

# -e 选项表示 every 的意思, 表示输出每一个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
# -a 选项表示不仅列当前⽤户的进程,也列出所有其他⽤户的进程
# -x 选项表示不仅列有控制终端的进程,也列出所有⽆控制终端的进程
# -j 选项表示列出与作业控制相关的信息
int main()
{
    while(true)
    {
        std::cout<<"I am a process pid:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

我们发现尽管时单进程,这个进程也拥有自己的进程组,而进程组id就是单进程的pid

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        while (true)
        {
            std::cout << "I am child process pid:" << getpid() << std::endl;
            sleep(1);
        }
    }
    std::cout << "I am father process pid:" << getpid() << std::endl;
    sleep(100);
    return 0;
}

接下来我们又创建了一个子进程,我们发现父子进程都是属于一个进程组的,并且进程组id是父进程的pid,因为父进程是这个进程组的第一个进程。在我们写的多进程程序中,都是以进程组的方式来执行任务的

1.2 组长进程

        每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。我们可以通过 ps 命 令看到组长进程的现象:

Shell
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
# 输出结果
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat
  • 进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
  • 进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。

注意: 主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。

二、会话

2.1 什么是会话

会话其实和进程组息息相关, 会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会 话也有一个会话 ID(SID),会话ID一般为会话中第一个进程组的第一个进程的id,

注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首 进程总是一个进程组的组长进程, 所以两者是等价的

以我们登录Linux服务器为例,服务器内部一定是安装了系统的,当我们输入账号密码要登录的时候,系统首先会创建一个终端文件和一个与之关联的bash进程,而这个bash进程也是一个进程组,在Linux中终端文件一般保存在在 /dev/pts 目录下,我们可以将终端文件理解为我们输入命令的窗口(当前我一共打开了两个窗口),每打开一个窗口就会新创建一个文件

我们也可以查看一下系统中的bash进程,发现这两个bash进程属于不同的进程组和会话

2.2 如何创建会话

可以调用 setseid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。

#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);

该接口调用之后会发生:

  • 调用进程会变成新会话的会话首进程。 此时, 新会话中只有唯一的一个进程
  • 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID
  • 该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端, 则调用之后会切断联系,因为处于不同的会话了

需要注意的是:

这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这 种情况, 我们通常的使用方法是先调用 fork 创建子进程, 父进程终止, 子进程继续 执行, 因为子进程会继承父进程的进程组 ID, 而进程 ID 则是新分配的, 就不会出现错误的情况。

三、控制终端

在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell 进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB 中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。默认情况下 没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到 显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:

  • 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或 伪终端设备)后,该终端就成为该会话的控制终端。
  • 建立与控制终端连接的会话首进程被称为控制进程。
  • 一个会话只能有一个前台进程组,但是可以有多个后台进程组
  • 无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号 发送给前台进程组的所有进程。
  • 前后台进程的显著区别是是否占用终端(或控制台)的输入输出资源。

证明:

        默认情况下bash进程一般为前台进程组,假如我们执行了一个死循环打印的程序,这死循环中我们在终端中输入命令会发现bash进程并不会做出响应,这是因为当我们执行打印程序时,系统将我们的进程组设置为了前台进程组,bash进程暂时被设置为了后台进程组,而我们在终端输入的数据是给前台进程的所以bash就收不到我们的指令了,而当我们的程序终止时,bash进程会切换为前台进程,此时我们又可以正常执行指令了

 四、作业控制

4-1 什么是作业(job)和作业控制(Job Control)?

作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含 一个进程,也可以包含多个进程,进程之间互相协作完成任务

Shell 分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台作业和任意多个后台作业,这称为作业控制。 

4.2 作业号

放在后台执⾏的程序或命令称为后台命令,可以在命令的后面加上&符号从而让 Shell 识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收 新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)。

我们可以直接通过输入 jobs 命令查看本用户当前后台执行或挂起的作业

▪ 参数 -l  则显示作业的详细信息

▪ 参数 -p 则只显示作业的 PID

关于默认作业:对于一个用户来说,只能有一个默认作业(+),同时也只能有一 个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。

  • + : 表示该作业号是默认作业
  • -:表示该作业即将成为默认作业
  • 无符号: 表示其他作业
4.3 作业控制

常见的作业状态如下表所示:

作业挂起

我们在执行某个作业时,可以通过 Ctrl+Z 键将该作业挂起,然后 Shell 会显示相 关的作业号、状态以及所执行的命令信息。

作业切回

如果想将挂起的作业切回,可以通过 fg(front ground 前台) 命令,fg 后面可以跟作业号或作业的命 令名称。如果参数缺省则会默认将作业号为 1 的作业切到前台来执行,若当前系统只有一个作业在后台进行,则可以直接使用 fg 命令不带参数直接切回。 具体的参数参考如下:

注意: 当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切 到前台执行,即带有“+”的作业号的作业

五、守护进程

5.1 什么是守护进程

守护进程(Daemon Process)是在Unix和类Unix操作系统(如Linux)中运行的一种特殊类型的进程。它们通常在系统启动时自动启动,并在后台运行,执行一些诸如监控或管理其他进程、系统服务、网络服务等任务。

守护进程的主要特点包括:

  1. 后台运行:守护进程在后台运行,不占用终端。这意味着用户不能直接与守护进程交互,除非通过某种形式的进程间通信(IPC)机制,如管道、信号、消息队列等。

  2. 独立于终端:守护进程一旦启动,就与启动它的终端或会话无关。即使终端关闭或会话结束,守护进程也会继续运行。

  3. 生命周期:守护进程通常设计为在系统运行期间持续运行,除非遇到错误、系统重启或管理员明确停止。

  4. 日志记录:守护进程通常会记录其运行状态、错误和警告到系统日志中,以便于管理员监控和故障排查。

  5. 低优先级:守护进程通常具有较低的进程优先级,以避免它们占用过多系统资源,影响其他关键任务。

  6. 无控制终端:守护进程没有控制终端。这意味着它们不能直接从终端接收输入或输出到终端。

5.2 守护进程实现
  • 创建子进程:父进程退出,子进程继续运行,避免该进程是一个进程组的组长,所以守护进程也是一种特殊的孤儿进程
  • 创建新会话:子进程调用 setid() 创建新的会话,成为会话领头进程,脱离原来的控制终端。
  • 改变工作目录:守护进程通常会改变其工作目录到根目录(/),以避免因非根目录不可访问而退出。
  • 设置文件权限掩码:守护进程通常会调用umask设置合适的文件权限掩码,以避免创建的文件具有不恰当的权限。
  • 关闭文件描述符:关闭文件描述符如标准输入、标准输出、标准错误,或者将他们重定向到    /dev/null 文件中,因为进程已经是守护进程了,不需要与用户关联了
  • /dev/null文件是一个特殊的字符类文件,当从这个文件中读取数据时什么也读不到,而当向这个文件写数据时,系统会默认丢弃

这里我们的参数ischdir和isclose是为了将是否改变进程的工作目录和关闭文件描述符暴露出来,供用户自己选择

#pragma once
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
const char *root = "/";
const char *dev_null = "/dev/null";
void Daemon(bool ischdir, bool isclose)
{
    // 1. 忽略可能引起程序异常退出的信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    // 2. 让自己不要成为组长
    if (fork() > 0)
        exit(0);
    // 3. 设置让自己成为一个新的会话, 后面的代码其实是子进程在走
    setsid();
    // 4. 每一个进程都有自己的 CWD,是否将当前进程的 CWD 更改成为 /根目录
    if (ischdir)
        chdir(root);
    // 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了
    if (isclose)
    {
        close(0);
        close(1);
        close(2);
    }
    else
    {
        // 这里一般建议就用这种
        int fd = open(dev_null, O_RDWR);
        if (fd > 0)
        {
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            close(fd);
        }
    }
}
5.3 如何将服务守护进程化
// ./server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage : " << argv[0] << " port" << std::endl;
        return 0;
    }
    uint16_t localport = std::stoi(argv[1]);
    Daemon(false, false);
    std::unique_ptr<TcpServer> svr(new TcpServer(localport,
                                                 HandlerRequest));
    svr->Loop();
    return 0;
}

此时服务端已经在后台开始运行,我们发现客户端可以正常进行业务处理

当我们关闭服务端会话,服务端依旧在后端正常执行,客户端依旧可以正常执行

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张呱呱_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值