Linux 进程间关系与守护进程

一.进程间关系

1.进程组

经过这么久的学习,我们对进程的概念已经很熟悉。其实对于每个进程,除了有一个唯一标识它的PID,它们往往还属于某个进程组,并且每个进程组也有一个独立标识的PGID。进程组是一个进程或多个进程的集合。我们知道,进程是用户的代理,那么进程组就类似于为了完成某项任务而成立的"专项小组"。我们可以看看进程组是什么:

cpp 复制代码
wujiahao@VM-12-14-ubuntu:~$ ps -o pid,pgid,ppid,comm | cat
    PID    PGID    PPID COMMAND
 578322  578322  578321 bash
 578480  578480  578322 ps
 578481  578480  578322 cat

我们可以看到,ps命令和cat命令的父进程都是bash,而ps和cat同属于一个进程组,因为他们的PGID相同。

再仔细观察,我们可以看到:对于ps,它的PID和PGID是相同的------我们称这样的进程叫做组长进程

• 进程组组⻓的作⽤: 进程组组⻓可以创建⼀个进程组或者创建该组中的进程

• 进程组的⽣命周期: 从进程组创建开始到其中最后⼀个进程离开为⽌。注意: 主要某个进程组中有⼀个进程存在, 则该进程组就存在, 这与其组⻓进程是否已经终⽌⽆关。

2.会话

2.1什么是会话

我们刚谈论了进程组的概念,一个进程组可以有一个或多个进程。那么会话是什么?它其实早就在我们之前的应用中出现过了。

会话可以看成是⼀个或多个进程组的集合, ⼀个会话可以包含多个进程组。每⼀个会话也有⼀个会话ID(SID)。

我们登录云服务器Linux使用的方法就是用终端xshell进行会话式 的登录。登录成功,系统必须为用户创建一个会话,会话内部默认一定有一个进程组叫bash!

我们通常用管道将几个进程编程一个进程组。例如上面的进程组3,可能由下面的命令形成:

proc 4 | proc 5 | proc 6 &(注意,这里&标识将进程调度到后台)

我们可以举一个具体的例子观察这个现象。

wujiahao@VM-12-14-ubuntu:~/TcpEchoServer$ sleep 100 | sleep 200 | sleep 300 &

1\] 581659 wujiahao@VM-12-14-ubuntu:\~$ ps ajx\|head -n1

可以看到,这三个sleep进程同属于一个进程组,因为他们的PGID相同

2.2进程组与任务

进程组和任务:任务就是某种工作,任务需要通过进程组来完成。

为什么我们直接启动一个任务(进程组),就无法输入命令并执行了?

为什么我们把任务放在后台执行,又能够执行对应的命令?

登录认证模块(账号密码)成功之后,会创建出bash进程和一个终端文件/dev/pts/XX,也就是说会打开标准输出标准错误标准输入。fork并且exec创建出bash。根据子进程对父进程资源的继承,所以所有其他进程都会默认打开标准输入标准输出标准错误。

键盘输入的数据据,必须明确指定一个进程处理(前台进程),所以bash默认在前台。

会话内部,进程组必须区分为前台进程组和后台进程组。为什么?

再一次会话中,有且只能有一个前台进程组,而后台进程组可以有多个。

对进程的相关操作:

jobs:查看当前系统的后台任务

fd 任务号:把指定的任务提到前台,front ground

ctrl+C:只能终止前台任务,后台任务不受键盘输入的影响

ctrl+Z:暂停进程或进程组,会被自动切换到后台,同时把bash进程调度到前台。

bg 任务:让后台任务运行起来。

2.3控制终端

在UNIX系统中,⽤⼾通过终端登录系统后得到⼀个Shell进程 ,这个终端成为Shell进程的控制终端 。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB中的信息,因此由Shell进程启动的其它进程 的控制终端也是这个终端 。默认情况下没有重定向 ,每个进程的标准输⼊、标准输出和标准错误都指向控制终端,进程从标准输⼊读也就是读⽤⼾的键盘输⼊,进程往标准输出或标准错误输出写也就是输出到显⽰器上。

