沐曦云原生二面:虚拟内存、容器网络与 goroutine 调度
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 堆内存和栈内存有什么区别?在物理层面哪个内存位更高?分配上有什么区别?
- 程序看到的地址是虚拟地址还是物理地址?
- 虚拟地址如何转换为物理地址?一开始的物理地址又是如何分配的?
- 容器的虚拟网络是如何实现的?
- 线程、进程、goroutine 的区别?
- 手撕:二叉树层序遍历。
《参考解析》
堆与栈:栈由编译器/运行时自动管理,随函数调用分配与释放,存放局部变量、参数、返回地址,分配是移动栈指针(几条指令),速度快、容量有限(默认几 MB,超了就是栈溢出);堆由程序显式或由 GC 管理,生命周期不确定,分配要走分配器(可能陷入内核 mmap/brk),速度慢、可能产生碎片。物理地址高低这个问题没有确定答案——两者都是虚拟地址空间里的区域,落在哪个物理页帧由内核决定,谈「谁更高」本身是个陷阱;能回答的是它们在同一进程虚拟地址空间里的典型布局(栈在高地址向下生长、堆在低地址向上生长,中间是 mmap 区)。分配的区别在于栈是连续的、堆是离散的。
虚拟地址与地址转换:程序看到的都是虚拟地址(每条指令、每个指针都是)。CPU 用 MMU 把虚拟地址按页(通常 4KB)翻译成物理地址:多级页表(x86-64 四级/五级)逐级索引,最后一级页表项给出物理页帧号 + 权限位;为了让翻译够快,用 TLB 缓存最近的映射。缺页(页表项不存在或页不在内存)会触发缺页中断,内核决定是分配零页、从磁盘换入,还是报段错误。
虚拟内存的物理页从哪来:内核在启动时把物理内存按页帧组织成伙伴系统(buddy allocator),按 2 的幂管理空闲块;进程要内存时内核从伙伴系统拿页帧,映射到进程的虚拟地址(这叫按需分页——只在实际访问时才真正分配)。用户态的 malloc 是另一层:小对象从 brk 堆里切,大对象直接 mmap,释放时可能归还也可能留在池里。容器里的「内存限制」是通过 cgroup 给这个进程组设的额度,物理页仍然来自同一套伙伴系统。
容器网络的实现:本质是网络命名空间 + 虚拟设备。每个容器有独立的 netns(自己的网卡、路由表、iptables 规则);veth pair 像一根网线,一端在容器 netns(eth0),另一端在宿主 netns 连到网桥(docker0/cni 网桥);网桥负责同宿主机内容器间的二层转发;跨主机则靠 Overlay(VXLAN 封装)或路由方案;对外暴露端口靠 iptables DNAT 或 eBPF。Kubernetes 里这套由 CNI 插件实现(Calico 走三层路由 + BGP,Flannel 走 VXLAN,Cilium 走 eBPF)。还要提 DNS:CoreDNS 提供 service 名解析,ClusterIP 是 iptables/IPVS 做的虚拟 IP 转发。
线程、进程、goroutine:进程是资源隔离的单位(独立地址空间、文件描述符、netns),线程是调度的单位(共享地址空间,有自己的栈与寄存器)。Linux 里两者都是 task,用 clone 的 flag 区分共享程度。goroutine 是 Go 运行时实现的用户态协程:栈从 2KB 起按需增长、由 GMP 调度器(G 协程、M 内核线程、P 处理器上下文)调度到 OS 线程上,切换不需要进内核,所以创建与切换成本远低于线程;代价是阻塞式系统调用会占用 M(运行时会再开线程),以及没有跨 goroutine 的抢占式优先级。channel 是它们之间的通信方式。
二叉树层序遍历:BFS + 队列。根入队,循环出队访问、把左右孩子入队;要按层分组就每轮记录当前队列长度、只处理这么多。时间复杂度 O(n),空间 O(宽度)。