Armv8 原子性:体系结构保证与编程语义

"atomic"同时出现在处理器手册、编程语言、操作系统和并发算法中。它们共享"不可分割"这一核心思想,却不处在同一个抽象层次。

在 Arm 体系结构中,atomic 首先描述内存访问的可观察性质 :一次访问是否会撕裂,一次写以怎样的方式进入多个观察者的视野。编程中的 atomic 则描述对象及其操作的并发语义:一次 Load、Store、CAS 或加法能否作为一个完整操作参与竞争,编译器和处理器是否共同维持了这种语义。

两者不能直接画等号。体系结构原子性为上层实现提供机制,编程原子性则规定最终必须呈现给程序的结果。本文以 Arm Architecture Reference Manual for A-profile architecture, Arm DDI 0487 Issue M.c 为主要依据,从这两个层次出发,分析它们各自解决的问题,以及它们如何连接起来。

1. 两个层次的 atomic

1.1 体系结构层:约束内存事件

处理器执行访存指令时,会产生一个或多个内存读写事件。Arm 所说的 atomic access,约束的是这些事件能够以什么形式被其他观察者看到。

这一层主要包含两类原子性:

  • 单副本原子性(single-copy atomicity):一次访问的内容是否作为一个整体被观察,即是否可能撕裂。
  • 多副本原子性(multi-copy atomicity):一次写传播给多个观察者时,哪些中间可见状态是允许的。

它们属于体系结构与内存模型的契约。契约规定可观察结果,却不要求处理器采用某一种内部实现。一条原子访问可以经历流水线停顿、推测执行或内部重放;实现也可以使用写缓冲、一致性互连以及其他微体系结构结构。只要对外结果符合规则,体系结构原子性就成立。

Arm 文档中的 atomic accessAtomic instruction 也不是同义词。前者是内存访问具有的性质,普通的对齐 LDR/STR 就可能产生单副本原子访问;后者通常特指 CASLDADD 等直接完成原子读改写的指令类别。是否原子是一项内存效果,是否属于 Atomic instruction 则是指令功能分类。

1.2 编程层:约束对象上的操作

编程语境中的 atomic 以一个原子对象为中心,并从操作的单线程定义出发。例如,递增操作在单线程中应当读取对象的当前值,将其加一后写回,并返回规定的结果;并发执行不能改变这项基本语义。

本文采用下面的判据定义编程原子操作:

如果一次并发执行的可观察结果,能够解释为其中各个已完成的完整操作按照某个先后顺序逐个发生,并且在这个顺序中,每个操作的返回值和对象状态变化仍然符合该操作的单线程定义,那么这些操作对该对象是原子的。

"按照某个先后顺序逐个发生"并不要求各操作在物理执行时间上互不重叠。它们可以同时读取、计算和尝试提交;原子性要求的是,最终结果必须等价于完整操作逐个执行的结果。

这个串行顺序不能任意安排。按照选定顺序逐个应用操作的单线程定义,必须能够得到实际执行中的返回值和对象状态。假设计数器初始为 0,两个并发操作都要求将计数器递增一次。如果两个操作完成后计数器最终为 1,这个结果就无法解释为两个完整递增依次执行:无论哪一个先执行,最终值都应当是 2。因此,普通的"读取---加一---写回"并不是原子递增。

两个操作在内部同时读到 0 本身并不违反原子性。关键在于,它们不能都基于这个旧值成功提交:一个操作改变对象后,另一个操作必须基于新的状态重新完成,或者按照该操作的定义报告失败。只有这样,所有成功操作才能排列成符合单线程定义的状态转换。

编程原子性不仅依赖硬件:

  • 语言内存模型规定哪些并发访问合法,以及原子对象具有什么语义。
  • 编译器必须保持访问的原子粒度,不能把操作变换成破坏语义的普通访问。
  • 运行库或内核负责选择适合当前处理器的实现。
  • 处理器和内存系统提供不可撕裂访问、独占访问或原子读改写等底层能力。

因此,编程中的 atomic 是一项跨越语言、编译器和硬件的端到端保证;处理器指令只是这条实现链的一部分。

原子性也不等于无锁性。某个编程原子操作可以由一条指令完成,可以由独占循环完成,也可以在运行库中借助隐藏的锁完成。它们的进展性质和性能不同,但只要都满足上层规定的不可分割语义,就可以实现同一个 atomic 接口。