另外会话、进程组以及控制终端还有⼀些其他的关系,我们在下边详细介绍:

◦ ⼀个会话可以有⼀个控制终端 ,通常会话**⾸进程**打开⼀个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。

◦ 建⽴与控制终端连接的会话⾸进程被称为控制进程

◦ ⼀个会话中的⼏个进程组可被分成**⼀个前台进程组** 以及**⼀个或者多个**后台进程组。

◦ 如果⼀个会话有⼀个控制终端,则它有⼀个前台进程组,会话中的其他进程组则为后台进程组。

◦ ⽆论何时进⼊终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。

◦ 如果终端接⼝检测到调制解调器(或⽹络)已经断开,则将挂断信号发送给控制进程(会话⾸进程)。

2.4创建会话

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

cpp 复制代码
NAME
       setsid - run a program in a new session

SYNOPSIS
       setsid [options] program [arguments]

DESCRIPTION
       setsid runs a program in a new session. The command calls fork(2) if already a process group leader. Otherwise, it executes a program in the current process. This default behavior is possible to
       override by the --fork option.

该接⼝调⽤之后会发⽣:

◦ 调⽤进程会变成新会话的会话**⾸进程(组长)** 。 此时, 新会话中只有唯⼀的⼀个进程

◦ 调⽤进程会变成进程组组⻓。 新进程组ID就是当前调⽤进程ID。

◦ 该进程没有控制终端。 如果在调⽤setsid之前该进程存在控制终端, 则调⽤之后会切断联系

根据setsid的特殊性,我们往往使用fork创建子进程来执行setsid。⼦进程继续执⾏, 因为⼦进程会继承⽗进程的进程组ID, ⽽进程ID则是新分配的, 就不会出现错误(父进程为组长)的情况。

3.作业

3.1作业控制

作业和进程组是一体两面 的关系。对于用户,作业指的是用户为了完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程。进程之间互相协作完成任务,通常是一个进程管道

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

例如,下列命令就是一个作业:它包含两个命令,执行时bash会再前台启动由两个进程组成的作业。

cat /etc/filesystems | head -n 5

3.2作业号与作业状态

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

例如下⾯的命令在后台启动了⼀个作业, 该作业由两个进程组成, 两个进程都在后台运⾏:

cat /etc/filesystems | grep ext &

1\] 2202 ext4 ext3 ext2 # 按下回⻋ \[1\]+ 完成 cat /etc/filesystems \| grep --color=auto ext

执行结果:

◦ 第⼀⾏表⽰作业号和进程ID, 可以看到作业号是1, 进程ID是2202

◦ 第3-4⾏表⽰该程序运⾏的结果, 过滤 /etc/filesystems 有关 ext 的内容

◦ 第6号分别表⽰作业号、默认作业、作业状态以及所执⾏的命令

关于默认作业:对于⼀个⽤⼾来说,只能有⼀个默认作业 (+) ,同时也只能有⼀个即将成为默

认作业的作业 (-) ,当默认作业退出后,该作业会成为默认作业。

▪ + : 表⽰该作业号是默认作业

▪ - :表⽰该作业即将成为默认作业

▪ ⽆符号: 表⽰其他作业

作业状态:

作业控制的功能:

二.守护进程

通过上面的讲解我们知道,登录就是建立会话的过程,而关闭终端可能会影响你的服务器的运行。

而日常使用的网络服务器,不能收到任何用户登录和注销的影响。

为服务器创立一个新会话 ,并将当前任务切换到新会话中。这样的进程,我们叫做守护进程!

守护进程vs前台进程vs后台进程

前台与后台同属于一个会话,拥有标准输入使用权的叫前台进程,守护进程是后台进程的一种,但她有自己的独立会话。

那么,我们如何将之前的服务,变成一个独立的绘画,并成为会话中独立的进程呢?

使用pid_t setsid创建新会话即可

守护进程实际上也是孤儿进程的一种,调用该接口的进程会变成新会话的会话首进程。

1.创建守护进程的步骤

1. 创建子进程,父进程退出

