Dalam pemacu peranti Linux, apabila berbilang unit pelaksanaan mengakses sumber yang sama pada masa yang sama, "keadaan perlumbaan" mungkin berlaku, yang membawa kepada ketidakkonsistenan data atau ranap sistem. Oleh itu, kita mesti melaksanakan kawalan serentak ke atas sumber yang dikongsi untuk memastikan akses yang saling eksklusif. Artikel ini akan memperkenalkan kaedah biasa untuk menyelesaikan kawalan konkurensi dalam kernel Linux, termasuk penyekat gangguan, operasi atom, kunci putaran, semafor, mutex, dsb., dan memberikan kod sampel yang sepadan.
Masalah yang mesti diselesaikan dalam pemacu peranti Linux ialah akses serentak sumber dikongsi oleh pelbagai proses Akses serentak akan membawa kepada keadaan perlumbaan.
Penyekat gangguan, operasi atom, kunci putaran, dan semafor adalah semua mekanisme untuk menyelesaikan masalah konkurensi. Penyekat gangguan jarang digunakan secara bersendirian, dan operasi atom hanya boleh dilakukan pada integer, jadi kunci putaran dan semafor adalah yang paling banyak digunakan.
Kunci putaran akan menyebabkan gelung tak terhingga, dan tiada sekatan dibenarkan semasa tempoh kunci, jadi kawasan kritikal kunci diperlukan kecil. Semafor membenarkan penyekatan bahagian kritikal dan boleh digunakan pada situasi di mana bahagian kritikal adalah besar.
Kunci putaran baca-tulis dan semaphore baca-tulis ialah kunci putaran dan semafor dengan keadaan santai masing-masing. Ia membenarkan berbilang unit pelaksanaan membaca serentak daripada sumber yang dikongsi.
Kawasan kod yang mengakses sumber dikongsi dipanggil bahagian kritikal Cara mudah dan bebas masalah untuk mengelakkan keadaan perlumbaan dalam satu CPU adalah dengan menyekat gangguan sistem sebelum memasuki bahagian kritikal. Penyamaran interrupt akan menghalang konkurensi antara gangguan dan proses daripada berlaku Selain itu, memandangkan penjadualan proses dan operasi lain kernel Linux bergantung pada gangguan, konkurensi antara proses preemption kernel juga boleh dielakkan.
local_irq_disable(); /* 屏蔽中断 */ ... critical section /* 临界区*/ ... local_irq_enable(); /* 开中断 */
Namun, memandangkan banyak operasi penting seperti I/O tak segerak dan penjadualan proses dalam Linux bergantung pada gangguan, adalah sangat berbahaya untuk menutup gangguan untuk masa yang lama dan penyamaran gangguan hanya berkesan untuk gangguan dalam CPU ini, jadi ia tidak dapat menyelesaikannya masalah yang disebabkan oleh SMP berbilang CPU persaingan. Dalam aplikasi sebenar, ia tidak disyorkan untuk menggunakannya secara langsung. Ia sesuai digunakan dalam kombinasi dengan kunci putaran di bawah.
Inti Linux menyediakan satu siri fungsi untuk melaksanakan operasi atom dalam kernel Fungsi ini dibahagikan kepada dua kategori, yang masing-masing melakukan operasi atom pada pembolehubah bit dan integer. Apa yang mereka ada persamaan ialah operasi adalah atom dalam semua keadaan dan kod kernel boleh memanggilnya dengan selamat tanpa diganggu.
Tetapkan nilai pembolehubah atom
#include void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为 i */ atomic_t v = ATOMIC_INIT(0); /* 定义原子变量 v 并初始化为 0 */
Dapatkan nilai pembolehubah atom
int atomic_read(atomic_t *v); /* 返回原子变量的值*/
Tambahan/penolakan pembolehubah atom
void atomic_add(int i, atomic_t *v); /* 原子变量增加 i */ void atomic_sub(int i, atomic_t *v); /* 原子变量减少 i */ void atomic_inc(atomic_t *v); /* 原子变量自增 1 */ void atomic_dec(atomic_t *v); /* 原子变量自减 1 */ /* 操作完结果==0, return true */ int atomic_inc_and_test(atomic_t *v); int atomic_dec_and_test(atomic_t *v); int atomic_sub_and_test(int i, atomic_t *v); /* 操作完结果 return true */ int atomic_add_negative(int i, atomic_t *v); /* 操作并返回结果 */ int atomic_add_return(int i, atomic_t *v); int atomic_sub_return(int i, atomic_t *v); int atomic_inc_return(atomic_t *v); int atomic_dec_return(atomic_t *v);
Operasi bit atom sangat pantas, secara amnya hanya memerlukan satu arahan mesin dan tidak perlu mematikan gangguan.
set/clear/togol
#include /* 更改指针addr所指数据的第nr位 */ void set_bit(nr, void *addr); void clear_bit(nr, void *addr); void change_bit(nr, void *addr);
ujian
int test_bit(nr, void *addr); /* 返回第nr位 */
Uji dan kendalikan
/* 操作第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设备驱动中的并发控制问题及其解决方法。通过使用合适的互斥机制,我们可以避免竞态的发生,提高设备驱动的稳定性和性能。在实际开发中,我们需要根据不同的场景选择最优的方案,并注意避免死锁、优先级反转等潜在的问题。
Atas ialah kandungan terperinci Bagaimana untuk menyelesaikan isu kawalan konkurensi dalam pemacu peranti Linux?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!