Linux 系统调试学习1

一个进程可以简单想成:

复制代码
Process
│
├── PID
├── 虚拟内存空间
├── 文件描述符
├── socket
├── 环境变量
├── 当前工作目录
├── 信号状态
└── 一个或多个线程

所以以后你排查一个机器人节点时,其实是在看:

复制代码
这个进程
用了多少 CPU
用了多少内存
开了多少 socket
开了多少文件
有多少线程
收到了什么 signal

这就是 Linux 系统工程的核心视角。


1、进程之间怎么通信?

既然两个进程内存默认隔离,那:

复制代码
mocap_driver_node

怎么把数据给:

复制代码
gmr_retarget_node

?

就需要:

复制代码
IPC
Inter-Process Communication
进程间通信

常见:

复制代码
pipe
socket
shared memory
message queue

而 ROS2 Topic 本质上也是在帮不同进程之间进行通信,只不过它通过 DDS / RMW 做了大量封装。

所以:

复制代码
进程隔离
↓
不能直接随便访问对方内存
↓
需要 IPC
对比 进程 线程
内存空间 相对独立 同进程共享
创建开销 较大 较小
通信 IPC 直接共享内存
故障隔离 较强 较弱
数据共享 麻烦一些 容易,但要同步
崩溃影响 通常本进程 可能拖垮整个进程

举个最直观的例子:

复制代码
Thread A 野指针崩了

整个:

复制代码
Process

通常都会崩。

不是只死 Thread A。

例如你看到:

复制代码
CPU 100%

先问:

复制代码
哪个进程?

用:

复制代码
top
ps

再问:

复制代码
这个进程哪个线程?

用:

复制代码
top -H

然后再:

复制代码
哪个函数?

用:

复制代码
perf

于是完整链路:

复制代码
系统异常
↓
进程
↓
线程
↓
函数

这就是前面 perf 那课已经碰到的思路。


查看自己的 shell PID

在 Bash:

复制代码
echo $$

会输出例如:

复制代码
2450

意思:

复制代码
当前 shell 进程的 PID = 2450

这个 $$ 以后写 shell 脚本也会见到。


查看 PID / PPID

可以:

复制代码
ps -ef

看到:

复制代码
UID   PID   PPID   CMD
skr   3000  2000   bash
skr   3100  3000   ./robot_control

说明:

复制代码
robot_control PID = 3100
父进程 PID = 3000

也就是 bash。


join() 为什么重要?

假设:

复制代码
std::thread worker(receiveLoop);

main 要退出前:

复制代码
worker.join();

意思大概就是:

等 worker 真正结束。

之前 TSan 课讲过:

复制代码
通知停止
↓
线程退出
↓
join
↓
释放资源

现在你应该理解得更具体了。


进程状态

ps / top 中可能看到:

复制代码
R
S
D
T
Z

现在先认识最常见几个。

复制代码
R
= Running / Runnable

正在跑或者准备被调度。

复制代码
S
= Sleeping

等待事件,比如:

复制代码
socket数据
condition variable
timer

非常正常。

复制代码
D
= Uninterruptible Sleep

通常和某些 I/O 等待有关。

复制代码
Z
= Zombie
僵尸进程

后面会讲。


什么时候更适合进程,什么时候线程?

先简单记:

更适合多进程

如果你重视:

复制代码
故障隔离
模块边界
独立重启
独立部署

例如:

复制代码
mocap_driver_node
gmr_node
robot_output_node

更适合多线程

如果你重视:

复制代码
低延迟共享数据
同一模块内并发
避免进程间序列化

例如:

复制代码
同一个 driver 内
receive thread
processing thread

现实系统经常:

多进程 + 每个进程内部多线程。

而不是二选一。


2、signal

一句话:

signal = Linux 内核或其他进程给某个进程发送的一种异步通知。

重点掌握:

复制代码
SIGINT
SIGTERM
SIGKILL
SIGSEGV
SIGABRT
SIGHUP

SIGINT:Ctrl+C

你在终端运行:

复制代码
./robot_control

然后按:

复制代码
Ctrl + C

终端通常会给前台进程发送:

复制代码
SIGINT

INT:

复制代码
Interrupt

所以:

复制代码
Ctrl+C
→ SIGINT

它的意思大概是:

用户请求中断程序。

复制代码
终端发 SIGINT
↓
ROS2 runtime 收到
↓
开始 shutdown
↓
executor 退出
↓
node 析构
↓
进程结束

