掘金 后端 ( ) • 2024-04-20 10:48

xshell原理

可能我们都在使用xshell时,都会遇到一些问题,就是你在xshell运行了你的服务器。可是你把xshell页面一关,你的服务器就自动关闭了,这是为什么呢??

本质是因为我们的xshell在登陆服务器时,会创建一个会话,而在这个会话中,只能允许一个进程在前台运行,多个进程在后台运行。而会话退出时会话的内容也会跟着退出,因为会话的内容都是以bash为父进程创建的。我们在命令行输入执行的进程,都是以bash为父进程创建的。

image.png

我们还要明白一个进程组的概念,我们可以分别执行 sleep 10000 | sleep 20000 | sleep 30000 &sleep 40000 | sleep 50000 | sleep 60000 & 命令,会建立6个睡眠的进程,而&的意思是在后台中运行。然后我们输入jobs,可以发现有2个进程组。

image.png

我们在查看进程,会发现有2个进程组,且进程组id和进程id相同的进程即为进程组组长。

image.png

而这个2个进程组组长都是由bash创建的。举个例子:

bash 就是大老板,它给了A一大笔钱,让A自己找几个人帮他完成一些任务。随后A就找了自己的兄弟一起给大老板bash干活。然后bash又找到B,给了B一大笔钱,让B自己组个队伍去帮他完成另一个任务。随后B也拉上了自己的兄弟们一起为大老板bash干活。

这里的大老板就是bash,A就是进程组组长,B是另一个进程组的组长。而它们都是为bash干活,而一旦bash退出,那么它们也自然会跟着退出。因为老板都挂了,进程组们肯定原地解散了。

而我们发现当命令行处于前台状态在运行进程时,就无法在使用命令行了。这本质的原因就是因为一个会话最多只允许一个进程在前台运行。

那么我们怎么让A和B不受会话的限制,即使会话退出了也不会影响它们运行呢?

很简单,我们让进程组组长不要替别人干活啦!自己出去单干,那么自己就是老大!让它们自己出去自成终端!自成进程组!自成会话!除非用户自己关闭或者操作系统挂了,那么它们就不在受到会话的影响。即使会话退出,它们依旧可以运行。这种进程我们把它称为守护进程 ,也可以叫做精灵进程 ,本质上还是一个孤儿进程

image.png

需要注意的是,想要自己出去单干,那么自己必须当老大。也就是说自己必须成为进程组的组长。

创建守护进程

上面我说过,守护进程的本质还是个孤儿进程,那么就意味着它必定会被操作系统领养。

实现守护进程我们可以分三个重要步骤,若干个小步骤。

1. 让调用的进程忽略掉异常信号

比如SIGPIPE信号,如果现在我要把服务器自身独立成为会话。那么要忽略掉这个信号,避免客户端搞事(在服务器读取时客户端关闭,服务器会收到SIGPIPE信号)。

2. 自成进程组

有一个setsid函数,可以让自己自成进程组,并自身处于新会话内,也就是说脱离了bash的掌控之中了!但是有个条件!那就是调用的进程本身不能是进程组! ,否则会调用失败!只要fork一下,那么fork出来的子进程就绝对部署进程组组长。这时候再把父进程立马退出,让子进程执行setsid。即可让子进程脱离bash的魔掌!

该函数调用成功返回进程 的pid,调用失败返回-1.

#include <unistd.h>
pid_t setsid(void);

手册对函数的说明:

image.png

3 .关闭或重定向进程默认打开的文件

如果我的服务器有大量的输出内容,如果不关闭默认打开的文件的话。那么可能会占用系统的IO资源。而在/dev/null 路径的null文件。是一个“黑洞” ,无论你往里面读还是写。都不会发生什么,可以认为是个垃圾桶,但是在垃圾桶里面还能捡到东西,而你这里面你读不到消息,写进去的消息也没有反应,所以我愿称之为"黑洞"。 我们可以直接关闭标准输入输出错误,但不建议,建议还是重定向到 /dev/null中。

4. 进程的执行路径发生更改(非必须)

我们都知道进程所在的目录路径是会保存在cwd中的,所以程序里 open一个文件如果不用绝对路径,那么自动从当前路径开始找,这是因为当前路径其实已经被进程保存在cwd中了。如果想要修改也是可以的,但是这并不是必须的。

接下来我们用代码来实现一下:

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

#define DEV "/dev/null"

void daemonSelf(const char* currpath = nullptr)
{
    //1.屏蔽信号
    signal(SIGPIPE,SIG_IGN); 

    //2.自成进程组
    if(fork() > 0 ) exit(0); //父进程秒退
    //子进程自成进程组
    pid_t ret = setsid(); 
    assert(ret != -1); 

    //3.关闭默认打开的文件
    int fd = open(DEV,O_RDWR); 
    if(fd < 0)
    {
        //文件打开失败,那么就关闭标准输入/输出/错误
        close(0);
        close(1);
        close(2);
    }else{
        //替换掉标准输入输出错误
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
    }

    //4. 路径修改
    if(currpath) chdir(currpath);
    close(fd);
}

那么接下来我们写个程序验证一下。

#include "deamon.hpp"
#include <iostream>
#include <string>
#include <unistd.h>

int main()
{
    daemonSelf(); //独立进程组
    FILE* testTxt = fopen("test.txt","a"); 
    int i = 0;
    while(1)
    {
        std::string msg = "hello" + std::to_string(i);
        fprintf(testTxt,msg.c_str());
        sleep(1);
    }

    return 0 ;
}

我们会发现进程一执行就结束了,可是我们明明写的是死循环啊。

image.png

这时候我们关掉vscode终端,打开另一个终端,查看test进程。

image.png

我们发现test进程已经脱离bash掌控了,也就意味着我们的程序可以24小时在服务器上运行了!