影石系统组 一二面OC面经

一面

1. 封装、继承、多态分别是什么?什么是封装?继承有哪些继承方式?

  1. 封装:将属性和方法封装到类中,隐藏内部实现细节,对外提供访问接口,保护数据。
  2. 继承 :子类复用父类的成员,扩展功能。
    • C++ 继承权限:公有 public、保护 protected、私有 private;还有形式上单继承、多继承、菱形继承。
    • Java 只支持单继承,无保护 / 私有继承。
  3. 多态:同一个方法调用,根据对象实际类型执行不同实现,依托继承 + 方法重写 + 父类引用指向子类对象。

2. 类中使用static修饰函数和变量,分别有什么作用?静态成员变量什么时候初始化?静态成员函数和普通成员函数相比,有什么限制?

  1. static 修饰变量:静态成员变量属于类,所有对象共享同一份,无需实例对象即可访问。
    • C++:main 执行前,类外初始化,仅一次;
    • 可以用来统计对象数量、全局计数器、类公共常量等。
    • 不随对象创建而分配内存,不随对象销毁释放。
  2. static 修饰函数:静态成员函数属于类,无需实例对象即可调用。
    • 静态函数属于类,不需要创建对象就可以直接调用 类名::函数名()。 一般用来操作静态成员变量,提供类层面的工具方法。
  3. 静态函数限制:没有 this 指针,无法访问非静态成员变量、无法调用非静态成员函数。

3. 类对象有哪些初始化方式?

  1. 默认构造初始化
  2. 有参构造函数初始化
  3. 拷贝构造初始化
  4. 初始化列表(构造初始化列表,推荐,优先执行)
  5. 移动构造初始化

4. 移动构造函数有什么特点?

  1. 参数为本类右值引用T&&;
  2. 转移临时对象资源,不做深拷贝,性能高;
  3. 接管资源后,源对象要置空,防止析构重复释放;
  4. 仅接收右值,可通过std::move将左值转为右值强制移动;
  5. 若自己写了析构 / 拷贝构造 / 拷贝赋值,编译器不再自动生成默认移动构造;
  6. 建议加noexcept,便于 STL 容器优化扩容。

5. 如何理解C++中的左值和右值?

  1. 左值:有名字、可取地址,生命周期长;
  2. 右值:临时对象 / 字面量,无名字,不可取地址,包含纯右值、将亡值;
  3. T&绑左值;const T&左右都能绑;T&&只能绑右值;
  4. std::move 仅仅类型转换,不移动资源,将左值转为将亡值;
  5. 移动构造参数是T&&,接收右值,实现资源转移。

6. Lambda表达式的机制是什么?Lambda表达式有什么作用?

一、Lambda 作用

Lambda 是匿名内联函数,可以在代码原地快速定义一个短小的函数,不用单独写全局函数 / 类成员函数。

  1. 简化代码,不用单独命名函数,适合 STL 算法(sort、for_each)的回调;
  2. 可以捕获当前作用域的变量,使用外面的数据,普通函数做不到;
  3. 可作为回调、异步任务、线程函数传入。

示例:

复制代码
sort(vec.begin(), vec.end(), [](int a,int b){return a>b;});

二、底层机制(核心考点)

Lambda 本质:编译器自动生成一个匿名类(仿函数类),然后创建这个类的临时对象。 仿函数:重载了operator()的类对象,可以像函数一样调用。

完整语法回顾

复制代码
[捕获列表](参数列表) mutable -> 返回类型 {函数体}
  1. []捕获列表:决定怎么拿外部变量
  2. ()形参列表
  3. mutable:允许修改值捕获的变量(默认 const)
  4. ->显式指定返回值,可省略(C++14 支持自动推导)
  5. {}函数体

捕获方式

  • [=] 值捕获:拷贝外部变量到仿函数成员,默认 const ,不能直接修改,要加mutable
  • [&] 引用捕获:保存变量引用,直接操作外部原变量
  • [this] 捕获 this 指针,访问类内成员
  • [x] 只值捕获 x;[&x]只引用捕获 x
  • [] 不捕获任何变量

捕获时机:Lambda 对象创建的时候捕获,不是调用的时候! 值捕获是拷贝那一刻的值;引用捕获是引用原变量。

7. Lambda所谓的"更方便",具体体现在哪里?

不用单独定义函数 / 仿函数,就地写回调,就近捕获局部变量,代码逻辑集中在一起,不用来回翻代码。

8. Lambda和普通函数相比有什么优势?能否从Lambda的底层实现讲讲它为什么更方便?

优势:

  1. 就地定义回调,逻辑和调用代码放在一起,阅读方便;
  2. 支持捕获局部变量,自动保存上下文,不需要手动封装结构体;
  3. 匿名,不污染命名空间,适合一次性短逻辑;
  4. 捕获方式灵活,值捕获 / 引用捕获 / 捕获 this 按需选择。

底层原理: Lambda 本质是编译器自动生成匿名仿函数类 ,重载operator();捕获列表决定编译器生成哪些成员变量来保存外部上下文。原来手写仿函数要写的类、成员、构造函数全部由编译器自动生成,所以代码写起来更简洁

9. 是否了解智能指针?什么是共享智能指针shared_ptr?

智能指针是基于 RAII 封装裸指针的类,对象析构时自动释放内存,避免内存泄漏。

shared_ptr 是共享智能指针,底层采用引用计数 ;多个 shared_ptr 可以共享同一块堆资源,每新增一个管理指针计数 + 1,每销毁一个计数 - 1;计数归 0 则释放内存。 缺点:有额外开销(要维护控制块);存在循环引用问题,需要 weak_ptr 解决。

10. 除了shared_ptr,其他智能指针有什么特点?

  1. unique_ptr:独占,不能拷贝,支持 move,无引用计数,开销最小;资源只能归一个管理者。
  2. shared_ptr:共享,引用计数,支持拷贝;多指针共管资源,存在循环引用问题,有额外控制块开销。
  3. weak_ptr:弱指针,配合 shared_ptr,不增加引用计数,仅观察;解决循环引用,不能单独使用。

11. unique_ptr有什么特点?

unique_ptr:独占,不能拷贝,支持 move,无引用计数,开销最小;资源只能归一个管理者。

12. weak_ptr有什么作用?

weak_ptr:弱指针,配合 shared_ptr,不增加引用计数,仅观察;解决循环引用,不能单独使用。

13. 数组和链表有什么区别?数组和链表分别适合什么场景?什么时候使用数组,什么时候使用链表?

数组是连续内存,支持随机访问,查找快,但中间增删要移动元素;链表节点分散靠指针连接,不支持随机访问,查找慢,但找到节点后增删只改指针。 需要随机访问、查询多增删少用数组;频繁在中间 / 头部插入删除,不需要随机访问时用链表

14. Linux中进程和线程有什么区别?

一、核心定义

  • 进程:资源分配的最小单位。每个进程有独立的地址空间、文件描述符、信号处理、堆、环境变量。进程之间完全隔离。
  • 线程 :CPU 调度的最小单位。一个进程内的多个线程共享进程地址空间,线程有自己私有的:栈、寄存器、线程局部存储 TLS。

二、详细对比

  1. 地址空间
    • 进程:独立虚拟地址空间,进程互不访问。
    • 线程:同进程内所有线程共享同一个虚拟地址空间,可以直接读写全局变量。
  2. 资源
    • 进程:独立 fd 表、信号处理、工作目录、用户 ID。
    • 线程:共享 fd、信号处理、工作目录、用户 ID。每个线程只有私有栈。
  3. 创建开销
    • 进程:fork(),拷贝页表,开销大。
    • 线程:pthread_create(),只创建 task_struct + 线程栈,开销远小于 fork。
  4. 通信方式
    • 进程间通信 IPC:管道、消息队列、共享内存、信号量、socket。跨进程通信麻烦。
    • 线程间通信:直接读写全局变量(配合互斥锁 / 条件变量),简单;但容易出现竞态。
  5. 崩溃影响
    • 一个进程崩溃,不会影响其他进程。
    • 一个线程崩溃(比如段错误),整个进程直接退出,所有线程一起挂掉。
  6. 切换开销
    • 进程切换:要切换页表、刷新 TLB,开销大。
    • 线程切换:同一地址空间,不用切换页表,TLB 不用刷新,开销小很多。

三、适用场景

✅ 多进程

  1. 任务之间隔离性要求高,一个任务崩溃不能影响其他任务(nginx 多进程、web 服务)
  2. 多核充分利用,不受 GIL 限制(Python)
  3. 计算任务相互独立

✅ 多线程

  1. 任务之间数据交互频繁,需要大量共享数据
  2. IO 密集型场景(网络服务,等待 socket 读写)
  3. 内存资源有限,不想重复占用大量内存

四、面试一句话精简版(口述)

进程是资源分配最小单位,拥有独立地址空间,相互隔离,创建和切换开销大;线程是 CPU 调度最小单位,同进程线程共享地址空间,切换开销小;一个线程崩溃会导致整个进程退出。进程通信要用 IPC,线程共享全局变量,需要锁保护。

15. 进程中的堆和栈是否在线程间共享?

堆属于进程地址空间,同一个进程内所有线程共享堆内存;栈是每个线程私有的,每个线程拥有独立栈空间,不能互相访问。全局变量共享,线程栈上的局部变量不共享。

16. 同一进程的线程还共享哪些资源?

同一进程的线程共享:虚拟地址空间(代码段、全局变量、堆)、文件描述符表、信号处理函数、工作目录、uid/gid、加载的动态库、进程资源限制。

线程私有:线程栈、寄存器、TLS、信号掩码、tid、errno。

17. mmap区域应该怎么理解?mmap得到的空间叫什么?

mmap 是 Linux 系统调用,把一段虚拟内存区域,映射到某个物理载体 (可以是文件、匿名物理内存)。进程的虚拟地址空间里这一块区域就叫 mmap 映射区。

注意:mmap 分配出来的内存不属于堆(heap),堆是 brk 系统调用管理的;mmap 是独立的虚拟内存区域。

两种常用 mmap

  1. 文件映射(file-backed mmap) 将文件的一部分映射到进程虚拟地址。访问这段虚拟地址,就相当于读写文件,由内核自动完成页故障加载磁盘数据。
  • 用途:大文件读写、动态库 .so 加载(so 就是通过 mmap 映射进进程地址空间)
  1. 匿名映射(anonymous mmap,MAP_ANONYMOUS) 不关联任何文件,映射到内核的匿名物理内存。
  • 用途:分配大块内存;malloc 分配大内存(>128KB 左右,不同 libc 配置)底层就是调用 mmap 匿名映射。

mmap 得到的空间叫什么

标准叫法:mmap 映射区 / 映射虚拟内存区域

  • 匿名 mmap:匿名映射区
  • 文件 mmap:文件映射区

进程地址空间布局: 代码段 → data/BSS → 堆 (brk 向上增长) → mmap 映射区(向下增长) → 线程栈

核心要点

  1. mmap 只建立虚拟地址到物理 / 文件的映射关系 ,调用 mmap 时不会立刻分配物理内存;只有第一次访问该虚拟地址时触发缺页异常,内核才分配物理页(惰性分配)。
  2. 虚拟内存属于进程,同进程多个线程都可以访问这块 mmap 区域(共享)。
  3. 映射区域释放:munmap;不能用 free 释放 mmap 出来的内存。
  4. malloc 小内存用 brk 在堆上扩展;大块内存直接 mmap,free 的时候会调用 munmap 直接归还物理内存。

18. mmap区域一般位于哪里?mmap区域有没有大小限制?

mmap 区域位于进程虚拟地址空间中堆和线程栈之间,堆向上增长,mmap 区域向下增长,动态库、匿名映射都放在这里。

mmap 存在多层限制:一是vm.max_map_count限制映射段的最大数量;二是RLIMIT_AS限制进程整体虚拟地址空间上限;三是物理内存 + swap 的实际容量。64 位下虚拟地址空间很大,一般容易先碰到映射段数量限制或者物理内存不足。

19. 文件描述符FD是否在线程间共享?

结论:同一个进程内所有线程共享文件描述符表,所以 FD 是线程共享的。

20. 全局变量是否在线程间共享?

同进程内所有线程共享全局变量(以及静态 static 变量)。

21. 一个进程有100个线程,向第99个线程发送SIGKILL,是该线程退出,还是整个进程退出?

整个进程直接退出。

22. malloc中常说的128 KB阈值是怎么确定的?

128KB 是 glibc ptmalloc 中 M_MMAP_THRESHOLD 的默认值,是工程经验权衡出来的,不是内核硬性规定。大于等于这个阈值的内存申请直接使用 mmap 匿名映射,free 时 munmap 一次性归还内存,防止大内存造成堆碎片;小于阈值用 sbrk 在堆上分配,小块内存可以复用堆中空闲块,减少系统调用开销。这个阈值默认支持动态调整,也可以通过 mallopt 手动修改。

23. 为什么超过128 KB使用mmap,小于128 KB使用其他方式?

核心是权衡系统调用开销和内存碎片。小于 128KB 的小块内存使用 brk 堆分配,free 后的 chunk 放入空闲链表可以复用,减少系统调用;小块就算产生碎片,浪费有限。大于等于 128KB 的大块内存采用 mmap 分配,free 时直接 munmap 将内存归还内核,防止大块空闲内存卡在堆中间,造成严重外部碎片。128KB 是 glibc ptmalloc 的经验阈值,不是内核强制规定,也可以用 mallopt 修改。

24. 小于128 KB的内存是怎么申请的?

小于 128KB 的内存,malloc 优先在用户态从 fastbin、smallbin 空闲链表寻找空闲 chunk;找不到就尝试切割堆末尾的 top chunk。如果 top chunk 空间不足,调用 sbrk 系统调用扩展堆的虚拟地址空间。sbrk 只扩大虚拟地址,物理内存在访问时才分配。free 后小块 chunk 放回空闲链表,内存保留在进程堆中,一般不归还内核。

25. brk扩展堆之后,malloc如何从堆中分配?

brk 扩展堆后,新增加的虚拟内存合并到 top chunk。后续 malloc 先查找 fastbin、smallbin 空闲链表;找不到合适 chunk 时,就切割 top chunk,分出需要的内存给用户,剩余部分继续保留为 top chunk,整个切割过程是用户态,不触发系统调用。如果 top chunk 空间不足,才再次调用 brk 扩容堆。top chunk 是堆尾部的空闲块,不属于任何 bin 链表。

26. 释放内存时会发生什么?

free 首先根据用户指针找到 chunk 头,校验合法性。如果是小于 128KB 的堆 chunk:标记为空闲,尝试合并相邻空闲块,放入 fastbin 或 bin 链表;如果空闲块是堆尾 top chunk 且大小超过 M_TRIM_THRESHOLD,则调用 sbrk 收缩堆,将内存归还内核。堆中间的空闲块不会还给内核,只能后续复用。如果是≥128KB 的 mmap 大块,free 直接调用 munmap 释放映射,内存一次性还给内核,不加入空闲链表。free 不会清空内存内容。

