深入了解linux网络—— 守护进程

一、进程组

学习过进程,我们知道进程有进程PID、状态等这些属性;

每个进程除了有自己的PID之外,每个进程还属于一个进程组。

进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。

进程组中也可能只存在一个进程

每一个进程组还存在一个唯一的进程组idPGID

组长进程:

每一个进程组都存在一个组长进程,进程组PGID就等于组长进程的PID

二、会话

1. 什么是会话

了解了进程组,那会话又是什么呢,二者又存在什么关系呢?

在刚看到进程信息中,处理父进程id、进程id、进程组id外,还存在一个信息SID;而SID就是会话id

会话 :就是一组进程为了完成同一项任务而组成的"班组",系统用它来做权限隔离、资源统计和终端管理。

在刚才查询到到会话id5960,且sleep 100的父进程id也是5960,那5960是什么呢?

可以看到5960bash进程。

  • 在登录shell时,操作系统会为我们创建一个bash进程,同时也会创建一个新会话(session
  • 每一个会话都有唯一的SID

2. 创建会话

简单了解了什么是会话,那我们可不可以通过代码(系统调用)来创建一个新的会话呢?

当然是可以的,我们可以调用setsid来创建一个独立的会话;调用setsid的前提是调用进程不是一个进程组组长。

c 复制代码
       pid_t setsid(void);
  • 调用进程会变成新会话的会话首进程;
  • 新会话中只有唯一的一个进程,就会变成进程组组长;新进程组的ID就是当前调用进程的ID
  • 该进程没有控制终端,如果调用setsid之前存在控制终端,则调用之后会切断联系。

注意:调用setsid的进程不能是进程组组长,通常情况下,先调用fork创建子进程,再让子进程调用setsid,父进程直接退出。

三、控制终端

控制终端是这一切的"调度中心",信号、输入、输出都通过它分发。

  1. 会话(session)
    一次"登录"就诞生一个会话,内核用 session ID 标记所有相关进程。
    会话首进程(session leader)是第一个打开该终端的进程,PID 即成 SID。
  2. 控制终端(controlling terminal)
    一旦终端设备被会话首进程打开,它就升级为整个会话的控制终端 ,信息记录在每份 PCB 里,fork 时自动继承。
    因此后代进程无论多少层,缺省的 stdin/stdout/stderr 都指向同一终端。
  3. 控制进程(controlling process)
    就是"会话首进程"本人;终端断开时,内核把 SIGHUP 发给它,由它负责清理整个会话。
  4. 进程组(process group)
    会话内部再细分为若干进程组,每个组有唯一 PGID,方便一次性信号投递。
    任意时刻只有一个组是前台进程组,其余全是后台进程组。
  5. 前台/后台规则
    • 终端输入只送给前台组;
    • Ctrl-C 产生 SIGINT、Ctrl-\ 产生 SIGQUIT,只发给前台组全体成员
    • 后台组若尝试读终端,会被 SIGTTIN 暂停;写终端则可能被 SIGTTOU 暂停(取决于 tostop 设置)。
  6. 挂断与回收
    调制解调器掉线、网络 SSH 断连、窗口关闭,终端驱动检测到载波丢失,立即向控制进程 发 SIGHUP;
    会话首进程收到后通常终止,内核随之向会话内所有进程再广播 SIGHUP,实现"一断全清"。

简单来说就是:"登录→创建会话→打开终端→终端成控制终端→首进程成控制进程→会话分前台/后台组→终端只理前台组→断线先杀控制进程→会话全灭。

四、作业控制

1. 作业

作业(job) 就是一次在 shell 命令行上提交给内核的一个或一组相连进程"

例如,这里启动的一个进程组,它就是一个作业。(启动的一个进程,也是一个进程组,也是一个作业)。

作业控制,简单来说就是:

使用用 Ctrl-Zbgfg 等命令,把正在跑的进程组(作业)随时暂停、扔进后台或再拉回前台继续运行的一套终端多任务切换机制。

2. 作业号

放在后台运行的程序,称之为后台进程。例如在启动程序时带上 &,让它在后台执行:

可以看到,启动sleep 1000 &后,获得了[1] 6512,一个是作业号、一个是进程id。

使用jobs命令可以查看所有后台作业。

使用jobs不仅可以查看到作业号,我们还能发现存在一个Running字段;它表示进程正在运行。

常见的作业状态:

除此之外,在[1]后面还存在一个+,这个表示什么意思呢?

多启动一些进程,我们会发现,除了存在一个+之外,还存在一个-

默认作业:在使用fg命令,不指明作业号时,就会将默认作业变成前台作业

  • + : 表示该作业默认作业
  • - : 表示该作业即将称为默认作业
  • 其他作业

当默认作业变成前台进程、或者执行结束后,带-的作业就会变成默认作业。

3. 作业挂起与切回

这里作业挂起和切回,和之前前台进程和后台进程一样。

作业挂起

当一个作业正在前台运行,键盘按下Ctrl + z,当前作业就暂停,并且变成后台作业。

前台进程获取键盘输入,不能被暂停。

作业切回

要将一个后台作业变成前台作业,就要使用fg命令了。

把"后台/已暂停"的作业重新切到前台运行,并把它对应的进程组绑定为终端的当前前台进程组。

使用格式 : fg 作业号

注意:如果直接使用fg命令,不跟作业号就会将默认作业切到前台运行。

4. 查看后台作业

要查看后台作业要使用的命令:jobs

  • -l : 显示后台作业的所有信息。
  • -p : 只查看作业的pid

5. 相关信号

这里键盘按下Ctrl + Z,将前台作业挂到后台,本质上是通过信号SIGTSEP完成的。

Ctrl + C就是像向台作业发送SIGINT信号。

Ctrl +\就是向前台作业发送SIGQUIT信号。

五、守护进程

简单了解了进程组、会话和作业;现在来看什么是守护进程。

首先,对于我们之前所实现了UDPTCP通信,在服务器上所部署的服务,它都是属于bash会话的;

bash会话是登录Shell时,操作系统自动给我们创建的,当退出Shell时,bash会话就退出了,我们所部署的服务也就终止了。

但是,我们想要的服务是一种存在的,不会受到我们的登录和退出的影响。

守护进程(daemon)就是脱离终端、在后台长期运行 、专门负责某项系统任务的进程。

简单来说就是:常驻后台、无终端、为系统/用户提供持续服务

所以,对于我们之前实现的Tcp通信,server服务要守护进程化。

要将一个进程变成守护进程,要进行以下处理:

  1. 忽略信号
  2. 创建新会话
  3. 修改工作路径
  4. 输入输出重定向

1. 忽略信号

对于一个守护进程,不希望因为某些信号,导致进程退出;

这里就要忽略掉某些信号,避免因为信号导致进程异常退出。

这里像SIGCHLDSIGPIPE信号都要忽略掉。(这里就简单忽略掉这两个信号)

cpp 复制代码
void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
}

