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相关性评分,适用于全文检索/日志分析。

相关推荐
超龄超能程序猿16 分钟前
Docker常用中间件部署笔记:MongoDB、Redis、MySQL、Tomcat快速搭建
笔记·docker·中间件
奔波霸的伶俐虫21 分钟前
windows docker desktop 安装修改镜像学习
学习·docker·容器
时兮兮时21 分钟前
CALIPSO垂直特征掩膜(VFM)—使用python绘制主类型、气溶胶和云的子类型
笔记·python·calipso
时兮兮时25 分钟前
MODIS Land Cover (MCD12Q1 and MCD12C1) Product—官方文档的中文翻译
笔记·mcd12q1
时兮兮时39 分钟前
Linux 服务器后台任务生存指南
linux·服务器·笔记
BullSmall1 小时前
《逍遥游》
学习
奔波霸的伶俐虫1 小时前
spring boot集成kafka学习
spring boot·学习·kafka
CCPC不拿奖不改名1 小时前
面向对象编程:继承与多态+面试习题
开发语言·数据结构·python·学习·面试·职场和发展
GHL2842710901 小时前
通义千问的 Function Call - demo学习
学习·ai·ai编程
知识分享小能手1 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04 中的服务器 —— 知识点详解 (22)
服务器·学习·ubuntu