你说这个比malloc效率高,那malloc是个什么样的过程你知道吗?
知道。malloc 并不是每次分配内存都会直接向操作系统申请物理内存。
首先它会对申请大小进行对齐,并优先从线程缓存,比如 tcache,以及分配器维护的其他空闲内存块中寻找合适的块。free 释放的小块内存通常也不会马上归还操作系统,而是先缓存起来供后续复用。
如果现有的空闲内存不足,malloc 才会通过 brk 扩展堆,或者通过 mmap 申请新的虚拟地址空间。
这里申请到的主要是虚拟地址空间,对应的物理页通常是按需分配的。程序真正访问这些页面时,如果还没有建立有效的物理页映射,就会发生缺页异常,由操作系统分配物理页并更新页表。
哈希表你知道是个什么功能吗?
哈希表是一种通过哈希函数把键映射到数组下标的数据结构,用来实现键值对的快速查找、插入和删除。
平均时间复杂度是 O(1) ,发生哈希冲突时通常通过链地址法或开放寻址法解决;。C++ 中常用的 unordered_map 和 unordered_set 就是哈希表。
创建本地分支可以使用:
git branch 分支名
这条命令只创建分支,不会切换过去。
如果希望创建后立即切换,可以使用:
git switch -c 分支名
操作系统这门课主要讲了哪些操作系统?
课程主要以 Linux/Unix 为重点,学习了进程与线程、内存管理、文件系统、I/O 等内容;
这些基本模块里,哪个模块最擅长?
进程和线程
每个进程都有自己的进程地址空间,内部有pcb,有指向文件描述符表的指针,指向虚拟地址空间的指针和一些其他的属性,
fork() 创建子进程。子进程会继承父进程的大部分资源如虚拟地址空间,文件描述符表,但地址空间使用写时复制,刚创建时并不会立即复制所有内存。
之后通常调用:
execve(...);
execve 不会创建新进程,而是用新的程序内容替换当前进程的地址空间。
父进程可以调用:
waitpid(pid, ...);
等待并回收子进程。
线程是轻量级进程,进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位
一个进程可以包含多个线程。多个线程共享整个进程的虚拟地址空间和文件描述符表,但在这个地址空间内部,每个线程有自己单独的一块栈空间。每个线程独立拥有自己的执行上下文,包括程序计数器、寄存器上下文、栈和调度信息。寄存器并不是每个线程物理独占一套,而是线程切换时由操作系统保存和恢复,从而让每个线程看起来都有自己的一套寄存器状态。
C++ 中:
std::thread t(func);
底层通常会进入 pthread,再通过 Linux 的线程创建机制创建内核可调度的线程。
内存管理
程序使用的是虚拟地址,不是直接使用物理内存地址。
CPU 访问虚拟地址时,大致经过:
虚拟地址
↓
TLB 或多级页表
↓
物理内存地址
硬件中的 MMU 负责地址转换。
每个进程通常拥有独立的虚拟地址空间,因此不同进程可以使用相同的虚拟地址,而实际映射到不同的物理内存。
一个典型进程的地址空间包括:
代码段
只读数据段
已初始化数据段
未初始化数据段
堆
共享库和 mmap 区域
栈
用户空间和内核空间通常分开:
- 用户程序运行在用户态
- Linux 内核运行在内核态
- 用户程序通过系统调用进入内核态
这种隔离可以防止普通程序直接破坏内核或其他进程。
3. 分页和页表
Linux 通常以页为单位管理内存,常见页大小是 4 KB。
页表记录:
虚拟页号 → 物理页框
页表项中还包含:对与这个地址的一些访问权限等
TLB 是 CPU 中保存部分地址转换结果的高速缓存,可以减少频繁查询页表的开销。
4. 缺页异常
如果进程访问的虚拟页没有映射到物理内存,就会产生缺页异常。
缺页异常不一定代表错误,可能是正常行为,例如:
- 第一次访问刚分配的内存
- 需要把文件内容加载到内存
- 需要执行写时复制
- 页面被换出到磁盘,需要重新加载
内核处理完成后,程序通常可以继续执行。
如果访问了完全非法的地址,或者违反了页面权限,就可能收到:
SIGSEGV
也就是常说的段错误。
6. 写时复制
fork() 后,父子进程最初可以共享相同的物理页面。
当某一方尝试修改页面时,内核才复制出一份新的物理页,再让修改方使用新页面。这就是写时复制,简称 COW。
它可以避免 fork 时立即复制整个进程的内存,提高创建进程的效率。
7. 内存回收
物理内存紧张时,Linux 可能:
- 回收文件页缓存
- 回收不常用的匿名页
- 将页面换出到 Swap
- 触发 OOM Killer,终止某些进程
因此,进程的虚拟内存大小不一定等于它当前实际占用的物理内存。
文件系统
磁盘上是按柱面磁道扇区来定位地址的,通过文件系统我们可以以连续地址的方式访问磁盘数据
磁盘先分区然后在分区上创建文件系统,把这个分区分成多个 block group,每个组中会有superblock inode bitmap、block bitmap、inode table 和 data block 等结构。访问一个文件时,VFS 会按照路径逐级解析,并优先查询 dentry cache。如果 dentry 命中,就可以快速得到对应 inode;如果没有命中,则通过根目录一层一层找到父目录 inode 找到该目录的数据块,在目录数据中查找"文件名到 inode 号"的映射,然后再获取对应 inode,在对应组内部添加相应信息,并建立 dentry 缓存。看看页缓存有没有inode对应数据,有的话直接拿没有去磁盘取根据 inode 中记录的 extent 等数据块映射信息,找到文件实际的数据块并读取内容。之后再次读取文件就通过fd指向struct file内部指向inode结构体,直接找到数据位置。
Linux 通过 VFS,也就是虚拟文件系统,向应用程序提供统一接口,应用程序不需要关心底层
硬链接和软链接的区别:
- 硬链接:多个文件名指向同一个 inode
- 软链接:保存另一个文件的路径
如果原文件删除,硬链接仍然可以访问数据;软链接可能变成悬空链接。
IO
就从操作系统向磁盘这些硬件设备读取写入内容
阻塞 I/O 的特点是:如果数据还没有准备好,调用线程会睡眠。
设置非阻塞后,如果数据暂时没有准备好,系统调用不会睡眠,而是立即返回,例如返回:
EAGAIN
应用程序需要自己再次尝试,这可能造成忙轮询,所以通常会和 I/O 多路复用一起使用。多路复用允许一个线程同时监控多个文件描述符。哪个文件描述符准备好了才通知对方
主要用epoll 有两种常见工作方式:
- 水平触发:只要还有数据可读,就会持续通知
- 边缘触发:状态从未就绪变为就绪时通知一次
边缘触发通常需要配合非阻塞 I/O,并循环读取,直到返回 EAGAIN,否则可能遗漏后续事件。
在您未来的职业规划中,希望寻找一个什么样的平台和团队?
我希望加入一个有实际业务和技术挑战的平台,能够参与核心 C++ 模块的开发,在项目中持续提升自己的工程能力。团队方面,希望大家目标一致、互相支持,也能给新人承担责任和成长的机会。长期来看,我希望和平台一起发展,持续创造价值。
对于岗位本身的发展,您的自我规划是什么?
我会分阶段规划。前期先熟悉业务、代码和开发流程,夯实 C++、Linux、并发和网络等基础,尽快独立负责模块;中期提升系统设计、性能优化和问题排查能力,能够承担更复杂的任务;长期希望成为团队中可靠的技术骨干,为项目持续创造价值。
您在使用 AI 工具吗?主要包括哪些?AI 是如何赋能您目前的工作的?请详细描述过程和当前掌握程度。
我平时会使用 AI 工具,主要包括 ChatGPT、GLM等通用大模型,以及 opencode或 mimocode codex等 这类代码辅助工具。
我的使用流程通常是:先自己分析背景,目标,解决问题步骤,让ai如何响应或者是针对什么用户针对什么情况把这些喂给ai,再把必要的代码、日志、接口和错误信息提供给 AI,让它辅助进行知识学习、方案设计、代码补全、测试编写和问题排查。AI 给出结果后,我会结合代码和实际运行结果进行验证,最后再自己修改和 review。
对 C++ 开发来说,AI 可以帮助我快速理解陌生代码,分析内存、并发、Linux 和网络问题,生成样例代码和测试用例,也能比较不同实现方案,提高开发和学习效率。
目前我掌握的是 AI 的应用层能力,能够拆解问题、组织上下文、编写有效提示词,并验证和落地 AI 的输出。对langchain有基础了解,但目前主要把 AI 作为开发辅助工具。涉及公司代码时,我也会进行脱敏,并遵守数据安全规范。
在必须按时交付的压力下,你是如何兼顾速度与质量的?
我会先和负责人确认交付范围、优先级和验收标准,把任务拆分成核心功能和后续优化,优先完成验收标准。开发过程中尽量复用成熟方案,采用小步提交,并同步进行单元测试、关键路径测试和代码 review。必要时调整范围或安排资源,确保在按时交付的同时守住功能正确性和稳定性底线。
你最近一段实习没有考虑转正是吗?为何选择这家初创公司?
是的,当时没有考虑转正,主要是实习岗位的后续发展方向与我的长期规划不完全一致,属于职业选择,并不是对公司或团队不认可。
选择初创公司,是因为岗位内容与我的 C++ 技术方向比较匹配,也能参与从需求、开发到上线的完整流程,承担更多实际责任。我了解初创公司的节奏比较快、要求综合能力较强,也愿意在这样的环境中持续积累和成长。
你认为自己更偏向哪种角色类型?比如技术深耕 or 团队领导?
现阶段我更偏向技术深耕型,希望先在 C++、操作系统、并发和性能优化等方面建立扎实能力,成为可靠的技术骨干。同时我也重视沟通协作,积累经验后愿意承担项目推进、技术分享和新人指导,逐步向技术负责人发展。
你能不能讲一下你当时是遇到了什么问题,接着你是怎么找到解决方案的?讲一下这整个过程。
可以这样回答:
我当时在做基于 Linux Reactor 的 HTTP 服务器,压测时发现 500 个慢客户端同时请求大文件,会导致服务内存持续上涨,正常请求也几乎无法响应。
我先让ai帮我测试几项监控指标,包括进程实际内存 RSS、每个连接待发送的数据量、当前打开的文件描述符数量,以及 Socket 的 EPOLLOUT 状态,用来确认是否存在慢客户端导致的发送阻塞和资源堆积。。最后定位到:客户端发送速度很慢,但服务端仍持续读请求、生成响应,未发送数据不断堆积在队列中,缺少有效的背压和资源上限。
我的解决方案是建立唯一的 FIFO 输出队列,遇到短写或
EAGAIN时保存发送进度;队列达到高水位就暂停EPOLLIN,降到低水位后再恢复。同时在响应入队前统一预留内存、文件 FD 和队列节点配额,使用 RAII 保证异常、关闭和超时路径都能正确释放资源。最后我用功能测试、ASan、TSan 和 Release 压测验证。500 个慢客户端场景下,内存从约 2.6GB 降到 5.3MB,正常请求从无法完成提升到约 4 万 QPS。这个过程让我确认,解决并发问题要先复现和量化,再根据状态和资源流向定位根因,最后用专项测试和压测验证方案。
"我当时遇到的问题是,大模型流式返回的数据,不能直接按每次网络回调来解析。一次回调可能只有半条 JSON,也可能包含多条消息,直接解析容易失败或者丢失内容。
我沿着'接收数据、解析 JSON、回调输出'这条链路排查,发现核心原因是:网络接收块的边界和业务消息的边界不一致。
所以我增加了接收缓冲区,先把数据拼接起来,再按协议分隔符提取完整消息;不完整的部分留到下次继续处理。比如 DeepSeek 按 SSE 的空行分隔,Ollama 按换行分隔,解析后统一通过回调输出增量文本。
验证重点是半条消息、多条消息合并,以及结束标记这些边界情况,检查内容是否完整、顺序是否正确。"