2. 创建新会话

对于一个守护进程,它不希望受到我们Shell登录和退出的影响,所以就要让该守护进程成为一个新的会话。

调用setsid可以创建一个新会话,调用进程成为会话首进程。

注意:调用setsid的进程,不能是进程组组长。

所以,调用setsid,要先创建子进程,让子进程去调用setsid,父进程退出。

这样子进程就变成了孤儿进程,被操作系统领养,进程退出时自动回收。

守护进程也是孤儿进程

cpp 复制代码
void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
    // 2. 创建新会话
    if(fork() != 0)
        exit(0);//成功父进程退出、创建子进程失败也直接退出
    setsid();
}

3. 修改工作路径

这里可能会感觉到很奇怪,为什么要修改工作路径呢?

守护进程要进行输入输出重定向到/dev/null,所有输出信息都被丢弃了;

而我们想要知道该服务的运行情况,就只能去看日志。

而将服务的工作路径修改到/中,这样就让服务以绝对路径的方式写入日志信息

防止它偶然占用任何可能后来被卸载的文件系统,导致管理员无法换盘、无法升级、甚至无法关机

修改工作路径,直接调用chdir即可:

cpp 复制代码
const char* root = "/";

void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
    // 2. 创建新会话
    if(fork() != 0)
        exit(0);//成功父进程退出、创建子进程失败也直接退出
    setsid();
    // 3. 修改工作路径
    chdir(root);
}