27. 被释放的内存缓存在哪里,是用户态还是内核态?

小于 128KB 堆内存 free 后,空闲 chunk 保存在 glibc 在用户态维护的 fastbin、smallbin 等空闲链表,内核不知道堆内部 chunk 空闲状态,这部分内存不会交还内核。大于等于 128KB 的 mmap 大块 free 时调用 munmap,直接归还内核,不会在用户态缓存。堆内部内存块的管理是用户态库的工作,内核只维护进程整体虚拟地址空间。

28. 如果申请11字节,brk扩大堆后,释放这11字节时,brk会把堆缩小吗?

不会缩小堆。申请 11 字节,ptmalloc 分配对齐后的完整 chunk;free 之后这个 chunk 变成堆中间的空闲块,进入 fastbin,不会合并到 top chunk。堆收缩需要堆尾部 top chunk 空闲内存超过 M_TRIM_THRESHOLD 默认 128KB 才会触发。堆中间释放的内存,无论大小,都不会调用 brk 缩小堆。

29. 如果不断申请小于128 KB的内存,堆岂不是会无限增长?

不断申请小于 128KB 的内存,如果只申请不释放,堆会持续 sbrk 扩容,但不会无限增长,受限于进程虚拟地址限制和物理内存 + swap 上限,内存耗尽会触发 OOM。如果申请后释放,空闲块会放在用户态 fastbin/smallbin 供后续复用,堆边界不会回落;堆中间的空闲 chunk 无法归还内核,只有堆尾 top chunk 空闲超过 M_TRIM_THRESHOLD 才会收缩堆。反复 malloc+free 小内存时,堆增长到一定程度就停止,但 RSS 不会下降。

RSS 是常驻内存集,代表进程当前存放在物理内存中的页面大小,不包含交换到磁盘的内存。Swap 是磁盘划分出的交换空间,物理内存紧张时,内核会把进程不常使用的匿名内存页写入 swap 磁盘,腾出物理内存;访问被换出的内存时再加载回内存。swap 由内核管理,用户态 malloc 库感知不到 swap。

30. 用户态分配器维护的内存池不是无限的,这个问题如何解决?

用户态内存池不是无限的,解决办法是内存池耗尽时通过 mmap 等系统调用动态向内核扩容;空闲大块内存可以 munmap 归还内核,同时使用多级内存池、空闲块合并减少碎片,还可设置最大内存上限做 OOM 兜底。

31. glibc如何拆分大块内存,并管理不同大小的空闲块?

glibc ptmalloc 使用 bins 数组分类管理空闲块,小块放 fastbin/smallbin,大块空闲块存放在 large bin,large bin 内空闲块按大小排序,采用 best-fit 最佳适配。分配时找到足够大的大块 chunk,若剩余部分能构成最小 chunk,则拆分:用户需要部分返回,剩余 remainder 放入 unsorted bin;unsorted bin 作为临时缓冲区,在下次 malloc 时将空闲块分拣归入对应的 small/large bin。超大内存(超过 mmap 阈值)直接 mmap,释放 munmap,不进 bin 链表。

32. 什么是页表?找到物理页之后,如何找到要访问的具体地址?一个物理页是4 KB,如何定位其中具体的一个字节?

1. 什么是页表

页表是内核维护的一张映射表,用来完成「虚拟地址 → 物理地址」的翻译。 CPU 的内存管理单元 MMU,在每次访问内存时,都会查页表,把进程给出的虚拟地址转换成物理内存地址;同时页表还保存权限位(可读、可写、是否存在),做缺页判断、内存保护。

每个进程有独立的页表(CR3 寄存器保存当前进程页表基地址),所以不同进程相同虚拟地址,可以映射到不同物理页。

2. 找到物理页之后,如何找到要访问的具体地址?

虚拟地址拆成两部分:虚拟页号 + 页内偏移

  1. MMU 使用虚拟页号 去查页表,得到物理页的起始地址(物理页基址);
  2. 页内偏移不变,直接复用;
  3. 最终物理地址 = 物理页基址 + 页内偏移。

核心:页内偏移不需要转换,虚拟地址和物理地址的偏移是完全一样的。

3. 4KB 物理页,怎么定位页内某个字节?

4KB = 4096 字节,\(2^{12}=4096\),所以页内偏移占虚拟地址低 12 位。

  • 低 12bit:页内偏移,范围 0 ~ 4095,正好可以索引一页内全部 4096 个字节。
  • 高地址位:虚拟页号,用来查页表拿到物理页首地址。

举例: 假设虚拟地址:0x12345ABC,4K 页

  • 低 12 位(0xABC)= 页内偏移,十进制 2748
  • 查页表得到物理页起始地址:0x80004000
  • 目标物理字节地址 = 0x80004000 + 0xABC

也就是说:物理页基址 + 12 位页内偏移,直接定位页内的那 1 个字节。

一句话精简背诵版

页表是 MMU 用来完成虚拟地址到物理地址转换的映射表。虚拟地址分为虚拟页号和页内偏移;虚拟页号查页表得到物理页起始地址,页内偏移保持不变。4KB 页面大小对应 12 位页内偏移,用物理页基址加上这个 12 位偏移,就能定位物理页内的具体字节。

