奇梦者 嵌入式开发软件开发 二面 面经

1. 简单介绍一下你自己和你最有成就感的项目

参考答案:

面试官您好,我是XXX。我主要的技术方向是嵌入式Linux和C++开发,有两年多的项目经验。

最有成就感的项目是我做的高性能HTTP服务器,这个项目让我对网络编程、并发处理、性能优化有了深入理解。

这个项目的挑战在于要支持高并发,我从零开始设计架构,选择了Reactor模式,用epoll实现IO多路复用,用线程池处理业务逻辑。

开发过程中遇到了很多问题,比如性能瓶颈、内存泄漏、多线程竞争等,通过性能分析、代码优化、架构调整逐一解决。最终实现了支持上万并发连接,QPS达到几万的目标。

这个项目让我学到了很多,不仅是技术层面的知识,还有如何分析问题、如何做技术选型、如何优化性能等能力。也让我认识到,做好一个项目需要扎实的基础、系统的思维、持续的学习和解决问题的决心。

2. 你的HTTP服务器项目中,详细说说epoll的工作原理,ET和LT模式有什么区别?如何选择?

参考答案:

epoll是Linux下高效的IO多路复用机制,解决了select和poll的性能问题。

epoll的核心系统调用:

  • epoll_create创建epoll实例
  • epoll_ctl注册或修改监听的文件描述符
  • epoll_wait等待事件发生

工作原理:

内核维护一个红黑树存储所有监听的fd,维护一个就绪链表存储有事件发生的fd。当fd上有事件发生时,内核通过回调函数将fd加入就绪链表。epoll_wait只需遍历就绪链表,时间复杂度是O(1),而select和poll需要遍历所有fd,时间复杂度是O(n)。这就是epoll在大量连接时性能优势明显的原因。

ET和LT模式的区别:

LT水平触发(默认模式):只要fd上有数据可读或可写,epoll_wait就会返回该fd。如果一次没有读完数据,下次epoll_wait还会返回该fd,使用简单但可能导致频繁触发。

ET边缘触发:只在状态变化时通知一次,比如从无数据到有数据,从不可写到可写。必须一次性把数据读完或写完,否则不会再通知,需要配合非阻塞IO和循环读写,使用复杂但效率更高。

如何选择:

如果追求简单可靠,用LT模式。如果追求极致性能,用ET模式。

我在项目中用的是ET模式,配合非阻塞IO,读数据时循环读取直到返回EAGAIN,写数据时如果缓冲区满了就注册EPOLLOUT事件,等可写时继续写。这样减少了epoll_wait的调用次数,提高了性能。

使用ET模式的注意事项:

  • 必须设置非阻塞IO,否则可能阻塞
  • 必须循环读写直到EAGAIN,否则会丢失数据
  • 要正确处理EAGAIN和EINTR错误
  • 要注意边界条件,比如对端关闭连接的情况

3. 说说你对虚拟内存的理解,进程的地址空间是如何布局的?什么是内存映射?

参考答案:

虚拟内存的概念:

虚拟内存是操作系统提供的抽象,让每个进程都认为自己拥有独立的、连续的地址空间。实际上,虚拟地址通过页表映射到物理内存,物理内存可能是不连续的。

虚拟内存的好处是进程间内存隔离、支持比物理内存更大的地址空间、方便内存管理和共享。

进程地址空间布局(从低到高):

  • 代码段:存储程序的机器指令,只读可执行
  • 数据段:已初始化数据段和BSS段,存储全局变量和静态变量
  • 堆:从低地址向高地址增长,用于动态内存分配
  • 内存映射区:用于mmap映射文件或共享内存
  • 栈:从高地址向低地址增长,存储函数调用的局部变量、参数、返回地址
  • 内核空间:最高地址,用户进程不能直接访问

内存映射mmap:

将文件或设备映射到进程的地址空间,之后可以像访问内存一样访问文件。

mmap的优点:

  • 减少数据拷贝,文件内容直接映射到用户空间
  • 不需要read/write系统调用在用户态和内核态之间拷贝数据
  • 操作系统自动做页面缓存和预读,提高效率

mmap的用途:

  • 大文件读写,比传统的read/write快
  • 进程间共享内存,多个进程mmap同一个文件
  • 访问硬件设备,如framebuffer、寄存器

使用mmap的注意事项:

  • 映射的大小不能超过文件大小,否则会触发SIGBUS信号
  • 写入后要调用msync同步到磁盘
  • munmap时要确保没有线程在访问映射区域
  • 要处理好文件大小变化的情况

4. 说说Linux下的进程和线程,它们的区别是什么?线程的实现方式有哪些?

参考答案:

基本概念:

进程是资源分配的基本单位,线程是调度的基本单位。进程拥有独立的地址空间、文件描述符、信号处理等资源,而线程共享进程的资源,每个线程只有独立的栈、寄存器和线程局部存储。

Linux的实现:

从内核角度看,Linux使用统一的task_struct表示执行单元,进程和线程都是task,区别在于创建时的clone标志。创建进程用fork,不共享资源。创建线程用clone,共享地址空间、文件描述符等资源。所以Linux的线程也叫轻量级进程LWP。

主要区别:

资源开销:创建进程需要拷贝页表、分配新的地址空间,开销大。创建线程只需分配栈空间,开销小。

通信方式:进程间通信需要IPC机制,比如管道、消息队列、共享内存等。线程间通信直接访问共享内存,但需要同步机制保护。

切换开销:进程切换需要切换地址空间、刷新TLB,开销大。线程切换只需切换栈和寄存器,开销小。

线程的实现方式:

用户级线程:完全在用户空间实现,内核不感知,线程切换不需要陷入内核,开销小,但无法利用多核,一个线程阻塞会导致整个进程阻塞。

内核级线程:由内核管理,可以利用多核,一个线程阻塞不影响其他线程,但切换需要陷入内核,开销大。

混合线程:结合了两者,用户空间有多个用户线程,映射到少量内核线程,平衡了性能和并发度。

实际应用:

Linux的线程是内核级线程,通过NPTL库实现,性能很好。POSIX线程pthread是标准的线程API,C++11引入了标准线程库std::thread,跨平台,使用更方便。

选择进程还是线程要看需求。如果需要隔离性、稳定性,用进程。如果需要高性能、频繁通信,用线程。很多服务器采用多进程+多线程的架构。

5. 说说你对死锁的理解,死锁的四个必要条件是什么?如何预防和检测死锁?

参考答案:

死锁的定义:

死锁是指多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。

死锁的四个必要条件:

  • 互斥条件:资源不能共享
  • 持有并等待:线程持有资源的同时等待其他资源
  • 不可剥夺:资源不能被强制抢占
  • 循环等待:存在线程等待环路

预防死锁:

破坏互斥条件:比较难,因为很多资源本身就是互斥的。

破坏持有并等待:要求线程一次性获取所有需要的资源,但会降低并发度和资源利用率。

破坏不可剥夺:让线程在获取资源失败时释放已持有的资源,用trylock实现。

破坏循环等待:最常用,规定所有线程按相同顺序获取锁,比如总是先获取ID小的锁。

检测

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式面试八股文全集 文章被收录于专栏

这是一个全面的嵌入式面试专栏。主要内容将包括:操作系统(进程管理、内存管理、文件系统等)、嵌入式系统(启动流程、驱动开发、中断管理等)、网络通信(TCP/IP协议栈、Socket编程等)、开发工具(交叉编译、调试工具等)以及实际项目经验分享。专栏将采用理论结合实践的方式,每个知识点都会附带相关的面试真题和答案解析。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务