cpp 复制代码
pid_t pid = fork();
if (pid < 0) {
    exit(EXIT_FAILURE);
} else if (pid > 0) {
    // 父进程退出,子进程成为孤儿进程,被init接管
    exit(EXIT_SUCCESS);
}

作用

  • 让出shell控制权

  • 子进程不再是进程组组长,为后续setsid创造条件

2. 创建新会话

cpp 复制代码
// 子进程创建新的会话
pid_t sid = setsid();
if (sid < 0) {
    exit(EXIT_FAILURE);
}

作用

  • 脱离原控制终端

  • 成为新会话的领头进程

  • 成为新进程组的组长

3. 再次fork(可选但推荐)

cpp 复制代码
// 再次fork,确保不是会话领头进程,防止意外获取控制终端
pid_t pid2 = fork();
if (pid2 < 0) {
    exit(EXIT_FAILURE);
} else if (pid2 > 0) {
    exit(EXIT_SUCCESS);
}

作用

  • 确保进程永远不会重新获得控制终端

  • 增加一层隔离

4. 改变工作目录

cpp 复制代码
// 改变工作目录到根目录,避免占用可卸载的文件系统
if (chdir("/") < 0) {
    exit(EXIT_FAILURE);
}

参数含义

  • Daemon(0, 0)中的第一个参数控制是否改变工作目录

  • 0表示不改变,1表示改变到根目录

5. 重设文件权限掩码

cpp 复制代码
// 重设文件权限掩码,增加守护进程创建文件的灵活性
umask(0);

6. 关闭文件描述符

cpp 复制代码
// 关闭所有打开的文件描述符
for (int i = sysconf(_SC_OPEN_MAX); i >= 0; i--) {
    close(i);
}

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

参数含义

  • Daemon(0, 0)中的第二个参数控制是否关闭标准文件描述符

  • 0表示不关闭,1表示关闭

2.完整的守护进程Daemon

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const std::string dev = "/dev/null";

// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{
    // 1. 忽略IO,子进程退出等相关的信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN); // SIG_DFL

    // 2. 父进程直接结束
    if (fork() > 0)
        exit(0);

    // 3. 只能是子进程,孤儿了,父进程就是1
    setsid(); // 成为一个独立的会话

    if(nochdir == 0) // 更改进程的工作路径???为什么??
        chdir("/");

    // 4. 依旧可能显示器,键盘,stdin,stdout,stderr关联的.
    //  守护进程,不从键盘输入,也不需要向显示器打印
    //  方法1:关闭0,1,2 -- 不推荐
    //  方法2:打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/null
    if (noclose == 0)
    {
        int fd = ::open(dev.c_str(), O_RDWR);
        if (fd < 0)
        {
            LOG(LogLevel::FATAL) << "open " << dev << " errno";
            exit(OPEN_ERR);
        }
        else
        {
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            close(fd);
        }
    }
}

3.将网络服务器守护进程化

cpp 复制代码
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;

}
相关推荐
java_logo2 小时前
Docker 容器化部署 QINGLONG 面板指南
java·运维·docker·容器·eureka·centos·rabbitmq
pale_moonlight2 小时前
五、Hbase基于环境搭建
linux·数据库·hbase
Nie_Xun2 小时前
Ubuntu 安装与 NVIDIA 显卡驱动配置 2篇
linux·运维·ubuntu
HIT_Weston2 小时前
25、【Ubuntu】【远程开发】内网穿透:密钥算法介绍(一)
linux·运维·tcp/ip·ubuntu
9ilk2 小时前
【基于one-loop-per-thread的高并发服务器】--- 自主实现HttpServer
linux·运维·服务器·c++·笔记·后端
HMS Core2 小时前
【FAQ】HarmonyOS SDK 闭源开放能力 — Push Kit
linux·python·华为·harmonyos
LFly_ice2 小时前
Docker核心概念与实战指南
运维·docker·容器
Once_day2 小时前
Linux之rsyslog(4)属性配置
linux·服务器
waving-black3 小时前
Linux中自定义服务开机自启nginx
linux·服务器·nginx