2. 单副本原子性:内存访问的完整性

2.1 从不撕裂理解单副本原子性

single-copy atomicity 中的"single-copy"并不是说系统中只能存在一份数据副本。它表示一次内存访问具有不可再分的体系结构效果。

假设一个 64 位对象原来为:

text 复制代码
0x00000000_00000000

处理器将它更新为:

text 复制代码
0xFFFFFFFF_FFFFFFFF

如果该写与并发读取在相应范围内都是单副本原子的,并且没有其他重叠写入,那么读取可以得到旧值或新值,不能得到:

text 复制代码
0xFFFFFFFF_00000000

这样的结果由新旧两次状态拼接而成,称为撕裂值。

因此,在最常见的同地址、同大小访问中,单副本原子性可以直接理解为:一次读取要么看到写入前的完整值,要么看到写入后的完整值,不会看到一次写入的部分效果。对于大小不同或地址部分重叠的访问,则需要按照 Arm ARM 的重叠访问规则单独分析,不能简单套用"旧值或新值"这一结论。

2.2 原子访问不等于不可中断执行

单副本原子性约束的是观察结果,而不是指令执行期间的控制流状态。一次访问可以在内部被拆分、停顿或重放,只要体系结构不允许其他观察者看到部分效果。

同样,一条指令也可能产生多个内存访问。对于按访问序列执行的指令,异常甚至可以出现在各子访问之间。因此,"一条汇编指令"与"一次不可分割的内存效果"是两个不同概念;原子粒度必须依据指令类型和 Arm ARM 的明确规则判断。

2.3 AArch64 的基本原子粒度

普通显式数据访问中,常见的基线规则如下:

访问形式 单副本原子性保证
单个通用寄存器的 Load/Store,地址按访问大小对齐 整次访问原子,通用寄存器访问最大为 64 位
LDP/STP 访问两个正确对齐的通用寄存器 每个寄存器对应的访问分别原子,不据此保证整个成对访问原子
单个不超过 64 位的 SIMD/FP 量,按访问大小对齐 整次访问原子
普通 128 位 SIMD/FP 访问,按 64 位对齐 基线规则把它视为两个 64 位原子访问
没有获得更大粒度保证的多字节访问 体系结构最低只保证每个字节的访问原子

例如:

asm 复制代码
LDR     X0, [X1]          // X1 按 8 字节对齐:64 位单副本原子读
STR     X2, [X3]          // X3 按 8 字节对齐:64 位单副本原子写

对齐是保证的一部分。"未对齐访问能够执行"并不等于"未对齐访问整体不撕裂";执行许可和原子粒度是两项独立属性。

2.4 FEAT_LSE2 对原子粒度的扩展

FEAT_LSE2 加强了 AArch64 对 Load/Store 的单副本原子性与对齐保证。其中与普通代码关系最密切的规则包括:

  • LDPLDNPSTP 访问两个 64 位寄存器时,如果整体访问按 16 字节对齐,且目标是 Inner Write-Back、Outer Write-Back 的 Normal Cacheable Memory,那么整个 16 字节访问是单副本原子的。
  • 上述成对指令访问少于 16 字节时,如果所有字节位于同一个按 16 字节对齐的 16 字节块内,并满足相同的内存属性,整个访问是单副本原子的。
  • 未按数据大小对齐的 Load/Store,如果所有字节仍位于同一个按 16 字节对齐的 16 字节块内,并满足相同的内存属性,也可以获得单副本原子性。

FEAT_LSE2 在 Armv8.4-A 中是必选特性;在更早的体系结构版本中不能无条件假定存在。因此,下面这条指令是否构成 128 位单副本原子写:

asm 复制代码
STP     X0, X1, [X2]

取决于处理器特性、整体对齐和目标内存属性,而不能仅从助记符推断。原子粒度始终是指令、访问大小、对齐、内存属性和体系结构特性共同决定的结果。

2.5 128 位访问的几种不同语义

128 位访问尤其能说明"指令宽度"与"原子宽度"并不相同:

访问方式 体系结构含义
基线条件下的LDP/STP,每个元素为 64 位 两个分别原子的 64 位访问
满足FEAT_LSE2、16 字节对齐及规定内存属性的 LDP/STP 整个 128 位访问单副本原子
装载两个 64 位量的LDXP 单独执行时不保证一次 128 位原子快照
成功写入两个 64 位量的STXP 整个成对写入单副本原子
CASP 对成对对象执行原子比较交换,受相应特性与对齐规则约束