33. 页表索引和页内偏移分别来自虚拟地址的哪些位?页表转换最开始的根页表基地址从哪里获得?

1. 虚拟地址的位划分

虚拟地址有效只用低 48 位,高位是符号扩展位,不参与地址翻译。

  • bit0 ~ bit11(最低 12 位):页内偏移,不参与页表查询,最后直接加到物理页基址。
  • bit12 ~ bit20:PTE 索引(第 4 级页表索引)
  • bit21 ~ bit29:PDE 索引(第 3 级)
  • bit30 ~ bit38:PDPTE 索引(第 2 级)
  • bit39 ~ bit47:PML4 索引(根页表索引,第 1 级)

记忆:最低 12 位 = 页内偏移;往上 4 段,每段 9bit,是 4 级页表各自的索引。

根页表的物理基地址存放在 CPU 的 CR3 寄存器,进程切换时由操作系统内核写入 CR3,MMU 从 CR3 读取该地址开始页表遍历

34. ARM架构中用户态使用TTBR0还是TTBR1?进程切换时,TTBR0和TTBR1是否都会切换?

ARMv8-AArch64 下,用户态访问用户虚拟地址使用 TTBR0_EL1;TTBR1_EL1 用于内核地址空间,所有进程共享。普通进程上下文切换只修改 TTBR0,TTBR1 不会跟着切换。

35. 地址转换时,是先查询缓存,还是先完成页表转换?是否了解TLB?TLB命中发生在页表遍历之前还是之后?

地址转换优先查询 TLB。TLB 是 CPU 内置的高速缓存,缓存虚拟页到物理页的映射。TLB 命中发生在页表遍历之前;命中则直接拿到物理页基址,不用访问内存遍历页表;TLB 缺失才去内存遍历页表,并把结果回填 TLB。

36. 能否讲一次Linux中中断从上到下的完整流程?中断上半部和下半部分别做什么?softirq、tasklet、工作队列和线程化中断分别怎么处理?你平常使用哪一种下半部机制?几种中断处理方式分别有什么区别?

一、Linux 中断从上到下完整流程

硬件中断 → CPU → 内核中断上半部 → 下半部

  1. 硬件触发中断:外设通过中断引脚向 CPU 发送中断信号。
  2. CPU 响应:CPU 在执行完当前指令后,暂停当前程序,保存现场(通用寄存器、程序计数器等),进入内核态。
  3. 硬件层面识别中断号:CPU 查询中断控制器(APIC/x86,GIC/ARM),拿到中断向量 / 中断号。
  4. 进入中断入口汇编:跳转到内核的中断入口汇编代码,保存中断上下文。
  5. 中断上半部(hardirq)执行 :调用该中断对应的中断处理函数(上半部)。
  6. 上半部快速退出 :上半部只做紧急、必须关中断的操作;把耗时工作延后交给下半部。
  7. 退出 hardirq 阶段,内核检查是否有待处理的下半部(softirq)。
  8. 执行下半部 :在中断上下文执行 softirq/tasklet;工作队列是内核进程上下文,交给内核线程去跑。
  9. 全部处理完毕,恢复寄存器现场,返回被打断的代码继续执行。

二、上半部 vs 下半部 职责

上半部(HardIRQ,硬中断)

特点:运行在中断上下文,通常关中断 / 关本地中断,执行要极快,不能睡眠。 做的事情:

  1. 确认中断来源,清除外设硬件中断标志(否则中断会反复触发)
  2. 读取硬件寄存器,拿到必要数据
  3. 把剩下耗时工作调度交给下半部,快速退出硬中断,尽快开放中断,避免屏蔽其他中断太久。

一句话:紧急、硬件相关、短时间的操作放上面;耗时、非硬件紧急操作丢下半部。

下半部(bottom half)

目的:把硬中断里耗时任务剥离,缩短关中断时间,提升系统实时性 。 下半部有 4 种:softirq、tasklet、workqueue、线程化中断(threaded irq)

1. softirq 软中断

  • 上下文:中断上下文,不能睡眠
  • 静态定义,编译时注册,不能动态随便新增;内核预定义固定类型(网络、定时器、块设备等)
  • 同类型 softirq 可在多个 CPU 上并行并发执行
  • 触发:raise_softirq()

特点:高性能,并发,但是并发会带来竞态,需要锁;驱动开发者一般不直接新增 softirq,内核内部使用居多。

2. tasklet(基于 softirq 实现,属于 softirq 的封装)

  • 上下文:中断上下文,不能睡眠
  • 动态创建,驱动最常用的旧方案
  • 同一份 tasklet 不会在多个 CPU 并发执行(自动串行),降低锁开销
  • 底层依赖HI_SOFTIRQ / TASKLET_SOFTIRQ这两个 softirq
  • 触发:tasklet_schedule()

3. workqueue 工作队列

  • 上下文:内核进程上下文(内核线程),可以睡眠、可以阻塞、可以申请 mutex
  • 把任务交给内核工作线程执行,不在中断上下文
  • 触发:schedule_work()
  • 适用:需要睡眠的场景(分配内存、互斥锁、读写磁盘)

