一、Linux 基础命令类(面试口吻回答)
1. 如何查找工程下是否存在某个文件?
面试官您好,查找工程下指定文件我常用 find 命令,核心用法:
find [工程目录路径] -name "目标文件名"
示例:工程路径 /home/project/robot,查找 config.json → find /home/project/robot -name "config.json";
扩展:忽略大小写加 -iname,限制查找深度(如仅查2层)加 -maxdepth 2,提升检索效率。
2. 如何查找哪些文件包含了某个头文件?
我一般用 grep 递归查找,核心写法:
grep -rl "#include <目标头文件.h>" [工程目录路径]
示例:找包含 robot_api.h 的文件 → grep -rl "#include <robot_api.h>" /home/project/robot;
参数说明:-r 递归遍历目录,-l 仅输出匹配文件名;兼容双引号头文件写法可简化为 grep -rl "robot_api.h" 工程路径。
3. 如何查看某个进程的线程状态?
- 精准查看:
top -H -p [进程PID],-H显示线程级信息,-p指定进程ID,可查线程CPU占用、状态、线程ID; - 简洁查看:
ps -T -p [PID],快速列出线程PID和状态; - 进阶排查:
pstack [PID],查看线程调用栈,定位线程阻塞问题。
二、编程手撕题(Go语言实现)
用无缓冲Channel实现三个协程轮流打印ABC
面试讲解
面试官您好!用无缓冲Channel实现该需求,核心利用无缓冲Channel的阻塞特性 :无缓冲Channel的发送/接收操作必须配对完成,否则会阻塞,以此精准控制协程的执行顺序,完全避免无关协程被唤醒的问题(替代sync.Cond的Signal()精准唤醒逻辑)。
实现思路:
- 定义3个无缓冲Channel(
chA、chB、chC),分别控制A、B、C协程的执行时机; - 初始化时仅向
chA发送信号(触发A协程执行),B、C协程初始阻塞; - 每个协程执行完打印后,向"下一个协程对应的Channel"发送信号,触发下一个协程执行,形成闭环。
Go代码实现
go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 定义3个无缓冲Channel,控制协程执行顺序
chA := make(chan struct{})
chB := make(chan struct{})
chC := make(chan struct{})
wg.Add(3)
// 打印A的协程:接收chA信号,执行后向chB发信号
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
<-chA // 阻塞,直到收到信号
fmt.Print("A")
chB <- struct{}{} // 触发B协程执行
}
}()
// 打印B的协程:接收chB信号,执行后向chC发信号
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
<-chB // 阻塞,直到收到信号
fmt.Print("B")
chC <- struct{}{} // 触发C协程执行
}
}()
// 打印C的协程:接收chC信号,执行后向chA发信号
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
<-chC // 阻塞,直到收到信号
fmt.Println("C")
chA <- struct{}{} // 触发A协程执行,形成闭环
}
}()
// 初始化:向chA发送信号,启动第一个协程(A)
chA <- struct{}{}
// 等待所有协程执行完毕
wg.Wait()
// 关闭Channel(可选,防止资源泄漏)
close(chA)
close(chB)
close(chC)
}
核心优势(面试补充)
- 更简洁 :相比
sync.Cond无需维护counter状态和互斥锁,完全通过Channel的阻塞特性控制顺序,代码量更少; - 天然线程安全:无缓冲Channel的发送/接收操作本身是线程安全的,无需额外加锁;
- 无无效唤醒:每个协程仅在收到对应Channel信号时执行,其他时间均阻塞,不存在"被唤醒后因状态不满足再次等待"的无效操作,效率更高。
执行逻辑拆解(面试口述版)
- 程序启动后,先向
chA发送空结构体,触发A协程从<-chA处唤醒,打印"A"; - A协程打印后向
chB发送信号,B协程从<-chB处唤醒,打印"B"; - B协程打印后向
chC发送信号,C协程从<-chC处唤醒,打印"C"; - C协程打印后向
chA发送信号,回到第一步,形成"A→B→C→A"的闭环; - 每个协程循环10次后结束,
wg.Wait()等待所有协程执行完毕,程序退出。
这种实现方式是Go语言"以通信代替共享内存"设计理念的典型体现,也是面试中更推荐的简洁方案。
三、网络编程高频考点
TCP三次握手阶段解答
① 客户端 net.Dial() 触发底层 connect(),发送SYN包启动三次握手;
② 服务端 Listen() 监听后收到SYN包,回复SYN+ACK完成第二次握手;
③ 客户端收到SYN+ACK后回复ACK,三次握手完成,Dial() 返回;
④ 服务端将已建立连接放入监听队列,Accept() 取出连接并返回;
综上,三次握手发生在客户端Dial()后、服务端Accept()返回前。
四、核心概念问答(面试口吻)
1. 进程具体有哪些资源
面试官您好,进程作为OS资源分配基本单位,占用资源分6类:
- 内存资源:独立地址空间(代码段/数据段/堆/栈/共享内存)、页表、内核态内存描述符(如Linux mm_struct);
- CPU资源:PCB(进程控制块),含PID、优先级、状态、CPU上下文(寄存器/PC)、调度信息;
- 文件IO资源:文件描述符表(FD)、文件锁、IO缓冲区、设备句柄;
- 通信资源:信号量、消息队列、管道、共享内存(IPC);
- 特权/上下文资源:UID/GID、环境变量、信号处理函数、网络连接(socket);
- 其他资源:定时器、进程组/会话、内存限制(ulimit)、CPU配额(cgroup)。
补充:线程共享进程资源,仅私有栈和CPU上下文;进程是"资源分配单位",线程是"调度单位"。
2. 其他语言协程的用户态实现
面试官您好,不同语言协程核心是"用户态调度+上下文切换+IO多路复用",主流实现如下:
| 语言 | 核心模型 | 实现细节 |
|---|---|---|
| Python | 协作式+事件循环 | async def/await定义协程,IO阻塞时主动让出CPU;底层epoll实现IO多路复用 |
| C++(libco) | 抢占+协作式 | ucontext_t做上下文切换;co_yield主动让出,SIGALRM信号强制抢占;epoll管理IO |
| Java(Loom) | JVM层Fiber | 用户态调度Fiber,IO阻塞自动让出CPU,复用JVM线程调度但无内核态切换 |
| Lua | 协作式+协程对象 | coroutine.create/resume/yield控制,无自动切换,适用于简单异步场景 |
核心共性:用户态上下文切换+调度器+IO多路复用,区别仅在调度方式(协作/抢占)和上下文实现(ucontext_t/字节码/VM)。
3. K8s中Go服务GOMAXPROCS默认值
面试官您好,核心结论分版本:
- Go 1.19及之前:默认等于宿主机CPU核数(无视Pod CPU配额);
- Go 1.20及之后:默认读取cgroup CPU限制(Pod配额),如Pod配2核则GOMAXPROCS=2。
补充:
- 原理:Go 1.20+读取/sys/fs/cgroup/cpu下的配额文件,适配容器资源;
- 建议:生产环境显式设置GOMAXPROCS(环境变量/代码),避免调度低效;
- 影响:Pod配额2核但GOMAXPROCS=16,会导致M远大于P,CPU上下文切换开销飙升。
4. 算法题:最长公共子序列(DP实现)
面试讲解
核心是状态定义+转移:
- 状态:
dp[i][j]表示s1前i个、s2前j个字符的LCS长度; - 转移:s1[i-1]==s2[j-1] →
dp[i][j] = dp[i-1][j-1]+1;否则dp[i][j] = max(dp[i-1][j], dp[i][j-1]); - 初始:
dp[0][j]=dp[i][0]=0。
Go代码(基础版)
go
package main
import "fmt"
func longestCommonSubsequence(text1, text2 string) int {
m, n := len(text1), len(text2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if text1[i-1] == text2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
return dp[m][n]
}
func max(a, b int) int { if a > b { return a }; return b }
func main() {
fmt.Println(longestCommonSubsequence("abcde", "ace")) // 输出3
}
优化版(空间O(n))
go
func longestCommonSubsequenceOpt(text1, text2 string) int {
m, n := len(text1), len(text2)
dp := make([]int, n+1)
for i := 1; i <= m; i++ {
prev := 0
for j := 1; j <= n; j++ {
temp := dp[j]
if text1[i-1] == text2[j-1] {
dp[j] = prev + 1
} else {
dp[j] = max(dp[j], dp[j-1])
}
prev = temp
}
}
return dp[n]
}
5. 算法题:10亿整数找最大100个数(小顶堆思路)
面试讲解
核心用小顶堆(O(NlogK)时间+O(K)空间),避免全量排序(O(NlogN)),步骤如下:
- 初始化大小为100的小顶堆,放入前100个整数;
- 遍历剩余整数:
- 若当前数>堆顶:弹出堆顶,插入当前数,调整堆;
- 若≤堆顶:直接跳过;
- 遍历完成后,堆中100个数即为最大;如需从大到小输出,反转堆元素即可。
补充:
- 数据读取:10亿数分批次流式读取,避免内存溢出;
- 分布式场景:先分节点各找Top100,再汇总找全局Top100。
Go代码实现
go
package main
import (
"container/heap"
"fmt"
)
// 小顶堆定义
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
// 找Top K最大数
func findTopK(nums []int, k int) []int {
if k <= 0 || len(nums) == 0 {
return nil
}
h := &IntHeap{}
heap.Init(h)
for _, num := range nums {
if h.Len() < k {
heap.Push(h, num)
} else if num > (*h)[0] {
heap.Pop(h)
heap.Push(h, num)
}
}
// 反转堆元素,从大到小输出
res := make([]int, k)
for i := k - 1; i >= 0; i-- {
res[i] = heap.Pop(h).(int)
}
return res
}
func main() {
nums := []int{5, 2, 9, 1, 7, 6, 8, 3, 4, 100, 99, 88}
fmt.Println(findTopK(nums, 3)) // 输出[100 99 88]
}
五、Go语言核心考点
1. defer函数能否修改变量
面试官您好,核心取决于变量类型和是否为返回值,分3种场景:
- 普通局部变量:可修改。defer延迟执行函数体,变量引用有效(如defer中i++,最终i值改变);
- 命名返回值 :可修改。如
func calc() (res int) { defer res++ ; return 1 },返回值为2(return先赋值,defer后修改); - 匿名返回值 :不可修改。如
func calc() int { var i int; defer i++ ; return i },return拷贝i值到匿名返回值,defer修改的是局部i,不影响返回值。
补充:defer注册时立刻计算参数值(如defer fmt.Println(i),注册时确定i值),但函数体延迟执行。
2. Go执行顺序:import/var/const/init()/main()
面试官您好,执行顺序为:
- import:递归初始化依赖包(main→pkgA→pkgB,先初始化pkgB);
- const:按声明顺序初始化包级常量(无依赖问题);
- var:常量初始化后,按"声明顺序+依赖分析"初始化包级变量;
- init():每个包var初始化完成后执行init(同一包多文件按文件名排序);
- main():所有依赖包+main包init执行完毕后,进入main函数(程序入口)。
示例:main导入pkg1 → import pkg1 → pkg1 const → pkg1 var → pkg1 init → main const → main var → main init → main()。
3. GMP调度模型
面试官您好,GMP是Go并发核心,对应3个组件:
- G(Goroutine):轻量级协程,默认栈2KB(动态扩缩),是任务执行单元;
- M(Machine):OS内核线程,G的执行载体,同一时间仅绑定1个P;
- P(Processor):逻辑处理器,维护本地G队列(LRQ),数量=GOMAXPROCS(默认CPU核数),决定并行执行的M数。
调度流程:
- 初始化GOMAXPROCS个P,每个P绑定1个M;
- 主G加入P的LRQ,M从LRQ取G执行;
- LRQ空时,P从全局队列(GRQ)/其他P的LRQ"偷取"G(负载均衡);
- G阻塞(syscall)时,M与P解绑,P绑定新M继续执行;G阻塞结束后入GRQ等待调度。
优势:用户态调度减少内核切换开销,多核并行提升执行效率。
4. 进程/线程/协程的区别
| 维度 | 进程 | 线程 | 协程(Goroutine) |
|---|---|---|---|
| 资源占用 | 大(独立地址空间) | 中(共享进程资源) | 极小(共享线程资源) |
| 调度方式 | 内核态(OS调度) | 内核态(OS调度) | 用户态(runtime) |
| 并发能力 | 低(单机数百个) | 中(单机数千个) | 极高(单机百万级) |
| 安全性 | 高(地址空间隔离) | 中(需锁同步) | 中(需channel/sync) |
| 生命周期 | OS管理 | OS管理 | 语言runtime管理 |
总结:进程是"资源分配单位",线程是"OS调度单位",协程是"用户态调度单位"。
5. 进程/线程/协程的通信方式
(1)进程间(IPC)
- 管道(匿名/命名)、消息队列、共享内存(最快,需锁同步)、信号量、信号、Socket(跨网络/本地)。
(2)线程间
- 共享内存(全局变量/堆,需Mutex/RWMutex/Cond同步)、TLS(线程本地存储)、信号量/条件变量。
(3)协程间
- Go:channel(推荐,通信实现共享)、sync包(共享内存+锁);
- 其他:Python(事件循环+队列)、C++ libco(全局队列+信号量)。
6. Channel底层实现原理
面试官您好,Channel底层是runtime/hchan结构体,核心字段:buf(环形队列)、sendq/recvq(阻塞G队列)、lock(互斥锁)、cap/len(容量/当前长度)。
工作流程:
- 无缓冲Channel(cap=0):发送/接收需配对,无对应方则阻塞,唤醒时直接拷贝数据;
- 缓冲Channel(cap>0):队列未满则入队,满则发送G阻塞;队列非空则出队,空则接收G阻塞;
补充:close(ch)会唤醒所有阻塞G(发送G panic,接收G取零值);单向Channel是编译期限制,底层仍是hchan;所有操作通过lock保证线程安全。
7. ES实现原理
面试官您好,ES基于Lucene,核心是"倒排索引+分片+近实时搜索":
- 倒排索引:Term(词条)→ Posting List(文档ID列表),词典用FST压缩,Posting List差值编码,支撑全文检索;
- 分布式架构 :索引拆分为主分片(不可改数量)+ 副本(高可用/分担读压力),按
hash(文档ID)%主分片数路由; - 近实时搜索:写入先存内存缓冲区+Translog,1秒刷新为Segment(不可修改),Translog阈值触发Flush合并Segment,延迟约1秒。
补充:支持IK分词(中文)、BM25相关性评分,适用于全文检索/日志分析。