Heim >System-Tutorial >LINUX >Wie kann man Probleme mit der Parallelitätskontrolle in Linux-Gerätetreibern lösen?

Wie kann man Probleme mit der Parallelitätskontrolle in Linux-Gerätetreibern lösen?

WBOY
WBOYnach vorne
2024-02-13 19:24:171134Durchsuche

Wenn in Linux-Gerätetreibern mehrere Ausführungseinheiten gleichzeitig auf dieselbe Ressource zugreifen, kann eine „Race Condition“ auftreten, die zu Dateninkonsistenzen oder Systemabstürzen führt. Daher müssen wir eine Parallelitätskontrolle für gemeinsam genutzte Ressourcen durchführen, um einen gegenseitig ausschließenden Zugriff sicherzustellen. In diesem Artikel werden gängige Methoden zur Lösung der Parallelitätskontrolle im Linux-Kernel vorgestellt, einschließlich Interrupt-Maskierung, atomare Operationen, Spin-Locks, Semaphore, Mutexe usw., und entsprechende Beispielcodes angegeben.

Wie kann man Probleme mit der Parallelitätskontrolle in Linux-Gerätetreibern lösen?

Ein Problem, das bei Linux-Gerätetreibern gelöst werden muss, ist der gleichzeitige Zugriff mehrerer Prozesse auf gemeinsam genutzte Ressourcen, der zu Race Conditions führt.
Interrupt-Maskierung, atomare Operationen, Spin-Locks und Semaphoren sind alles Mechanismen zur Lösung von Parallelitätsproblemen. Interrupt-Maskierung wird selten allein verwendet, und atomare Operationen können nur für Ganzzahlen ausgeführt werden, sodass Spin-Locks und Semaphoren am häufigsten verwendet werden.
Die Spin-Sperre führt zu einer Endlosschleife und während der Sperrperiode ist keine Blockierung zulässig. Daher muss der kritische Bereich der Sperre klein sein. Das Semaphor ermöglicht die Blockierung kritischer Abschnitte und kann in Situationen angewendet werden, in denen der kritische Abschnitt groß ist.
Lese-/Schreibsperren und Lese-/Schreibsemaphoren sind Spinsperren bzw. Semaphoren mit gelockerten Bedingungen. Sie ermöglichen es mehreren Ausführungseinheiten, gleichzeitig von gemeinsam genutzten Ressourcen zu lesen.


Unterbrechungsmaske

Der Codebereich, der auf gemeinsam genutzte Ressourcen zugreift, wird als kritische Abschnitte bezeichnet. Eine einfache und problemlose Möglichkeit, Race Conditions innerhalb einer einzelnen CPU zu vermeiden, besteht darin, Systeminterrupts zu blockieren, bevor der kritische Abschnitt betreten wird. Die Interrupt-Maskierung verhindert das Auftreten einer Parallelität zwischen Interrupts und Prozessen. Da die Prozessplanung und andere Vorgänge des Linux-Kernels auf Interrupts basieren, kann außerdem eine Parallelität zwischen Kernel-Preemption-Prozessen vermieden werden.

    local_irq_disable(); /* 屏蔽中断 */
    ...
    critical section /* 临界区*/
    ...
    local_irq_enable(); /* 开中断 */

Da jedoch viele wichtige Vorgänge wie asynchrone E/A und Prozessplanung in Linux auf Interrupts angewiesen sind, ist es sehr gefährlich, Interrupts über einen längeren Zeitraum zu maskieren, und die Interrupt-Maskierung ist nur für Interrupts in dieser CPU wirksam und kann daher nicht gelöst werden Das durch SMP verursachte Problem ist die Konkurrenz mehrerer CPUs. In tatsächlichen Anwendungen wird die direkte Verwendung nicht empfohlen. Es eignet sich für die Verwendung in Kombination mit der unten aufgeführten Drehsperre.


Atomoperationen

Der Linux-Kernel bietet eine Reihe von Funktionen zum Implementieren atomarer Operationen im Kernel. Diese Funktionen sind in zwei Kategorien unterteilt, die atomare Operationen für Bit- bzw. Ganzzahlvariablen ausführen. Gemeinsam ist ihnen, dass die Operationen unter allen Umständen atomar sind und der Kernel-Code sie sicher und ohne Unterbrechung aufrufen kann.

Ganzzahlige atomare Operationen

  • Legen Sie den Wert atomarer Variablen fest

    #include 
    void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为 i */
    atomic_t v = ATOMIC_INIT(0);         /* 定义原子变量 v 并初始化为 0 */
    
  • Ermitteln Sie den Wert der atomaren Variablen

    int atomic_read(atomic_t *v); /* 返回原子变量的值*/
    
  • Addition/Subtraktion atomarer Variablen

    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);
    

bit-atomare Operationen

Bitatomare Operationen sind sehr schnell und erfordern im Allgemeinen nur einen Maschinenbefehl und keine Notwendigkeit, Interrupts auszuschalten.

  • einstellen/löschen/umschalten

    #include 
    
    /* 更改指针addr所指数据的第nr位 */
    void set_bit(nr, void *addr);
    void clear_bit(nr, void *addr);
    void change_bit(nr, void *addr);
    
  • Test

    int test_bit(nr, void *addr); /* 返回第nr位 */
    
  • Testen und betreiben

    /* 操作第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)

自旋锁(spinlock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁, 在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置( test-and-set) 某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行; 如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“ 测试并设置” 操作,即进行所谓的“ 自旋”,通俗地说就是“在原地打转”。 当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置” 操作向其调用者报告锁已释放。

Basic

  • 定义/初始化

    #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 Spinlocks

粒度更小,可多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

顺序锁(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(Read-Copy-Update)

对于被 RCU 保护的共享数据结构,读执行单元不需要获得任何锁就可以访问它,因此读执行单元没有任何同步开销。使用 RCU 的写执行单元在访问它前需首先拷贝一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的 CPU 都退出对共享数据的操作的时候。写执行单元的同步开销则取决于使用的写执行单元间同步机制。RCU在驱动中很少使用,这里暂不详述。

注意事项

  • 自旋锁实际上是忙等锁,当锁不可用时, CPU 一直循环执行“测试并设置”该锁直到可用而取得该锁, CPU 在等待自旋锁时不做任何有用的工作,仅仅是等待。 因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。 当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
  • 自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的 CPU 想第二次获得这个自旋锁,则该 CPU 将死锁。
  • 自旋锁锁定期间不能调用可能引起进程调度而导致休眠的函数。如果进程获得自旋锁之后再阻塞, 如调用 copy_from_user()、 copy_to_user()、 kmalloc()和 msleep()等函数,则可能导致内核的崩溃。

信号量 semaphore

使用方式和自旋锁类似,不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

  • 定义/初始化

    #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);
    

Reader/Writer Semaphores

读写信号量与信号量的关系与读写自旋锁和自旋锁的关系类似,读写信号量可能引起进程阻塞,但它可允许 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);
    

完成量 completion

轻量级,用于一个执行单元等待另一个执行单元执行完某事。

  • 定义/初始化

    #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设备驱动中的并发控制问题及其解决方法。通过使用合适的互斥机制,我们可以避免竞态的发生,提高设备驱动的稳定性和性能。在实际开发中,我们需要根据不同的场景选择最优的方案,并注意避免死锁、优先级反转等潜在的问题。

Das obige ist der detaillierte Inhalt vonWie kann man Probleme mit der Parallelitätskontrolle in Linux-Gerätetreibern lösen?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:lxlinux.net. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen