【Linux网络加餐(二)】《进程组,会话,控制终端,作业控制和守护进程》---详解

一.进程组

1.什么是进程组

之前我们提到了进程的概念, 其实每⼀个进程除了有⼀个进程ID(PID)之外 还属于⼀个进程组

进程组是⼀个或者多个进程的集合, ⼀个进程组可以包含多个进程。 每⼀个进程组也有⼀个唯⼀的进程组ID(PGID), 并且这个PGID类似于进程ID, 同样是⼀个正整数, 可以存放在pid_t数据类型中。

我们来验证看一下:

bash 复制代码
sleep 100000 | sleep 200000 | sleep 300000

**管道 | 的含义:把前一个命令输出交给下一个命令;管道会创建子进程,每个 sleep 都是独立进程管道 sleep a | sleep b | sleep c:**同时启动 3 个 sleep,并行运行,不是挨个等。。

  1. PPID 全部是 3940927 :这个是你当前 shell(bash),三个 sleep 全都是 shell 直接创建的子进程
  2. PGID 进程组 ID 统一等于第一个 sleep 的 PID (3969646)
  3. shell 执行管道命令时,会把整条管道所有进程放到同一个进程组,管道第一个进程 PID 作为整个组的 PGID(组长)。

写个代码验证一下:


看指令:

bash 复制代码
ps -eo pid,pgid,ppid,comm | grep process_task
# -e 选项表⽰every的意思, 表⽰输出每⼀个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列

用处 :专门看某个进程的:自己 PID、进程组 ID、父进程 ID,快速判断谁是进程组组长(PID==PGID 就是组长)。

2.组⻓进程

每⼀个进程组都有⼀个组⻓进程。组⻓进程的ID等于其进程ID。我们可以通过ps命令看到组⻓进程的现象:

bash 复制代码
 ps -o pid,pgid,ppid,comm | cat
  1. 只输出 4 列:PID、PGID、PPID、程序名 comm;不加 -e,只显示当前终端的进程(不是系统全部进程)。
  2. | cat 管道,把 ps 输出交给 cat;cat 就是原样打印输入。

从结果上看ps进程的PID和PGID相同 , 那也就是说明ps进程是该进程组的组⻓进程, 该进程组包括ps和cat两个进程。

  • 进程组组⻓的作⽤: 进程组组⻓可以创建⼀个进程组或者创建该组中的进程
  • 进程组的⽣命周期: 从进程组创建开始到其中最后⼀个进程离开为⽌。

注意: 主要某个进程组中有⼀个进程存在, 则该进程组就存在, 这与其组⻓进程是否已经终⽌⽆关。


二.会话

1.什么是会话

刚刚我们谈到了进程组的概念, 那么会话⼜是什么呢? 会话其实和进程组息息相关, 会话可以看成是⼀个或多个进程组的集合, ⼀个会话可以包含多个进程组。每⼀个会话也有⼀个会话ID(SID)

通常我们都是使⽤管道将⼏个进程编成⼀个进程组。 如上图的进程组2和进程组3可能是由下列命令形成的:

cpp 复制代码
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表⽰将进程组放在后台执⾏

举个例子看指令:

cpp 复制代码
# ⽤管道和sleep组成⼀个进程组放在后台运⾏
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &
# 查看ps命令打出来的列描述信息
[node@localhost code]$ ps axj | head -n1
# 过滤sleep相关的进程信息
[node@localhost code]$ ps axj | grep sleep | grep -v grep
# a选项表⽰不仅列当前⽤⼾的进程,也列出所有其他⽤⼾的进程
# x选项表⽰不仅列有控制终端的进程,也列出所有⽆控制终端的进程
# j选项表⽰列出与作业控制相关的信息, 作业控制后续会讲
# grep的-v选项表⽰反向过滤, 即不过滤带有grep字段相关的进程

sleep 100 | sleep 200 | sleep 300 & 整条管道,sleep 100 是进程组组长,三个 sleep 属于同一个进程组,&放到后台。

ps axj | head -n1 打印 ps axj 的表头,能看到 PID PGID SID这些列。

ps axj | grep sleep | grep -v grep过滤出 sleep 进程;grep -v grep把 grep 自身进程过滤掉,只看 sleep。

看结果:

三个进程 PGID 全部等于 sleep 100 的 PID (4002066) ,证明整条管道所有进程属于同一个进程组,组长是管道第一个命令


2. 如何创建会话

可以调⽤setseid函数来创建⼀个会话, 前提是调⽤进程不能是⼀个进程组的组⻓。

cpp 复制代码
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);

该接⼝调⽤之后会发⽣:

  • 调⽤进程会变成新会话的会话⾸进程。 此时, 新会话中只有唯⼀的⼀个进程
  • 调⽤进程会变成进程组组⻓。 新进程组ID就是当前调⽤进程ID
  • 该进程没有控制终端。 如果在调⽤setsid之前该进程存在控制终端, 则调⽤之后会切断联系

需要注意的是: 这个接⼝如果调⽤进程原来是进程组组⻓, 则会报错, 为了避免这种情况, 我们通常的使⽤⽅法是先调⽤fork创建⼦进程, ⽗进程终⽌, ⼦进程继续执⾏, 因为⼦进程会继承⽗进程的进程组ID, ⽽进程ID则是新分配的, 就不会出现错误的情况。

  • PID = PGID = SID = 4004044 子进程调用setsid()之后,自身成为新会话的会话首进程,同时也是新进程组的组长进程。新会话内目前仅有这一个进程。
  • TTY 列为 ? 进程已经脱离原来的控制终端,不再和启动它的终端绑定。此时即使关闭原来的终端窗口,该子进程依然可以继续在后台运行。
  • PPID 为 1 父进程执行完毕退出,子进程被 systemd (init 进程) 收养。

3.会话ID(SID)

会话ID(Session ID)是一个用于标识进程所属会话的标识符。简单来说:

  1. 会话首进程:每个会话都有一个领头进程,称为"会话首进程"(Session Leader)。它是一个普通进程,拥有唯一的进程ID(PID)。

  2. 会话ID的定义 :会话ID就是会话首进程的进程ID。因为会话首进程的PID在整个系统中是唯一的,所以用它来作为整个会话的标识符非常合适。

  3. 与进程组ID的关系:在某些资料中,会话ID也被称为"会话首进程的进程组ID"。这是因为:

  • 会话首进程同时也是它所在进程组的组长(Process Group Leader)
  • 进程组组长的PID = 该进程组的PGID(进程组ID)
  • 因此,会话首进程的PID = 会话首进程的PGID = 会话ID
  • 所以两者在数值上是等价

速记:

bash 复制代码
会话ID = 会话首进程的PID = 会话首进程的PGID

三.控制终端

1.什么是控制终端

在 Unix/Linux 系统中,控制终端是与一个会话相关联的终端设备。当用户通过终端(物理终端、SSH 连接或伪终端)登录系统后,系统会为该登录会话分配一个终端设备,这个终端就成为该会话的控制终端。

控制终端的信息存储在每个进程的 PCB(进程控制块,即内核中的 task_struct)中,具体位于 signal->tty 字段,指向对应的终端设备结构体。由于 fork() 会复制父进程的 PCB,因此由 Shell 启动的所有子进程都会继承相同的控制终端。

控制终端的作用:

  • **标准 I/O 关联:**默认情况下(没有重定向),进程的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)都指向控制终端。这意味着程序从键盘读取输入,将输出显示在显示器上。
  • **进程组分类:**在一个拥有控制终端的会话中,进程组被划分为:

前台进程组(Foreground Process Group):唯一拥有终端控制权,可以读写终端;

后台进程组(Background Process Group):不能读取终端(否则收到 SIGTTIN),写入时可能受影响。

  • 信号转发:

按下 Ctrl+C(中断键)或 Ctrl+\(退出键)时,终端驱动程序会向前台进程组中的所有进程发送 SIGINT 或 SIGQUIT 信号;

当终端连接异常断开(如 SSH 断开、网络中断)时,内核会向控制进程(会话首进程)发送 SIGHUP 信号,该信号默认会终止进程,并进一步传递给整个会话。

这些特性的关系如下图所⽰:


四.作业控制

1. 什么是作业(job)和作业控制(Job Control)?

作业是针对用户来讲,⽤⼾完成某项任务⽽启动的进程,⼀个作业既可以只包含⼀个进程,也可以

包含多个进程,进程之间互相协作完成任务, 通常是⼀个进程管道。

Shell分前后台来控制的不是进程⽽是作业 或者进程组。⼀个前台作业可以由多个进程组成,⼀个

后台作业也可以由多个进程组成,Shell可以同时运⾏⼀个前台作业和任意多个后台作业,这称为

作业控制

作业控制的实操:

fg/bg 与信号控制(核心考点)

操作 信号 效果 作业状态 能否恢复
fg 作业号 --- 后台作业切到前台 前台进程组 ---
Ctrl+C SIGINT 终止整个前台进程组 作业被删除 不能恢复
Ctrl+Z SIGTSTP 暂停整个前台进程组 保留,状态 Stopped ✅可 bg/fg 恢复
bg 作业号 SIGCONT 暂停作业转到后台继续 恢复 Running ---
kill -SIGINT %作业号 SIGINT 复现 Ctrl+C 效果 作业销毁 不能恢复

命令对照速查

命令 含义
jobs 查看后台作业列表
jobs -l 查看作业 + PID
fg %2 作业 2 切前台
bg %2 作业 2 放后台运行
Ctrl+C 终止整个前台进程组
Ctrl+Z 暂停整个前台进程组

2. 作业号

放在后台执⾏的程序或命令称为后台命令,可以在命令的后⾯加上 & 符号从⽽让Shell识别这是⼀

个后台命令,后台命令不⽤等待该命令执⾏完成,就可⽴即接收新的命令,另外后台进程执⾏完后

返回⼀个作业号以及⼀个进程号(PID)


3.作业状态

常⻅的作业状态如下表所⽰:


4.作业的挂起与切回

(1) 作业挂起

我们在执⾏某个作业时,可以通过 Ctrl+Z 键将该作业挂起,然后Shell会显⽰相关的作业号、状

态以及所执⾏的命令信息。

例如我们运⾏⼀个死循环的程序, 通过 Ctrl+Z 将该作业挂起, 观察⼀下对应的作业状态:

下⾯我运⾏这个程序, 通过 Ctrl+Z 将该作业挂起:

可以发现通过 Ctrl+Z 将作业挂起, 该作业状态已经变为了停⽌状态。

(2) 作业切回

如果想将挂起的作业切回,可以通过 fg 命令, fg 后⾯可以跟 作业号 或 作业的命令名称 。如

果参数缺省则会默认将作业号为1的作业切到前台来执⾏,若当前系统只有⼀个作业在后台进⾏,

则可以直接使⽤fg命令不带参数直接切回。 具体的参数参考如下:

如我们把刚刚挂起来的 ./process_task 作业切回到前台:

运⾏结果为开始⽆限循环打印 hello , 可以发现该作业已经切换到前台了。

注意:当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执⾏,即带

有"+"的作业号的作业


5.查看后台执⾏或挂起的作业

我们可以直接通过输⼊ jobs 命令查看本⽤⼾当前后台执⾏或挂起的作业

  • 参数 -l 则显⽰作业的详细信息
  • 参数 -p 则只显⽰作业的PID

例如, 我们先在后台及前台运⾏两个作业, 并将前台作业挂起, 来⽤ jobs 命令查看作业相关的

信息:

bash 复制代码
# 在后台运⾏⼀个作业sleep
[node@localhost code]$ sleep 300 &
# 运⾏刚才的死循环可执⾏程序
[node@localhost code]$ ./test
# 键⼊Ctrl + Z 挂起作业
# 使⽤jobs命令查看后台及挂起的作业
[node@localhost code]$ jobs -l

运⾏结果如下所⽰:

6. 作业控制相关的信号

⾯我们提到了键⼊ Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送⾄前台进

程组作业中的所有进程, 后台进程组中的作业不受影响。 在unix系统中, 存在3个特殊字符可以使得终端驱动程序产⽣信号, 并将信号发送⾄前台进程组作业, 它们分别是:

  • Ctrl + C : 中断字符, 会产⽣ SIGINT 信号
  • Ctrl + \ : 退出字符, 会产⽣ SIGQUIT 信号
  • Ctrl + Z :挂起字符, 会产⽣ STGTSTP 信号

终端的I/O(即标准输⼊和标准输出)和终端产⽣的信号总是从前台进程组作业连接打破实际终端。我

们可以通过下体来看到作业控制的功能:

这是 Linux 终端、会话、前台 / 后台进程组、信号的整体工作流程图。

  1. 顶层init系统进程 --> getty --> login --> 登录 shell (bash) 用户登录后,shell 就是这个会话的会话首进程,绑定控制终端。
  2. shell 通过setpgid()创建两类进程组
  • 前台进程组 :当前正在占用终端的一组进程
    • 键盘输入输出直接给到它
    • Ctrl+C / Ctrl+Z产生的信号,发给整个前台进程组
  • 后台进程组 :后台作业(&启动)
    • 不能直接读终端输入,读终端会收到SIGTTIN被暂停;写终端会收到SIGTTOU
  1. 终端驱动是中间枢纽
  • tcsetpgrp():shell 调用这个系统调用,切换哪个进程组是前台 (就是fg/bg命令底层做的事)。
  • 用户键盘操作交给终端驱动,再把信号转发给前台进程组全体进程
  • 后台进程组想读终端,终端驱动抛出SIGTTIN,进程被暂停。
  1. shell 监控子进程状态变化,维护 jobs 作业列表。

总结登:录 shell 管理同一个终端下的前台进程组、后台进程组;终端的输入、键盘信号只作用于前台进程组;后台进程组访问终端会被信号暂停;fg/bg本质就是调用tcsetpgrp切换前台进程组


五. 守护进程

通俗讲:Linux 后台长期运行的服务进程,脱离终端,关闭 ssh / 登录窗口也不会死掉,开机可以自启。

核心特点

  1. 脱离控制终端 :没有终端,ps看 TTY 那一列显示?,关闭终端、退出登录不会被杀死。
  2. 后台静默运行,不和用户交互,专门做服务(比如 nginx、mysql、sshd 都是守护进程)。
  3. 会话、进程组独立,不受 shell 作业控制影响,jobs命令看不到它。

这里面的重点:setsid () 是核心,调用后进程变成新会话首进程,脱离终端,TTY 变成?。

在前面我们也说过这个函数,话不多说来看到代码:

Daemon.hpp

cpp 复制代码
#pragma once 

#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>

const char *root = "/";
const char *dev_null = "/dev/null";

void Daemon(bool ischdir,bool isclose)
{   //1.忽略可能引起进程异常的退出信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);

    //2.让自己不要成为组长
    if(fork()>0)
    {
        exit(0);
    }

    //3.设置让自己成为一个新的会话,后面的代码其实是子进程在走
    setsid();

    //4.每一个进程都哟自己的CWD,是否当前进程的CWD更改为/根目录
    if(ischdir)
    {
        chdir(root);
    }

    //5.已经变成守护进程,不需要用户的输入和输出,错误进行关联了
    if(isclose)
    {
        ::close(0);
        ::close(1);
        ::close(2);
    }
    else
    {
        //这里一般建议两种
        int fd = open(dev_null,O_RDWR);
        if(fd>0)
        {
            dup2(fd,0);
            dup2(fd,1);
            dup2(fd,2);
            close2(fd);
        }
    }

}

process_task.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
#include <cstdlib>
#include"Daemon.hpp"


int main()
{
    Daemon(false, false);

    // 打开日志文件
    int log_fd = open("/tmp/mydaemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (log_fd < 0) {
        exit(1);
    }

    // 主循环:每10秒写一次日志
    while (true) {
        std::string msg = "进程 " + std::to_string(getpid()) + " 还活着\n";
        write(log_fd, msg.c_str(), msg.size());
        sleep(10);
    }

    close(log_fd);
    return 0;
}

Daemon() 函数的目标是:将当前进程变成一个守护进程(Daemon)

第1步:忽略信号

cpp 复制代码
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);

SIGCHLD 信号

  • 产生时机:当子进程终止、暂停或恢复时,内核会向父进程发送 SIGCHLD 信号。

  • 忽略的原因:守护进程通常会创建子进程来处理任务(比如 Web 服务器接收新连接后 fork 子进程处理)。如果不处理 SIGCHLD,子进程退出后会变成僵尸进程(Zombie),占用进程表项。

  • SIG_IGN 的作用:告诉内核"我对这个信号不感兴趣,子进程退出时自动回收资源,不需要通知我"。这是一种简化的处理方式,避免写复杂的信号处理函数。

SIGPIPE 信号

  • 产生时机:当进程向一个已经关闭的 socket 管道写入数据时,内核会发送 SIGPIPE

  • 忽略的原因 :守护进程经常涉及网络通信(比如 HTTP 服务)。如果客户端突然断开连接,服务端继续写入就会触发 SIGPIPE,默认行为是终止整个进程。忽略后,写入操作会返回 -1 并设置 errnoEPIPE,由程序自己处理错误,而不是崩溃退出。

为什么要在守护进程里忽略这两个信号?

守护进程设计原则之一就是稳定可靠,不能因为网络异常或子进程退出而意外终止。


第2步:创建子进程,父进程退出

cpp 复制代码
if (fork() > 0) {
    exit(0);
}

为什么要 fork 一次?

在 Unix/Linux 中,进程组组长(Process Group Leader)不能调用 setsid() 创建新会话。如果直接让一个组长进程调用 setsid(),会返回 -1 失败。

操作 说明
fork() 创建子进程,子进程的 PPID(父进程 ID)是原来的父进程
父进程 exit(0) 父进程立即退出,子进程变成孤儿进程,被 init(PID=1)收养
子进程继续执行 子进程的 PGID 和 SID 仍然是父进程的值,但它不再是组长,因为它是新 fork 出来的,PGID 不等于自己的 PID

此时的状态:

  • 子进程 PID ≠ PGID(不是组长)

  • 子进程可以安全调用 setsid()


第3步:创建新会话

cpp 复制代码
setsid();

setsid() 做了什么?这是最核心的一步,调用后会发生三件事:

效果 说明
创建新会话 当前进程成为该会话的会话首进程(Session Leader),会话 ID = 当前进程的 PID
创建新进程组 当前进程成为该进程组的组长(Group Leader),进程组 ID = 当前进程的 PID
脱离原控制终端 进程不再有任何控制终端,即使它之前有(比如继承自 Shell),也会被切断

此时的状态

bash 复制代码
PID = PGID = SID = 当前进程的PID
当前进程既是进程组组长,也是会话首进程
没有控制终端

第4步:切换工作目录

cpp 复制代码
if (ischdir) {
    chdir(root);  // root = "/"
}

切换到 / 根目录的好处

根目录永远是稳定的,不会因为挂载/卸载而变化

补充: 这是一个可选操作,不是所有守护进程都必须切到 /。有些守护进程需要特定工作目录,比如日志服务可能需要 /var/log。


第5步:处理标准输入/输出/错误

cpp 复制代码
if (isclose) {
    ::close(0);
    ::close(1);
    ::close(2);
} else {
    int fd = open(dev_null, O_RDWR);
    if (fd > 0) {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

:: 前缀 = 我要用全局的那个函数,别用别的同名函数替代我

相当于给编译器一个"强制指定",保证你用的是系统提供的 close 函数

简单记忆:

直接关闭 ,可能被复用 容易出 bug重定向到 /dev/null , 始终安全是标准做法。

为什么要处理这三个文件描述符?

一句话:守护进程在后台运行,没终端可交互。不管的话,万一 printf 或报错写到不存在的终端上,程序会崩。


两种处理方式的核心区别

方式 本质 最大风险
直接关闭 close(0);close(1);close(2); 释放掉 0、1、2 后面 open()socket() 会重复利用这三个号,本来以为是标准I/O的代码可能误操作到真正的文件/网络,造成混乱
重定向到 /dev/null 0、1、2 始终有效,但指向"黑洞" 几乎没有风险,写入被丢弃,读取返回 EOF

**重定向 = 改变数据流向:**本来数据要去 A 地方,现在让它去 B 地方。

cpp 复制代码
┌─────────────────────────────────────────────────────────┐
│  原始进程(可能是 Shell 启动的,有控制终端)            │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  1. 忽略 SIGCHLD 和 SIGPIPE                            │
│     防止子进程变僵尸、防止管道异常终止进程              │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  2. fork() 创建子进程,父进程退出                      │
│     子进程不再是组长,可以调用 setsid()                │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  3. setsid() 创建新会话                                │
│      成为会话首进程                                  │
│      成为进程组组长                                  │
│      脱离原控制终端                                  │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  4. chdir("/") 切换工作目录                           │
│     避免文件系统无法卸载、路径失效等问题              │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  5. 关闭/重定向 标准输入/输出/错误                    │
│     守护进程不需要终端交互                            │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
            成为合格的守护进程,开始执行主逻辑
cpp 复制代码
Daemon(true, false);
//      ↑      ↑
//      |      └── 第2个参数:isclose = false(不关闭,重定向到 /dev/null)
//      └───────── 第1个参数:ischdir = true(切换到根目录 /)

如果把这个改一下改为true呢:

注意这里要重新杀一下进程


六. 如何将服务守护进程化

cpp 复制代码
// ./server port

int main(int argc, char *argv[])
{
    // 1. 检查命令行参数
    if (argc != 2) {
        std::cout << "Usage : " << argv[0] << " port" << std::endl;
        return 0;
    }

    // 2. 解析端口号
    uint16_t localport = std::stoi(argv[1]);

    // 3. 变成守护进程(不切目录,重定向到 /dev/null)
    Daemon(false, false);

    // 4. 创建 TCP 服务器
    std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));

    // 5. 启动服务器主循环(永久运行)
    svr->Loop();

    return 0;
}

变成守护进程

cpp 复制代码
Daemon(false, false);

作用: 让程序变成后台守护进程。

参数 含义
ischdir false 不切换工作目录(保持在运行目录)
isclose false 重定向到 /dev/null(标准输出被吞掉)

为什么要变成守护进程?

  • 服务器需要长期运行(7×24小时)

  • 不能被终端关闭影响(SSH断开也不怕)

  • 不能在终端打印干扰用户

相关推荐
名字还没想好☜1 小时前
Docker 数据卷实战:volume、bind mount、tmpfs 到底怎么选,数据持久化与权限坑
运维·docker·容器·kubernetes
天道jimmy1 小时前
VulnHub 系列:Prime,1
linux·运维·服务器
xiaoye-duck1 小时前
《Linux 网络编程》深入传输层 UDP 协议原理:端口、报文、缓冲区与内核源码解析
linux·网络·udp
Lumistory1 小时前
从OPPO长安研发中心看头部科技企业的照明运维选择
大数据·运维·人工智能·光照贴图
好评1242 小时前
【Linux】数据链路层
linux·运维·网络
辻弋2013 小时前
自动检测硬件生成驱动清单、实时监控FPS、120秒性能记录——30+项优化模块化开关,所有改动可精确复原
服务器·windows·游戏引擎·电脑·开源软件
CC城子3 小时前
EtherNet/IP I/O 丢包排查(linux系统)
linux·单片机·tcp/ip·ethernetip
新时代牛马3 小时前
Linux 信号处理完整篇:从sigaction、掩码到内核发送与可重入排障
linux·运维·信号处理
lpfasd1233 小时前
Docker存储清理与防膨胀实践
运维·docker·容器