联发科 嵌入式开发 二面 面经总结

1. 简单介绍一下你自己和你做过的项目

参考答案:

面试官您好,我是XXX。我主要的技术方向是嵌入式Linux和C++开发。

我做过一个高性能HTTP服务器项目,使用epoll实现IO多路复用,支持上万并发连接。还实现了一个异步日志库,采用双缓冲和无锁队列技术,保证高性能。另外我还参与过XX项目,负责XX模块。

我对操作系统原理、网络编程、多线程编程比较熟悉,也了解一些硬件相关的知识。希望通过这次面试能有机会深入学习嵌入式底层开发。

2. 你的HTTP服务器项目中,如果并发量继续增大,可能会遇到什么瓶颈?你会如何优化?

参考答案:

如果并发量继续增大,可能会遇到几个瓶颈:

瓶颈分析:

  • 文件描述符限制:Linux默认单进程最多1024个fd,可以通过ulimit -n调整,但也不是无限的
  • 内存瓶颈:每个连接都要占用内存存储状态和缓冲区,百万连接可能需要几GB内存
  • CPU瓶颈:虽然epoll是O(1)的,但处理大量事件仍需要CPU时间
  • 网络带宽:这是物理限制

优化方案:

架构层面,我会采用多进程架构,主进程负责监听,fork多个工作进程,每个进程处理一部分连接,利用多核CPU。可以用SO_REUSEPORT让多个进程监听同一端口,内核自动负载均衡。

内存优化方面,连接对象用内存池管理,读写缓冲区按需分配,空闲连接不占用缓冲区。还可以用零拷贝技术,sendfile直接从文件发送到socket,避免用户态和内核态之间的数据拷贝。如果是静态文件服务器,可以用mmap将文件映射到内存,多个连接共享同一份文件内容。

连接管理方面,对于长连接要设置合理的keepalive时间,定时清理僵尸连接。还要调整内核参数,比如增大TCP缓冲区、启用TCP_DEFER_ACCEPT、调整TIME_WAIT时间等。

如果单机还不够,就要考虑分布式架构,用负载均衡器分发请求到多台服务器。

3. 说说Linux下虚拟内存的工作原理,页表是如何组织的?什么是TLB?

参考答案:

虚拟内存是操作系统提供的抽象,每个进程看到的是连续的虚拟地址空间,实际映射到不连续的物理内存。虚拟地址通过MMU硬件转换为物理地址,转换的依据是页表。虚拟内存的好处是进程间内存隔离、支持比物理内存更大的地址空间、方便内存管理和共享。

页表组织:

页表的组织通常是多级结构。以x86-64为例,使用四级页表,虚拟地址64位中有效的48位被分成5部分:9位页全局目录、9位页上级目录、9位页中间目录、9位页表、12位页内偏移。每一级页表都是一个数组,用虚拟地址的相应位作为索引,找到下一级页表的物理地址,最后一级页表找到物理页框号,加上页内偏移就得到物理地址。多级页表的好处是节省空间,只为实际使用的虚拟地址分配页表,代价是多次内存访问。

TLB机制:

TLB是Translation Lookaside Buffer,是CPU内的缓存,存储最近使用的虚拟地址到物理地址的映射。因为页表在内存中,每次地址转换都要访问内存太慢,TLB缓存了常用的映射,命中TLB时地址转换只需几个时钟周期。TLB通常很小,几十到几百个表项,采用全相联或组相联结构。

进程切换时需要刷新TLB,因为不同进程的虚拟地址映射不同,现代CPU支持ASID标识不同进程,避免每次切换都刷新。修改页表后也要刷新TLB,否则会用到旧的映射。

TLB对性能影响很大,TLB miss会导致页表遍历,慢很多。提高TLB命中率的方法有使用大页,比如2MB或1GB的页,可以用更少的TLB表项覆盖更大的地址空间。还要注意内存访问的局部性,尽量连续访问内存。

4. 说说Linux下进程调度的原理,CFS调度器是如何工作的?实时调度和普通调度有什么区别?

参考答案:

Linux的进程调度负责决定哪个进程获得CPU时间。Linux支持多种调度策略,普通进程用CFS完全公平调度器,实时进程用FIFO或RR调度器。

CFS调度器:

