Linux Kernel驱动开发中常用的并发和竞争处理机制浅析

Linux是一个multi-taskOS。从User Space来看有个多个processes,从Kernel Space来看有多个kernel control path。何谓kernel control path?举例如下:

1.       当一个User Application invoke一个system call,这个system call的执行过程就是一个kernel control path

2.       一个Kernel Thread也是一个kernel control path

3.       处理一个interrtupt,包括我们自己定义的interrtupt handler的执行以及deferrable function,也是一个kernel control path

 

而无论这些User Processes还是Kernel control path,它们都是穿插执行的(interleaved)。一个可能被另一个打断执行。否则的话,Linux OS就不能称之为multi-task了。我们这里主要讨论在Kernel Control Path,它其实也涵盖了大多Processes中碰到的情形。

在一段时间里面,多个kernel control path穿插执行(当然,对单CPUarchtecture来说,任何一个时间点都只会有一个kernel control path 可以获得CPU并执行)。这个我们就叫做concurrency。而它们之间如果有sharing resource(如:datavariabledata structure),那么必然产生race conditions。比如说一个kernel control path刚好写了某个sharing data structure一半,接着被另一个kernel control path打断,而新的这个kernel control path要读这个sharing data structure,而此时该sharing data structure是不一致的(non-consistent);这就是race condition。要解决这个问题,关键就在于要保持access这些sharing resource时保持原子性(atomic),即要么压根儿没有access到,要么完成所希望完成的所有的操作。

讨论:

1.       LCD Framebuffer其实也有同样的问题,如果某个process要画“A”,画了一半的时候,另一个processscheduled并画“B”,那么我们就可能看到半个“A”和一个“B”,这显然不是我们想要的。大家不妨讨论以下QT/Qtopia是如何实现的?还可能有哪些解决方法?

1.1                   System callkernel threads之间的concurrency以及相应的解决方法

运行在user spaceprocess invoke某个system call(如read)时进入了该system callkernel control path,该kernel control path就会和另一个user space process invoke的同一个或不同的system call存在concurrency,同时也可能会和某个kernel thread存在concurrency。如果它们有sharingdatavariabledata structure,那么就有race condition。这个和user space 的多个processesuser spacesharing resource时存在concurrencyrace condition几乎是一致的。只是这些sharing resource一个存在user space,一个在kernel space。而在kernel spacerace condition更容易发生,因为随便定义一个global variable就有可能发生。而user space反而不容易发生。

讨论:

2.       为什么user space 不容易发生?这是因为OS的设计本身就尽量保证各个不同的proecesses的独立性,但是它们在有意建立sharing memorysys_shmget、或者访问同一个flash上的file是也是有可能发生的。

System call或kernel threads之间的concurrency的解决方法相对而言是最为丰富的,下面我们逐一描述之。

注:这里所谓system call主要是指device driver中我们实现的一部分,如readwriteLinux Kernel本身的部分,这方面是比较确保没有问题的。

1.1.1              Semaphore and mutex

学过OS都会知道PV operation,当process Aaccess某个sharing resource,首先判定该sharing resource是否已经被其它的,如process B拥有在accessing中?是,则进入sleep,等待process B释放它;否,则获得该resource,并且标志其已经在accessing中,并开始使用之。这个过程通常叫P operation反之,当使用完成后会释放它、如果有process正在等待该sharing resource,那么就wake up它。这个过程叫V operation

sharing resource可以有多个,如N个,那么:

1.       P operation等价于:N――;ifN >=0access else go to sleep

2.       V operation等价于:N++;ifN < 0wake up P operationsleep的其中的一个process

 

这个N加上PV operation实际上就semaphore,当N1时,概念上就是一个mutex

讨论:

3.       Sharing resource也可以是一段code,这段code我们一般叫做critical sectioncritical section可以简单的理解为要access共用的datavariabledata structure的一段code,通过mutex我们就可以控制该段code的并发执行。

下面我们来看看Linux Kernel的实现:

struct semaphore {

   atomic_t count;   //对应于前面的N

   int sleepers;    //记录有多少个processkernel thread正在等待该semaphore可用

   wait_queue_head_t wait; // wait queue.

};

