SSH连接原理与守护进程实战

通过前面的大致框架+udp+tcp的学习,我们已经大致了解了整个网络是如何搭建的

本篇章将通过网络的视角重新认识一下我们经常使用的软件xshell

目录

通过网络重识shell

编写一个自己的守护进程

总结


通过网络重识shell

我们重新回顾之前的一个小点,再次理解xshell

我们在腾讯或者阿里云上租用的云服务器,只要启动起来就默认启动一个SSH服务程序(通常是sshd)sshd是一个守护进程,后台运行,它是默认监听一个22端口的,xshell是一个本地程序,xshell支持ssh协议,所以通过xshell连接云服务器就是公网ip+端口访问,本质相当于两个进程互相通信,看到同一份资源(网络资源),ssh协议是应用层协议

这里可以看到我的xshell上面需要配置连接哪个公网ip,协议使用的是SSH,端口号是22

|---|---------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
| 1 | sshd 主进程持续监听 22 端口,等待连接请求 | Xshell 发起 TCP 连接请求,目标是云服务器公网 IP:22 |
| 2 | 云平台 NAT 网关将公网 IP 请求转发到服务器内网 IP:22,sshd 主进程接收到连接 | - |
| 3 | sshd 主进程调用 fork() 创建 sshd 子进程(继承主进程的网络连接) | - |
| 4 | sshd 子进程与 Xshell 进行 SSH 加密握手:1. 协商加密算法(如 AES)2. 身份验证(密码 / 密钥校验)3. 建立加密通信通道(完成协议的初始化,后续协议进程好加密解密) | Xshell 响应加密握手,发送账号密码 / 密钥进行验证 |
| 5 | 验证通过后,sshd 子进程调用 fork后,exec() 系统调用:- 替换sshd的子进程的子进程的程序镜像为用户默认 Shell(如 bash)- 进程权限从 root 降为登录用户权限 (如 ubuntu) | Xshell 显示服务器的终端提示符(如 ubuntu@server:~$),连接建立完成 |

执行了三次握手
会话本质就是一个客户端,主进程sshd 时刻 listen,来一个客户端就建立三次握手,三次握手完之后accept获取,然后创建一个新的sockfd给这个客户端和云服务器通信,然后fork(),sshd的子进程继承了父进程sshd的文件描述符,继承完之后父进程sshd就会删掉这个accept上来的fd,sshd主进程不需要处理数据传输,保留fd会浪费系统资源;(这里不明白的看我的Linux系统编程章节),子进程进行身份验证等等功能,验证完后初始化SSH加密协议的一些字段(后续作为协议层处理协议的加密解密)+创建子进程,exec(bash)替换程序,sshd的子进程和bash通过管道进行通信,sshd的子进程直接操控sockfd的接收和发送缓冲区,加密和解密,后续通过管道让bash拿到的数据是明文的,只担心处理命令的逻辑即可,bash会修改标准输入输出关联到这个管道。

比如命令ls执行,客户端与云服务器之间经过这次建立好了连接,后续就是客户端和sshd子进程进行通信,ls执行,然后经过SSH协议加密,加密之后传输到了sockfd,此时sshd子进程通过sockfd读取到数据,然后解密,解密之后写进管道,bash直接从标准输入的接收缓冲区(因为之前重定向了)拿到解密的ls,之后fork创建bash子进程,exec(ls)程序替换执行ls,返回结果给bash,bash把结果经过管道交给sshd子进程加密,放到标准输出的发送缓冲区,客户端的接收缓冲区收到后进行解密然后呈现在标准输出(屏幕)

注意:外部命令通过 fork+exec 执行,内置命令(如 cd、export)直接在 Bash 主进程执行

再来一个客户端就在创建一个子进程在程序替换成bash程序(重复执行上述流程)
这期间来一个ls,vim,sleep我们都叫作业(bash 对 "当前会话中启动的进程 / 进程组" 的统称)

前台任务:占用 bash 的标准输入 / 输出(管道),必须等它执行完,bash 才能接收新命令(比如vimls);

后台任务:不占用 bash 的标准输入,在后台运行,bash 可立即接收新命令(比如sleep 100 &)。

一个会话同一时间只有一个前台任务,可以有多个后台,本质就是你只有一个标准输入标准输出,来一个任务bash就fork+exec来执行
jobs:查询作业号

ctrl+z:暂停任务

fg:把后台任务切换到前台

bg:把暂停任务切换到后台

先执行sleep,然后vim,vim的时候ctrl+z(暂停前台任务),后面jobs查询,可以看到状态,一个sleep后台一直再跑,前台暂停了,此时fg作业号

又回到了vim的界面,说明此时回到前台任务

