< Linux > 守护进程

目录

1、守护进程

        守护进程的概念

        进程组和会话

2、守护进程化的方式

        TCP网络程序(守护进程化)

        TCP网络程序(守护进程化)gitee地址

        daemon创建守护进程

        nohup命令


1、守护进程

守护进程的概念

守护进程也叫做精灵进程,是运行在后台的一种特殊进程他独立于控制终端并且可以周期性的执行某种任务或者等待处理某些发生的事件。

  • 守护进程是非常有用的进程,在Linux当中大多数服务器用的就是守护进程比如Web服务器httpd等,同时守护进程完成很多系统的任务。当Linux系统启动的时候,会启动很多系统服务,这些进程服务是没有终端的也就是说你把终端关闭了这些系统服务是不会停止的,他们一直运行着他们有一个名字就叫做守护进程。

一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则,一直会在运行。


进程组和会话

进程组的相关概念:

  • 进程除了有进程的PID之外还有一个进程组,进程组是由一个进程或者多个进程组成。通常他们与同一作业相关联可以收到同一终端的信号
  • 每个进程组有唯一的进程组ID,每个进程组有一个进程组组长。如何判断一个进程是不是这个进程组的组长了,通常进程组ID等于进程ID那么这个进程就是对应进程组的组长。

会话的相关概念:

  • 会话是有一个或者多个进程组组成的集合
  • 一个会话可以有一个终端,建立与控制终端连接的会话首进程被成为控制进程,一个会话的几个进程组可以分为前台进程和后台进程,而这些进程组的控制终端相同也就是sesion id是一样的当用户使用ctr +c 产生SIGINT信号时内核会发送信号给相应前台进程组的所有进程如果我运行一个程序我们想要把他放到后台运行我们可以在可执行程序的后面加一个& 举个列子:./test & 如果我们想要把他提到前台进程我们可以使用fg

我们使用如下监控脚本先观察一个现象:

[xzy@ecs-333953 date37]$ ps axj | head -1 && ps axj | grep sshd

< Linux > 守护进程_第1张图片

上述第一行以 -D 结尾的就是服务器(守护进程)。它的PPID是1。下面来介绍上述选项的意义:

  • COMMAND:启动的进程命令名称
  • TIME:进程启动的时长
  • UID:是谁启动的
  • STAT:状态
  • TPGID:当前进程组和终端的关系(如果是-1,则没有任何关系)
  • TTY:代表哪一个终端
  • SID:当前进程的会话ID
  • PGID:当前进程所属的进程组
  • PID:当前进程自己的ID
  • PPID:当前进程的父进程的ID

如下,我把三个进程放到后台运行,并使用如下监控脚本观察现象:

[root@xzy-centos ~]# ps ajx | head -1 && ps axj | grep sleep

< Linux > 守护进程_第2张图片

  • 上述我创建了三个进程,可以确定的是这三个进程的PPID都是一样的,因为父进程都是Bash。上述三个进程是属于同一个进程组(PGID)的,且会发现启动的第一个进程是进程组的组长

上述三个进程的会话ID(SID)为6821,即bash。我们通过下面的监控脚本观察:

< Linux > 守护进程_第3张图片

看如下的图:

< Linux > 守护进程_第4张图片

  • 一旦我们登陆linux,linux会我们创建一个会话。会话内部由多个进程组构成。登陆后会给我们加载bash,所以其内部必须有一个前台进程组(任何时刻,只能有一个前台进程组)。0个或多个后台进程组。

再使用如下监控脚本观察上述的会话6821:

[root@xzy-centos ~]# ps axj | head -1 && ps ajx | grep 6821

< Linux > 守护进程_第5张图片

图中可以看出,bash自己就是一个进程,自己就是进程组的组长,也是会话中的老大(会话前台进程组)。 所以一开始创建的三个进程,它们的会话SID均为6821,即bash:

< Linux > 守护进程_第6张图片

  • 后续如果我们再自己启动新进程 && 启动进程组,它依旧属于bash自己的会话。

我们使用jobs命令查看系统中的任务:

先前这三个进程是放到后台运行的。我们使用fg 1命令把此任务提到前台:

< Linux > 守护进程_第7张图片

  • 当把后台进程提到前台后,会发现我shell命令用不起来了。因为我们只能有一个前提进程组。当我们把刚才的后台进程提到前台,那么我bash命令行解释器会自动退到后台进程组 。那么自然就没有办法接受你的输入了。

