Linux 内核学习(13) --- linux 内核并发与竞态

Linux 内核基础

linux 内核版本号分为主版本号(major),次版本号(minor)和修订号
一般来说次版本号是偶数的版本是稳定版本 比如 Linux 5.10.xx

内核驱动和应用程序有两个明显的区别:

  1. 内核驱动程序没有链接的概念,内核模块仅仅链接到内核,因此它能调用的程序仅仅是内核导出的那些函数,不存在任何可以链接的函数库
    linux 中大多数相关的头文件都放在 include/linuxinclude/asm 目录中
  2. 应用程序的段错误是无害的(不会影响其他进程),因为进程空间是受保护的,但是内核驱动的错误有可能影响整个系统
内核空间和用户空间

模块运行在所谓的内核空间中,应用程序运行在用户空间中,这个概念是操作系统的理论基础之一,操作系统必须负责程序的独立操作并保护资源不受非法访问,这个重要任务只有在 CPU 能够保护的系统软件不受应用程序破坏时才能完成,Unix 中,内核运行在最高级别,这个级别可以进行所有的操作

应用程序运行在最低级别,即所谓的用户态,在这个级别中,操作系统控制对硬件的直接访问以及对内存的非法授权访问,
用户空间和内核空间不仅说明两个模式有不同的优先权等级,同时内核空间和用户空间有各自的内存映射和地址空间
执行系统调用的内核代码运行在进程上下文中,它代表调用进程执行操作,因此能够访问进程空间所有数据,处理内核中断的代码是异步的,与任何一个特定进程无关
userspace_kernelspace

内核中的并发

并发可能发生的条件如下:

  1. 有可能有多个进程同时在使用我们的驱动程序
    多个用户进程可以同时打开同一个设备文件(例如 /dev/mydevice),从而同时调用驱动程序的 readwriteioctl 等函数,这些函数可能操作同一个硬件或同一份内核数据

  2. 大多数设备能中断处理器,而中断处理器异步运行,可能在驱动程序正试图处理其他任务时被调用
    设备完成操作后(比如数据到达、DMA 完成) 设备完成操作(比如数据到达、DMA 完成) 后就会发出硬件中断,中断处理程序(ISR) 会异步的,强制性的打断当前正在执行的任何内核代码(包括驱动自身的其他部分) 去处理中断,如果 ISR 和驱动程序的其他部分(比如系统调用) 访问同一份共享数据(比如缓冲区指针,状态寄存器) 就会产生竞态

  3. Linux 可能运行在 SMP(symmetric multiprocessor,SMP)上,因此可能有多个 CPU 运行我们的驱动程序
    在现代多核 CPU 上,内核和驱动代码可以真正地同时在不同的 CPU 核心上执行,这是最典型的并发场景

内核抢占:
即使是在单核 CPU 上,如果内核配置了抢占(PREEMPT),一个进程在内核态执行时(例如正在执行驱动程序的某个函数)也有可能被优先级更高的进程抢占,当它被重新调度的时候,共享的数据已经改变了

所以 Linux 内核代码包括驱动程序代码必须时可以重入的,它必须保证可以同时运行在多个上下文中,
因此,内核数据结构需要仔细设计才能保证多个线程分开执行,访问共享数据的代码也必须避免破坏共享数据,要编写能够处理并发问题同时避免竞态的代码

竞态条件的含义

定义: 两个或者多个执行路径以无法预知的顺序访问和操作同一份数据或者硬件资源,最终导致结果依赖于这种不可控的时序
产生的问题可能有:

  • 数据不一致:一个链表被一个线程修改(添加或者删除节点)时,另一个线程正在遍历它,可能导致崩溃或者丢失
  • 计数器错误:两个线程同时读取一个计数器(值为5),各自加1后写回,结果应该是7,但由于竞态可能变成了6
  • 硬件状态混乱:一个任务正在设置寄存器准备 DMA 传输,被中断打断,ISR 误读未配置完成的寄存器状态,导致操作失败或者崩溃
Linux 内核的同步机制

