ホームページ >システムチュートリアル >Linux >Linux ドライバーの同時実行制御テクノロジー: 原則と実践
組み込み Linux 開発者は、次の質問に遭遇するかもしれません。複数のタスクまたはスレッド間でデバイス リソースを安全に共有するにはどうすればよいですか?データの競合や不整合を回避するにはどうすればよいですか?システムのパフォーマンスと信頼性を向上させるにはどうすればよいでしょうか?これらの問題はすべて、同時実行制御テクノロジ、つまり共有リソースへの複数の実行エンティティのアクセスを調整する方法に関係します。この記事では、アトミック操作、スピンロック、セマフォ、ミューテックスロック、読み書きロック、シーケンシャルロック、RCUなど、Linuxドライバーで一般的に使用される同時実行制御技術を紹介し、その使用例と注意点を示します。 。 案件。
重要なリソースの効果的な管理を実現するために、アプリケーション層プログラムには同時実行性を制御するためのアトミック変数、条件変数、およびセマフォがあります。同じ問題は、ドライバーが複数のアプリケーション層プログラムによって呼び出されるなど、ドライバー開発にも存在します。このとき、ドライバー内のグローバル変数は複数のアプリケーション層プロセスのプロセス空間に同時に属することになります。この場合、同時実行性を制御するためにいくつかのテクノロジも使用する必要があります。この記事では、カーネルにおける次の同時実行制御テクノロジの技術的特性とアプリケーション シナリオについて説明します。
名前が示すように、すべての割り込みをブロックすることを意味します。組み込みシステムでは、割り込みシールドには 1. ハードウェア インターフェイス シールド、2. ハードウェア GIC シールド、3. CPU (カーネル) シールドの 3 つのレベルがあります。 インターフェイスでブロックされている場合、割り込みは失われ、まったく見つけることができません。 GIC でシールドされている場合、シールド期間中に irq_1、irq_2、irq_3 割り込みが発生すると、保留フラグが 1 つしかないため、irq_3 が最終的に到来したときに保留フラグが設定され、その後シールドがブロック解除され、 CPU は保留中のビットがあることを検出します。ビットが設定されている場合でも処理されますが、1 と 2 は確実に失われます。 ARM でのシールド、つまりカーネル内のシールドは設定によって異なります。local_irq_disable の場合は失われると失われます。インターフェイスでのシールドと同じです。 local_irq_save, これは 2 番目と同じで、最後の割り込みを追跡します。カーネルには、割り込みをカウントし、この期間中に発生した割り込みの数を知るための対応するメカニズムもあります。ただし、実際の操作では、ほとんどの場合、追跡しません。割り込みが非常に重要でない限り、見逃した割り込み。
ここで説明しているのは、カーネルでの割り込みマスキングです。カーネル内の多くの重要な操作は割り込みに依存しているため、すべての割り込みをマスクすることは非常に危険です。内部で実行されるコードはできるだけ高速でなければなりません。さらに、カーネルのプロセス スケジューリングも割り込みによって駆動されるため、非常に危険ですスリープをトリガーする可能性のあるコードがあってはなりません。そうでない場合はウェイクアップできません。 割り込みマスキングは、この CPU の割り込みをマスクするだけであるため、SMP によって引き起こされる競合問題は解決されないことに注意してください。通常、割り込みマスキングは、アクセス スピンを防ぐためにスピン ロックと組み合わせて使用されます。ロックで保護されているセクションが割り込みによって中断されました
アトミック操作とは、中断できない操作です。アプリケーション層の概念と同じです。カーネル内のアトミック操作テンプレートは次のとおりです:
スピンロック
スピン ロックは、セマフォとミューテックスの基礎となる実装ツールです。
比較\タイプ | 従来のスピンロック | 読み取りおよび書き込みスピン ロック | シーケンシャルロック | #RCU メカニズム | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
応募機会 | ロッカー専用にする必要があるリソース | ライター専用のリソースが必要です | 同時に読み取りと書き込みがほとんど行われないリソース | リソースをもっと読み、書く量を減らす | |||||||||
読み取り/読み取りの同時実行性 | #xxx√ | √ | √ | ||||||||||
#xxx | #xxx√ | √ | 書き込み書き込み同時実行性 | ||||||||||
#xxx | √ |
和其他锁机制的一样,使用自旋锁保护数据分为抢锁-操作-解锁,下面就是一个典型的使用锁的流程,通过自旋锁实现一个文件只被一个进程打开。 int cnt=0; lock_t lock; static int my_open() { lock(&lock); if(cnt){ unlock(&lock) } cnt++; unlock(&lock); } static int release() { lock(&lock); cnt--; unlock(&lock); } 传统自旋锁是一种比较粗暴的自旋锁,使用这种锁的时候,被锁定的临界区域不允许其他CPU访问,需要注意的是,尽管获得锁之后执行的临界区操作不会被其他CPU和本CPU内其他抢占进程的打扰,但是仍然会被中断和底半部的影响,所以通常我们会使用下述API中的衍生版本,比如上文中提到的将自旋锁+中断屏蔽来防止使用自旋锁访问临界资源的时候被中断打断,对应的宏函数就是spin_lock_irq和spin_lock_irqsave。 //定义并初始化自旋锁 spinlock_t spinlock void spin_lock_init(spinlock_t *); //加锁 //spin_lock - 加锁函数(忙等) void spin_lock(spinlock_t *lock); int spin_trylock(spinlock_t *lock); spin_lock_irq(); //=spin_lock() + local_irq_disable() spin_lock_irqsave(); //= spin_lock() + lock_irq_save(); spin_lock_bh(); //=spin_lock() + local_bh_disable(); //解锁 void spin_unlock(spinlock_t *lock); spin_unlock_irq(); //=spin_unlock() + local_irq_enable() spin_unlock_irqrestore(); //= spin_unlock() + lock_irq_restore(); spin_unlock_bh(); //=spin_unlock() + local_bh_enable(); 读写自旋锁传统的自旋锁粗暴的将临界资源划归给一个CPU,但是很多资源都不会因为读而被破坏,所以我们可以允许多个CPU同时读临界资源,但不允许同时写资源,类似于应用层的文件锁,内核的读写锁机制同样有下述互斥原则:
//include/linux/rwlock.h //定义并初始化自旋锁 rwlock_t rwlock; void rwlock_init(rwlock_t *lock); //加读锁 void read_lock(rwlock_t *lock); int read_trylock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock,unsigned long flags); void read_lock_irq(rwlock_t *lock, unsigned long flags); void read_lock_bh(rwlock_t *lock); //解读锁 void read_unlock(rwlock_t *lock) void read_unlock_irqrestrore(rwlock_t *lock,unsigned long flags); void read_unlock_irq(rwlock_t *lock, unsigned long flags); void read_unlock_bh(rwlock_t *lock); //加写锁 void write_lock(rwlock_t *lock) int write_trylock(rwlock_t *lock) void write_lock_irqsave(rwlock_t *lock,unsigned long flags); void write_lock_irq(rwlock_t *lock, unsigned long flags); void write_lock_bh(rwlock_t *lock); //解写锁 void write_unlock(rwlock_t *lock) void write_unlock_irqrestrore(rwlock_t *lock,unsigned long flags); void write_unlock_irq(rwlock_t *lock, unsigned long flags); void write_unlock_bh(rwlock_t *lock); 顺序锁顺序锁可以看作读写锁的升级版,读写锁不允许同时存在读者和写者,顺序锁对这一情况进行了改良,它允许写者和读者同时访问临界区,不必再向读写锁那样读者要读必须等待写者写完,写者要写必须等待读者读完。不过,使用顺序锁的时候,临界区不能有指针,因为写者可能会修改指针的指向,如果读者去读,就会Oops,此外,如果读者读过的数据被写者改变了,读者需要重新读,以维持数据是最新的,虽然有这两个约束,但是顺序锁提供了比读写锁更大的灵活性。对于写者+写者的情况,顺序锁的机制和读写锁一样,必须等!
//include/linux/seqlock.h //定义顺序锁 struct seqlock_t sl; //获取顺序锁 void write_seqlock(seqlock_t *sl); void write_tryseqlock(seqlock_t *sl); void write_seqlock_irqsave(lock,flags); //=local_irq_save() + write_seqlock() void write_seqlock_irq(seqlock_t *sl); //=local_irq_disable() + write_seqlock() void write_seqlock_bh(seqlock_t *sl); //local_bh_disable() + write_seqlock() //释放顺序锁 void write_sequnlock(seqlock_t *sl); void write_sequnlock_irqsave(lock,flags); //=local_irq_restore() + write_sequnlock() void write_sequnlock_irq(seqlock_t *sl); //=local_irq_enable() + write_sequnlock() void write_sequnlock_bh(seqlock_t *sl); //local_bh_enable() + write_sequnlock() //读开始 unsigned read_seqbegin(const seqlock_t *sl); read_seqbegin_irqsave(lock,flags); //=local_irq_save() + read_seqbegin(); //重读 int read_seqretry(const seqlock_t *sl,unsigned iv); read_seqretry_irqrestore(lock,iv,flags); //=local_irq_restore() + read_seqretry(); RCURCU即Read-Copy Update,即读者直接读,写者先拷贝再择时更新,是另外一种读写锁的升级版,这种机制在VFS层被大量使用。正如其名,读者访问临界资源不需要锁,从下面的rcu_read_lock的定义即可看出,写者在写之前先将临界资源进行备份,去修改这个副本,等所有的CPU都退出对这块临界区的引用后,再通过回调机制,将引用这块资源的原指针指向已经修改的备份。从中可以看出,在RCU机制下,读者的开销大大降低,也没有顺序锁的指针问题,但是写者的开销很大,所以RCU适用于读多写少的临界资源。如果写操作很多,就有可能将读操作节约的性能抵消掉,得不偿失。
内核会为每一个CPU维护两个数据结构-rcu_data和rcu_bh_data,他们用于保存回调函数,函数call_rcu()把回调函数注册到rcu_data,而call_rcu_bh()则把回调函数注册到rcu_bh_data,在每一个数据结构上,回调函数们会组成一个队列。 使用RCU时,读执行单元必须提供一个信号给写执行单元以便写执行单元能够确定数据可以被安全地释放或修改的时机。内核中有一个专门的垃圾收集器来探测读执行单元的信号,一旦所有的读执行单元都已经发送信号告诉收集器自己都没有使用RCU的数据结构,收集器就调用回调函数完成最后的数据释放或修改操作。 读读即RCU中的R,从下面的宏定义可以看出,读操作其实就是禁止内核的抢占调度,并没有使用一个锁对象。 //读锁定 //include/linux/rcupdate.h rcu_read_lock(); //preempt_disbale() rcu_read_lock_bh(); //local_bh_disable() //读解锁 rcu_read_unlock() //preempt_enable() rcu_read_unlock_bh(); //local_bh_enable() 同步同步即是RCU写操作的最后一个步骤-Update,下面这个接口会则色写执行单元,直到所有的读执行单元已经完成读执行单元临界区,写执行单元才可以继续下一步操作。如果有多个RCU写执行单元调用该函数,他们将在一个grace period(即所有的读执行单元已经完成对临界区的访问)之后全部被唤醒。 synchrosize_rcu() 挂起回调下面这个接口也由RCU写执行单元调用,它不会使写执行单元阻塞,因而可以在中断上下文或软中断中使用,该函数把func挂接到RCU回调函数链上,然后立即返回。函数sychrosize_rcu()其实也会调用call_rcu()。 void call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *rcu)); 下面这个接口会将软中断的完成也当作经历一个quiecent state(静默状态),因此如果写执行单元调用了该函数,在进程上下文的读执行单元必须使用rcu_read_lock_bh(); void call_rcu_bh(struct rcu_head *head,void (*func)(struct rcu_head *rcu)); RCU机制被大量的运用在内核链表的读写中,下面这些就是内核中使用RCU机制保护的数据结构,函数的功能和非RCU版本一样,具体可以参考内核文件**”include/linux/list.h”**,只不过这里的操作都会使用RCU保护起来。 void list_add_rcu(struct list_head *new, struct list_head *head); void list_add_tail_rcu(struct list_head *new,struct list_head *head); void list_del_rcu(struct list_head *entry); void list_replace_rcu(struct list_head *old,struct list_head *new); list_for_each_rcu(pos,head); list_for_each_safe_rcu(pos,n,head); list_for_each_entry_rcu(pos,head,member); void hlist_del_rcu(struct hlist_node *n); void hlist_add_head_rcu(struct hlist_node *n, struct hlist_head *h); list_for_each_rcu(pos,head); hlist_for_each_entry_rcu(tpos,pos,head,member); 信号量自旋锁一节提过,如果一个CPU不能获取临界资源,就会造成”原地自旋”,所以自旋锁保护的临界区的执行时间不能太长,但如果我们的确需要保护一段执行时间比较长的临界区呢?答案就是信号量 使用信号量,如果试图获取信号量的进程获取失败,内核就会将其调度为睡眠状态,执行其他进程,避免了CPU的忙等。不过,进程上下文的切换也是有成本的,所以通常,信号量在内核中都是只用于保护大块临界区。 此外,一个进程一旦无法获取期待的信号量就会进入睡眠,所以信号量保护的临界区可以有睡眠的代码。在这方面,自旋锁保护的区域是不能睡眠也不能执行schedule()的,因为一旦一个抢到了锁的CPU进行了进程上下文的切换或睡眠,那么其他等待这个自旋锁的CPU就会一直在那忙等,就永远无法等到这个锁,,形成死锁,除非有其他进程将其唤醒(通常都不会有)。 也正是由于信号量操作可能引起阻塞,所以信号量不能用于中断上下文。总结一下刚才罗嗦这一段: ############プロジェクト### ###信号###
|
以上がLinux ドライバーの同時実行制御テクノロジー: 原則と実践の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。