57、Linux内核中的原子操作与锁机制

Linux内核中的原子操作与锁机制

1. RMW原子操作概述

RMW(Read-Modify-Write)原子操作是一组更高级的原子操作符,可用于对设备或外设寄存器执行原子位操作,即安全且不可分割地进行按位操作。在设备驱动开发中,这是一项常用的技术。

在进行RMW原子操作之前,需要对访问外设设备(芯片)的内存和寄存器有基本的了解。通常,我们需要对寄存器进行位操作,如按位与( & )和按位或( | ),以修改寄存器的值,设置或清除其中的某些位。但仅使用C语言操作来查询或设置设备寄存器是不够的,还需要考虑并发问题。

2. 寄存器基础
  • 字节和位 :一个字节由8位组成,从最低有效位(LSB,位0)到最高有效位(MSB,位7)。在 include/linux/bits.h 中,通过 BITS_PER_BYTE 宏进行了正式定义。
  • 寄存器 :寄存器是外设设备中的一小块内存,其位宽通常为8、16或32位。设备寄存器提供控制、状态等信息,并且通常是可编程的。

例如,假设有一个设备有两个8位宽的寄存器:状态寄存器和控制寄存器。在头文件中可以这样描述:

#define REG_BASE        0x5a00
#define STATUS_REG      (REG_BASE+0x0)
#define CTRL_REG        (REG_BASE+0x1)
3. RMW序列

要修改寄存器的值,通常遵循以下RMW序列:
1. 将寄存器的当前值读取到一个临时变量中。
2. 修改临时变量的值。
3. 将修改后的临时变量写回寄存器。

以下是一个打开虚构设备的伪代码示例:

turn_on_dev()
{
    u8 tmp;
    tmp = ioread8(CTRL_REG);  /* read: current register value into tmp */
    tmp |= 0x80;              /* modify: set bit 7 (MSB) */
    iowrite8(tmp, CTRL_REG);  /* write: new tmp value into register */
}

然而,这种方法存在并发和数据竞争问题,因为寄存器是全局共享的可写内存位置,访问它构成了一个临界区,需要进行保护。可以使用自旋锁来保护临界区,但对于处理小整数等情况,使用原子操作符是更好的选择。

4. 原子操作API

Linux提供了一系列原子操作API,包括非RMW操作和RMW操作。以下是部分RMW原子操作的分类和相关API:
| 操作类型 | API |
| ---- | ---- |
| 算术操作 | atomic_{add,sub,inc,dec}() atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}() atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}() |
| 位操作 | atomic_{and,or,xor,andnot}() atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}() |
| 交换操作 | atomic_xchg{,_relaxed,_acquire,_release}() atomic_cmpxchg{,_relaxed,_acquire,_release}() atomic_try_cmpxchg{,_relaxed,_acquire,_release}() |
| 引用计数 | atomic_add_unless() atomic_inc_not_zero() atomic_sub_and_test() atomic_dec_and_test() |
| 其他操作 | atomic_inc_and_test() atomic_add_negative() atomic_dec_unless_positive() atomic_inc_unless_negative() |

5. RMW位操作符的使用

可以使用 set_bit() API来原子地将寄存器或内存项中的任意位设置为1:

void set_bit(unsigned int nr, volatile unsigned long *p);

例如,使用RMW原子操作符可以用一行代码安全地打开虚构设备:

set_bit(7, CTRL_REG);

以下是常见的RMW位操作原子API及其说明:
| RMW位操作原子API | 说明 |
| ---- | ---- |
| void set_bit(unsigned int nr, volatile unsigned long *p); | 原子地将 p 的第 nr 位设置为1 |
| void clear_bit(unsigned int nr, volatile unsigned long *p) | 原子地将 p 的第 nr 位清除为0 |
| void change_bit(unsigned int nr, volatile unsigned long *p) | 原子地翻转 p 的第 nr 位 |
| int test_and_set_bit(unsigned int nr, volatile unsigned long *p) | 原子地将 p 的第 nr 位设置为1,并返回该位的前一个值 |
| int test_and_clear_bit(unsigned int nr, volatile unsigned long *p) | 原子地将 p 的第 nr 位清除为0,并返回该位的前一个值 |
| int test_and_change_bit(unsigned int nr, volatile unsigned long *p) | 原子地翻转 p 的第 nr 位,并返回该位的前一个值 |

需要注意的是,这些原子API不仅相对于运行它们的CPU核心是原子的,而且相对于所有其他核心也是原子的。在多核系统中进行并行原子操作时,如果可能发生竞争,必须使用锁(通常是自旋锁)来保护临界区。

6. RMW位操作符示例

以下是一个简单的内核模块示例,展示了如何使用Linux内核的RMW原子位操作符:

// ch13/1_rmw_atomic_bitops/rmw_atomic_bitops.c
#include <linux/spinlock.h>
#include <linux/atomic.h>
#include <linux/bitops.h>
#include "../../convenient.h"