Linux 内核提供了一整套丰富的同步原语来解决不同场景下的并发问题:

  • 中断屏蔽:单处理器上,临时禁止中断可以防止与中断处理程序的竞态,但是在 SMP 上无效

  • 自旋锁(Spinlock)
    用于短期、轻量级的锁定,特别是在中断上下文或不能睡眠的地方
    请求锁的线程会 自旋(忙等待),直到锁可用, 适用于持有锁时间非常短的场景

  • 信号量(Semaphore)和互斥锁(Mutex)
    用于可能长时间持有锁的场景,当锁不可用时,请求线程会进入睡眠状态,让出 CPU 适用于进程上下文
    互斥锁是二值信号量(只有0 和 1)的一种更简洁、更安全的实现

  • 完成量(Completion)
    用于一个任务等待另一个任务完成某个特定操作,比信号量更轻量、意图更明确

  • 读写锁(Read Write Lock)
    一种无锁同步机制,适用于读操作极其频繁、写操作相对较少的数据结构(如路由表读操作完全无锁,性能极高;写操作通过复制-更新-替换的方式保证一致性

  • 原子操作(Atomic Operation)
    对整数进行的、不可分割的读取-修改-写入操作(如 atomic_incatomic_dec_and_test),用于实现计数器或简单的标志位,是最高效的同步方式

用户空间写驱动程序

用户空间写驱动程序的优点:

  1. 可以和 C 库链接,驱动程序本身就可以完成很多任务逻辑
  2. 驱动程序被挂起,简单的 kill 掉它就可以,驱动程序带来的问题不会挂起整个系统,除非所驱动的硬件发生严重的故障
  3. 用户内存可以换出,如果驱动程序很大但是不经常使用,除了正常使用外,不会占用太多内存
  4. 良好设计的用户空间驱动程序任然支持并发访问
  5. 避免许可证问题

USB 的驱动程序可以在用户空间书写,比如 libusb 项目,通常用户空间的驱动被设计为一个服务器进程,其任务是替代内核作为硬件控制的唯一代理,X server 是一个典型的例子

缺点:(中断处理,特权用户,内存可能被换出,io 端口的处理)

  1. 中断在用户空间不可用,对于这个限制,某些平台上也有相应的解决办法,比如IA32 上的 vm86 系统调用
  2. 只有通过 mmap 映射 /dev/mem 才可以直接访问内存,但是只有特权用户才能执行这个操作
  3. 响应时间很慢,因为客户端和硬件之间传递数据和动作需要上下文切换
  4. 用户空间不能处理一些特别重要的设备,包括但不限于网络接口和块设备
  5. 只有调用 ioperm 或者 iopl 后才可以访问 io 端口,但是不是所有的平台都支持这两个系统调用,并且访问 /dev//port 可能非常慢,因为并非十分有效。同样只有特权用户才可以引用这些系统调用和访问设备文件
  6. 如果驱动程序被换出到磁盘,响应时间将让人难以接受,使用 mlock 系统调用获获取可以缓解这个问题,但是由于用户空间一般需要链接多个库,因此需要占用多个内存页,同样 mlock 只有特权用户才能使用
内核中的 DEBUG 选项汇总

CONFIG_DEBUG_KERNEL
这个选项只是使其他调试选项可用; 它应当打开, 但是它自己不激活任何的特性.

CONFIG_DEBUG_SLAB
这个重要的选项打开了内核内存分配函数的几类检查; 激活这些检查, 就可能探测到一些内存覆盖和遗漏初始化的错误. 被分配的每一个字节在递交给调用者之前都设成 0xa5, 随后在释放时被设成 0x6b. 你在任何时候如果见到任一个这种"坏"模式重复出现在你的驱动输出(或者常常在一个oops 的列表), 你会确切知道去找什么类型的错误. 当激活调试, 内核还会在每个分配的内存对象的前后放置特别的守护值; 如果这些值曾被改动, 内核知道有人已覆盖了一个内存分配区,

CONFIG_DEBUG_PAGEALLOC
释放时,全部内存页从内核地址中移出,这个选项会显著拖慢系统,但是它也能快速指出某些类型的内存损坏错误

CONFIG_DEBUG_SPINLOCK
激活这个选项, 内核捕捉对未初始化的自旋锁的操作,以及各种其他的错误( 例如 2 次解锁同一个锁 ).

CONFIG_DEBUG_SPINLOCK_SLEEP
这个选项激活对持有自旋锁时进入睡眠的检查. 实际上,如果你调用一个可能会睡眠的函数, 它就产生问题, 即便这个有疑问的调用没有睡眠.

CONFIG_INIT_DEBUG
用__init (或者 __initdata) 标志的项在系统初始化或者模块加载后都被丢弃. 这个选项激活了对代码的检查, 这些代码试图在初始化完成后存取初始化时内存.

CONFIG_DEBUG_INFO
这个选项使得内核在建立时包含完整的调试信息. 如果你想使用 gdb 调试内核, 你将需要这些信息. 如果你打算使用 gdb, 你还要激活CONFIG_FRAME_POINTER.

CONFIG_DEBUG_STACKOVERFLOW
CONFIG_DEBUG_STACK_USAGE
这些选项能帮助跟踪内核堆栈溢出. 堆栈溢出的确证是一个 oops 输出, 但是没有任何形式的合理的回溯. 第一个选项给内核增加了明确的溢出检查; 第 2 个使得内核监测堆栈使用并作一些统计, 这些统计可以用 SysRq 键得到.

内核 printk

printk 允许你根据消息的严重程度对其分类, 通过附加不同的记录级别或者优先级在消息上. 你常常用一个宏定义来指示记录级别. 例如,
KERN_INFO, 是消息记录级别的一种可能值. 记录宏定义扩展成一个字串, 在编译时会与消息文本连接在一起; 这就是为什么下面的在优先级和格式串之间没有逗号的原因.

有 8 种可能的记录字串, 在头文件 <linux/kernel.h> 里定义; 我们按照严重性递减的顺序列出它们:

打印级别说明
KERN_EMERG用于紧急消息, 常常是那些崩溃前的消息.
KERN_ALERT需要立刻动作的情形.
KERN_CRIT严重情况, 常常与严重的硬件或者软件失效有关.
KERN_ERR用来报告错误情况; 设备驱动常常使用 KERN_ERR 来报告硬件故障.
KERN_WARNING有问题的情况的警告, 这些情况自己不会引起系统的严重问题.
KERN_NOTICE正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告.
KERN_INFO信息型消息. 在这个级别, 很多驱动在启动时打印它们发现的硬件的信息…
KERN_DEBUG每个字串( 在宏定义扩展里 )代表一个在角括号中的整数. 整数的范围从 0 到
7, 越小的数表示越大的优先级.

一条没有指定优先级的 printk 语句缺省是 DEFAULT_MESSAGE_LOGLEVEL, 在
kernel/printk.c 里指定作为一个整数. 在 2.6.10 内核中,
DEFAULT_MESSAGE_LOGLEVELKERN_WARNING,其他的内核版本也可能有不一样的值

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值