CFS的核心思想是让每个进程获得公平的CPU时间。它维护一个红黑树,按进程的虚拟运行时间vruntime排序,vruntime小的进程排在左边。调度时选择vruntime最小的进程运行,运行一段时间后vruntime增加,重新插入红黑树。这样保证了所有进程的vruntime趋于相等,实现公平。

进程的优先级通过nice值体现,nice值越小优先级越高,vruntime增长越慢,获得更多CPU时间。CFS没有固定的时间片,而是根据进程数量动态调整,保证调度延迟在可接受范围内。

实时调度:

实时调度和普通调度的区别在于,实时进程的优先级高于所有普通进程,只要有实时进程就绪,就会抢占普通进程。

实时调度有两种策略:

  • SCHED_FIFO是先进先出,高优先级进程一直运行直到主动让出CPU或被更高优先级进程抢占
  • SCHED_RR是轮转,同优先级的进程按时间片轮流执行

实时调度适合对延迟敏感的任务,但要小心使用,实时进程如果不主动让出CPU会饿死其他进程。Linux还支持SCHED_DEADLINE调度,基于截止时间调度,适合周期性实时任务。

调度器的选择要根据应用需求,普通应用用CFS,对延迟敏感的用实时调度,但要注意实时进程可能影响系统稳定性。

5. 说说C++的内存管理,new和malloc的区别是什么?如何检测内存泄漏?

参考答案:

new和malloc的区别:

new和malloc都用于动态分配内存,但有几个重要区别:

  • new是C++运算符,malloc是C库函数
  • new分配内存后会调用构造函数初始化对象,delete释放内存前会调用析构函数,而malloc/free只是分配和释放内存,不会调用构造析构函数
  • new返回的是对象类型的指针,类型安全,malloc返回void*需要强制转换
  • new分配失败会抛出bad_alloc异常,malloc返回NULL
  • new的大小由编译器计算,malloc需要手动指定字节数
  • new的底层通常会调用malloc,但也可以重载operator new使用自定义的内存分配器

还有placement new,可以在已分配的内存上构造对象,不分配新内存。

检测内存泄漏:

检测内存泄漏有几种方法:

  • 最简单的是在程序结束时检查分配和释放是否配对,可以重载new/delete记录分配信息
  • valgrind的memcheck工具可以检测内存泄漏、越界访问、使用未初始化内存等问题,虽然会让程序变慢但很有效
  • AddressSanitizer是编译器内置的工具,编译时加-fsanitize=address,运行时会检测内存错误,性能损失比valgrind小
  • 使用智能指针避免内存泄漏,unique_ptr和shared_ptr会自动释放内存

在代码审查时要注意new和delete配对,异常安全,避免在构造函数中分配资源但析构函数未释放。使用RAII原则,资源获取即初始化,对象销毁时自动释放资源。

在嵌入式系统中,内存泄漏更严重,因为内存有限,长时间运行后可能耗尽内存。我会尽量避免动态分配,用静态分配或内存池。如果必须动态分配,会用智能指针管理,定期检查内存使用情况,用工具检测泄漏。

6. 说说你对多线程编程的理解,线程安全是什么?如何实现线程安全的单例模式?

参考答案:

多线程编程是指一个进程内有多个执行流并发执行,可以充分利用多核CPU,提高程序性能。但多线程也带来了复杂性,主要是共享数据的同步问题。

线程安全:

线程安全是指多个线程访问共享数据时,程序的行为是正确的,不会出现数据竞争、死锁等问题。实现线程安全的方法有加锁保护临界区、使用原子操作、使用线程局部存储、使用无锁数据结构等。

线程安全的单例模式:

单例模式保证一个类只有一个实例,线程安全的单例有几种实现方式:

  • 饿汉式:在程序启动时就创建实例,线程安全但可能浪费内存
  • 懒汉式:在第一次使用时创建,但需要考虑线程安全
  • 双检锁模式:先检查实例是否存在,不存在再加锁创建,但在C++中需要注意内存序问题,要用atomic和memory_order_acquire/release

C++11提供了更简单的方法,利用局部静态变量的初始化是线程安全的

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

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

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

全部评论

相关推荐

点赞 评论 收藏
分享
zbk1:学院本找嵌入式我觉得不太行,不要被培训班忽悠了,老老实实读个研吧。
点赞 评论 收藏
分享
03-04 07:14
门头沟学院 C++
何木健一:去啥?你能考虑去就是思想有问题,当然一周到岗一天可以考虑一下😨
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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