static unsigned long mem;
static u64 t1, t2; 
static int MSB = BITS_PER_BYTE - 1;
DEFINE_SPINLOCK(slock);

#define SHOW(n, p, msg) do {                                   \
    pr_info("%2d:%27s: mem : %3ld = 0x%02lx\n", n, msg, p, p); \
} while (0)

static int __init atomic_rmw_bitops_init(void)
{
    int i = 1, ret;
    pr_info("%s: inserted\n", OURMODNAME);
    SHOW(i++, mem, "at init");
    setmsb_optimal(i++);
    setmsb_suboptimal(i++);
    clear_bit(MSB, &mem);
    SHOW(i++, mem, "clear_bit(7,&mem)");
    change_bit(MSB, &mem);
    SHOW(i++, mem, "change_bit(7,&mem)");
    ret = test_and_set_bit(0, &mem);
    SHOW(i++, mem, "test_and_set_bit(0,&mem)");
    pr_info(" ret = %d\n", ret);
    ret = test_and_clear_bit(0, &mem);
    SHOW(i++, mem, "test_and_clear_bit(0,&mem)");
    pr_info(" ret (prev value of bit 0) = %d\n", ret);
    ret = test_and_change_bit(1, &mem);
    SHOW(i++, mem, "test_and_change_bit(1,&mem)");
    pr_info(" ret (prev value of bit 1) = %d\n", ret);
    pr_info("%2d: test_bit(%d-0,&mem):\n", i, MSB);
    for (i = MSB; i >= 0; i--)
        pr_info(" bit %d (0x%02lx) : %s\n", i, BIT(i), test_bit(i, &mem)?"set":"cleared");

    return 0; /* success */
}

/* Set the MSB; optimally, with the set_bit() RMW atomic API */
static inline void setmsb_optimal(int i)
{
    t1 = ktime_get_real_ns();
    set_bit(MSB, &mem);
    t2 = ktime_get_real_ns();
    SHOW(i, mem, "set_bit(7,&mem)");
    SHOW_DELTA(t2, t1);
}

/* Set the MSB; the traditional way, using a spinlock to protect the RMW
 * critical section  */
static inline void setmsb_suboptimal(int i)
{
    u8 tmp;
    t1 = ktime_get_real_ns();
    spin_lock(&slock);
    /* critical section: RMW : read, modify, write */
    tmp = mem;
    tmp |= 0x80; // 0x80 = 1000 0000 binary
    mem = tmp;
    spin_unlock(&slock);
    t2 = ktime_get_real_ns();
    SHOW(i, mem, "set msb suboptimal: 7,&mem");
    SHOW_DELTA(t2, t1);
}

这个示例比较了使用 set_bit() RMW原子API和传统的自旋锁保护RMW操作的方法。结果表明,使用RMW位操作符不仅代码更简单,而且执行速度更快。

7. 高效搜索位掩码

内核提供了一些API来高效地扫描给定的位掩码,这些API的原型在 include/asm-generic/bitops/find.h 中:
- unsigned long find_first_bit(const unsigned long *addr, unsigned long size) :查找内存区域中第一个设置为1的位,返回该位的编号;如果没有设置为1的位,则返回 size
- unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size) :查找内存区域中第一个清除为0的位,返回该位的编号;如果没有清除为0的位,则返回 size

其他相关的例程还包括 find_next_bit() find_next_and_bit() find_last_bit() 等。在 <linux/bitops.h> 头文件中还可以找到其他有趣的宏,如 for_each_{clear,set}_bit{_from}()

8. 读写自旋锁

在处理全局共享的可写数据结构(如大型双向循环链表)时,访问该数据结构构成了一个临界区,需要进行保护。如果只是对链表进行非阻塞的搜索操作,可能会考虑不使用锁,但即使是读取共享可写数据也需要保护,以避免同时发生的写入操作导致脏读或撕裂读。

传统的自旋锁在多核系统中可能会导致性能问题,因为多个线程同时尝试获取锁时,只有一个线程能够成功,其他线程需要等待,从而导致执行序列化。而读写自旋锁可以解决这个问题。

读写自旋锁的工作原理如下:
- 所有执行读取操作的线程请求读锁。
- 任何需要写入访问的线程请求独占写锁。
- 只要没有写锁正在使用,读锁会立即授予请求的线程,允许多个读者并发访问数据。
- 当有写线程请求写锁时,它必须等待所有读者释放读锁,然后获得独占写锁进行写入操作。

读写自旋锁的基本API如下:

#include <linux/rwlock.h>
rwlock_t mylist_lock;

void read_lock(rwlock_t *lock);
void write_lock(rwlock_t *lock);

例如,内核的tty层在处理安全注意键(SAK)时,需要遍历所有任务并杀死与TTY设备相关的进程。在这个过程中,它使用了读写自旋锁 tasklist_lock 来保护对任务列表的访问:

// drivers/tty/tty_io.c
void __do_SAK(struct tty_struct *tty)
{
    [...]
    read_lock(&tasklist_lock);
    /* Kill the entire session */
    do_each_pid_task(session, PIDTYPE_SID, p) {
        tty_notice(tty, "SAK: killed process %d (%s): by session\n", task_pid_nr(p), p->comm);
        group_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_SID);
    } while_each_pid_task(session, PIDTYPE_SID, p);
    [...]
    /* Now kill any processes that happen to have the tty open */
    do_each_thread(g, p) {
        [...]
    } while_each_thread(g, p);
    read_unlock(&tasklist_lock);
}

需要注意的是,有些情况下可能无法使用某些锁,如 tasklist_lock 没有被导出,因此无法在自定义的内核模块中使用。

综上所述,RMW原子操作和读写自旋锁是Linux内核中处理并发和数据安全的重要机制,在设备驱动开发和内核编程中具有重要的应用价值。

Linux内核中的原子操作与锁机制

9. 读写自旋锁的性能优势分析

为了更清晰地理解读写自旋锁的性能优势,我们可以通过一个简单的流程图来展示传统自旋锁和读写自旋锁在多核系统中的不同处理方式。

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([多个线程请求访问数据]):::startend --> B{使用哪种锁?}:::decision
    B -->|传统自旋锁| C(依次获取自旋锁):::process
    C --> D(串行执行操作):::process
    D --> E(释放自旋锁):::process
    B -->|读写自旋锁| F{操作类型?}:::decision
    F -->|读操作| G(获取读锁):::process
    G --> H(并发执行读操作):::process
    H --> I(释放读锁):::process
    F -->|写操作| J(等待所有读锁释放):::process
    J --> K(获取写锁):::process
    K --> L(独占执行写操作):::process
    L --> M(释放写锁):::process

从这个流程图可以看出,传统自旋锁会导致多个线程串行执行,而读写自旋锁允许多个读线程并发执行,只有写线程需要等待所有读线程完成后才能执行,从而提高了系统的并发性能。

10. 读写自旋锁的使用示例

下面是一个简单的伪代码示例,展示了如何使用读写自旋锁来保护一个全局的双向循环链表:

#include <linux/rwlock.h>

// 定义读写自旋锁
rwlock_t mylist_lock;

// 链表头
struct list_head listhead;

// 初始化链表和锁
void init_list_and_lock() {
    INIT_LIST_HEAD(&listhead);
    rwlock_init(&mylist_lock);
}

// 读操作示例:搜索链表
void search_list() {
    struct list_head *p;

    read_lock(&mylist_lock);
    for (p = &listhead; (p = next_node(p)) != &listhead; ) {
        // ... 搜索操作 ...
        // 如果找到目标,跳出循环
        if (found) {
            break;
        }
    }
    read_unlock(&mylist_lock);
}

// 写操作示例:向链表中插入节点
void insert_node(struct list_head *new_node) {
    write_lock(&mylist_lock);
    // 插入新节点
    list_add(new_node, &listhead);
    write_unlock(&mylist_lock);
}

在这个示例中, search_list() 函数使用读锁来保护对链表的搜索操作,允许多个线程同时进行搜索。而 insert_node() 函数使用写锁来保护对链表的插入操作,确保在插入节点时不会有其他线程同时进行读写操作。

11. 原子操作和锁机制的总结

在Linux内核编程中,原子操作和锁机制是保证数据安全和并发性能的重要手段。

  • 原子操作 :RMW原子操作提供了一种安全、高效的方式来对寄存器和内存进行位操作。通过使用原子操作符,可以避免并发和数据竞争问题,并且代码更加简洁。例如,使用 set_bit() 等API可以原子地设置、清除或翻转寄存器中的位。
  • 锁机制 :自旋锁和读写自旋锁是常用的锁机制。自旋锁适用于短时间的临界区保护,而读写自旋锁则适用于读多写少的场景,可以提高系统的并发性能。在使用锁时,需要注意避免死锁和性能问题。

以下是一个总结表格,对比了不同操作和锁机制的特点:

操作/机制 特点 适用场景
RMW原子操作 安全、高效、代码简洁,避免并发问题 对寄存器和内存进行位操作
传统自旋锁 简单直接,保证数据一致性,但可能导致性能问题 短时间的临界区保护
读写自旋锁 允许多个读线程并发访问,提高并发性能 读多写少的场景
12. 实际应用中的注意事项

在实际应用中,使用原子操作和锁机制时需要注意以下几点:
- 并发控制 :确保在多核系统中对共享数据的访问是安全的,避免数据竞争和脏读。
- 性能优化 :根据具体的应用场景选择合适的操作和锁机制,避免不必要的锁竞争和性能损失。
- 死锁预防 :在使用锁时,要注意锁的获取和释放顺序,避免死锁的发生。
- 代码可读性 :虽然原子操作和锁机制可以提高性能和安全性,但也要确保代码的可读性和可维护性。

通过合理使用原子操作和锁机制,可以提高Linux内核程序的稳定性和性能,确保系统在高并发环境下的正常运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值