综上:

  • 我们在命令行中启动一个进程,现在就可以叫做在会话中启动一个进程组,来完成某种任务。
  • 所有会话内的进程fork创建子进程,一般而言依旧属于当前会话。

像平时当我们觉得windows卡顿的时候,我们可能会重新注销一下。注销就是让用户退出登陆后再重新登陆,那么此时就相当于给你新建一个会话。卡顿是因为你本次登陆过程中启动了很多任务,且都属于同一个会话,注销本质就是把你内部会话的所有进程组删掉。

注意:

  • 在登录的状态时,新起了一个网络服务器,创建好之后,再派生的子进程也属于当前会话,所以我们就不能让这个网络服务器属于这个会话内容,要不然它会受到用户的登录和注销的影响。
  • 所以当我们有个网络服务的时候,应该脱离这个会话,让它独立的在计算机里自成进程组,自成新会话。这样在两个用户同时登录的时候,形成的两个会话是独立的,在操作各自的bash不会互相影响。
  • 像这种自成进程组,自成新会话,而且周而复始进行的进程称为守护进程(精灵进程)。

2、守护进程化的方式

我们这里有三种方式让自己的进程守护进程化:

  • 自己写daemon函数,推荐使用这种方式(下面的TCP网络程序中的daemon函数就是自己模拟实现的)
  • 用系统的daemon函数
  • nohup命令

TCP网络程序(守护进程化)

我们上篇博文的TCP网络程序是在前台进行的,但是实际上服务器并不是在前台运行的,而是在后台进行的。所以现在对TCP网络程序的代码进行修改,使其守护进程化。让服务器在后台运行。我们创建daemon.hpp文件完成守护进程的主要逻辑。代码逻辑如下:

  • 调用signal函数忽略SIGPIPE信号
  • 更改进程的工作目录(选做)
  • fork子进程,exit退出父进程。让自己不要成为进程组组长。从而保证后续不会再和其他终端相关联。
  • 调用setsid函数设置自己是一个独立的会话
  • 将标准输入、标准输出、标准错误重定向到/dev/null。

生产守护进程需要调用setsid函数,注意点如下:

  • 调用setsid创建新会话的目的,是让当前进程自成会话,与当前bash脱离关系(创建守护进程的核心)。
  • 调用setsid创建新会话时,要求调用进程不能是进程组组长,但是当我们在命令行上启动多个进程协同完成某种任务时,其中第一个被创建出来的进程就是组长进程,因此我们需要fork创建子进程,让子进程调用setsid创建新会话并继续执行后续代码,而父进程我们直接让其退出即可。此时子进程就不是组长进程了,而是独立会话的守护进程。
  • 当服务端给客户端写入时,但是客户端突然关掉了,那就是向一个不存在的文件描述符写入,此时服务端会收到SIGPIPE信号而自动终止
  • 当前进程有自己的工作目录,有时候守护进程想要更改自己的工作目录,一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。我们可以使用chdir更改进程的工作目录,不过此操作不强求。
  • 守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出、标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),类似于Linux下的一个“文件黑洞” or “垃圾桶”,通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)

dameon.hpp文件代码如下:

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void daemonize()
{
    int fd = 0;
    // 1、1、忽略SIGPIPE
    signal(SIGPIPE, SIG_IGN);
    // 2、更改进程的工作目录
    //  chdir();
    // 3、让自己不要成为进程组组长
    if (fork() > 0)
        exit(0);
    // 4、设置自己是一个独立的会话
    setsid();
    // 5、重定向0,1,2
    if ((fd == open("/dev/null", O_RDWR) != -1)) // fd == 3
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 6、关闭掉不需要的fd
        if (fd > STDERR_FILENO)
            close(fd);
    }
}

上述我们将标准输入、标准输出、标准错误重定向到/dev/null,那么我运行后打印的日志也就不见了。解决办法如下:我们在log.hpp打印日志文件那,用宏定义一个serverTcp.log文件。在log.hpp文件内部定义一个Log类,在此类内定义一个logFd变量,内部实现一个enable函数,enable处理过程如下:

  • 利用open函数打开此文件,返回到logFd
  • 利用dup2把标准输入,标准输出重定向到logFd文件
#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};