4. 输入输出重定向

在守护进程中,可能存在从标准输入中获取、向标准输出、标准错误中输出数据;

这里就可以将进程的012(标准输入、标准输出、标准错误)重定向到/dev/null中。

/dev/null文件:

  1. 写进去就消失------返回成功,但数据立即丢弃,磁盘不增一寸。
  2. 读出来永远 EOF------立即返回 0 字节,不阻塞、不等待。

这样进程输出信息就会被丢弃,从标准输入中获取为空。

cpp 复制代码
const char* root = "/";
const char* dev_null = "/dev/null";
void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
    // 2. 创建新会话
    if(fork() != 0)
        exit(0);//成功父进程退出、创建子进程失败也直接退出
    setsid();
    // 3. 修改工作路径
    chdir(root);
    // 4. 重定向
    int fd = open(dev_null,O_RDWR);//以读写方式打开
    if(fd > 0)
    {
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
    }
}

5. 系统调用daemon

这里我们自己简单实现了一个Daemon,让进程守护进程化。

当然,也存在现成的daemon供我们使用:

这里daemon存在两个参数:

nochdir

简单来说就是:如果传递0,就将进程的工作目录修改为/

noclose

简单来说:如果传递0,就表示将标准输入、输出和错误重定向到/dev/null

否则就只关闭标准输入、输出和错误。

六、服务守护进程化

对于之前实现了Tcp网络版本计算器,这里将它守护进程化:

(这里就不修改工作路径了)

详细代码:linux: linux学习

cpp 复制代码
//tcpserver.cc
int main(int agrc, char *argv[])
{
    if (agrc != 2)
    {
        std::cout << "err usage : " << argv[0] << " port" << std::endl;
        exit(1);
    }
    int port = std::stoi(argv[1]);
    daemon(1,0);// 不修改工作路径 输入输出重定向
    //Daemon();
    ENABLEFILE();
    NetCal cal;
    std::unique_ptr<Protocol> pro = std::make_unique<Protocol>([&cal](Request &res) -> Responce
                                                               { return cal.Handler(res); });
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&pro](std::shared_ptr<Socket> fd, InetAddr &client)
                                                                  { pro->GetRequest(fd, client); });
    tsvr->Start();
    return 0;
}

此外,守护进程通常以d结尾

这里也进行简单修改,编译形成tcpserverd可执行程序。

此时,我们再使用客户端去访问服务器,也是能够获得回应的。


本篇文章到这里就结束了,感谢支持

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

相关推荐
wheeldown7 小时前
【Linux】从内存布局到信号屏蔽:Linux 内核态与用户态交互核心知识点汇总
linux·运维·服务器
落羽的落羽7 小时前
【Linux系统】C/C++的调试器gdb/cgdb,从入门到精通
linux·服务器·c语言·c++·人工智能·学习·机器学习
张彦峰ZYF7 小时前
高频面试题(含笔试高频算法整理)基本总结回顾5
linux·运维·服务器
liuccn7 小时前
Ubuntu 22.04 离线升级 OpenSSH 到 9.8p1
linux·ubuntu·github
我是Feri7 小时前
HarmonyOS6.0开发实战:HTTP 网络请求与 API 交互全指南
网络·http·harmonyos·openharmonyos·harmonyos6.0
HaiLang_IT7 小时前
2026届 网络与信息安全专业毕业设计选题推荐与指导(含热门研究方向)
网络·安全·信息安全
DO_Community7 小时前
裸金属 vs. 虚拟化 GPU 服务器:AI 训练与推理应该怎么选
运维·服务器·人工智能·llm·大语言模型
徐子元竟然被占了!!7 小时前
Linux的df和du
linux·运维·服务器
集大周杰伦7 小时前
Linux网络编程核心实践:TCP/UDP socket与epoll高并发服务器构建
linux·tcp/ip·网络编程·socket·字节序·套接字·i/o多路复用