number headings: auto, first-level 2, max 6, 1.1
2 通用部分
2.1 如果Linux中的一个进程的某个线程崩溃了,进程会变成什么状态
- 在C和C++中,一个进程中的某个线程崩溃时,该进程一定会崩溃并变成"僵尸"状态或"已终止"状态:
- 如果一个父进程的子进程的某个线程崩溃时,该子进程被父进程调用
wait
或 waitpid
回收处理之前会保持僵尸进程状态。
- 如果父进程调用了
wait
等接口回收了子进程,则该子进程会变成"已终止"状态。这个父进程可以是 init
进程或者 shell
进程等。
- 但是在Python和Java等语言中,一个进程中的某个线程崩溃并不一定会导致整个进程崩溃,例如JVM拦截了
SIGSEGV
(段错误)等信号,从而阻止整个进程崩溃。
2.2 Linux中的内存命令有哪些
- 任务管理器类命令:
top
、 htop
- todo
2.3 Linux的设备和驱动是如何进行匹配的
- 对于非可拔插式设备,其通常会直接定义于设备树中,并参与内核的编译。比如CPU、内存节点、总线、时钟源、GPIO等。设备树在内核启动的时候就会被加载,随后查找驱动程序并尝试匹配。
- 对于可插拔式设备,比如USB、PCIe设备等,这些设备通常不会被静态的定义于设备树中,但是USB和PCIe的控制器会被定义于设备树中,这些控制器检测新设备的插入并通知系统加载对应的驱动程序。
- TODO
2.4 Linux中如果使用malloc申请一片内存,存入数据后释放,这片地址的值会怎么样,程序能否继续读写,数据是否还存在
- 在内存被申请释放后,这片内存数据不会立即被清除,这片内存上的数据会存放一段时间直到这片内存对应的物理空间被再次使用。
- 在这片内存被释放后,该片内存已经不再归程序所有,其数据可靠性不会有任何保障。
- 在这片内存被申请回收后,指向这片内存的指针会被称作"悬挂指针",此时如果再去进行读写操作则是未定义行为
2.6 Linux中的设备可以分为哪些类型
- 在Linux系统中,设备主要可以分为字符设备、块设备和网络设备三大类。其主要特征为:
- 字符设备:可以逐字节进行数据读写的设备,即以字节流顺序访问的设备。通常不支持随机访问。例如串口、触摸屏、鼠标、磁带机等。
- 块设备:允许按照任意顺序进行访问,以块为单位进行操作的设备。例如硬盘、EMMC等。
- 网络设备:面向数据包的发送和接收的设备,内核与网络设备之间的通信主要使用Socket。
2.7 讲一下Linux系统中字符设备的基本框架
- 通常来说,Linux系统中的字符设备是以内核模块的方式进行编写的,其在编译时可以手动选择编译为模块或编译进内核。但是无论哪种方式,其都需要至少实现一个内核模块的基本框架。该框架要求的必要部分有:
- 模块加载函数
- 模块许可证声明
2.8 讲一讲在Linux中驱动GPIO的大概过程
2.10 讲一讲设备号及其动态、静态分配
在Linux系统中,设备号是用来唯一标识系统中的设备的。在Linux2.6.0之后,设备号被定义
2.12 什么是per-CPU变量
per-CPU是每一个CPU上都会独立产生一份实例的变量,通常用于高频局部修改,低频全局读写的使用场景,例如计数器、统计信息等。其基本特性有:
- 每个CPU上都有一个独立的实例副本,彼此独立互不干扰,每个CPU只可见自己CPU的对应副本。
- 进行局部修改时会关闭抢占,确保每次局部修改的过程中不会切换CPU。
- per-CPU不负责实现全局读写时的互斥功能,需要开发者自行实现。
例如在计数器实现中,使用per-CPU变量,单独统计每个CPU上发生事件的次数,当需要汇总时则通过其他的互斥机制实现汇总。具体可见per-CPU变量。
4 设备树专题
4.1 讲一下设备树的生命周期
- dts源文件被dtc编译为dtb。
- dtb被存储到启动镜像中(通常与内核镜像一起存储,在启动时被一起加载)。
- bootloader从启动镜像中加载dtb,在经过可选的修改后被存入内存。
- 在加载内核时,DTB在内存中的地址会被传递给kernel(通常以启动参数方式传递)。
- 内核使用FDT初始化硬件,并生成扩展设备树,该拓展设备树在启动后可以被更改。
5 IO专题
5.1 讲一下poll类IO多路复用模型中,poll类系统调用是被如何阻塞的,又是被如何唤醒的。
- poll类IO多路复用模型是指
select
、 poll
、 epoll
这种IO多路复用模型,其对用户态的表现为可以用一个线程使用上述poll类函数监听多个文件的多种操作。在阻塞与返回方面,这类函数有如下的特性:
- 当其所监听的事件均为发生,且未到超时时间时,该线程会被阻塞。
- 当所监听的多个文件的多种事件中的任意一个或多个事件发生时,该线程会被唤醒并返回。
- 当其设置的阻塞时间超时时,该线程会被唤醒并返回。
- 这三个系统调用的区别主要在于数据结构的管理上。而在该系统调用的阻塞功能的实现上这三个系统调用的实现是一致的:
- 其在使用
select
、 poll
或 epoll
系统调用后,当不满足返回条件时,这些线程会被系统使用 poll_schedule_timeout
将线程状态修改为可中断的普通休眠( TASK_INTERRUPTIBLE
),随后设置超时唤醒时间点,并进入休眠。在此步骤中并没有设置事件唤醒条件。
- 当到达预定的最大阻塞时间后,会被定时唤醒。
- 当监听的事件发生时,文件系统或驱动程序会调用
wake_up_interruptible()
唤醒等待队列上的所有普通休眠线程。
poll_schedule_timeout
被唤醒后并不会像普通的休眠函数一样校验是否满足唤醒条件,因此poll类系统调用会被唤醒并调用 file_operations
中的 poll
函数:
poll
函数不负责poll类系统调用中的任何逻辑,直接使用 poll_wait
将线程塞到等待队列中,注意:
- 当队列上以及存在该线程时,
poll_wait
不会重复插入。
- 系统调用不负责等待队列的维护,只负责使用
poll_schedule_timeout
执行休眠。
poll
函数不管用户具体在等待什么事件,只负责检查可读、可写等可监听事件的mask返回。
- 随后即可检测到其所监听的文件是否发生了预期的事件或发生了错误,同时也确保了用户线程依旧在等待队列中。
- 若发生了预期的事件或错误,则poll类系统调用会被返回到用户态。
- 若没有发生预期的事件和错误,则poll类系统调用会将线程再次塞回
poll_schedule_timeout
等待超时唤醒。
5.2 在一个线程正在对某一文件执行write的过程中,另一线程执行close会发生什么
5.3 在一个线程正在对某一文件执行close的过程中,另一线程执行write会发生什么
7 多线程及其调度专题
7.1 在Linux内核的休眠类函数(wait_*)的条件判定中,能否申请互斥锁
7.2 如何处理中断服务程序和普通内核函数之间的互斥需求
- 在中断服务程序中,必须禁止使用任何可能导致休眠的函数,因此绝对不能使用普通的信号量或互斥锁。而在中断服务中,可以使用自旋锁等非休眠锁、各种无锁操作以及延迟工作等机制,其具体如下:
- 使用自旋锁,则需要考虑在普通内核函数已经持有该自旋锁时触发中断的场景。为了避免中断函数在这种场景下被自旋锁轮询等待从而可能导致的死锁问题,必须选择是:
- 中断在无法获取到该自旋锁时(
try_lock
)跳过使用该锁相关资源,但是可能导致业务逻辑问题。
- 在普通内核函数中使用自旋锁时,使用
spin_lock_irq*
函数关闭中断。
- 使用无锁机制,例如顺序锁、RCU等。
- 使用延迟工作,将中断中的部分工作内容延迟到进程的上下文中执行。
7.3 为什么中断服务程序中要禁止调用可能会导致休眠的函数