需要 128 位编程原子操作时,编译器或运行库会根据目标处理器选择 LDP、独占序列、CASP 或锁辅助路径。程序所依赖的应当是上层接口承诺的语义,而不是某一种固定指令序列。

2.6 AArch32 中的 64 位访问

在 AArch32 状态下,对齐的普通字访问具有 32 位单副本原子性。LDRD/STRD 一般按两个 32 位字访问理解,不能仅凭它们在一条指令中传送 64 位数据,就推导出面向处理器观察者的普通 64 位原子保证。

对于按 8 字节对齐的地址,LDREXD 以及成功的 STREXD 所产生的双字访问具有 64 位单副本原子性。这使 32 位处理器能够以独占访问为基础实现 64 位原子对象和原子读改写。

3. 多副本原子性:写的传播约束

单副本原子性关注一次访问内部是否完整;多副本原子性关注一次写在多个观察者之间如何变得可见。前者约束访问的内容边界 ,后者约束写的传播边界

3.1 同一位置的一致性顺序与严格 MCA

Arm ARM 对严格 multi-copy atomic 写给出两个条件:

  1. 同一位置的所有写被串行化,所有观察者以相同顺序观察这些写;某个观察者可以跳过部分中间写。
  2. 在所有观察者都观察到某次写之前,对该位置的读取不能返回该写的值。

第一条建立同一位置的共同写顺序,属于一致性的核心性质;第二条进一步限制写何时能够被读取返回。只满足第一条并不足以构成严格 MCA。

若两个处理器分别向同一位置写入 W1W2,系统会为它们确定共同的一致性顺序。观察者可以只看到其中一部分写,却不能以彼此相反的顺序观察同一组写。这个性质保证了单个位置不会为不同处理器形成互相矛盾的历史,但它不决定并发写中哪一个在前,也不把依赖旧值的更新变成原子读改写。

3.2 Arm 的 Other-multi-copy atomic 模型


Arm Architecture Reference Manual for A-profile architecture, Arm DDI 0487 Issue M.c

Issue M.c 中的 Arm 内存模型是 Other-multi-copy atomic(O-MCA) 。它对一次写 W 的可见范围施加如下约束:

  1. 发出 W 的观察者可以先于其他观察者读取到自己的写。
  2. 一旦某个不同的观察者已经观察到 W,所有以一致方式访问该位置的其他观察者也必须已经观察到 W


https://developer.arm.com/community/arm-community-blogs/b/tools-software-ides-blog/posts/armv8-sequential-consistency

第二条就是 O-MCA 在同址一致性之外增加的传播约束。它排除了 W 已经对某个其他观察者可见,却仍然只因传播尚未到达而对其余一致性观察者不可见的状态。换句话说,从体系结构的可观察关系看,一次写在"写入者以外的观察者"中不能逐个传播;这些观察者对该写的可见性近似是全有或全无。

例如,处理器 A 写入 X=1,B、C、D 都以一致方式访问 X。O-MCA 允许 A 通过自身的写入路径提前读到 1,此时 B、C、D 仍未观察到该写。但是,一旦 B 已经观察到 X=1,C 和 D 就不能在一个被确定为更晚的观察点上,仅仅因为该写"还没有传播过来"而继续停留在 X=0

这里的"已经观察到"不是要求每个处理器实际执行一次读取,也不是要求它们必须返回 1。如果随后还有 X=2,C 可以跳过 1 直接观察到 2;它只是不能仍停留在 X=1 之前。该约束描述的是内存模型中的可见关系,也不意味着所有缓存必须在同一个物理时钟沿更新。若 C 对 X=0 的读取发生在 B 观察到 X=1 之前,则仍然是允许的。

O-MCA 因而保留了写入者提前观察自身写入的空间,同时排除了写在多个"其他观察者"之间任意、逐个传播的行为。它强于只规定同一位置写顺序的模型,又不同于没有写入者例外的严格 MCA。

同址一致性、O-MCA 与严格 MCA 的关系可以概括为:

text 复制代码
同一位置的一致性顺序:规定多次写的共同先后
O-MCA:规定一次写对其他观察者不能只传播给其中一部分
严格 MCA:进一步排除写入者提前观察自身写的例外