#define LOGFILE "serverTcp.log"
class Log
{
public:
    Log()
        : logFd(-1)
    {
    }
    void enable()
    {
        umask(0);
        logFd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
        assert(logFd != -1);
        dup2(logFd, 1);
        dup2(logFd, 2);
    }
    ~Log()
    {
        if (logFd != -1)
        {
            fsync(logFd);
            close(logFd);
        }
    }

private:
    int logFd;
};
// 打印日志函数
void logMessage(int level, const char *format, ...) // logMessage(DEBUG, "%d", 10);
{
    // 确保level等级是有效的
    assert(level >= DEBUG);
    assert(level <= FATAL);

    // 获取当前的使用者(环境变量)
    char *name = getenv("USER");

    char logInfo[1024];
    // 定义一个va_list类型的指针表示可变参数列表类型
    va_list ap;
    // 初始化此指针变量
    va_start(ap, format);
    // 将可变参数格式化输出到一个字符数组logInfo里
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
    // 置空此指针变量
    va_end(ap);

    // umask(0);
    // int fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
    // assert(fd >= 0);

    FILE *out = (level == FATAL) ? stderr : stdout;

    // 日志等级,打印日志的时间,打印日志的用户名
    fprintf(out, "%s | %u | %s | %s\n",
            log_level[level],
            (unsigned int)time(nullptr),
            name == nullptr ? "unknow" : name,
            logInfo);
    fflush(out);        // 将C缓冲区的数据刷新到OS
    fsync(fileno(out)); // 将OS中的数据尽快刷盘
}

我们只需要在服务端的main函数命令行参数信息处理后调用此daemon函数即可:

< Linux > 守护进程_第8张图片

测试结果:

  • 现在我们运行服务端,通过下面的监控脚本辅助观察信息:
[xzy@ecs-333953 tcp]$ ps axj | head -1 && ps axj | grep serverTcp
[xzy@ecs-333953 tcp]$ ps axj | head -1 && ps axj | grep sshd

运行代码,用ps命令查看该进程,会发现该进程的TPGID为-1,TTY显示的是,也就意味着该进程已经与终端去关联了。此外PID、PGID、SID均是一样的,可以看出已经是守护进程了:

< Linux > 守护进程_第9张图片

现在就相当于把代码部署到了Linux中,现在运行客户端,能够正常与服务端通信。即使我们把电脑关掉了,此服务端也是一直在运行的。除非我们kill -9杀掉此进程。

< Linux > 守护进程_第10张图片


TCP网络程序(守护进程化)gitee地址

 最终版本的TCP网络程序(守护进程化)总代码的gitee地址如下:

  • TCP网络程序(守护进程化)源码汇总

daemon创建守护进程

实际当我们创建守护进程时可以直接调用daemon接口进行创建,daemon函数的函数原型如下:

#include 
int daemon(int nochdir, int noclose);

参数说明:

  • 如果参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理。
  • 如果参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,否则不做处理。

调用示例:

#include 

int main()
{
	daemon(0, 0);
	while (1);
	return 0;
}
  • 调用daemon函数创建的守护进程与我们原生创建的守护进程差距不大,唯一区别就是daemon函数创建出来的守护进程,既是组长进程也是会话首进程。
  • 也就是说系统实现的daemon函数没有防止守护进程打开终端,因此我们实现的反而比系统更加完善。

< Linux > 守护进程_第11张图片


nohup命令

来看我如下test.cc文件下的代码:

#include 
#include 
#include 
int main()
{
    while (true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }
    return 0;
}

我们用此小程序来充当网络服务器,当我们正常编译运行后,此程序默认是在前台进行的:

< Linux > 守护进程_第12张图片

我们可以使用如下的指令将其变成后台进程:

nphup ./a.out &

< Linux > 守护进程_第13张图片

此时会发现我nohup.out文件的大小在不断增大,使用如下命令帮助我们观察现象:

[xzy@ecs-333953 hello]$ ps axj | head -1 && ps ajx | grep a.out
[xzy@ecs-333953 hello]$ ps ajx | grep 15318

< Linux > 守护进程_第14张图片

 通过测试可以看出nohup进程当前正在运行,且PID和PGID是一样的,自成进程组,它所属的会话是15318(属于bash),此进程依旧是在本会话内部,并非是守护进程,但是已经很接近了。使用nohup命令可以让此进程不受用户退出和登陆的影响,已经是后台进程。其实也算是守护进程了。如下我们退出linux,重新登陆,再次执行刚才的ps命令:

< Linux > 守护进程_第15张图片

你可能感兴趣的:(Linux,网络,linux,守护进程)