所以:

复制代码
Ctrl+C

其实就是 signal 机制在工作。


SIGTERM:请求进程正常终止

执行:

复制代码
kill 4321

默认发:

复制代码
SIGTERM

TERM:

复制代码
Terminate

可以理解:

请你尽快正常结束。

进程可以捕获 SIGTERM,然后:

复制代码
停止接收新任务
↓
停止控制循环
↓
关闭 socket
↓
断开 EtherCAT
↓
保存日志
↓
释放资源
↓
退出

所以对于机器人服务:

复制代码
SIGTERM

通常比 SIGKILL 更友好。


SIGKILL:真正的"强杀"

执行:

复制代码
kill -9 4321

这里:

复制代码
9

通常就是:

复制代码
SIGKILL

SIGKILL 非常特殊:

进程不能捕获、不能忽略、不能处理。

Linux 直接终止它。

所以:

复制代码
kill -9

确实很强,程序没有机会:

复制代码
释放资源
保存数据
关闭设备
safe disable
写最后日志
join 线程

SIGSEGV:段错误

这个你前面已经见过。

复制代码
SIGSEGV

通常表示:

非法访问内存。

例如:

复制代码
int* p = nullptr;
*p = 10;

程序可能:

复制代码
SIGSEGV
↓
Segmentation fault
↓
core dump

然后你:

复制代码
gdb
core dump
ASan

排查。

所以现在能串起来:

复制代码
非法内存访问
↓
内核发现
↓
向进程发送 SIGSEGV
↓
默认动作:终止
↓
可能生成 core dump

SIGABRT

SIGABRT:

复制代码
Abort

一般意味着:

程序主动或运行库要求"异常终止"。

例如:

复制代码
std::abort();

会触发:

复制代码
SIGABRT

另外某些严重运行时错误,也可能最终导致:

复制代码
SIGABRT

SIGSEGV

更多想到:

复制代码
非法内存访问

而:

复制代码
SIGABRT

更多想到:

复制代码
程序/运行库主动终止

SIGHUP

复制代码
Hangup

历史上和终端断开有关。

现代 Linux 服务里经常把它用作:

复制代码
重新加载配置

例如某些 daemon:

复制代码
kill -HUP PID

不会完全退出,而是:

复制代码
重新读取配置文件

是不是这样取决于具体程序。

所以:

signal 的名字是一回事,程序怎么处理又是另一回事。


怎么查看 signal 编号?

可以:

复制代码
kill -l

会看到:

复制代码
1  SIGHUP
2  SIGINT
...
9  SIGKILL
...
11 SIGSEGV
15 SIGTERM

你不需要全部记。

重点:

复制代码
2   SIGINT
9   SIGKILL
11  SIGSEGV
15  SIGTERM

以后看到:

复制代码
exit signal 11

你应该反应:

复制代码
SIGSEGV

kill -TERM 有什么区别?

Ctrl+C:

复制代码
SIGINT

kill PID:

复制代码
SIGTERM

它们都可以请求退出,但来源和语义稍有不同。

通常:

复制代码
SIGINT
= 交互用户中断

SIGTERM
= 系统/管理工具请求终止

例如:

复制代码
systemd stop

通常更接近:

复制代码
SIGTERM

所以服务程序一定应该认真处理 SIGTERM。


怎么找到某个进程 PID?

最基本:

复制代码
ps -ef | grep robot_control

也可以:

复制代码
pgrep robot_control

如果想看详细:

复制代码
pgrep -a robot_control

例如:

复制代码
4321 ./robot_control --config robot.yaml

signal 和 core dump 连起来

有些 signal 默认动作会:

复制代码
终止进程
+
生成 core

例如常见:

复制代码
SIGSEGV
SIGABRT

所以:

复制代码
程序崩了
↓
看日志里的 signal
↓
判断大方向
↓
coredumpctl
↓
gdb

这就是一条完整故障分析链。


3、socket

socket = 进程和网络协议栈之间的一个通信端点。

例如:

复制代码
你的程序
   ↓
socket
   ↓
Linux 内核网络协议栈
   ↓
网卡
   ↓
网络

程序不会直接"控制网卡发 UDP"。

它通常是:

复制代码
创建 socket
↓
bind / connect
↓
send / recv
↓
内核帮你处理协议和网卡

socket()
↓
内核创建 socket 对象
↓
返回一个 fd
↓
程序以后通过 fd 操作它

例如:

复制代码
recv(fd, ...);
send(fd, ...);
close(fd);