多副本原子性仍然只讨论传播性质。它不会自动建立不同地址之间的发布关系,也不会使普通的复合更新具备编程原子性。

4. 编程中的 atomic:对象与操作

4.1 原子对象是语言层的抽象

程序不是直接对"内存事件"编程,而是对变量、对象和操作编程。语言或内核把某个对象声明为 atomic,意味着并发代码必须通过规定的原子接口访问它;这些访问参与该对象的原子语义,并由编译器和底层实现共同维持。

以 C/C++ 为例,普通对象上的冲突并发访问可能构成数据竞争;底层硬件即使保证一次对齐读取不撕裂,也不能使这种程序自动合法。volatile 只约束部分编译器行为,同样不提供语言级原子对象语义。

因此,体系结构的单副本原子访问通常是实现原子对象的必要材料,却不是完整的语言契约。语言级 atomic 还必须处理编译器转换、并发访问规则以及所请求的内存顺序。

4.2 原子 Load/Store 与原子读改写

编程原子操作可以分成两类。

第一类是单向访问:

text 复制代码
atomic load       读取一个原子对象
atomic store      写入一个原子对象

当对象大小、对齐和目标平台满足条件时,它们往往可以直接映射为普通的单副本原子 Load/Store,再由编译器保证访问不会被拆分、合并或替换为不符合语言语义的形式。

第二类是读改写(read-modify-write,RMW):

text 复制代码
compare-and-swap
exchange
fetch-add
fetch-or

RMW 不仅要求读取和写入各自完整,还要求二者之间不能被竞争者插入一个会导致更新丢失的操作。它必须以一个整体参与目标对象的修改顺序。

4.3 单次访问原子不能构成原子加法

下面三条指令中的 Load 和 Store 可以各自具有单副本原子性:

asm 复制代码
LDR     X0, [X1]
ADD     X0, X0, #1
STR     X0, [X1]

但整个序列不是原子读改写。两个处理器可以同时读到 10,分别计算出 11,再先后写回 11,从而丢失一次递增。

单副本原子性排除的是"半个旧值与半个新值"的撕裂;原子 RMW 排除的是竞争操作插入读取与写回之间。两者面对的是不同的并发失效模式。

4.4 CAS 的编程语义

CAS(Compare-And-Swap)描述以下不可分割的状态转换:

c 复制代码
old = *p;
if (old == expected)
        *p = desired;
return old;

这段代码只表达语义,逐行执行并不原子。真正的 CAS 要求读取、比较和条件写入共同构成一次 RMW:若目标仍等于 expected,则将它替换为 desired;无论是否成功,都返回操作所观察到的旧值。

CAS 因此首先是一种上层操作语义。它可以直接映射为同名硬件指令,也可以由另一种体系结构原语实现。理解这一点,才能准确说明 CAS 与 LL/SC 的关系:它们不处在完全相同的层次。

5. 从编程语义到 Arm 指令

5.1 LSE:用一条指令完成原子读改写

FEAT_LSE 为 AArch64 提供 CASSWPLDADDLDCLRLDEORLDSET 等原子读改写指令。例如:

asm 复制代码
CAS     X1, X2, [X0]       // [X0] == X1 时写入 X2;X1 返回旧值

这条指令直接生成原子比较交换的内存效果,使上层 CAS 的读取、判断和条件写入作为一次完整的状态转换发生。类似地,LDADD 能原子地取得旧值并提交加法结果。

FEAT_LSEFEAT_LSE2 分工不同:LSE 增加原子 RMW 指令,LSE2 主要加强普通 Load/Store 的单副本原子性与对齐规则。二者都与 atomic 有关,但解决的不是同一个问题。

5.2 LL/SC:用条件提交构造原子操作

在没有 LSE 指令或实现选择不使用 LSE 时,AArch64 可以通过 LDXR/STXR 独占访问实现同样的上层语义。下面的代码直接构造一个 64 位 CAS:

asm 复制代码
// X0 = 对象地址,X1 = expected,X2 = desired
// 返回时 X0 = 操作观察到的旧值
cas_u64:
1:      LDXR    X3, [X0]
        CMP     X3, X1
        B.NE    2f
        STXR    W4, X2, [X0]
        CBNZ    W4, 1b
        MOV     X0, X3
        RET
2:      CLREX
        MOV     X0, X3
        RET