注意:如果关闭终端,断开连接,此时所有的任务都会中断
守护进程:是脱离终端、在后台长期运行的系统级进程 ,完全独立于 SSH/Shell 会话 ------ 哪怕关闭所有 SSH 连接,守护进程依然会运行,这是它和普通后台任务(sleep 100 &)最核心的区别,并且不受bash控制,系统进程控制PID=1,自成会话自成进程组

普通后台任务:关闭终端,直接就结束了,生命周期和bash同步
注意:创建的守护进程不能是组长

比如bash,他去fork+exec(ls),那这个ls的组长是bash,那后面bash不能单独拉出来成为守护进程等

假设如果允许,那ls究竟是跟谁,bash成为了新的组的组长,ls要过去吗还是属于原来的组,如果两个都可以,那信号发送的话是两个都发送?

所以这里不允许组长再去创建守护进程

编写一个自己的守护进程

setsid():

作用:创建新会话,脱离原终端控制,成为新会话首进程(核心:脱离终端依赖)

由于我们一开始通过./test,允许程序,这个交给bash去创建子进程然后进行exec(test)允许,我们可以在其中调用setsid使其脱离bash的管理,成为守护进程

注意不能是组长即可
编写守护进程的时候需要忽略异常的信号,守护进程的核心特性是 "脱离终端 / SSH 会话",但内核仍可能向其发送终端相关信号(比如误判进程关联终端),忽略这些信号能杜绝 "无意义的终止",守护进程是 "系统级后台进程"(如sshd/nginx),其生命周期应由显式指令kill/systemctl stop)管理,而非随机的异常信号

基本是忽略SIGHUP,SIGPIPE等

忽略信号的系统调用接口:signal()


/proc/:此目录是系统进程级目录,一个伪文件系统,实际并不在硬盘当中存储,是内核映射出来的,方便你通过文件接口访问内核数据

打开之后进入一个进程看,里面有个cwd,指向进程当前工作目录(Current Working Directory)的符号链接

对于我们之前别写的程序,为什么open不存在时创建是在当前目录下,因为cwd中记录了一个路径,会使用这个路劲创建

这个进程可执行路径发送更改你可选或者不可选都行

使用chdir函数


/dev/null:

这个是 Linux 系统中的 **/dev/null设备文件 **,它是一个特殊的 "空设备",核心作用是接收任何写入的数据并直接丢弃,读取它则返回空 。这是一个字符设备文件(和磁盘文件、目录的类型不同,是内核提供的虚拟设备);

我们后面编写的守护进程是后台进程,跟终端没关系,所以我们需要把标准输入输出重定向到这个设备,相当于丢弃

如果不重定向,直接关闭,有些printf函数都是默认打印到0/1/2的,可能会阻塞

进程分配时会从低到高,所以read可能会占用0,后面你输入输出函数往里面写就不对了(数据污染)
思路:

第一步先忽略掉一些影响守护进程的信号

第二步fork创建子进程(虽然说你的进程的组长是bash,可以直接setsid自成组长)

风险:

a. 在你setsid之前仍属于bash子进程,bash可能误发信号让你再调用setsid之前挂掉(可以先fork,然后直接exit父进程,此时fork之后的子进程就可以避免信号残留风险)

b. 有时候不是让bash启动,而是系统直接启动,此时你进程就是组长,那调用setsid就会失败,只有fork出来的子进程能够保证什么时候都不会是组长

c. 直接 setsid ():你的进程会成为 "新会话的首进程",若代码中误操作打开/dev/tty(比如日志输出到终端)这个设备不是dev/null,(如果是首进程打开时内核会强制分配一个终端,如果不是首进程会打开失败)内核会让该进程重新关联终端(违背守护进程 "脱离终端" 的核心诉求);

总结:两个fork能够规避所有情况, 第一次 fork+setsid () 让子进程成为会话首进程,第二次 fork () 让孙子进程成为 "非会话首进程"------ 即使打开/dev/tty,也不会关联终端,彻底杜绝风险。(第一次fork是防止组长,第二次fork是防止成为会话首进程)

一般来说生成环境是两次fork,当然如果你能保证代码不关联dev设备你也可以一次

第三步守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件

第四步可选:进程执行路径发送改变

总结:本次编写我们使用两次fork+重定向

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <cstring>
#include <sys/stat.h>
#include <fcntl.h>

// 编写成一个函数?或者类进行管理(但好像也不需要管理啥?后面可以自己改)