4. 线程化中断 threaded irq(request_threaded_irq)

  • 上半部:hardirq,只做最少量硬件清标志;
  • 下半部:内核线程(进程上下文),可以睡眠
  • 本质:内核帮你创建一个专属内核线程,中断来了唤醒该线程执行业务逻辑
  • 适合:中断处理比较耗时,不想占用硬中断 / 软中断上下文
机制 上下文 能否睡眠 并发特性 使用者
softirq 中断上下文 ❌禁止睡眠 同类型可多 CPU 并发 内核子系统(网络、timer),驱动不推荐新增
tasklet 中断上下文 ❌禁止睡眠 同一个 tasklet 串行,不会并行 旧驱动,简单短任务
workqueue 内核进程上下文 ✅可以睡眠 可配置并发 / 串行 绝大多数驱动,最常用
threaded irq 上半部:中断上下文;下半部:内核线程 下半部✅可睡眠 线程调度,受调度器管理 耗时中断处理

37. 在键盘输入hello world,最终显示到屏幕上,整个链路是什么?键盘事件经过中断处理后,最终是怎么显示到屏幕上的?

  1. 按下键盘 H 键,键盘控制器触发硬件中断;
  2. CPU 进入硬中断上半部,读取硬件扫描码,下半部交给 input 子系统;
  3. input 子系统把扫描码转为 keycode,封装 input_event 事件放入事件缓冲区;
  4. bash 终端通过 read 读取按键事件,翻译成 ASCII 字符 'h';
  5. 终端开启回显,调用 write,将 'h' 写入 TTY;
  6. 内核 TTY 子系统交给控制台驱动,加载字库,把字符像素写入帧缓冲显存;
  7. 显示控制器不断扫描帧缓冲,输出像素信号到显示器,屏幕上显示 h。

38. I²C的通信机制是什么?I²C是不是半双工?I²C完成数据传输后还要做什么?

1. 起始信号 S(Start)

SCL 保持高电平不变,SDA 从高 → 拉低

  • SCL:高
  • SDA:高 → 低

要点:只有 SCL 为高的时候,SDA 跳变,才是起始;SCL 低时改变 SDA 只是普通数据 bit。

2. 发送从机地址 + 读写位(8bit,MSB 先行)

一共 8 个时钟脉冲,每一个 bit:SCL 先拉低 → 稳定 SDA → SCL 拉高采样 循环对每一位:

  1. SCL = 低;主机设置 SDA 电平(准备好当前 bit)
  2. SCL = 高;从机在 SCL 高的时候读取 SDA 上的 bit

8bit 依次送出(7 位从地址 + R/W 位,高位先发)

3. ACK 应答(第 9 个时钟)

8 个 bit 发完,进入第 9 个时钟周期:

  1. 主机把 SCL 拉低;主机释放 SDA(不再驱动 SDA,SDA 交给从机控制)
  2. 主机拉高 SCL
    • ✅ ACK:从机把 SDA 拉低
    • ❌ NACK:从机不动作,SDA 保持高
  3. 主机再把 SCL 拉低,结束 ACK 周期

接收方(从机)在 SCL 高时拉低 SDA 代表 ACK。

4. 数据字节传输(每一个数据字节,和地址发送时序完全一样)

每个字节 8bit,高位优先: 每个 bit:

  1. SCL 拉低 → 发送方设置 SDA 电平
  2. SCL 拉高 → 接收方采样 SDA 8bit 发送完成,同样进入第 9 个时钟,做 ACK/NACK。

5. 停止信号 P(Stop)

SCL 保持高电平不变,SDA 由低 → 拉高

  • SCL:高
  • SDA:低 → 高 总线回到空闲状态:SCL = 高,SDA = 高。

I²C 是同步串行总线,SDA 数据线、SCL 时钟线,开漏带上拉;主机产生时钟,通信以起始信号开始,发送从机地址 + 读写位,每字节后接收方回复 ACK,高位先行逐位传输。I²C 属于半双工,单根 SDA 无法同时收发。传输完成后主机发送停止信号释放总线,校验 ACK/NACK 判断传输状态,驱动释放总线锁;也可使用重复起始信号不释放总线,继续传输。

39. SPI相对于I²C有什么区别?SPI有哪些工作模式?SPI的四种模式由哪些因素决定?

一、SPI 和 I²C 的区别

表格

对比项 I²C SPI
信号线 2 根:SDA、SCL;开漏输出,需要外部上拉 4 根(标准):SCLK、MOSI、MISO、CS;推挽输出,无需上拉
通信方式 半双工 全双工,MOSI/MISO 同时收发
时钟 从机没有时钟,主机产生 SCL,同步 主机产生 SCLK 同步时钟
寻址 7/10 位从机地址,多从机靠地址区分 无地址,靠独立 CS 片选线选择从机
速率 相对低,一般几百 kHz~ 几 MHz 更快,几十 MHz 很常见
拓扑 两线总线,多个从机并联在同一总线 每个从机单独 CS 线;也可以菊花链
应答机制 有 ACK/NACK 应答 没有硬件应答,需要软件校验数据
硬件复杂度 简单,IO 少 IO 引脚更多