这就是 Linux 很经典的:

Everything is a file-like interface

不是说 socket 真的是磁盘文件,而是 Linux 用统一的 fd 接口管理很多内核资源。


怎么查谁占用了端口?

以后非常实用。

例如:

复制代码
ss -lunp

看 UDP。

或者:

复制代码
ss -ltnp

看 TCP。

例如可能看到:

复制代码
udp  UNCONN  0  0  0.0.0.0:7014   ... users:(("mocap",pid=4321,fd=7))

这里你一下就看到:

复制代码
port = 7014
PID = 4321
fd = 7

这三个概念全串起来了。


socket buffer 是什么?

Linux 内核会给 socket 维护:

复制代码
发送缓冲区
接收缓冲区

例如 UDP 包到了以后,不是一定立刻进你的用户代码。

而是先:

复制代码
进入内核 socket receive buffer

然后你:

复制代码
recvfrom()

再把数据从内核拿到用户空间。

如果:

复制代码
你处理不够快

buffer 越积越多。

最后:

复制代码
满

可能开始丢包。


EINTR

一个阻塞系统调用被 signal 打断了。

例如:

复制代码
recv()
↓
正在阻塞
↓
SIGINT到达
↓
recv返回错误
↓
errno = EINTR

所以:

复制代码
signal

和:

复制代码
socket blocking I/O

也会互相影响。

这就是为什么 Linux 系统知识必须串着学。

复制代码
Network
↓
NIC
↓
Kernel Network Stack
↓
Socket Receive Buffer
↓
fd
↓
recvfrom()
↓
User Buffer
↓
Queue
↓
Worker

这就是机器人系统工程师应该逐渐形成的层次感。

复制代码
socket
= 内核提供的通信对象

socket()
→ 返回 fd

以及:

复制代码
blocking
= 没数据时线程等待

non-blocking
= 没数据立即返回

4、file descriptor

fd = 进程用来引用某个已打开内核对象的整数编号。

例如:

复制代码
int fd = open("config.txt", O_RDONLY);

可能返回:

复制代码
fd = 3

或者:

复制代码
int sock = socket(AF_INET, SOCK_DGRAM, 0);

可能返回:

复制代码
sock = 4

这里:

复制代码
3
4

都叫:

复制代码
file descriptor

简称:

复制代码
fd

假设:

复制代码
fd = 5

这个 5 本身不是文件,也不是 socket。

它只是:

当前进程里的一个"索引号"。

可以想象成:

复制代码
Process
│
├── fd 0 → stdin
├── fd 1 → stdout
├── fd 2 → stderr
├── fd 3 → config.txt
├── fd 4 → UDP socket
└── fd 5 → device

所以:

复制代码
fd
= 进程内部访问内核资源的句柄

open

比如:

复制代码
int fd = open("/tmp/data.txt", O_RDONLY);

大概可以理解:

复制代码
应用调用 open()
↓
进入内核
↓
内核找到文件
↓
建立打开状态
↓
给进程分配一个 fd
↓
返回整数

以后:

复制代码
read(fd, ...)

Linux 就知道:

这个 fd 对应哪个打开的对象。


用完以后:

复制代码
close(fd);

意思:

我这个进程不再需要这个 fd 了。

如果一直不 close:

复制代码
fd 会一直占用

这就叫:

复制代码
fd leak
文件描述符泄漏

怎么查看一个进程打开了哪些 fd?

Linux 非常好用:

复制代码
ls -l /proc/PID/fd

例如:

复制代码
ls -l /proc/4321/fd

可能看到:

复制代码
0 -> /dev/pts/0
1 -> /dev/pts/0
2 -> /dev/pts/0
3 -> socket:[123456]
4 -> /tmp/robot.log
5 -> socket:[123789]

这非常直观。

如果你怀疑:

复制代码
程序 fd 泄漏

可以:

复制代码
ls /proc/4321/fd | wc -l

得到:

复制代码
28

过 10 分钟:

复制代码
120

再过 10 分钟:

复制代码
300

那就非常可疑。

如果数目:

复制代码
一直增长不下降

说明可能有 fd 泄漏。

另一个非常常用的工具:

复制代码
lsof -p 4321

lsof:

复制代码
list open files

它可以列出进程打开的:

复制代码
普通文件
socket
pipe
设备
共享库

非常适合查:

复制代码
这个进程到底打开了什么?

fd 是:

进程内部编号。

所以:

复制代码
fd 3

单独拿出来没有意义。

必须结合:

复制代码
哪个进程

一起看。


PID + fd 才能真正定位资源

所以你以后经常看到:

复制代码
PID 4321
fd 7

意思是:

进程 4321 的第 7 号文件描述符。

这就是为什么 ss -lunp 可能显示:

复制代码
pid=4321,fd=7

现在你应该能完全看懂了。

同一个进程的线程共享 fd 表。

比如:

复制代码
Thread A
创建 socket fd=7

Thread B 通常也可以:

复制代码
使用 fd=7

因为:

复制代码
fd table

属于进程资源。

这也意味着:

复制代码
Thread A close(fd)

而 Thread B 正在:

复制代码
recv(fd)

可能产生复杂的并发行为。

所以 fd 生命周期也要同步。


shutdown() 和 close()

TCP socket 经常见:

复制代码
shutdown(fd, SHUT_RDWR);
close(fd);

简单理解:

复制代码
shutdown
= 关闭 socket 通信方向

例如:

复制代码
不再读
不再写

而:

复制代码
close
= 当前进程释放这个 fd

两者不是同一个概念。

现阶段知道区别即可,不需要深入 TCP 状态机。


5、poll

poll = 把一组 fd 交给内核,问"这里面哪些现在可以读、可以写、或者出错了?"

例如:

复制代码
fd 3 → UDP A
fd 4 → UDP B
fd 5 → TCP
fd 6 → pipe

你不需要:

复制代码
轮流 recv
轮流试
轮流空转

而是:

复制代码
poll()
↓
线程睡眠
↓
某个 fd 有事件
↓
内核唤醒线程
↓
你处理对应 fd

没有任何 fd 有事
↓
线程睡眠

只有:

复制代码
数据来了
连接断了
超时了

才醒。

所以:

poll/epoll 本质是在做"事件驱动等待"。


POLLIN

这个 fd 现在有数据可以读。

比如:

复制代码
UDP 包到了
↓
POLLIN

或者 TCP:

复制代码
对端数据到了
↓
POLLIN

程序看到:

复制代码
if (fds[i].revents & POLLIN)
{
    recv(...);
}

就去读。


poll timeout

例如:

复制代码
poll(fds, n, 1000);

情况 1:某个 fd 很快有事件

比如 20 ms 后来了数据:

复制代码
20ms
↓
poll 立即返回

不会傻等 1 秒。

情况 2:1 秒都没事件

复制代码
poll 返回 0

表示:

timeout。

情况 3:出错

复制代码
poll 返回负数

再看:

复制代码
errno

epoll

epoll 是 Linux 专门为大量 fd 事件管理设计的一套机制。

你可以先理解:

复制代码
poll
= 每次把整组 fd 拿去问内核

epoll
= 先把要关心的 fd 注册给内核
  以后只等"哪些 fd 真有事件"

这是最重要的区别之一。

epoll 常见三步:

复制代码
① epoll_create
↓
创建 epoll 实例

② epoll_ctl
↓
把 fd 注册进去

③ epoll_wait
↓
等待事件

也就是说:

复制代码
创建一个"事件管理器"
↓
告诉它我关心哪些 fd
↓
以后一直问:
谁有事?

fd 数量少
逻辑简单
→ poll 很合适

大量 fd
高并发连接
→ epoll 更适合

最常见:

复制代码
EPOLLIN

表示:

复制代码
可读

还有:

复制代码
EPOLLOUT

表示:

复制代码
可写

以及:

复制代码
EPOLLERR

表示:

复制代码
错误

EPOLLHUP

表示:

复制代码
挂断 / 对端关闭相关事件

现在先认识即可。


LT 和 ET

epoll 里以后很容易听到:

复制代码
Level Triggered
LT

和:

复制代码
Edge Triggered
ET

先用大白话理解。

LT:只要还有数据,就一直提醒你

假设 socket 里:

复制代码
有 1000 bytes

你只读:

复制代码
100 bytes

还剩:

复制代码
900 bytes

LT 模式下:

复制代码
下次 epoll_wait
↓
还会告诉你
这个 fd 可读

因为:

复制代码
里面还有数据

这比较容易写对。


ET:状态变化时重点提醒一次

ET 更像:

复制代码
刚才没数据
↓
现在有数据
↓
提醒你

如果你收到事件以后:

复制代码
只读了一点

剩下的数据可能不会像 LT 那样持续提醒你。

所以 ET 常要求:

复制代码
non-blocking
+
一直读到 EAGAIN

也就是:

复制代码
一次事件来了
↓
把当前能读的数据尽量读干净

LT
= 只要条件还成立,就继续提醒

ET
= 更强调状态边沿变化,需要更谨慎处理

实际项目里:

先把 LT 写对,比为了"高级"强上 ET 更重要。


poll / epoll 和 blocking/non-blocking

复制代码
poll/epoll
+
non-blocking socket

为什么?

因为:

复制代码
先用 poll/epoll 等待
↓
确认 fd 有事件
↓
再 read/recv

如果某些边界情况下:

复制代码
状态已经变了

non-blocking 可以避免:

复制代码
线程意外卡死在 read

尤其 ET 模式里更常见。


eventfd

先认识概念。

Linux 可以创建一种:

复制代码
eventfd

它本身也是:

复制代码
fd

可以用来:

复制代码
线程间通知事件

例如:

复制代码
main thread
↓
要停止 receiver
↓
向 eventfd 写一下

而 receiver 线程:

复制代码
epoll_wait()

同时等:

复制代码
socket fd
+
eventfd

于是:

复制代码
数据来了
→ 处理数据

停止事件来了
→ 退出线程

这个设计非常漂亮。


timerfd

Linux 还有:

复制代码
timerfd

它也是 fd。

你可以:

复制代码
创建一个定时器
↓
定时到期
↓
这个 fd 变成可读

于是 epoll 可以同时等:

复制代码
socket
timer
event

全部统一成 fd。

这就是 Linux fd 抽象很强的地方。只解决:

怎么高效等多个 I/O 事件。

如果:

复制代码
收到一帧
↓
处理 50ms

那你的 event loop 还是被拖住。

常见架构:

复制代码
epoll thread
↓
快速收数据
↓
投递 queue
↓
worker thread处理

所以:

复制代码
I/O multiplexing

和:

复制代码
task parallelism

是两件事。


I/O multiplexing

这就是 poll/epoll 所属的大类:

复制代码
I/O multiplexing
I/O 多路复用

意思不是:

复制代码
同时传很多路数据

而是:

一个执行线程,同时管理多个 I/O fd 的就绪事件。

这个词面试很常见。

复制代码
select
= 老牌方案,有 fd 数量/接口方面限制

poll
= 接口更直接,没有 select 那种固定 fd_set 限制

epoll
= Linux 特有,更适合大量 fd 高并发事件管理

不要现在深挖每个内核复杂度。


怎么判断 poll/epoll 问题?

比如进程:

复制代码
CPU 100%

strace 发现一直:

复制代码
recvfrom(...)= -1 EAGAIN
recvfrom(...)= -1 EAGAIN
recvfrom(...)= -1 EAGAIN

疯狂刷。

这通常说明:

复制代码
non-blocking socket
+
busy polling

可能需要:

复制代码
poll/epoll

而不是不停重试。


6、CPU

以后 top 里可能看到:

复制代码
us
sy
id
wa

先认识最重要几个。

复制代码
us
= user

用户态代码占用。

比如:

复制代码
GMR
Eigen
protobuf
你的 C++ 逻辑

复制代码
sy
= system

内核态 CPU。

比如:

复制代码
socket 系统调用
文件 I/O
网络栈
驱动

复制代码
id
= idle

CPU 空闲。


复制代码
wa
= I/O wait

CPU 在等待某些 I/O 完成相关状态。

现阶段先有这个概念即可。


上下文切换是什么?

假设 CPU 只有一个核。

现在:

复制代码
Thread A
Thread B
Thread C

都想运行。

CPU 不可能真正同时跑三个。

于是:

复制代码
跑 A
↓
保存 A 状态
↓
跑 B
↓
保存 B 状态
↓
跑 C

这就叫:

复制代码
context switch
上下文切换

多线程不是免费的。

线程太多可能带来:

复制代码
调度开销
cache miss
上下文切换

所以:

线程不是越多越快。


memory:程序内存

一个进程的内存可以粗略想成:

复制代码
Process Memory
│
├── code
├── stack
├── heap
├── shared libraries
├── mmap
└── shared memory

你现在不用背内存布局。

重点是理解几个常见指标:

复制代码
VIRT
RES / RSS
SHR

VIRT:

复制代码
Virtual Memory

简单理解:

进程拥有或映射的虚拟地址空间总量。

例如:

复制代码
VIRT = 8 GB

不代表:

复制代码
真的用了 8 GB 物理内存

因为里面可能包括:

复制代码
共享库
mmap
预留地址空间
尚未实际触碰的内存

所以:

不要看到 VIRT 很大就直接判断内存泄漏。

RSS:

复制代码
Resident Set Size

top 里经常叫:

复制代码
RES

它更接近:

当前真正驻留在物理 RAM 里的进程内存。

所以如果你排查:

复制代码
程序是不是实际越来越吃内存

通常:

复制代码
RSS / RES

比:

复制代码
VIRT

更有参考价值。

SHR:

复制代码
Shared Memory

简单理解:

其中有一部分内存是可共享的。

例如:

复制代码
动态库代码页
共享内存

多个进程可能共同使用。

所以:

复制代码
RES

也不能简单相加就认为等于整机实际独占内存。

内存统计比 CPU 稍微复杂。

OOM:

复制代码
Out Of Memory

内存耗尽。

Linux 在极端情况下可能启动:

复制代码
OOM Killer

选择某些进程杀掉,以保证系统继续运行。

日志里可能看到:

复制代码
Killed process xxxx
Out of memory

机器人上如果控制程序被 OOM Killer 干掉:

复制代码
控制进程突然消失

所以生产系统要关注内存边界。


swap

如果 RAM 不够,Linux 可能把部分内存页交换到:

复制代码
swap

可以简单理解:

复制代码
RAM 不够
↓
把一部分暂时不用的页面放到磁盘

普通桌面系统这是正常机制。

但实时控制非常不喜欢:

复制代码
page 被 swap out
↓
实时线程突然访问
↓
需要从磁盘调回来
↓
延迟巨大

这就是后面为什么要学:

复制代码
mlockall

CPU cache

CPU 比 RAM 快很多。

所以现代 CPU 有:

复制代码
L1
L2
L3 cache

如果数据刚好在 cache:

复制代码
访问快

如果总是:

复制代码
cache miss

就要去更慢的内存层级。

所以性能问题不只是:

复制代码
"这个函数做了多少次加法"

还包括:

复制代码
数据布局
内存访问模式
cache locality

这就是为什么连续数组通常很高效。


怎么查看进程内存?

很简单。

例如:

复制代码
ps -o pid,%cpu,%mem,rss,vsz,cmd -p 4321

可能看到:

复制代码
PID   %CPU  %MEM   RSS    VSZ
4321  80.0   2.5   ...    ...

其中:

复制代码
RSS

通常单位是 KB。


CPU / memory 排障可以形成这个流程

程序变慢:

复制代码
① 看 CPU
↓
哪个进程高?

② 看线程
↓
哪个线程高?

③ perf
↓
哪个函数高?

如果:

复制代码
CPU 不高
但机器很卡

再看:

复制代码
memory
swap
I/O wait
page fault

如果:

复制代码
内存持续增长

再:

复制代码
RSS趋势
↓
ASan/LSan/Valgrind
↓
代码生命周期

CPU 100%
≈ 一个逻辑核满载

CPU >100%
≈ 多线程用了多个核

VIRT
= 虚拟地址空间

RSS / RES
= 更接近实际驻留物理内存
相关推荐
Lsetea1 小时前
OpenSSL报certificate chain too long:error 22与verify_depth深度排查
linux·https·ssl证书·openssl·证书链
不像程序员的程序媛2 小时前
linux常见文本操作命令
linux·运维·服务器
木木学AI2 小时前
电话机器人被打断后还能接住问题吗?实时语音交互的四项测试
机器人
xier_ran2 小时前
【infra之路】AI 编译器学习笔记(二):重拾 C++——移动语义与 LLVM 风格的类型转换
人工智能·笔记·学习
wangbing11253 小时前
开发指南149-IotDB-常用命令
linux·运维·iotdb
Dovis(誓平步青云)3 小时前
常用命令藏得太深?怎么做一个快捷命令坞
运维·前端·自动化·音视频·prometheus
看着博客敲代码3 小时前
WorkBuddy 接上 Kali MCP 做自动化渗透:信息收集到初步验证的落地记录
linux·人工智能·安全·网络安全·自动化
szxinmai主板定制专家3 小时前
储能EMS + 协调控制器实战:RK3568/3576/3588+FPGA+AI 故障录波与诊断
人工智能·深度学习·机器学习·fpga开发·rk3588·rk3576·协调控制器
志栋智能3 小时前
运维超自动化的知识管理与传承
运维·自动化