腾讯面经,有点难度~

今天分享组织内的朋友在腾讯安全的实习面经。
内容涵盖了 QPS 测试方法、SQL 聚合查询、Linux 进程管理、Redis 数据结构与持久化、NAT 原理、Docker 隔离机制、Go 语言 GMP 调度模型、协程控制、系统调用流程、变量逃逸分析及 map 操作等等知识点。
下面是我整理的面经详解:
面经详解
一个表,里面有数据列,id, name, class,查学生最喜欢的前 10 个课程,sql 语句实现
解释:
SELECT class, COUNT(*) AS popularity
:选择class
列,并对每个class
分组内的记录进行计数,将计数结果命名为popularity
。FROM your_table_name
:指定要查询的表名。GROUP BY class
:按照class
列进行分组,这样每个不同的class
会形成一个组。ORDER BY popularity DESC
:按照popularity
列进行降序排序,使得受欢迎程度高的课程排在前面。LIMIT 10
:只返回前 10 条记录,即最受欢迎的前 10 个课程。
怎么用 linux 命令查 PID,ps grep, pgrep, top
ps + grep:
示例:
ps -ef | grep "program_name"
。ps -ef
会列出所有进程的详细信息,grep "program_name"
会在这些信息中过滤出包含program_name
的行,其中就包含了进程的 PID。例如,要查找nginx
进程的 PID,可以使用ps -ef | grep "nginx"
。pgrep:
示例:
pgrep "program_name"
。pgrep
命令会直接根据进程名查找并返回进程的 PID。例如,pgrep "httpd"
会返回所有httpd
进程的 PID。top:
运行
top
命令会进入一个动态的进程监控界面,在这个界面中可以看到各个进程的信息,包括 PID。可以使用/
键输入进程名进行搜索,找到目标进程的 PID。按q
键可以退出top
界面。
查当前有哪些进程, ps
ps -e:显示所有进程的信息,包括其他用户的进程。例如,
ps -e
会列出系统中所有正在运行的进程。ps -ef:显示更详细的进程信息,包括进程的完整命令行、父进程 ID 等。例如,
ps -ef
可以清晰地看到每个进程的启动命令和父进程的关系。ps aux:也是常用的查看所有进程信息的命令,
a
表示显示所有用户的进程,u
表示以用户为中心显示详细信息,x
表示显示没有控制终端的进程。例如,ps aux
可以全面地查看系统中所有进程的详细状态。
Redis 有哪些数据结构
String(字符串):是 Redis 最基本的数据结构,它可以存储字符串、整数或浮点数。可以进行设置值、获取值、递增、递减等操作,常用于缓存、计数器等场景。
Hash(哈希):是一个键值对的集合,适合存储对象。例如,可以将用户信息存储在一个哈希中,每个字段对应一个属性,如用户名、年龄等。
List(列表):是一个双向链表,支持在列表的两端进行插入和删除操作。可以用于实现消息队列、栈等数据结构。
Set(集合):是一个无序且唯一的数据集合,支持添加、删除、判断元素是否存在等操作。可以用于去重、交集、并集等操作,例如实现共同好友功能。
ZSet(有序集合):和集合类似,但每个元素都有一个分数,根据分数进行排序。常用于排行榜、热门列表等场景。
持久化 RDB,AOF,save/applendly
RDB(Redis Database):
原理:RDB 是将 Redis 在某个时间点的数据快照保存到磁盘上。可以通过手动执行
SAVE
或BGSAVE
命令来触发持久化操作,也可以通过配置定期执行。优点:文件紧凑,适合用于备份和恢复大规模数据,恢复速度快。
缺点:可能会丢失最后一次快照之后的数据,因为它是定期进行快照的。
AOF(Append Only File):
原理:AOF 是将 Redis 执行的所有写操作以日志的形式追加到文件末尾。当 Redis 重启时,会重新执行这些写操作来恢复数据。
优点:数据安全性高,因为它记录了每一个写操作,最多只会丢失最后一次同步到磁盘的数据。
缺点:文件体积较大,恢复速度相对较慢。
save/apply:
SAVE
命令会阻塞 Redis 服务器,直到 RDB 文件创建完成,期间服务器不能处理其他请求。BGSAVE
命令会在后台异步创建 RDB 文件,不会阻塞服务器的正常运行。
Zset 的底层原理。哈希和跳表
哈希表:Zset 使用哈希表来存储成员和分数的映射关系,这样可以在 O(1)O(1) 的时间复杂度内快速查找某个成员的分数。
跳表:跳表是一种有序的数据结构,它通过在每个节点中维护多个指针,使得查找、插入和删除操作的平均时间复杂度为 O(logn)O(log**n) 。Zset 使用跳表来实现按分数排序的功能,这样可以快速找到分数范围内的成员。当进行范围查询时,跳表可以高效地定位到起始位置,并按照顺序遍历成员。
nat 是怎么实现的,如果内网有两个不同 ip 访问百度,百度返回消息,怎么知道要返回给哪个 ip?
NAT(Network Address Translation)实现原理:
IP 地址转换:NAT 设备会将内网的私有 IP 地址转换为外网的公有 IP 地址。当内网主机发起对外网的请求时,NAT 设备会将请求报文中的源 IP 地址替换为自己的公有 IP 地址。
端口映射:除了 IP 地址转换,NAT 设备还会进行端口映射。它会为每个内网主机的请求分配一个唯一的端口号,将这个端口号与内网主机的 IP 地址和端口号进行映射,并记录在映射表中。
映射表:NAT 设备维护一个映射表,记录了内网 IP 地址、端口号与外网 IP 地址、端口号的对应关系。当外网服务器返回响应时,NAT 设备会根据响应报文中的目的 IP 地址和端口号,在映射表中查找对应的内网主机,并将响应转发给该主机。
百度返回消息的处理:当内网有两个不同 IP 访问百度时,NAT 设备会为这两个请求分配不同的端口号。百度返回的消息中包含目的 IP 地址(NAT 设备的公有 IP 地址)和目的端口号,NAT 设备根据目的端口号在映射表中查找对应的内网主机 IP 地址和端口号,然后将消息转发给相应的内网主机。
ping 操作,为什么用 ping 域名没通,ping ip 通了,是因为什么
DNS 解析问题:当使用
ping
域名时,系统需要先通过 DNS 服务器将域名解析为对应的 IP 地址。如果 DNS 服务器配置错误、DNS 服务器故障或域名解析记录存在问题,就会导致域名无法解析为正确的 IP 地址,从而使得ping
域名不通。而直接ping
IP 地址则绕过了 DNS 解析过程,所以可以正常通信。防火墙问题:防火墙可能会限制对某些域名的访问,但不会限制对 IP 地址的访问。例如,防火墙可能配置了只允许访问特定 IP 地址的规则,而对域名的访问进行了阻止。在这种情况下,
ping
域名会失败,而ping
IP 地址可以正常进行。
docker 的隔离性内部是怎么实现的,命名空间,
Docker 的隔离性主要通过 Linux 内核的命名空间(Namespaces)和控制组(cgroups)来实现,这里主要介绍命名空间:
PID 命名空间:每个 Docker 容器都有自己独立的 PID 命名空间,这意味着容器内的进程有自己独立的进程 ID,与宿主机和其他容器内的进程 ID 相互隔离。容器内的进程只能看到自己命名空间内的进程,无法直接访问宿主机或其他容器内的进程。
Network 命名空间:每个容器都有自己独立的网络命名空间,拥有独立的网络栈,包括网络接口、IP 地址、路由表等。容器可以有自己的 IP 地址和端口,与宿主机和其他容器的网络环境相互隔离。
Mount 命名空间:容器有自己独立的挂载点,这意味着容器内的文件系统与宿主机和其他容器的文件系统相互隔离。容器可以有自己独立的根文件系统,并且可以在容器内进行挂载和卸载操作,而不会影响到宿主机和其他容器。
UTS 命名空间:容器可以有自己独立的主机名和域名,与宿主机和其他容器的 UTS(Unix Time - Sharing System)环境相互隔离。这样,容器内的进程可以认为自己运行在一个独立的主机上。
IPC 命名空间:容器有自己独立的进程间通信(IPC)机制,如共享内存、消息队列等。容器内的进程只能与同一容器内的其他进程进行 IPC 通信,无法直接与宿主机或其他容器内的进程进行 IPC 通信。
GMP
定义:GMP 是 Go 语言的调度模型,包含三个核心组件:
G(Goroutine):轻量级线程,由 Go 运行时管理。
M(Machine):操作系统线程,实际执行 Goroutine 的实体。
P(Processor):逻辑处理器,管理本地 Goroutine 队列和资源。
调度机制:
P 绑定到 M 后,从本地队列或全局队列获取 G 执行。
当 G 遇到阻塞(如系统调用),M 会释放 P 并进入阻塞状态,其他 M 可接管 P 继续执行其他 G。
通过工作窃取(Work Stealing)平衡各 P 的负载。
优势:减少线程切换开销,实现高并发和高效资源利用。
defer 与 panic,10 个协程用 ctrl+c 停止,会发生什么
defer 与 panic 的执行顺序:
触发
panic
后,当前函数会停止执行,逐层执行defer
。若
defer
中调用recover()
,程序恢复执行;否则程序终止。10 个协程被 ctrl+c 停止的影响:
ctrl+c
发送SIGINT
信号,默认终止整个进程。所有协程会立即停止,未执行的
defer
可能无法触发。资源泄漏风险:如未释放的文件句柄、数据库连接等。
解决方案:监听信号量(如
signal.Notify
),在退出前执行清理逻辑。
一个协程在什么情况下可以直接停止,如何杀死它,抢占
协程无法被直接杀死:
Go 语言设计上不提供强制终止协程的 API,需通过协作式退出(如
context.Context
传递取消信号)。抢占式调度条件:
协程执行时间超过 10ms(Go 1.14+支持基于信号的抢占)。
协程进入函数调用或发生系统调用。
实现优雅退出:
系统调用的过程是怎样的
简单流程:
用户程序通过库函数(如
read()
)触发系统调用。CPU 切换到内核态,执行内核中对应的系统调用处理函数。
内核完成操作后,返回结果到用户态,程序继续执行。
详细步骤:
参数准备:用户程序将系统调用号和参数存入寄存器。
触发软中断:通过
syscall
指令或中断向量(如 x86 的int 0x80
)进入内核。内核路由:根据系统调用号查表跳转到对应处理函数(如
sys_read
)。权限检查:验证用户空间内存地址合法性。
执行操作:内核完成文件读写、进程创建等操作。
返回结果:将返回值存入寄存器,切换回用户态。
逃逸场景
变量逃逸到堆的常见场景:
返回局部变量地址:函数返回局部变量的指针,导致变量生命周期延长。
闭包引用:闭包引用外部变量,变量需在堆上分配。
动态类型:如
interface{}
类型变量可能触发逃逸。大对象:超过栈空间限制的对象(默认栈大小 1~2MB)。
发送指针到 channel:接收方可能在其他协程访问变量。
查看逃逸分析:通过
go build -gcflags="-m"
编译输出逃逸信息。
栈空间的最大分配量
默认限制:Go 1.18+中,单个协程栈初始大小为 2KB,动态扩容上限通常为 1GB(受操作系统限制)。
调整栈大小:可通过
runtime/debug.SetMaxStack
修改最大栈大小,但需谨慎避免内存浪费。
局部变量分配到栈还是堆
分配规则:
栈分配:变量生命周期仅限于函数内,且未发生逃逸。
堆分配:变量被外部引用(逃逸),或大小超过栈容量。
示例:
map 判断数据是否存在及删除键
判断存在性:
删除键:
底层实现:
哈希表结构:通过哈希函数计算桶位置,解决冲突采用链表或开放寻址。
删除操作:标记对应键值对为删除状态,后续插入可复用空间。
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:infoq 面试群。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/983d518c882df36738a7e5fd3】。文章转载请联系作者。
评论