Linux SMP 实现机制深度剖析

Linux SMP 实现机制深度剖析

1. 概览摘要

先把话说在前面:早期的 Linux 0.11 是典型的单 CPU 内核, 不支持 SMP(Symmetric Multi-Processing, 对称多处理). 真正在 PC 平台上较稳定、系统化地支持 SMP, 大概是 2.0 之后的事. 下面的分析会以现代 Linux 内核(2.6 以后为主)为参照, 但讲的都是 Linux 核心机制, 而不是拍脑袋的理论

你可以把单核系统想象成一个只有一个厨师的小厨房:所有菜都得排队做;SMP 则是有多名厨师、多个灶台, 但共享同一套菜谱(内核代码)和冰箱(物理内存). 难点不在于"多找几个厨师", 而在于:

  • 菜怎么分工(调度)
  • 避免抢错食材(并发冲突、锁)
  • 保证大家看到的库存是一致的(缓存一致性、内存屏障)
  • 某个厨师出问题时, 怎么协调其他人(IPI、CPU hotplug)

本文涉及CPU 启动、CPU 间通信、调度与负载均衡、锁与并发控制、内存一致性与屏障、per-CPU 数据结构等关键点


2. 核心概念详解

2.1 关键术语

术语 含义 在 SMP 中的角色
SMP (Symmetric MP) 对称多处理, 每个 CPU 权限对等 所有 CPU 共享同一内核与内存
AP (Application Processor) 除 BSP 外的其他 CPU 需要由 BSP 唤醒和初始化
BSP (Bootstrap Processor) 系统上电后首先启动的 CPU 负责内核引导、其他 CPU 启动
IPI (Inter-Processor Interrupt) 处理器间中断 用于跨 CPU 通知、TLB 刷新等
Local APIC / IO APIC 本地/IO 中断控制器 管理中断路由和分发
Per-CPU 变量 每个 CPU 独立的一份变量副本 避免多核竞争、提高缓存命中
Spinlock 自旋锁 短时加锁, 不睡眠, 常用于中断/底半部
RCU Read-Copy-Update 读多写少的高并发读优化机制
Memory Barrier 内存屏障 控制 CPU/编译器重排序, 保障时序关系

2.2 举个栗子

  1. 单核 vs SMP

    • 单核:一个收银员 + 一条队伍, 所有顾客排队结账
    • SMP:多个收银员共用一个收银系统和一个库存数据库, 顾客可以去任意收银台. 问题是:
      • 库存扣减要一致(缓存一致性)
      • 有活动时要所有收银台一起参与(IPI 广播)
  2. Spinlock

    • 类似办公室里的一把"签字笔", 谁拿到笔, 谁就能在文件上签字;
    • 如果笔正在别人手里, 你只能在旁边干等(自旋), 不能离开(不能睡眠)
  3. Per-CPU 数据

    • 每个收银台桌上都放一只小钱盒(每 CPU 单独一份统计),
    • 最终结算时再把所有小钱盒里的钱加总,
    • 平时没人抢一个盒子用, 冲突少、效率高

2.3 与其它模式对比

模式 特点 优点 缺点
UP(单处理器) 只有一个 CPU 实现简单, 无锁或少锁 无法利用多核
SMP 所有 CPU 对等, 统一内存 编程模型简单(统一视图) 锁竞争、缓存抖动
NUMA 非一致内存访问, 多节点 可扩展到很多 CPU 内存访问成本差异大, 调度复杂

Linux 主流内核在抽象层以 SMP 为基准模型, 底层则会针对 NUMA、超线程、CPU 拓扑做复杂的优化


3. 实现机制深度剖析

3.1 数据结构概览