P operation为:

static inline void down(struct semaphore * sem) //不能被signal wake up,前面分析过类似的东西

static inline int down_interruptible (struct semaphore * sem)  //可以被signal wake up

static inline int down_trylock(struct semaphore *sem) //可获得就用,不可获得不会go to sleep,而直接返回非零值

 

V operation为:

static inline void up(struct semaphore * sem)

 

我们通常可以用Linux Kernel实现的一些helper functionmacro进行初始化:

static inline void sema_init(struct semaphore *sem, int val)

__SEMAPHORE_INIT(name, cnt)   

__DECLARE_SEMAPHORE_GENERIC(name,count)

DECLARE_MUTEX(name)

DECLARE_MUTEX_LOCKED(name)

static inline void init_MUTEX(struct semaphore *sem)

static inline void init_MUTEX_LOCKED(struct semaphore *sem)

其中name是该semaphorevariable name。这些都很简单,需要的话对照Linux Kernelsource code过一遍就行了。

1.1.1.1       down为例子分析一下Kernel是如何实现的

我们可以一起看一下代码:down--->__down_op--->__down_failed--->__down

讨论:

4.       对于semaphore available时,down中都是assembler 实现的,为什么呢?这个主要是从performance来考虑的,所以semaphoredown成功概率较高的情形下,其performance是较高的。这也就是说如果从performance的角度来考虑,semaphore比较适合用于down 成功概率高的应用场合。

1.1.2              rt_mutexsys_futex

Semaphore and mutex都不能解决优先级翻转的问题,在Linux Kernel我们可以使用rt_mutex_lock。

用户程序可以使用pthread lib中的__pthread_mutex_lock,它就是通过调用sys_futex来加锁的。sys_futex也可以解决优先级翻转的问题。

这部分我想以后找个时间我会详细分析补充之。

1.1.3              Read/Write Semaphores

当多个kernel control path只是读或是读多写少的时候,Semaphoreperformance就相对较低了,因为多个同时读是没有问题的。Linux Kernel为了提高读多写少时的performance引入了Read/Write Semaphores。它的基本定义如下:

1.       读请求时,如果没有writer,此时就算有其它的reader,读请求成功,并开始读。

2.       写请求时,只有在即没有reader,也没有writer,写请求才可以成功,并开始写。

我们要注意的是:读和写是由designer决定的,如果明明进行读请求,但是designer却进行了写,那么这个designer犯的bug,而不是rw semaphore的问题。

 

不同的architecturerw semaphore的实现有所不同。但是ARM architecture并没有自己的实现,而是采用了generic的实现。其header file为:include/linux/rwsem.h。其中我们看下面一段:

#ifdef CONFIG_RWSEM_GENERIC_SPINLOCK

#include <linux/rwsem-spinlock.h> /* use a generic implementation */

#else

#include <asm/rwsem.h> /* use an arch-specific implementation */

#endif

我们看看linux/rwsem-spinlock.h中:

/*

 * the rw-semaphore definition

 * - if activity is 0 then there are no active readers or writers

 * - if activity is +ve then that is the number of active readers

 * - if activity is -1 then there is one active writer

 * - if wait_list is not empty, then there are processes waiting for the semaphore

 */

struct rw_semaphore {

   __s32                   activity;

   spinlock_t             wait_lock;

   struct list_head      wait_list;

#ifdef CONFIG_DEBUG_LOCK_ALLOC

   struct lockdep_map dep_map;  //debug有关,我们不用管

#endif

};

我们可以用init_rwsem进行初始化:init_rwsem---> __init_rwsem (__init_rwsem要看rwsem-spinlock.c中的实现)

P operation为:

1.       for reader void down_read(struct rw_semaphore *sem)

int down_read_trylock(struct rw_semaphore *sem)

2.       for writer  void down_write(struct rw_semaphore *sem)

int down_write_trylock(struct rw_semaphore *sem)

 

V operation

1.       for readervoid up_read(struct rw_semaphore *sem)

2.       for writervoid up_write(struct rw_semaphore *sem)

1.1.3.1       down_write为例子分析一下Kernel是如何实现的

down_write--->__down_write--->__down_write_nested