一句话口述: I2C 是半双工,两根线,靠地址寻址,有 ACK,速率较低;SPI 是全双工,四根线,靠 CS 片选选设备,无硬件应答,通信速度更快。

二、SPI 的四种工作模式

SPI 模式由 CPOL(时钟极性) 和 CPHA(时钟相位) 两个参数组合决定,共 4 种模式:

  • CPOL:Clock Polarity,时钟空闲电平
    • CPOL=0:空闲时 SCLK = 低电平
    • CPOL=1:空闲时 SCLK = 高电平
  • CPHA:Clock Phase,采样相位(什么时候采样数据)
    • CPHA=0:SCLK 第一个边沿采样数据
    • CPHA=1:SCLK 第二个边沿采样数据

边沿定义: 上升沿:低→高;下降沿:高→低

4 种模式:

  1. Mode0:CPOL=0,CPHA=0(最常用!) 空闲 SCLK 低;第一个上升沿采样
  2. Mode1:CPOL=0,CPHA=1 空闲 SCLK 低;第二个下降沿采样
  3. Mode2:CPOL=1,CPHA=0 空闲 SCLK 高;第一个下降沿采样
  4. Mode3:CPOL=1,CPHA=1 空闲 SCLK 高;第二个上升沿采样

重点记忆:Mode0 使用最多,很多 Flash、ADC 默认是 Mode0

✅面试精简背诵版

SPI 相比 I2C:SPI 全双工、四根信号线,靠 CS 片选选择从设备,无硬件 ACK,通信速率更高;I2C 半双工,两根线,使用从机地址寻址,自带 ACK 应答,速率更低。 SPI 一共 4 种模式,由CPOL 时钟极性 和CPHA 时钟相位共同决定。CPOL 决定 SCLK 空闲状态电平;CPHA 决定在第几个时钟边沿采样数据。

40. UART的通信机制是什么?UART是全双工还是半双工?UART有几根主要信号线?UART是否需要时钟线?

1. UART 通信机制

UART 是异步串行通信,发送端把并行数据转为串行 bit 流,接收端把串行 bit 恢复成并行数据。

  • 异步:收发双方不共用时钟线,依靠双方预先约定好相同波特率、数据位、校验位、停止位。
  • 帧格式(一帧):起始位 (1bit 低电平) + 数据位 (5~8bit,低位先行 LSB first) + 可选校验位 + 停止位 (1~2bit 高电平)
  • 空闲状态:总线保持高电平。
  • 发送:空闲高 → 拉低作为起始位,通知接收方数据来了;然后依次发送数据 bit、校验位、停止位。
  • 接收:接收端持续检测电平,检测到下降沿(起始位),按约定波特率采样每一位,把串行 bit 拼成字节。

核心:没有共享时钟,靠波特率采样,对时钟精度有要求,误差大会丢数据。

2. UART 是全双工还是半双工

标准 UART 是全双工。 TX(发送)、RX(接收)是两根独立信号线,可以同时收发。

注意:单总线的半双工版本(如 RS485)不属于 UART 本身,是外部收发芯片做的。单纯 UART (TX/RX) 全双工。

3. UART 主要信号线

最简 UART(3 线):

  1. TX:发送端输出
  2. RX:接收端输入
  3. GND:共地(必须!参考电平)

硬件流控版本(5 线,RTS/CTS): RTS(请求发送)、CTS(清除发送),用于流量控制,防止接收缓冲区溢出。

面试回答优先:最简是 TX、RX、GND 三根;带硬件流控增加 RTS、CTS。

4. UART 是否需要时钟线

不需要单独的外部时钟线 。 UART 属于异步通信,收发双方各自使用本地内部时钟,只要双方波特率配置一致即可,不需要像 I2C/SPI 那样传输共享时钟信号。

✅ 面试精简背诵版

UART 是异步串行通信,收发双方约定波特率,数据以帧传输:起始位 + 数据位 + 校验位 + 停止位;检测到起始位下降沿后按波特率采样 bit。UART 是全双工;基础信号线为 TX、RX、GND 三根,带硬件流控增加 RTS/CTS。UART 不需要外部共享时钟线,收发使用各自本地时钟。

二面

1. 什么是生产者、消费者?消费者包含哪些类型?

生产者消费者是经典并发模型:生产者生成数据放入共享缓冲区,消费者从缓冲区取出数据处理,缓冲区用来解耦两者。 消费者分三类:①中断上下文消费者(硬中断、softirq、tasklet,不可睡眠);②内核线程消费者(工作队列、线程化中断,进程上下文,可以睡眠);③用户态应用进程,在用户空间读取缓冲区数据。

2. 项目如何保障缓冲区buffer同步?