以 x86 为例, SMP 相关的核心结构大致分布在:

  • arch/x86/kernel/smp.c:CPU 启动、IPI、smp_call_function 系列
  • kernel/sched/core.c / kernel/sched/fair.c:调度器与多队列负载均衡
  • kernel/locking/*.c:自旋锁、读写锁、RCU 等
  • kernel/cpu.c:CPU hotplug 相关
  • arch/x86/mm/tlb.c:TLB shootdown IPI 相关

下面用一些简化版结构来说明设计思路(非内核原样代码):

c 复制代码
// 典型的 CPU 描述结构 (简化)
struct cpu_info {
    int id;              // 逻辑 CPU ID
    int online;          // 是否在线
    int apic_id;         // 对应的 APIC ID
    int numa_node;       // 所在 NUMA 节点
    struct rq *rq;       // 调度就绪队列
    void *percpu_data;   // per-CPU 数据基址
};
c 复制代码
// 每 CPU 运行队列 (调度器核心数据之一, 简化自 rq)
struct rq {
    raw_spinlock_t lock;   // 保护该队列的自旋锁
    struct task_struct *curr;   // 当前在此 CPU 运行的进程
    struct cfs_rq cfs;     // CFS 调度队列 (红黑树等)
    unsigned long nr_running;   // 就绪进程数量
    int cpu;               // 所属 CPU
};
c 复制代码
// IPI 处理相关 (高度简化)
typedef void (*smp_fn)(void *info);

struct smp_call_item {
    smp_fn func;
    void *info;
    struct list_head list;
};

struct smp_call_queue {
    raw_spinlock_t lock;
    struct list_head list;   // 待执行的函数队列
};

3.2 CPU 启动流程

3.2.1 BSP 启动 AP 的整体流程

系统上电, BSP 启动
内核初始化完成
扫描可用 CPU/APIC 信息
为每个 AP 准备启动代码和栈
通过 APIC 发送 INIT IPI
发送 STARTUP IPI(SIPI)
AP 从实模式/保护模式启动代码入口开始执行
AP 初始化 GDT/IDT/分页等
AP 调用 cpu_init, 加入调度器
所有 CPU 在线, SMP 启动完成

大致上:

  1. BSP 完成常规的内核启动(类似 0.11 的单核起步流程)
  2. 探测硬件, 构建 CPU 拓扑结构(ACPI/MADT、MP Table 等)
  3. 为每个即将启动的 AP 准备栈、页表、启动跳板代码
  4. 通过 Local APIC 对应地址写寄存器, 发送 IPI 给目标 AP:
    • INIT IPI:复位 AP 到已知状态
    • SIPI(Startup IPI):告诉 AP 从某段物理地址开始执行
  5. AP 跳到一段汇编启动代码, 逐步切到保护模式/长模式、加载 GDT/IDT、页表
  6. AP 调用 start_secondary() 一类函数, 执行本地初始化(本地定时器、中断、per-CPU 结构等), 最后进入调度循环
3.2.2 启动入口的简化代码示意

文件大致位置:arch/x86/kernel/head_64.S / smpboot.c

c 复制代码
// 伪代码: AP 启动主流程
void start_secondary(void)
{
    cpu_init();            // 初始化 CPU 本地状态, per-CPU 结构
    smp_callin();          // 与 BSP 同步, 通知自己已经在线
    setup_local_APIC();    // 本地 APIC 初始化
    calibrate_delay();     // BogoMIPS 校准, delay loop 相关
    notify_cpu_starting(); // 通知其他子系统 (如 RCU) 本 CPU 启动
    scheduler_start();     // 进入调度循环, 开始执行进程
}

注:这段是高度抽象的示意代码, 只为了说明流程关联


3.3 CPU 间通信:IPI 与 smp_call_function

3.3.1 IPI 类型

常见 IPI 类型(以 x86 为例)包括:

  • TLB shootdown IPI:某 CPU 修改页表后, 需要通知其他 CPU 刷新对应 TLB 项
  • reschedule IPI:让另外一个 CPU 尽快重新调度, 例如负载均衡、优先级更新
  • call_function IPI:让其他 CPU 执行某个回调函数(如刷新某些 per-CPU 状态)
  • stop IPI:停机、panic 等场景中, 让所有 CPU 停在安全位置
3.3.2 smp_call_function 伪代码

文件大致位置:kernel/smp.c / arch/x86/kernel/smp.c

c 复制代码
// 向其他 CPU 广播调用 func(info)
int smp_call_function(smp_fn func, void *info, bool wait)
{
    struct smp_call_item item;
    item.func = func;
    item.info = info;

    for_each_online_cpu(cpu) {
        if (cpu == smp_processor_id())
            continue;

        enqueue_call(cpu, &item);     // 加入目标 CPU 的队列
        send_IPI(cpu, IPI_CALL_FUNC); // 触发 IPI
    }

    if (wait)
        wait_for_all_cpu_done();

    return 0;
}
c 复制代码
// 目标 CPU 的 IPI 处理函数 (高度简化)
void handle_call_function_ipi(void)
{
    struct smp_call_item *item;

    while ((item = dequeue_call(this_cpu))) {
        item->func(item->info); // 在当前 CPU 上执行回调
    }

    notify_caller_if_needed();
}

这套机制把"在某个 CPU 上执行某个函数"抽象成通用服务, 其他子系统(如 TLB、RCU、时钟同步)都基于它构建高层逻辑
CPU1 (目标) 本地APIC CPU0 (调用方) CPU1 (目标) 本地APIC CPU0 (调用方) 构造 smp_call_item enqueue_call(队列) 发送 IPI_CALL_FUNC 触发 IPI IPI handler 取出队列项 执行 func(info) (可选) 完成通知


3.4 调度与负载均衡:多运行队列模型

3.4.1 多队列调度器

从 2.6 时代开始, Linux 调度器演进到每 CPU 一个就绪队列 的模型(struct rq), 再通过周期性的负载均衡实现跨 CPU 迁移:

  • 每个 CPU 有独立的 rq, 减少锁竞争
  • 普通情况下, 任务尽量留在本 CPU(cache-friendly)
  • 周期性检查各 CPU 负载, 必要时从繁忙 CPU 把任务"偷"到空闲 CPU(work stealing)

CPU2
CPU1
CPU0
负载均衡
负载均衡
就绪队列 rq0
就绪队列 rq1
就绪队列 rq2

3.4.2 核心调度循环伪代码
c 复制代码
// 伪代码, 概念类似 schedule()
void schedule(void)
{
    struct rq *rq = this_rq();
    struct task_struct *prev = rq->curr;
    struct task_struct *next;

    raw_spin_lock(&rq->lock);

    put_prev_task(rq, prev);         // 把当前进程放回就绪队列
    next = pick_next_task(rq);       // 从就绪队列选择下一个
    rq->curr = next;

    context_switch(prev, next);      // 切换寄存器、栈、地址空间等

    raw_spin_unlock(&rq->lock);
}
c 复制代码
// 周期性负载均衡 (高度简化)
void rebalance_domains(int cpu)
{
    struct rq *rq = cpu_rq(cpu);
    raw_spin_lock(&rq->lock);

    if (rq->nr_running < threshold) {
        int src = find_busiest_cpu();
        if (src >= 0)
            steal_tasks(cpu, src);  // 从繁忙 CPU 偷几个任务过来
    }

    raw_spin_unlock(&rq->lock);
}

这里的难点在于:

  • 锁粒度:避免全局大锁, 尽量用每 CPU 自旋锁
  • 拓扑感知:在 NUMA/多级缓存拓扑下, 优先在"距离近"的 CPU 之间平衡
  • 实时进程/亲和性:调度器要尊重 CPU 亲和性、实时优先级等约束

3.5 锁与并发控制:Spinlock / RCU / seqlock

3.5.1 自旋锁基础实现

文件大致位置:kernel/locking/spinlock.carch/*/include/asm/spinlock.h