讨论:

5.       CONFIG_XXX是否define,一般由make menuconfig时候决定,通常我们可以从xxx_defconfig文件中找到是否define

6.       readerwriterpriority是一样的,reader必须等writer完成,writer也必须等reader完成。

1.1.4              Completionwait_event

Completion原本是Linux kernel为了解决semaphoreSMP上的一个issue而设计的(SMP我们不作讨论,这个问题在ULK5.2.10中有详细的描述,有兴趣的同仁去看看ULK好了。),但是它无疑对UP上的某些问题提供了很直观的解决方法,这个场景就是:Kernel Control Path A做一件事情之前,要等另外Kernel Control Path B一件事情完成。

使用completion我们可以形象的实现为:

先定义struct completion XXX_completion

Kernel Control Path Await_for_completion(& XXX_completion)

Kernel Control Path B:完成该工作后complete(& XXX_completion)

实际上在UP上我们可以semaphore来代替实现,只是没有这么直观了。

 

另外和completion类似的有一个wait_event,我们之前在4.2.2看过。基本上wait_event可以完全替换completion了。

1.1.5              Spin lock

Spin lock对以单CPUnon-preemptivekernel而言,其实什么也没有做,所以我们这里就不详述了。既然什么也没有做,自然在我们的驱动中如果使用spin lock其实没有任何意义,也起不到lock的作用。

为了便于有兴趣的同仁去阅读kernelcode,我简单提几点:

1.       我们先用ARM926,其对应的architecture versionARMv5ARMv5是不支持SMP的,从ARMv6才开始支持。但是目前Kernel已经开始支持ARMv6了,include/asm-arm/spin_lock.h就是为ARMv6以及以上的版本而设计的。

2.       对于ARMv5及以下,并没有这样一个head file,其对应的head fileuni-processor共用的include/linux/spinlock_up.h,这里up就是uni-processor的意思。

 

另外Linux Kernel还提供了:

1.       spin_lock_irq:在UP下其实就是disable ARM interrupt

2.       spin_lock_bh:在UP下其实就是disable buttom half

3.       spin_lock_irqsave/ spin_unlock_irqrestorestoredisablerestore irq

1.1.6              Seqlocks

前面我们分析read/write semaphore的时候曾经讨论过:read/write semamphorereaderwriterpriority是一样的,reader必须等writer完成,writer也必须等reader完成。那么如果在某种应用场合下,如果writer有较高要求:

1.       writer要求在确定的时间内必须完成。

2.       writer不允许进入sleep

此时如果我们还是用read/write semaphorewriter有可能由于要等待所有的readers都完成而等待很长的时间。通常如果interrupt handlerwriter的话,那么就属于这种情况了。

Linux Kernel为此实现了seqlock,在uni-processornon-preemtiveconfig下其基本概念为:

1.       for reader,读的时候尽管去读,读完看看这段时间里面有没有被写过,如果没有这次读自然是成功了,如果有则重新读过。

2.       for writer,写的时候尽管去写。

我们请看kernel自身的一个例子:readertick_setup_periodic或者__get_realtime_clock_tswritertick_periodic,这个也是我们在第7章中分析过的。

讨论:

7.       For SMPwrite_seqlock通过spin_lock进行多个writers之间的保护。而for UPspin_lock却什么也没有保护,从UP迁移到SMP,这里是会有port的问题的。

8.       uni-processor,只能有一个writer,多个writers是不能保证consistent的。Why?

9.       for UP,缺点在于read的时间不确定,但是如果write很少的场合,那么read的时间基本上还是能够确定的。如果write太多,每次read的内容也较多,也就意味着一次read过程中,被write的可能性很高,如此就导致reader需要反复的read,那么就不适合了。

10.   对于有多个writer,由于每次writer的内容不能保证一致,所以各个writer之间必须要进行互斥了。多个reader当然没有问题。

1.1.7              Read-Copy Update (RCU)

For SMP,以后补充吧

1.1.8              Atomic operation

有很多时候,我们需要synchronization的仅仅是一个integer,比如说多个kernel control pathaccess某个resource,之后会对number++,以记录所有kernel control path总共access的次数。但是number++这样一行看起来很简单的语句在ARMv5中也需要三条instructions来完成:

1.       ldr:从memoryload numberregister

2.       add:将该register中的内容加一。

3.       str:将该register中的内容store number所在的memory中。

如果在12之间被另一个要做number++kernel control path打断的话,那么number就少1了。在ARMv5中解决的方法也很简单,就是disable interrupt来保证13不会被打断:

static inline int atomic_add_return(int i, atomic_t *v)

{

   unsigned long flags;

   int val;

 

   raw_local_irq_save(flags);

   val = v->counter;

   v->counter = val += i;

   raw_local_irq_restore(flags);

 

   return val;

}

Linux Kernel还提供了一系列的atomic operation,大家可以参考LDD5.7.2

1.1.9              Interrupt Enabling and Disabling

这是最为彻底的做法,一旦interrupt disabling,那么接下来的code会被打断,这是保证atomic operation的做法。但是它保证的只是我自己不会被别人打断,但是我是否打断了别人,那就不得而知了。但是如果别人在也同样使用了Interrupt Enabling and Disabling,那么我们才能知道相互之间是不会再有打断或干扰的了。

事实上我也知道其实前面一节atomic operaionARMv5上的实现就是通过interrupt disabling来实现的。Interrupt Enabling and Disabling更多的是应用在后面将描述的场景下,后面还会详述。

讨论:

11.   ARMv5下,实际上atomic operationInterrupt Enabling and Disabling就是一项技术,只是atomic operation封装好了Interrupt Enabling and Disabling。但是在ARMv6,由于其本提供了很多atomicintructionatomic operation更为简单了。

12.   之前曾经反复讨论过,interrupt disabling的时间一定要控制的非常精准、一定要很短,通常要求都要在1us之内。它的优点就在于,之后的执行时间很确定,更不需要进入sleep。这是semaphore所不具备的。

1.2                   System call/kernel threadinterrupt handler(含deferrable function)之间的concurrency以及相应的解决方法

这种情况其实是device driver设计中经常遇到,我们从两个角度来看:

1.       对于interrupt handler(或deferrable function)来说,system callkernel thread是不会打断它的,所以它并不担心non-consistence的问题。

2.       反之则不然。

 

所以对于此种情形,我们要考虑的是system callkernel threadaccess一些sharing resource的中间被interrupt handler打断,导致non-consistence。由于interrupt handler不能使用可能导致sleepfunction,所以我们也就不能使用semaphore

最为常用的方法是system callkernel thread 在访问这些sharing resource之前就disable Interrupt ,那么就自然不担心互斥的问题了。如果是system callkernel threaddeferable function之间,那么我们可以不用disable interrupt,只要disable bottom half就可以了:local_bh_disable/local_bh_enable。

有时候为了尽量不影响系统整体的并发性,我们并不需要disable ARMglobal interrupt,而用disable_irq只关闭对应的irq就可以了。

理论上我们可以在interrupt handler中使用:

1.       write_seqlock,但是这个也仅仅在UP中使用,而在system callkernel thread中使用read_seqbegin

2.       complete,而在system callkernel thread中使用wait_for_completion。这和我们之前介绍的wait_event类似。

但是这些相对不常用,也建议尽量少这样用吧。

1.3                   Deferable functioninterrupt handler之间的concurrency以及相应的解决方法

类似11.2,但是在deferable function不能使用wait_for_completionwait_event,不再赘述。

1.4                   interrupt handler之间的concurrency以及相应的解决方法

方法基本上就是local_irq_disable/local_irq_enable或是disable_irq/enable_irq

1.5                   Lock free algorithm – circular buffer介绍

对于仅有一个producerconsumer的应用场景,良好的设计可以保证两者lock free并发访问circular buffer。其实这个在LDD中就详述。这里就不罗嗦了。

1.6                   ARM Linux下的exception补充介绍

ARM下面的exception都在ARM vector table中有对应的entry,如SWIdata abortintruction pre-fetch abort, undefined intruction,甚至resetException的处理其实也是一个kernel control path,也存在之前所讨论的问题,但是这些Linux Kernel本身已经帮我们考虑了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值