12.15 学习笔记

一、Linux 基础命令类(面试口吻回答)

1. 如何查找工程下是否存在某个文件?

面试官您好,查找工程下指定文件我常用 find 命令,核心用法:
find [工程目录路径] -name "目标文件名"

示例:工程路径 /home/project/robot,查找 config.jsonfind /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.CondSignal()精准唤醒逻辑)。

实现思路:

  1. 定义3个无缓冲Channel(chAchBchC),分别控制A、B、C协程的执行时机;
  2. 初始化时仅向chA发送信号(触发A协程执行),B、C协程初始阻塞;
  3. 每个协程执行完打印后,向"下一个协程对应的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)
}
核心优势(面试补充)
  1. 更简洁 :相比sync.Cond无需维护counter状态和互斥锁,完全通过Channel的阻塞特性控制顺序,代码量更少;
  2. 天然线程安全:无缓冲Channel的发送/接收操作本身是线程安全的,无需额外加锁;
  3. 无无效唤醒:每个协程仅在收到对应Channel信号时执行,其他时间均阻塞,不存在"被唤醒后因状态不满足再次等待"的无效操作,效率更高。
执行逻辑拆解(面试口述版)
  1. 程序启动后,先向chA发送空结构体,触发A协程从<-chA处唤醒,打印"A";
  2. A协程打印后向chB发送信号,B协程从<-chB处唤醒,打印"B";
  3. B协程打印后向chC发送信号,C协程从<-chC处唤醒,打印"C";
  4. C协程打印后向chA发送信号,回到第一步,形成"A→B→C→A"的闭环;
  5. 每个协程循环10次后结束,wg.Wait()等待所有协程执行完毕,程序退出。

这种实现方式是Go语言"以通信代替共享内存"设计理念的典型体现,也是面试中更推荐的简洁方案。

三、网络编程高频考点

TCP三次握手阶段解答

① 客户端 net.Dial() 触发底层 connect(),发送SYN包启动三次握手;

② 服务端 Listen() 监听后收到SYN包,回复SYN+ACK完成第二次握手;

③ 客户端收到SYN+ACK后回复ACK,三次握手完成,Dial() 返回;

④ 服务端将已建立连接放入监听队列,Accept() 取出连接并返回;

综上,三次握手发生在客户端Dial()后、服务端Accept()返回前

四、核心概念问答(面试口吻)

1. 进程具体有哪些资源

面试官您好,进程作为OS资源分配基本单位,占用资源分6类:

  1. 内存资源:独立地址空间(代码段/数据段/堆/栈/共享内存)、页表、内核态内存描述符(如Linux mm_struct);
  2. CPU资源:PCB(进程控制块),含PID、优先级、状态、CPU上下文(寄存器/PC)、调度信息;
  3. 文件IO资源:文件描述符表(FD)、文件锁、IO缓冲区、设备句柄;
  4. 通信资源:信号量、消息队列、管道、共享内存(IPC);
  5. 特权/上下文资源:UID/GID、环境变量、信号处理函数、网络连接(socket);
  6. 其他资源:定时器、进程组/会话、内存限制(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)),步骤如下:

  1. 初始化大小为100的小顶堆,放入前100个整数;
  2. 遍历剩余整数:
    • 若当前数>堆顶:弹出堆顶,插入当前数,调整堆;
    • 若≤堆顶:直接跳过;
  3. 遍历完成后,堆中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种场景:

  1. 普通局部变量:可修改。defer延迟执行函数体,变量引用有效(如defer中i++,最终i值改变);
  2. 命名返回值 :可修改。如func calc() (res int) { defer res++ ; return 1 },返回值为2(return先赋值,defer后修改);
  3. 匿名返回值 :不可修改。如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()

面试官您好,执行顺序为:

  1. import:递归初始化依赖包(main→pkgA→pkgB,先初始化pkgB);
  2. const:按声明顺序初始化包级常量(无依赖问题);
  3. var:常量初始化后,按"声明顺序+依赖分析"初始化包级变量;
  4. init():每个包var初始化完成后执行init(同一包多文件按文件名排序);
  5. 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数。

调度流程

  1. 初始化GOMAXPROCS个P,每个P绑定1个M;
  2. 主G加入P的LRQ,M从LRQ取G执行;
  3. LRQ空时,P从全局队列(GRQ)/其他P的LRQ"偷取"G(负载均衡);
  4. 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,核心是"倒排索引+分片+近实时搜索":

  1. 倒排索引:Term(词条)→ Posting List(文档ID列表),词典用FST压缩,Posting List差值编码,支撑全文检索;
  2. 分布式架构 :索引拆分为主分片(不可改数量)+ 副本(高可用/分担读压力),按hash(文档ID)%主分片数路由;
  3. 近实时搜索:写入先存内存缓冲区+Translog,1秒刷新为Segment(不可修改),Translog阈值触发Flush合并Segment,延迟约1秒。

补充:支持IK分词(中文)、BM25相关性评分,适用于全文检索/日志分析。

相关推荐
shenghaide_jiahu6 小时前
数学分析简明教程——6.3
学习
三川6986 小时前
AVL树的学习
数据结构·学习·算法
Da Da 泓6 小时前
多线程(四)【线程安全问题】
java·开发语言·jvm·学习·安全·多线程·线程安全问题
福尔摩斯张6 小时前
TCP协议深度解析:从报文格式到连接管理(超详细)
linux·c语言·网络·c++·笔记·网络协议·tcp/ip
羽沢316 小时前
一些css属性学习
前端·css·学习
走在路上的菜鸟6 小时前
Android学Dart学习笔记第十四节 库和导库
android·笔记·学习·flutter
好奇龙猫7 小时前
【AI学习-comfyUI学习-第十七六节-SUPIR放大(XL模型专属)-各个部分学习-记录】
人工智能·学习
windfantasy19907 小时前
青少年编程考级:建立学习目标,提升综合素养的有效途径
人工智能·学习·青少年编程
IMPYLH7 小时前
Lua 的 Debug(调试) 模块
开发语言·笔记·python·单元测试·lua·fastapi