c 复制代码
typedef struct {
    volatile int locked;
} raw_spinlock_t;

// 简化版 test-and-set 自旋锁
void raw_spin_lock(raw_spinlock_t *lock)
{
    while (1) {
        // 原子交换, xchg 返回旧值
        if (xchg(&lock->locked, 1) == 0)
            break;  // 拿到锁

        // 没拿到锁, 就自旋等一会
        cpu_relax();     // 提示 CPU 做 pause 等优化指令
    }

    smp_mb();  // 获取锁后的内存屏障
}

void raw_spin_unlock(raw_spinlock_t *lock)
{
    smp_mb();          // 释放前的屏障
    lock->locked = 0;  // 简化:写 0 即释放
}

这里要点:

  • xchg 是硬件提供的原子指令(如 x86 上的 lock xchg
  • 在多核下, 自旋锁保证同一时刻只有一个 CPU 进入临界区
  • smp_mb() 等一类屏障防止指令和内存访问在锁操作前后非法重排序
3.5.2 RCU / seqlock 简要对比
机制 使用场景 读路径开销 写路径开销 典型特点
Spinlock 读写都需要互斥 简单直接, 容易死锁
RW lock 读多写少 低(多读共享) 高(独占写) 写时阻塞所有读
Seqlock 读多写少, 读可重试 无需锁, 但可能重试 读不阻塞写, 适合时间戳类
RCU 极端读多写少 非常低 高且复杂 读几乎无锁, 写需要分期回收

例如 seqlock 的简化版:

c 复制代码
typedef struct {
    volatile unsigned seq;
    raw_spinlock_t lock;
} seqlock_t;

unsigned read_seqbegin(seqlock_t *sl)
{
    unsigned s;
    do {
        s = sl->seq;
    } while (s & 1); // 奇数表示正在写
    smp_rmb();
    return s;
}

int read_seqretry(seqlock_t *sl, unsigned start)
{
    smp_rmb();
    return (start != sl->seq);
}

void write_seqlock(seqlock_t *sl)
{
    raw_spin_lock(&sl->lock);
    sl->seq++;
    smp_wmb();
}

void write_sequnlock(seqlock_t *sl)
{
    smp_wmb();
    sl->seq++;
    raw_spin_unlock(&sl->lock);
}

读者不加锁, 只是检查 seq 是否在读期间发生变化;如果变了, 就再读一遍. 典型应用是时间戳、jiffies、stat 之类的只读大多数


3.6 内存一致性与屏障

多核系统中, 即便有硬件缓存一致性协议, 仍然会因为:

  • CPU 的乱序执行
  • 编译器优化重排序

导致**"代码顺序" ≠ "真正生效的内存访问顺序"**

Linux 提供了跨架构统一的屏障接口:

  • smp_mb():全内存屏障
  • smp_rmb():读屏障
  • smp_wmb():写屏障
  • 以及带依赖的、RCU 专用的屏障等

典型例子:发布-订阅(producer-consumer) 模式

c 复制代码
// 生产者 CPU
data = new_value;
smp_wmb();           // 确保 data 写入在前
ready = 1;           // 通知消费者

// 消费者 CPU
while (!ready)
    cpu_relax();
smp_rmb();           // 确保下面读 data 不会被提前
use(data);

如果没有屏障, 可能出现消费者看到 ready == 1, 但 data 仍是旧值的情况

用数学一点的说法, smp_wmb()smp_rmb() 建立了一个偏序关系
write(data)→wmb/rmbread(data) \text{write}(data) \xrightarrow{\text{wmb/rmb}} \text{read}(data) write(data)wmb/rmb read(data)

防止这条"因果链"被硬件/编译器打乱


3.7 Per-CPU 数据与 cache 友好设计

问题:多 CPU 同时频繁读写同一个全局变量, 会导致:

  • cache line 不断在 CPU 间迁移(false sharing);
  • 性能抖动、锁竞争严重

解决:将一些变量改成 per-CPU 形式, 每个 CPU 维护一份, 必要时再合并

c 复制代码
// 声明一个 per-CPU 计数器 (概念性示例)
DEFINE_PER_CPU(unsigned long, packets_processed);

// 在某个 CPU 上增加
void inc_packets(void)
{
    unsigned long *cnt = this_cpu_ptr(&packets_processed);
    (*cnt)++;
}

// 汇总所有 CPU 上的计数
unsigned long sum_packets(void)
{
    unsigned long total = 0;
    int cpu;

    for_each_online_cpu(cpu) {
        total += per_cpu(packets_processed, cpu);
    }

    return total;
}

这样的设计:

  • 常规路径(increment)完全本地化, 无需锁、无竞争;
  • 汇总路径较少调用, 即使需要遍历所有 CPU, 也能接受

4. 设计思想与架构

4.1 为什么采用这样的 SMP 方案?

核心目标可以总结为三点:

  1. 对上保持简单的"统一系统"抽象

    • 对用户态和大多数内核子系统而言, 看起来就像一个更快的单机,
    • 不要求应用程序显式感知多核(除非做亲和性/NUMA 优化)
  2. 对下充分榨干硬件能力

    • 利用 APIC / IPI / per-CPU / cache 等各种硬件特性
    • 尽量减少跨 CPU 共享、避免 cache line 抖动
  3. 在可维护性和性能之间取平衡

    • 一味上锁很简单但性能糟糕;
    • 完全无锁(lock-free)算法虽爽, 但难以验证正确性并普及
    • Linux 采用"多种工具组合":自旋锁、RW锁、RCU、seqlock、per-CPU 等, 针对不同场景选最合适的

4.2 解决了哪些痛点?

  • 从单核向多核横向扩展:充分利用多核 CPU, 提升吞吐
  • 避免全局大锁(BKL)成为瓶颈:通过细粒度锁和 per-CPU 数据, 把锁粒度拆小
  • 在高并发 IO / 网络场景中, 能让多个 CPU 并行处理不同连接、不同 softirq

4.3 局限性与代价

  • 复杂性

    • SMP 相关路径极其复杂, 涉及调度、内存管理、锁、架构细节
    • bug 一般为竞态条件和死锁, 复现与调试都异常困难
  • NUMA 下的扩展问题

    • SMP 模型在统一内存延迟假设下比较自然,
    • 但在 NUMA 大规模多核上, 单纯 SMP 抽象已不足, 需要额外的调度域和内存亲和策略
  • 调试难度

    • 多核竞态可能只在极端高负载、特定拓扑、罕见时序下触发,
    • 传统的"单步调试"手段很难直接用

4.4 替代方案对比

方案 思想 优点 缺点
大内核锁 (BKL) 整个内核一把大锁 实现简单 完全无法扩展, 多核几乎浪费
细粒度锁 + per-CPU 当前 Linux 内核路径 折中可维护性与性能 设计难度高
微内核 + 消息传递 通过 IPC 串联服务 模块隔离好 上下文切换/IPC 成本高
用户态多线程 + 单核内核 内核不支持 SMP 内核简化 性能受限, 应用负担大

Linux 的路线:以单体内核 + 细粒度并发为主轴, 渐进式演化


5. 实践示例:构造一个极简"多核友好计数器"

下面给一个小型示例, 用 per-CPU 计数器 + IPI 的方式, 模拟"在所有 CPU 上统计某事件次数"的场景. 示例代码是可编译运行的内核模块伪代码风格(但删减了宏和 API 细节, 仅示意结构)

5.1 场景描述

  • 每个 CPU 本地计数一个事件(比如一次软中断)
  • 用户通过 /procdebugfs 读到全局总和 + 每 CPU 分布
  • 当某个 CPU 发起"刷新", 通过 smp_call_function() 让其他 CPU 把本地计数刷到一个共享数组中, 然后统一打印

5.2 核心代码

c 复制代码
// 示例文件: kernel/smp_demo.c (假想路径)

#define NR_CPUS 64

static DEFINE_PER_CPU(unsigned long, local_cnt);
static unsigned long snapshot[NR_CPUS];

static void flush_local_cnt(void *info)
{
    int cpu = smp_processor_id();
    unsigned long v = this_cpu_read(local_cnt);

    snapshot[cpu] = v;
}

void event_occurs(void)
{
    this_cpu_inc(local_cnt);
}

// 由某个 CPU 发起全局刷新
void flush_all_cpus(void)
{
    // 先刷新自己
    flush_local_cnt(NULL);

    // 再让其他 CPU 刷
    smp_call_function(flush_local_cnt, NULL, 1);

    // 此时 snapshot[] 中就是所有 CPU 的值
    // 可以安全地在当前 CPU 上做统计/输出
    unsigned long total = 0;
    int cpu;
    for_each_online_cpu(cpu)
        total += snapshot[cpu];

    pr_info("total=%lu\n", total);
}

真正的内核模块需要 module_init / module_exitprocfs/debugfs 接口、错误处理等, 这里略去不表, 只保留与 SMP 相关的刻画

5.3 编译运行思路

在真正内核树中, 可以:

  1. 将上述文件加入 Makefile
  2. 启用 SMP 配置编译内核或作为模块编译
  3. 在运行的 SMP Linux 系统上加载模块, 通过某个触发接口(如 echo 1 >/sys/.../flush)调用 flush_all_cpus()

预期效果:

  • 在多核系统上无论跑多少次 event_occurs(),
  • 每次 flush_all_cpus() 都能统计到当前时刻各 CPU 上的计数之和

6. 工具与调试

调试 SMP 问题离不开一些经典工具:

工具/命令 用途 示例
top / htop 观察多核利用率 htop, 按 1 展开 per-CPU
perf 性能采样、锁竞争分析 perf record -g -a sleep 10
ftrace / trace-cmd 内核函数/事件跟踪 trace-cmd record -e sched*
lockstat / perf lock 锁竞争统计 perf lock record ./app
gdb + qemu 内核单步/断点调试 在 qemu 中跑 SMP 内核, gdb 远程连接
sysctl kernel.sched_* 调整调度器参数 sysctl kernel.sched_migration_cost_ns

在调试并发问题时, 常用套路:

  1. 利用 ftrace 记录 sched_switch、IPI 处理、RCU 回调等关键事件
  2. perflockstat 看某个自旋锁是否成为热点
  3. 在 qemu/kvm 的测试环境中重现竞态, 配合 CONFIG_DEBUG_LOCKDEPCONFIG_PROVE_LOCKING 等内核调试选项

7. 架构总览

最后用一张整体架构图, 把前面拆开的点串起来
Kernel
Hardware
CPU0
CPU1
CPU2
共享内存
Local & IO APIC
调度器\nper-CPU rq
锁子系统\nspinlock/RW/RCU/seqlock
内存管理\nTLB, 页表, TLB shootdown
per-CPU 数据\n统计/状态
SMP 核心\nCPU 启动/拓扑/CPU hotplug

逻辑连接可以简化为:

  • SMP 核心 (SMPCore) 负责:
    • 多 CPU 启动、CPU 在线/下线、拓扑信息提供、IPI 基础设施
  • 调度器 (Sched)
    • 使用 per-CPU 运行队列和锁, 基于拓扑做负载均衡
  • 内存子系统 (MM)
    • 通过 IPI 做 TLB shootdown, 使用锁/RCU 保证映射更新一致
  • 锁子系统 (Locking)
    • 提供自旋锁/RCU 等并发原语, 供全内核使用
  • per-CPU 子系统 (PerCPU)
    • 提供性能友好的本地化存储区域, 减少共享冲突

8. 总结

最后用一个小表格, 把本文覆盖的关键技术点做个回顾:

技术点 关键作用 你需要记住什么
CPU 启动 & APIC 多 CPU 上电和初始化 BSP 唤醒 AP, APIC 负责 IPI 与中断路由
IPI & smp_call_function CPU 间通信 让某个函数在另一 CPU 上执行, 是构造高层同步的基础设施
多队列调度器 多核负载均衡 每 CPU 一条就绪队列 + 周期性 work stealing
自旋锁 / seqlock / RCU 并发控制 根据读写比例和实时性选择合适机制
内存屏障 时序保证 防止 CPU/编译器把关键读写重排序
per-CPU 数据 性能优化 把"共享变量"拆散成"每 CPU 各一份"降低竞争
CPU 拓扑 & NUMA 扩展到多插槽多节点 调度器和内存分配必须感知物理拓扑

归纳几点:

  1. Linux SMP 实现不是单一模块, 而是一整套从CPU 启动、APIC、IPI 到调度器、锁、内存模型的协同设计
  2. 真正的难点在于在性能、复杂性和可维护性之间找到平衡点, 而不是简单地加几把锁
  3. per-CPU 数据、细粒度锁、RCU、seqlock 等机制, 分别对应不同场景, 是 Linux 多年演化的产物
  4. 调度器的"每 CPU 运行队列 + 拓扑感知负载均衡"设计, 是现代 SMP 利用度的关键
  5. 内存屏障和内存模型是所有并发原语的底层基石, 一旦理解不透, 后面一切都容易出错
  6. 在调试和优化 SMP 性能时, 要善用 perfftrace、lockdep 等工具, 而不是盲目猜测
相关推荐
2501_906150562 小时前
私有部署问卷系统操作实战记录-DWSurvey
java·运维·服务器·spring·开源
wuk9982 小时前
使用PCA算法进行故障诊断的MATLAB仿真
算法·matlab
知识分享小能手2 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04的Linux网络配置(14)
linux·学习·ubuntu
额呃呃2 小时前
二分查找细节理解
数据结构·算法
钦拆大仁2 小时前
单点登录SSO登录你了解多少
服务器·sso
皇族崛起2 小时前
【视觉多模态】- scannet 数据的 Ubuntu 百度网盘全速下载
linux·ubuntu·3d建模·dubbo
无尽的罚坐人生2 小时前
hot 100 283. 移动零
数据结构·算法·双指针
CAU界编程小白2 小时前
Linux系统编程系列之进程控制(下)
linux·进程控制
永远都不秃头的程序员(互关)3 小时前
C++动态数组实战:从手写到vector优化
c++·算法