LDXR 读取目标并建立独占监视。STXR 只有在监视仍然有效时才提交;若竞争访问或其他事件使监视失效,它会报告失败而不修改目标,代码随后重新读取并重试。

CAS 的判定条件是"当前值是否等于期望值",Store-Conditional 的判定条件是"独占监视是否仍然有效"。LL/SC 循环把这两种条件组合起来,从而实现 CAS 语义。成功的 STXR 提交这次状态更新;此前的失败尝试没有向目标对象提交部分结果。

这段序列可以在 LDXRSTXR 之间被中断,也可能重试多次,但它仍能实现一个原子操作。它再次说明:编程原子性要求的是结果不可分割,而不是实现过程不可中断。

AArch32 中的 LDREX/STREXLDREXD/STREXD 承担相同角色。编译器、运行库或 Linux 内核可以在 LSE 指令与独占循环之间选择,对调用方维持同一个原子操作接口。

5.3 Linux AArch32 atomic_t:一个真实的 LL/SC 实现

Linux 的 AArch32 实现给出了一个典型的工程实例。在 ARMv6 及更高版本的 arch/arm/include/asm/atomic.h 中,整数原子操作由下面的宏生成。具体函数名和封装方式会随内核版本演进,但独占循环的核心结构保持一致:

c 复制代码
#define ATOMIC_OP(op, c_op, asm_op)                                  \
static inline void arch_atomic_##op(int i, atomic_t *v)              \
{                                                                    \
    unsigned long tmp;                                                \
    int result;                                                       \
                                                                     \
    prefetchw(&v->counter);                                           \
    __asm__ __volatile__("@ atomic_" #op "\n"                       \
"1:     ldrex   %0, [%3]\n"                                         \
"       " #asm_op " %0, %0, %4\n"                                  \
"       strex   %1, %0, [%3]\n"                                     \
"       teq     %1, #0\n"                                            \
"       bne     1b"                                                    \
    : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)              \
    : "r" (&v->counter), "Ir" (i)                                   \
    : "cc");                                                         \
}

源码随后通过 ATOMIC_OPS(add, +=, add) 生成加法接口;其中 ATOMIC_OP 接收到 add, +=, add 这组参数后,得到的 arch_atomic_add(i, v) 在语义上相当于:

c 复制代码
do {
    old = LDREX(&v->counter);
    new = old + i;
} while (STREX(&v->counter, new) != 0);

实际汇编直接复用 %0ldrex 将旧值读入 resultadd 在寄存器中把它更新为新值,strex 再尝试把新值写回。strex 的状态写入 %1:返回 0 表示提交成功,非 0 表示本次条件写入没有发生。teqbne 1b 因此会在失败时回到 ldrex,重新读取当前值并重新计算。

假设计数器初始为 0,两个核心同时执行加一。它们可能都通过 ldrex 读到 0,也都会在各自的寄存器中算出 1;但其中一个核心成功执行 strex 后,另一个核心的条件写入不能再基于原来的独占状态提交。失败的一方重新读取 1,计算出 2,然后再次尝试提交。最终结果是 2,等价于两个完整的加一操作依次执行,而不是两个普通的"读、改、写"相互覆盖。

这段代码也划清了几个容易混淆的边界。prefetchw() 只是写预取优化,不提供原子性;__asm__ __volatile__ 和操作数约束用于约束编译器处理这段代码的方式,也不产生处理器之间的原子提交。真正使更新成为 atomic 的,是 LDREX/STREX、独占监视以及失败后的重试。至于该操作与其他内存访问之间需要何种顺序,仍属于内存顺序问题,而不是这个独占循环本身所表达的全部内容。

这个实例表明,一个编程原子操作可以由多条能够被中断、交错和重试的指令组成。实现过程不是"一次执行完且不可打断",但只有成功提交的完整更新才会成为对象的新状态,所以并发结果仍然符合该操作的单线程定义。

5.4 映射关系决定正确性,不决定固定指令形态

同一个上层 atomic 操作可以有不同实现:

text 复制代码
64 位 atomic load    -> 对齐的 LDR,或带顺序语义的 LDAR
64 位 atomic store   -> 对齐的 STR,或带顺序语义的 STLR
atomic fetch-add     -> LDADD,或 LDXR/STXR 重试循环
compare-and-swap     -> CAS,或 LDXR/STXR 重试循环
128 位 atomic Load/Store -> 满足条件的 LSE2 访问、独占序列或运行库路径
128 位 compare-and-swap  -> CASP、独占序列或运行库路径

