Linux是一个multi-task的OS。从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穿插执行(当然,对单CPU的archtecture来说,任何一个时间点都只会有一个kernel control path 可以获得CPU并执行)。这个我们就叫做concurrency。而它们之间如果有sharing resource(如:data、variable,data 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”,画了一半的时候,另一个process被scheduled并画“B”,那么我们就可能看到半个“A”和一个“B”,这显然不是我们想要的。大家不妨讨论以下QT/Qtopia是如何实现的?还可能有哪些解决方法?
1.1 System call或kernel threads之间的concurrency以及相应的解决方法
运行在user space的process invoke某个system call(如read)时进入了该system call的kernel control path,该kernel control path就会和另一个user space process invoke的同一个或不同的system call存在concurrency,同时也可能会和某个kernel thread存在concurrency。如果它们有sharing的data、variable,data structure,那么就有race condition。这个和user space 的多个processes在user space有sharing resource时存在concurrency和race condition几乎是一致的。只是这些sharing resource一个存在user space,一个在kernel space。而在kernel space,race condition更容易发生,因为随便定义一个global variable就有可能发生。而user space反而不容易发生。
讨论:
2. 为什么user space 不容易发生?这是因为OS的设计本身就尽量保证各个不同的proecesses的独立性,但是它们在有意建立sharing memory(sys_shmget)、或者访问同一个flash上的file是也是有可能发生的。
System call或kernel threads之间的concurrency的解决方法相对而言是最为丰富的,下面我们逐一描述之。
注:这里所谓system call主要是指device driver中我们实现的一部分,如read、write,Linux Kernel本身的部分,这方面是比较确保没有问题的。
学过OS都会知道PV operation,当process A要access某个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――;if(N >=0)access; else go to sleep。
2. V operation等价于:N++;if(N <= 0)wake up 在P operation中sleep的其中的一个process。
这个N加上PV operation实际上就semaphore,当N为1时,概念上就是一个mutex。
讨论:
3. Sharing resource也可以是一段code,这段code我们一般叫做critical section,critical section可以简单的理解为要access共用的data、variable,data structure的一段code,通过mutex我们就可以控制该段code的并发执行。
下面我们来看看Linux Kernel的实现:
struct semaphore {
atomic_t count; //对应于前面的N
int sleepers; //记录有多少个process或kernel 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 function或macro进行初始化:
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是该semaphore的variable name。这些都很简单,需要的话对照Linux Kernel的source code过一遍就行了。
1.1.1.1 以down为例子分析一下Kernel是如何实现的
我们可以一起看一下代码:down--->__down_op--->__down_failed--->__down
讨论:
4. 对于semaphore available时,down中都是assembler 实现的,为什么呢?这个主要是从performance来考虑的,所以semaphore在down成功概率较高的情形下,其performance是较高的。这也就是说如果从performance的角度来考虑,semaphore比较适合用于down 成功概率高的应用场合。
1.1.2 rt_mutex、sys_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只是读或是读多写少的时候,Semaphore的performance就相对较低了,因为多个同时读是没有问题的。Linux Kernel为了提高读多写少时的performance引入了Read/Write Semaphores。它的基本定义如下:
1. 读请求时,如果没有writer,此时就算有其它的reader,读请求成功,并开始读。
2. 写请求时,只有在即没有reader,也没有writer,写请求才可以成功,并开始写。
我们要注意的是:读和写是由designer决定的,如果明明进行读请求,但是designer却进行了写,那么这个designer犯的bug,而不是rw semaphore的问题。
不同的architecture,rw 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 reader:void up_read(struct rw_semaphore *sem)
2. for writer:void 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. reader和writer的priority是一样的,reader必须等writer完成,writer也必须等reader完成。
1.1.4 Completion和wait_event
Completion原本是Linux kernel为了解决semaphore在SMP上的一个issue而设计的(SMP我们不作讨论,这个问题在ULK之5.2.10中有详细的描述,有兴趣的同仁去看看ULK好了。),但是它无疑对UP上的某些问题提供了很直观的解决方法,这个场景就是:Kernel Control Path A做一件事情之前,要等另外Kernel Control Path B一件事情完成。
使用completion我们可以形象的实现为:
先定义struct completion XXX_completion
Kernel Control Path A:wait_for_completion(& XXX_completion)
Kernel Control Path B:完成该工作后complete(& XXX_completion)
实际上在UP上我们可以semaphore来代替实现,只是没有这么直观了。
另外和completion类似的有一个wait_event,我们之前在4.2.2看过。基本上wait_event可以完全替换completion了。
Spin lock对以单CPU、non-preemptive的kernel而言,其实什么也没有做,所以我们这里就不详述了。既然什么也没有做,自然在我们的驱动中如果使用spin lock其实没有任何意义,也起不到lock的作用。
为了便于有兴趣的同仁去阅读kernel的code,我简单提几点:
1. 我们先用ARM926,其对应的architecture version为ARMv5,ARMv5是不支持SMP的,从ARMv6才开始支持。但是目前Kernel已经开始支持ARMv6了,include/asm-arm/spin_lock.h就是为ARMv6以及以上的版本而设计的。
2. 对于ARMv5及以下,并没有这样一个head file,其对应的head file为uni-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_irqrestore:store、disable、restore irq。
前面我们分析read/write semaphore的时候曾经讨论过:read/write semamphore的reader和writer的priority是一样的,reader必须等writer完成,writer也必须等reader完成。那么如果在某种应用场合下,如果writer有较高要求:
1. writer要求在确定的时间内必须完成。
2. writer不允许进入sleep。
此时如果我们还是用read/write semaphore,writer有可能由于要等待所有的readers都完成而等待很长的时间。通常如果interrupt handler是writer的话,那么就属于这种情况了。
Linux Kernel为此实现了seqlock,在uni-processor和non-preemtive的config下其基本概念为:
1. for reader,读的时候尽管去读,读完看看这段时间里面有没有被写过,如果没有这次读自然是成功了,如果有则重新读过。
2. for writer,写的时候尽管去写。
我们请看kernel自身的一个例子:reader:tick_setup_periodic或者__get_realtime_clock_ts;writer:tick_periodic,这个也是我们在第7章中分析过的。
讨论:
7. For SMP,write_seqlock通过spin_lock进行多个writers之间的保护。而for UP,spin_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当然没有问题。
For SMP,以后补充吧
有很多时候,我们需要synchronization的仅仅是一个integer,比如说多个kernel control path要access某个resource,之后会对number++,以记录所有kernel control path总共access的次数。但是number++这样一行看起来很简单的语句在ARMv5中也需要三条instructions来完成:
1. ldr:从memory中load number到register中
2. add:将该register中的内容加一。
3. str:将该register中的内容store 到number所在的memory中。
如果在1,2之间被另一个要做number++的kernel control path打断的话,那么number就少1了。在ARMv5中解决的方法也很简单,就是disable interrupt来保证1-3不会被打断:
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,大家可以参考LDD之5.7.2。
1.1.9 Interrupt Enabling and Disabling
这是最为彻底的做法,一旦interrupt disabling,那么接下来的code会被打断,这是保证atomic operation的做法。但是它保证的只是我自己不会被别人打断,但是我是否打断了别人,那就不得而知了。但是如果别人在也同样使用了Interrupt Enabling and Disabling,那么我们才能知道相互之间是不会再有打断或干扰的了。
事实上我也知道其实前面一节atomic operaion在ARMv5上的实现就是通过interrupt disabling来实现的。Interrupt Enabling and Disabling更多的是应用在后面将描述的场景下,后面还会详述。
讨论:
11. 在ARMv5下,实际上atomic operation和Interrupt Enabling and Disabling就是一项技术,只是atomic operation封装好了Interrupt Enabling and Disabling。但是在ARMv6,由于其本提供了很多atomic的intruction,atomic operation更为简单了。
12. 之前曾经反复讨论过,interrupt disabling的时间一定要控制的非常精准、一定要很短,通常要求都要在1us之内。它的优点就在于,之后的执行时间很确定,更不需要进入sleep。这是semaphore所不具备的。
1.2 System call/kernel thread与interrupt handler(含deferrable function)之间的concurrency以及相应的解决方法
这种情况其实是device driver设计中经常遇到,我们从两个角度来看:
1. 对于interrupt handler(或deferrable function)来说,system call或kernel thread是不会打断它的,所以它并不担心non-consistence的问题。
2. 反之则不然。
所以对于此种情形,我们要考虑的是system call或kernel thread在access一些sharing resource的中间被interrupt handler打断,导致non-consistence。由于interrupt handler不能使用可能导致sleep的function,所以我们也就不能使用semaphore。
最为常用的方法是system call或kernel thread 在访问这些sharing resource之前就disable Interrupt ,那么就自然不担心互斥的问题了。如果是system call或kernel thread和deferable function之间,那么我们可以不用disable interrupt,只要disable bottom half就可以了:local_bh_disable/local_bh_enable。
有时候为了尽量不影响系统整体的并发性,我们并不需要disable ARM的global interrupt,而用disable_irq只关闭对应的irq就可以了。
理论上我们可以在interrupt handler中使用:
1. write_seqlock,但是这个也仅仅在UP中使用,而在system call或kernel thread中使用read_seqbegin。
2. complete,而在system call或kernel thread中使用wait_for_completion。这和我们之前介绍的wait_event类似。
但是这些相对不常用,也建议尽量少这样用吧。
1.3 Deferable function和interrupt handler之间的concurrency以及相应的解决方法
类似11.2,但是在deferable function不能使用wait_for_completion或wait_event,不再赘述。
1.4 interrupt handler之间的concurrency以及相应的解决方法
方法基本上就是local_irq_disable/local_irq_enable或是disable_irq/enable_irq。
1.5 Lock free algorithm – circular buffer介绍
对于仅有一个producer和consumer的应用场景,良好的设计可以保证两者lock free并发访问circular buffer。其实这个在LDD中就详述。这里就不罗嗦了。
1.6 ARM Linux下的exception补充介绍
ARM下面的exception都在ARM vector table中有对应的entry,如SWI,data abort,intruction pre-fetch abort, undefined intruction,甚至reset。Exception的处理其实也是一个kernel control path,也存在之前所讨论的问题,但是这些Linux Kernel本身已经帮我们考虑了。