1. 互斥:保证同一时刻只有一方操作 buffer(保护临界区)

  1. 互斥锁 mutex 适用:进程上下文,允许睡眠 。 场景:工作队列、内核线程、用户态 pthread。 限制:中断上下文不能用 mutex,mutex 睡眠会崩溃。
  2. 自旋锁 spinlock 适用:中断上下文 + 进程上下文并发 。 特点:拿不到锁时原地忙等,不能睡眠 。 注意:进程和中断竞争时,要用spin_lock_irqsave关本地中断,防止中断抢占自旋锁持有者造成死锁。
  3. 原子变量 / 位操作 极简场景,只保护单个整形标记(比如 buffer 的读写索引、count),不适合保护复杂缓冲区。

2. 条件等待:解决缓冲区空 / 满,避免忙轮询

  1. 条件变量 wait_queue(等待队列,内核) /pthread_cond(用户态)

    • buffer 空 → 消费者休眠,加入等待队列;生产者放数据后唤醒消费者
    • buffer 满 → 生产者休眠;消费者取走数据后唤醒生产者

    优点:不占用 CPU,不是死循环轮询。

内核里:等待队列 + 自旋锁 /mutex 组合是环形 buffer 标配。

3. 无锁环形缓冲区(单生产者单消费者,SPSC)

不需要锁!依靠读写索引原子操作、内存屏障smp_mb()。 前提:只有一个生产者、一个消费者。 多生产者或者多消费者,无锁方案会失效,仍然需要锁。

3. gdb常用命令有哪些?如何查看单个变量?如何查看单线程/全部线程调用栈?

gdb 常用命令:r 运行、b 设断点、n 单步跳过、s 单步进入、c 继续、finish 函数返回、q 退出。 查看单个变量使用print(p)命令,支持指定进制打印,p *ptr查看指针指向内容。 bt查看当前线程 调用栈;info threads查看全部线程;thread apply all bt打印所有线程的调用栈。

4. top如何查看us、sy占用?对应优化思路是什么?

执行 top,CPU 行直接看到 us、sy。us 是用户态 CPU 占用,是应用用户代码消耗;

sy 是内核态占用,是系统调用、内核代码、锁、中断等内核行为消耗。

us 高:优化应用算法,消除无效循环,减少重复计算。

sy 高:减少频繁系统调用,优化锁竞争,减少频繁内存申请释放,优化中断与缺页。

5. 多路摄像头存在哪些性能瓶颈?会遇到哪些问题?摄像头数量能否无限制增加?

多路摄像头瓶颈主要是总线带宽、CPU/ISP 算力、内存、中断与 I2C 等外设资源。常见问题:丢帧、帧延迟抖动、多相机时间不同步、OOM 内存溢出、USB 设备掉线、锁竞争、功耗散热问题。摄像头数量不能无限制增加,受接口数量、带宽、算力、内存、外设资源约束;USB 集线器扩展端口也不能突破 USB 控制器总带宽上限。

6. 两个线程循环执行int i、i++操作,最终打印i的值大概是多少?i++是否原子操作?结果有可能等于201吗?

i++ 不是原子操作,分为读、加、写三步,会产生竞态。两个线程各循环 100 次 i++,最终 i 的值落在 100~200 之间;最大是 200。不可能等于 201,总共只有 200 次自增,i 不可能超过 200。

7. 给i添加volatile修饰后最终结果会如何?原因是什么?volatile典型使用场景?

添加 volatile 修饰后,i 的结果依旧在 100~200 之间,无法保证等于 200。volatile 仅禁止编译器优化,强制每次读写访问内存,但不能保证 i++ 是原子操作,也无法阻止 CPU 指令重排,无法解决多线程竞态。

8. 想要保证i最终结果固定为200,有哪些实现方案?

  1. 互斥锁 mutex:将 i++ 包裹在加锁解锁临界区,保证同一时刻只有一个线程执行自增;适合用户态,允许睡眠。
  2. 原子变量 atomic:使用硬件原子指令完成 i 自增,无需锁,性能更高;C++ 用 std::atomic,内核用 atomic_t。
  3. 自旋锁 spinlock:内核场景使用,忙等,不能睡眠。
  4. 取消多线程,单线程串行执行,无并发竞争。 仅 volatile 修饰变量无法保证结果正确,不能解决竞态。

9. 开放问题:谈谈你对AI的理解以及日常使用AI的方式?

10. 设计题:设计AI接入自动化调试工具,你认为核心卡点是什么?

相关推荐
徐小黑ACG2 小时前
Golang 基础06 接口interface
开发语言·后端·golang
春涧草茶2 小时前
慢就是快12-12手动抛出异常
java·linux·前端
半杯咖啡半行码2 小时前
Qt 开发基础:版本演进、常用工具与多平台安装
开发语言·qt
下页、再停留2 小时前
【C#桌面客户端系列学习-8】批量导出数据为csv格式
开发语言·c#·visual studio
菜鸟~noob2332 小时前
【电子战】 第26篇:大气衰减——氧气与水汽吸收【含matlab代码】
开发语言·matlab
菜鸟~noob2332 小时前
【电子战】第25篇:自由空间、双射线与菲涅尔区【含matlab代码】
开发语言·算法·matlab
企业数字化笔记2 小时前
AI工具参数很多怎么办?预设、表单校验、危险参数与配置审计
java·spring boot·python·音视频
梅菲塔亚2 小时前
华为云从入门到实战
开发语言·华为云·php