Linux 장치 드라이버에서 여러 실행 단위가 동시에 동일한 리소스에 액세스하면 "경합 조건"이 발생하여 데이터 불일치 또는 시스템 충돌이 발생할 수 있습니다. 따라서 상호 배타적인 액세스를 보장하려면 공유 리소스에 대한 동시성 제어를 수행해야 합니다. 이 기사에서는 인터럽트 마스킹, 원자 연산, 스핀 잠금, 세마포어, 뮤텍스 등을 포함하여 Linux 커널에서 동시성 제어를 해결하는 일반적인 방법을 소개하고 해당 샘플 코드를 제공합니다.
Linux 장치 드라이버에서 해결해야 할 문제는 여러 프로세스에 의한 공유 리소스에 대한 동시 액세스로 인해 경쟁 조건이 발생한다는 것입니다.
인터럽트 마스킹, 원자 연산, 스핀 잠금 및 세마포어는 모두 동시성 문제를 해결하는 메커니즘입니다. 인터럽트 마스킹은 단독으로 사용되는 경우가 거의 없으며 원자 연산은 정수에 대해서만 수행할 수 있으므로 스핀 잠금 및 세마포어가 가장 널리 사용됩니다.
스핀락은 무한루프를 일으키게 되며, 락 기간 중에는 블로킹이 허용되지 않기 때문에 락의 임계영역이 작아야 한다. 세마포어는 임계 섹션 차단을 허용하며 임계 섹션이 큰 상황에 적용될 수 있습니다.
읽기-쓰기 스핀 잠금 및 읽기-쓰기 세마포는 각각 완화된 조건을 갖는 스핀 잠금 및 세마포입니다. 이를 통해 여러 실행 단위가 공유 리소스에서 동시에 읽을 수 있습니다.
공유 리소스에 액세스하는 코드 영역을 임계 섹션이라고 합니다. 단일 CPU 내에서 경합 상태를 방지하는 간단하고 문제 없는 방법은 임계 섹션에 들어가기 전에 시스템 인터럽트를 차단하는 것입니다. 인터럽트 마스킹은 인터럽트와 프로세스 간의 동시성이 발생하는 것을 방지합니다. 또한 Linux 커널의 프로세스 스케줄링 및 기타 작업은 인터럽트에 의존하므로 커널 선점 프로세스 간의 동시성도 피할 수 있습니다.
으아아아그러나 Linux의 비동기 I/O 및 프로세스 스케줄링과 같은 많은 중요한 작업은 인터럽트에 의존하기 때문에 인터럽트를 오랫동안 마스크하는 것은 매우 위험하며 인터럽트 마스킹은 이 CPU의 인터럽트에만 효과적이므로 해결할 수 없습니다. SMP 다중 CPU 경쟁으로 인해 발생하는 문제입니다. 실제 응용에서는 직접 사용하는 것을 권장하지 않으며, 아래의 스핀락과 함께 사용하는 것이 적합합니다.
Linux 커널은 커널에서 원자 연산을 구현하는 일련의 함수를 제공합니다. 이러한 함수는 각각 비트 및 정수 변수에 대해 원자 연산을 수행하는 두 가지 범주로 나뉩니다. 이들의 공통점은 모든 상황에서 작업이 원자적이고 커널 코드가 중단 없이 안전하게 호출할 수 있다는 것입니다.
원자 변수의 값 설정
으아아아원자 변수 값 가져오기
으아아아원자변수 덧셈/뺄셈
으아아아비트 원자 연산은 매우 빠르며 일반적으로 하나의 기계 명령만 필요하며 인터럽트를 끌 필요가 없습니다.
설정/삭제/전환
으아아아테스트
으아아아테스트 및 작동
/* 操作第nr位,并返回操作前的值 */ int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr);
自旋锁(spinlock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁, 在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置( test-and-set) 某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行; 如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“ 测试并设置” 操作,即进行所谓的“ 自旋”,通俗地说就是“在原地打转”。 当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置” 操作向其调用者报告锁已释放。
定义/初始化
#include /* 静态初始化 */ spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /* 动态初始化 */ void spin_lock_init(spinlock_t *lock);
获取/释放
/* 基本操作 */ void spin_lock(spinlock_t *lock); void spin_unlock(spinlock_t *lock); /* 保存中断状态并关闭 == spin_lock() + local_irq_save() */ void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); void spin_unlock_irqsave(spinlock_t *lock, unsigned long flags); /* 忽略操作前中断状态 */ void spin_lock_irq(spinlock_t *lock); void spin_unlock_irq(spinlock_t *lock); /* 关闭中断底部(即关闭软件中断,打开硬件中断,详见后续中断的讲解) */ void spin_lock_bh(spinlock_t *lock); void spin_unlock_bh(spinlock_t *lock); /* 非阻塞获取,成功返回非0 */ int spin_trylock(spinlock_t *lock); int spin_trylock_bh(spinlock_t *lock);
粒度更小,可多Reader同时读,但Writer只能单独,且读与写不能同时,适用于写很少读很多的情况。
定义/初始化
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */ rwlock_t my_rwlock; rwlock_init(&my_rwlock); /* 动态初始化 */
读
void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock); void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock);
写
void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock);
顺序锁(seqlock)是对读写锁的一种优化,采用了重读机制,读写不相互阻塞。
定义/初始化
#include seqlock_t lock1 = SEQLOCK_UNLOCKED; /* 静态 */ seqlock_t lock2; seqlock_init(&lock2); /* 动态 */
读
/* 读之前先获取个顺序号,读完与当前顺序号对比,如不一致则重读 */ unsigned int seq; do { seq = read_seqbegin(&the_lock); /* Do what you need to do */ } while read_seqretry(&the_lock, seq); /* 如果这个锁可能会出现在中断程序中获取,则在这里应使用关中断版本 */ unsigned int read_seqbegin_irqsave(seqlock_t *lock,unsigned long flags); int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq,unsigned long flags);
写
void write_seqlock(seqlock_t *lock); void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags); void write_seqlock_irq(seqlock_t *lock); void write_seqlock_bh(seqlock_t *lock); int write_tryseqlock(seqlock_t *lock); void write_sequnlock(seqlock_t *lock); void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags); void write_sequnlock_irq(seqlock_t *lock); void write_sequnlock_bh(seqlock_t *lock);
对于被 RCU 保护的共享数据结构,读执行单元不需要获得任何锁就可以访问它,因此读执行单元没有任何同步开销。使用 RCU 的写执行单元在访问它前需首先拷贝一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的 CPU 都退出对共享数据的操作的时候。写执行单元的同步开销则取决于使用的写执行单元间同步机制。RCU在驱动中很少使用,这里暂不详述。
使用方式和自旋锁类似,不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
定义/初始化
#include struct semaphore sem; void sema_init(struct semaphore *sem, int val); /* 通常我们将val的值置1,即使用互斥模式 */ DECLARE_MUTEX(name); DECLARE_MUTEX_LOCKED(name); void init_MUTEX(struct semaphore *sem); void init_MUTEX_LOCKED(struct semaphore *sem);
获得信号量
void down(struct semaphore * sem); /* 信号量减1, 会导致睡眠,因此不能在中断上下文使用 */ int down_interruptible(struct semaphore * sem); /* 与down不同的是,进入睡眠后的进程可被打断返回非0 */ int down_trylock(struct semaphore * sem); /* 非阻塞版本,获得返回0,不会导致睡眠,可在中断上下文使用 */
释放信号量
void up(struct semaphore * sem);
读写信号量与信号量的关系与读写自旋锁和自旋锁的关系类似,读写信号量可能引起进程阻塞,但它可允许 N 个读执行单元同时访问共享资源, 而最多只能有 1 个写执行单元。因此,读写信号量是一种相对放宽条件的粒度稍大于信号量的互斥机制。
定义/初始化
#include struct rw_semaphore; void init_rwsem(struct rw_semaphore *sem);
读
void down_read(struct rw_semaphore *sem); int down_read_trylock(struct rw_semaphore *sem); void up_read(struct rw_semaphore *sem);
写
/* 写比读优先级高,写时所有读只能等待 */ void down_write(struct rw_semaphore *sem); int down_write_trylock(struct rw_semaphore *sem); void up_write(struct rw_semaphore *sem);
轻量级,用于一个执行单元等待另一个执行单元执行完某事。
定义/初始化
#include /* 静态 */ DECLARE_COMPLETION(name); /* 动态 */ struct completion my_completion; init_completion(struct completion *c); INIT_COMPLETION(struct completion c); /* 重新初始化已经定义并使用过的 completion */
等待完成
void wait_for_completion(struct completion *c);
完成信号
void complete(struct completion *c); /* 唤醒1个 */ void complete_all(struct completion *c); /* 唤醒所有waiter */ void complete_and_exit(struct completion *c, long retval); /* call complete() and exit(retval) */
本文总结了Linux设备驱动中的并发控制问题及其解决方法。通过使用合适的互斥机制,我们可以避免竞态的发生,提高设备驱动的稳定性和性能。在实际开发中,我们需要根据不同的场景选择最优的方案,并注意避免死锁、优先级反转等潜在的问题。
위 내용은 Linux 장치 드라이버의 동시성 제어 문제를 해결하는 방법은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!