反过来,同一条机器指令在不同对齐、内存属性或体系结构特性下,也可能具有不同的原子粒度。于是,可靠的实现必须同时满足两端:上层先给出所需语义,下层再证明选定的指令序列确实实现了它。

6. 原子性与内存顺序

原子性规定一次操作会不会以部分状态出现;内存顺序规定这个操作与其他内存事件之间具有怎样的关系。二者相互独立,又经常在同步算法中组合。

一个 relaxed 原子加法仍然是不可分割的 RMW,只是它通常不承担其他地址的发布或获取。Release、Acquire 和更强的顺序语义,则在原子对象之外建立程序所需的跨地址关系。

因此,在普通 LDRADDSTR 序列中插入屏障,不能把它变成原子加法:屏障可以约束这些访问与其他访问的顺序,却不能阻止竞争者进入 Load 与 Store 之间。反过来,使用 LDXR/STXRLDADD 完成了目标对象的原子更新,也不代表周围数据自动获得所需顺序。

Arm 为独占访问提供 LDAXRSTLXR,并为 LSE 原子指令提供 Acquire、Release 及 Acquire-Release 变体,正是为了让实现分别选择:

text 复制代码
目标对象的不可分割更新    +    操作前后所需的内存顺序

完整的同步原语通常同时包含这两部分,但分析时必须把它们分开。

7. 一条完整的理解路径

Armv8 原子性可以沿着下面的层次理解:

text 复制代码
编程语义
  原子对象、Load/Store、CAS、RMW、串行执行语义、内存顺序
                         |
                         | 编译器、运行库或内核进行映射
                         v
体系结构机制
  单副本原子访问、独占访问、LSE 原子指令、O-MCA
                         |
                         | 处理器与内存系统实现
                         v
可观察的内存行为
  不撕裂、条件提交、共同写顺序、受约束的写传播

这条路径中的每一层都有自己的问题:

  1. 在编程层,先确定对象需要普通原子 Load/Store,还是不可分割的读改写,并选择所需内存顺序。
  2. 在映射层,确认编译器或内核使用了能保持该语义的接口与实现,不能用普通对象或 volatile 替代。
  3. 在体系结构层,检查指令产生的访问数量、原子粒度、对齐、内存属性和可选特性。
  4. 在多处理器层,区分单次访问的完整性、同一位置的一致性顺序和写传播约束。

最终,体系结构 atomic 与编程 atomic 的关系可以概括为:

体系结构原子性规定硬件能够提供哪些不可分割的内存效果;编程原子性规定并发程序必须看到怎样的完整操作。前者是实现机制和证明基础,后者是软件真正依赖的语义契约。

只有把这两个层次连接起来,才能准确判断一条指令是否足以实现某个原子操作,也才能理解为什么多条可被中断的指令仍然可以构成一次可靠的 atomic 更新。


主要参考资料

相关推荐
王琦03181 小时前
Linux的文件管理
linux·运维·服务器
Android系统攻城狮2 小时前
Linux PipeWire深度解析之pw_stream_new调用流程与实战(四十二)
linux·运维·服务器·音频进阶·pipewire音频进阶
蜉蝣fuyou2 小时前
VMware多版本安装包
linux·vmware·虚拟机
不会代码的小猴2 小时前
Linux note2
linux·笔记
雾里0不看花2 小时前
【App Service Linux】在Linux App Service中安装 tcpdump 并抓取网络包
linux·网络·tcpdump
神神道呵he2 小时前
[深度学习] 大模型学习-RAG技术全景解析
人工智能·深度学习·学习
MartinYeung53 小时前
[论文学习]AgentSafetyBench:大语言模型智能体安全评估基准深度分析
学习·安全·语言模型
Dawn-bit3 小时前
Linux日志处理三剑客之基础篇:(基础正则+扩展正则)
linux·运维·服务器·正则表达式·云计算·运维开发
深圳市爱派派智能科技有限公司3 小时前
“高通 QCS8550 嵌入式主板 EC-A8550JD4:48 TOPS 算力上板“
网络·嵌入式·边缘计算·高通·qcs8550·天启智能·ec-a8550jd4
☆凡尘清心☆3 小时前
Linux运维故障排查速查命令清单
linux·运维·服务器