面灵AI→

百度后端一面面经:GMP 调度、切片机制与容器隔离

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 项目深挖(讲讲你做的项目与其中的难点)。
  3. 操作系统内存管理有哪些策略?
  4. CPU 访问内存的过程是怎样的?
  5. 讲一下 GMP 调度模型,P 在模型里的作用是什么?为什么一定要有 P?其中谁可以通过人为控制,P 大于、等于、小于 CPU 核心数,这三种分别适用于什么情况?
  6. 切片的底层机制是什么,怎么实现的?使用过程中需要注意什么问题?
  7. 数据库的事务隔离级别有哪些?脏读和幻读分别是什么?
  8. Redis 常用的数据类型有哪些?
  9. Redis 的主从是怎么解决数据一致性问题的,它们之间怎么同步数据?
  10. Docker 的 CMD 和 ENTRYPOINT 两个语法的区别?
  11. 讲一下 k8s 提交一个 Pod,它的创建过程会经过哪些环节?
  12. 创建一个容器,它依赖了 Linux 内核的哪些机制?Namespace 和 Cgroup 起什么作用?
  13. 算法:交换链表中的节点。

《参考解析》

内存管理与 CPU 访存:把地址翻译这条链讲通就赢了一半。操作系统内存管理的策略分几层:物理内存分配用连续分配(首次适应、最佳适应、伙伴系统)与非连续分配(分页、分段、段页式),虚拟内存用请求调页按需加载,页面置换有 FIFO、LRU、Clock 等算法,分配器层面还有 slab、buddy 和用户态的 malloc arena。CPU 访问一个虚拟地址的过程是:CPU 先查 TLB,命中直接得到物理地址;未命中则由 MMU 逐级遍历页表(x86-64 常见四级:PGD→PUD→PMD→PTE)取出物理页框号,与页内偏移拼成物理地址,并把结果回填 TLB;如果页表项显示页面不在内存或权限不符,触发缺页异常,内核负责换入页面或分配零页,再重新执行这条指令。追问通常落在页表太大(多级页表 + 大页)、TLB 抖动(工作集超过 TLB 覆盖范围)以及写时复制这几处,答的时候带上具体机制而不是只背名词。

GMP 与 P 的作用:P 不是可有可无的中间层。 G 是 goroutine,M 是内核线程,P 是逻辑处理器,它持有本地运行队列、mcache、栈分配等调度所需的资源。M 必须先绑定一个 P 才能执行 G,这是「必须有 P」的原因:一来它把可运行的 G 做了分片,减少全局队列锁竞争;二来内存分配器与调度器耦合,mcache 挂在小对象分配路径上,需要 P 提供无锁的本地缓存。P 的数量由 GOMAXPROCS 控制,可以人为设置。P 数大于 CPU 核心数:适合大量阻塞在 IO 或系统调用上的场景,能减少 M 因阻塞而导致其他 G 排队等待的情况,但上下文切换与调度开销上升,GC 扫描的栈也更多;P 数等于核心数:默认值,CPU 密集型任务吞吐最佳;P 数小于核心数:适合与其他进程争抢 CPU、或希望降低调度抖动和内存占用(每个 P 都有 mcache 和调度结构)的场景。答题时补一句工作窃取:某个 P 的本地队列空了会从全局队列或其他 P 的队列尾部偷一半任务,避免负载不均。

切片底层:它就是「指针 + 长度 + 容量」的三元组。运行时它是 SliceHeader(array、len、cap),所以切片传参只是拷贝 24 字节的头,底层数组是共享的。append 时如果容量够就原地写并返回新的头(len 加一),容量不够就申请一块更大的数组(小切片翻倍、大切片按约 1.25 倍增长),拷贝旧数据后再追加——这意味着扩容前后指向的数组可能不同。由此产生几个高频坑:函数内 append 后不返回新切片,调用方看到的还是旧长度;对同一底层数组的两个切片做修改会互相可见(s2 := s1[:2] 之后写 s2 会改到 s1 的元素);用 s1 = append(s1, x) 之后如果发生扩容,之前保存的别名切片就与它分家了。另外还有内存泄漏:从一个大切片里切出一小块长期持有,会拖住整个底层数组,解决办法是 copy 出一个独立的新切片;需要限制后续 append 覆盖原数组时可以用三索引切片 s[a:b:b]。

事务隔离与 Redis 主从:一个讲清异常,一个讲清丢失窗口。四种隔离级别的递进关系要背成因果链:读未提交会产生脏读;读已提交解决脏读,但仍可能不可重复读(同一行两次读结果不同)与幻读(同一条件两次查行数不同);可重复读解决不可重复读,MySQL 的 InnoDB 在 RR 下用 MVCC 提供一致性快照读,并用临键锁(记录锁 + 间隙锁)在「当前读」时阻止其他事务插入,从而基本消除幻读;串行化靠加锁串行执行。要分清快照读(普通 SELECT)与当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)。Redis 部分,常用数据类型是 string、list、hash、set、zset,另有 bitmap、HyperLogLog、GEO、Stream 等扩展结构。主从同步分两步:从库首次连接发起全量同步,主库 fork 出 RDB 快照加缓冲区增量数据一起发送;之后靠 repl_backlog 与复制偏移量做增量同步(PSYNC 带 offset),断线重连时若 offset 还在 backlog 内则只补差量,否则回退到全量。要注意复制默认是异步的,主库写成功不代表从库已落盘,故障切换存在丢数据的窗口,对一致性敏感的场景可以用 WAIT 命令或 Redis 集群的多数派机制,但代价是延迟。

容器与 k8s:Pod 创建是一条串行的控制链路,隔离靠内核三件套。 Pod 的创建流程可以按组件顺序说:用户 kubectl 提交 YAML → API Server 鉴权、准入控制(Admission)后把对象写入 etcd → Controller Manager 里的对应控制器补上 ReplicaSet/OwnerReference 等字段 → Scheduler 依据资源请求、亲和性、污点容忍选出节点并回写绑定信息 → 目标节点的 kubelet watch 到 Pod,调用 CRI 拉镜像、创建 Pod 沙箱(先起 pause 容器)与业务容器,调用 CNI 配网络、CSI 挂存储 → 容器启动后探针通过则更新状态。容器依赖的内核机制主要是 Namespace 与 Cgroup:Namespace 负责「看得见什么」(PID、NET、MNT、UTS、IPC、USER、CGROUP 等各自一套视图),Cgroup 负责「能用多少」(CPU 配额、内存上限与 OOM 控制、IO 带宽),再加上 capabilities、seccomp、AppArmor 做权限收敛。Docker 的 CMD 是容器启动的默认命令,会被 docker run 后面的参数整体覆盖;ENTRYPOINT 是固定入口不会被覆盖,CMD 此时作为它的默认参数被追加,所以常见组合是 ENTRYPOINT 写可执行程序、CMD 写默认参数。算法题「交换链表中的节点」用哑节点加三指针原地改指向即可,注意头节点被交换、相邻节点、只剩一个节点这几类边界。