#define DEV "/dev/null"  
void daemon(const char*path=nullptr)
{
    // 第一步屏蔽信号
    signal(SIGHUP, SIG_IGN);  // 忽略SIGHUP信号
    signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号

    // 第二步,创建子进程
    pid_t pid = fork();
    if (pid < 0){
        std::cout << "fork error:" << errno << strerror(errno) << std::endl;
        exit(1); // 退出
    }
    if (pid > 0){
        // 此时是父进程,直接关掉
        exit(0);
    }

    // 走到这里绝对是创建出来的子进程
    if (setsid() < 0)
    {
        // 创建失败
        std::cout << "setsid error:" << errno << strerror(errno) << std::endl;
        exit(1);
    }
    // 这里创建成功,我们选择两次次fork
    pid = fork();
    if(pid<0){
        std::cout << "fork error:" << errno << strerror(errno) << std::endl;
    }
    if(pid>0){
        // 父进程直接退出
        exit(0);
    }
    //这里绝对不是首进程,避免终端tty的影响

    // 第三步把关联的文件描述符关闭
    int fd = open(DEV, O_RDWR);
    if(fd>=0){
        //打开成功,重定向到这个设备
        dup2(fd, 0);//先关闭0,然后让0指向fd对应的设备
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
    else{
        //打开失败,直接关掉
        close(0);
        close(1);
        close(2);
    }

    if(path){
        //要求修改路径
        chdir(path);
    }
}

使用ps可以查询进程相关信息

可以看到本次守护进程时第一条,第二条是我们执行ps这条命令

第一条中它的ppid是1,说明是孤儿进程,已经被内核接管了,PID和PGID不是同一个说明不是组长,并且终端没有打印消息,说明以后台守护进程一直再运行,TTY是?已经说明是脱离终端了

重定向到当前文件下,然后查看当前文件是否有helloworld

没问题直接重定向过去了

总结

辨析shell、ssh、bash呢???

Shell 是「操作系统的命令解释器总称」,BashShell 的一种(最常用),SSH 是「远程通信协议 / 工具」,它依赖 Bash/Shell 完成远程命令的解析和执行。

如果你在云服务器本地直接敲命令,那就是由bash直接执行你的命令,解析你的命令

如果你是远端,那就是通过SSH协议加密后传输到远端,远端解析后给bash执行

Shell 是 "人机交互的命令解释器抽象层",Bash 是它最常用的具体实现;SSH 是 "加密的远程通信工具",它通过调用远程服务器的 Bash/Shell,把 "本地输入的命令" 转化为 "远程内核的执行动作"。

为什么bash需要创建子进程去执行命令呢???

Bash 作为「命令解释器」,需要保证自身 "不被替换、不退出、能持续接收新命令"------ 如果 Bash 直接执行命令(用 exec 替换自身),执行完命令后 Bash 就会消失,无法继续交互;而 fork 子进程执行命令,既能完成命令执行,又能保留 Bash 主进程的 "控制权"

如果直接exec替身,每个命令的执行可能修改「工作目录、环境变量、文件描述符」等,如果 Bash 直接执行,这些修改会污染自身

  • Bash 是 "公司老板",外部命令是 "员工"------ 老板不会自己去干活(否则公司没人管),而是派员工(子进程)去干,干完后员工离职,老板继续管公司;
  • 内置命令是 "老板自己的事"(如调整公司地址、制定规则)------ 必须老板自己做,交给员工做没用。

至此关于shell的所有认知我们进行了重构,后续的学习会更加清晰

sshd 本身就是典型的守护进程,其运行机制与我们手动开发的守护进程一致 ------ 脱离终端、后台长期运行、由 PID=1 进程接管

相关推荐
食咗未6 小时前
Linux lrzsz文件传输工具的使用
linux·测试工具
HalvmånEver6 小时前
Linux:基于匿名管道创建出简易进程池(进程间通信五)
linux·运维·服务器·c++·进程池·管道pipe
工程师老罗6 小时前
龙芯2k0300 U盘烧录Linux系统,从Ubuntu到PMON自动启动
linux·运维·ubuntu
DeepFlow 零侵扰全栈可观测6 小时前
民生银行云原生业务的 eBPF 可观测性建设实践
运维·开发语言·分布式·云原生·金融·php
梦想的旅途26 小时前
关于企业微信外部群主动调用的RPA技术实现
运维·自动化
Ronin3056 小时前
【Linux网络】基于Reactor反应堆模式的高并发服务器
linux·网络·reactor·epoll·非阻塞·et模式·高并发服务器
胖咕噜的稞达鸭6 小时前
库的原理和制作 动态库如何和可执行程序相关联,为什么程序入口点不是main函数,GOT表,PIC地址无关代码(2)
linux·c语言·开发语言·网络
a41324476 小时前
ubuntu25安装deepseek32b量化版
linux·运维·服务器·ubuntu
m0_612591976 小时前
大型企业服务器托管选型指南:尚航科技的综合优势与适用场景分析
运维·服务器·科技
skywalk81636 小时前
JWT_SECRET 是 JSON Web Token (JWT) 的密钥,用于服务器生成令牌和验证令牌
运维·服务器·json