内核同步(笔记)

内核如何为不同的请求提供服务

内核看作是必须满足两种不同请求的侍者:一种来自顾客,一种来自数量有限的老板,策略如下:

  1. 老板提出请求时,如果侍者空闲,则侍者开始为老板服务
  2. 如果老板提出请求时侍者正为顾客服务,那么侍者停止为老板服务,开始为老板服务
  3. 如果一个老板提出请求时侍者正为另一个老板服务,那么侍者停止为第一个老板服务,为第二个老板服务,服务完毕继续为第一个老板服务
  4. 一个老板可能命令侍者停止正为顾客提供的服务,侍者完成对老板最近请求后,可能会暂时不理会原来的顾客而为新的顾客服务。
老板的请求相当于中断,顾客的请求相当于用户态进程发出的系统调用或异常。第4条原则与linux2.6内核中的内核抢占相关。

内核抢占

无论在抢占内核还是非抢占内核中,运行在内核态的进程都可以自动放弃CPU,比如,进程由于等待资源不得不转入睡眠状态,我们将这种切换称为计划性进程切换。抢占式内核在响应引起进程切换的异步事件的方式上与非抢占内核不同,我们把这种进程切换叫做强制性进程切换

所有的进程切换都是由宏switch_to完成的。

抢占内核的特点是:一个在内核态运行的进程,可能在执行内核函数期间被另一个进程取代

使内核可抢占的目的是减少用户态进程的分派延迟(dispatch latency),即从进程变为可执行状态到他实际开始运行之间的时间间隔。

thread_info描述符的preempt_count字段大于0时,就禁止内核抢占。在如下三种情况中,取值都会大于0:

  1. 内核正在执行中断服务例程
  2. 可延迟函数被禁止
  3. 通过把抢占式计数器设置为正数而显示的禁用内核抢占
下面是处理抢占计数器字段的宏

preempt_count 在thread_info描述符中选择preempt_count字段

preempt_disable 使抢占计数器的值加1

preempt_enable_no_resched 使抢占计数器的值减1

preempt_enable 使计数器的值减1,并在thread_info描述符的TIF_NEED_RESCHED标志被置为1的情况下,调用preempt_schedule

get_cpu 与preempt_disable相似,但要返回本地CPU的数量

put_cpu 与preempt_enable相同

put_cpu_no_resched 与preempt_enable_no_resched相同

#define preempt_count() (current_thread_info()->preempt_count)
#define preempt_disable() \                                                      
do { \                                                                           
    inc_preempt_count(); \                                                       
    barrier(); \                                                                 
} while (0)                                                                      
                                                                                 
#define preempt_enable_no_resched() \                                            
do { \                                                                           
    barrier(); \                                                                 
    dec_preempt_count(); \                                                       
} while (0)
#define preempt_enable() \                                                          
do { \                                                                              
    preempt_enable_no_resched(); \                                                  
    preempt_check_resched(); \                                                      
} while (0)
#define preempt_check_resched() \                                                   
do { \                                                                              
    if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \                             
        preempt_schedule(); \                                                       
} while (0)
#define get_cpu()       ({ preempt_disable(); smp_processor_id(); })             
#define put_cpu()       preempt_enable()                                            
#define put_cpu_no_resched()    preempt_enable_no_resched()

asmlinkage void __sched preempt_schedule(void)                                      
{
    struct thread_info *ti = current_thread_info();
#ifdef CONFIG_PREEMPT_BKL
    struct task_struct *task = current;                                             
    int saved_lock_depth;
#endif
    /*
     * If there is a non-zero preempt_count or interrupts are disabled,
     * we do not want to preempt the current task.  Just return..
     */
    if (unlikely(ti->preempt_count || irqs_disabled()))
        return;                                                                     

need_resched:                                                                       
    add_preempt_count(PREEMPT_ACTIVE);                                              
    /*                                                                              
     * We keep the big kernel semaphore locked, but we                              
     * clear ->lock_depth so that schedule() doesnt                                 
     * auto-release the semaphore:                                                  
     */                                                                             
#ifdef CONFIG_PREEMPT_BKL                                                           
    saved_lock_depth = task->lock_depth;                                         
    task->lock_depth = -1;                                                       
#endif                                                                           
    schedule();                                                                  
#ifdef CONFIG_PREEMPT_BKL                                                        
    task->lock_depth = saved_lock_depth;                                         
#endif                                                                           
    sub_preempt_count(PREEMPT_ACTIVE);                                           
                                                                                 
    /* we could miss a preemption opportunity between schedule and now */        
    barrier();                                                                   
    if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))                            
        goto need_resched;                                                       
}
linux2.6独具特色的允许用户在编译内核时通过选项设置来禁用或启用内核抢占
什么时候同步是必需的

临界区是 一段代码,在其他的内核控制路径能够进入临界区前,进入临界区的内核控制路径必须全部执行完这段代码。

什么时候同步是不必要的

中断处理程序和tasklet不必编写成可重入的函数

仅被软中断和tasklet访问的每CPU变量不需要同步。

仅被一种tasklet访问的数据结构不需要同步。

同步原语

内核使用的各种同步技术
技术 说明 适用范围
每CPU变量 在CPU之间复制数据结构 所有CPU
原子操作 对一个计数器原子的“读-修改-写”的指令 所有CPU
内存屏障 避免指令重新排序 本地CPU或所有CPU
自旋锁 加锁时忙等 所有CPU
信号量 加锁时阻塞等待(睡眠) 所有CPU
顺序锁 基于访问计数器的锁 所有CPU
本地中断的禁止 禁止单个CPU上的中断处理 本地CPU
本地软中断的禁止 禁止单个CPU上的可延迟函数处理 本地CPU
读-拷贝-更新(RCU) 通过指针而不是锁来访问共享数据结构 所有CPU

最好的同步技术是把设计不需要同步的内核放在首位。每一种显示的同步原语都有 不可忽视的性能开销
最简单也是最重要的同步技术,包括把内核变量生命为每CPU变量(per-cpu variable)。 每CPU变量基本上只能在特殊情况下使用,就是确定在系统的CPU上的数据在逻辑上是独立的
每CPU变量的数组元素在主存中排列以使每个数据结构存放在硬件高速缓存的不同行,所以, 对每CPU数组的并发访问不会导致高速缓存行的窃用和失效。每CPU变量对来自 异步函数(中断处理和可延迟函数)的访问不提供保护。在单处理器和多处理器系统中,内核抢占可能使每CPU变量产生竞争条件, 内核控制路径应该在禁用抢占的情况下访问每CPU变量
为每CPU变量提供的函数和宏,下面是针对SMP提供的宏
#define DEFINE_PER_CPU(type, name) \//静态分配一个每CPU数组,数组名为name,结构类型为type
    __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name
#define __get_cpu_var(var) per_cpu(var, smp_processor_id())//选择每CPU数组var的本地CPU元素
#define get_cpu_var(var) (*({ preempt_disable(); &__get_cpu_var(var); }))//先禁用内核抢占,然后在每CPU数组var中,为本地CPU选择元素
#define put_cpu_var(var) preempt_enable()//启用内核抢占
#define alloc_percpu(type) \//动态分配type类型数据结构的每CPU数组,并返回他的地址
    ((type *)(__alloc_percpu(sizeof(type), __alignof__(type))))
static inline void *__alloc_percpu(size_t size, size_t align)                       
{                                                                                   
    void *ret = kmalloc(size, GFP_KERNEL);                                          
    if (ret)                                                                        
        memset(ret, 0, size);                                                       
    return ret;                                                                     
}
static inline void free_percpu(const void *ptr)//释放动态分配的每CPU数组,ptr指示其地址
{                                                                                   
    kfree(ptr);                                                                     
}
#define per_cpu_ptr(ptr, cpu)                   \//返回每CPU数组中与参数cpu对应的CPU元素地址,参数ptr给出数组地址
({                                              \                                   
        struct percpu_data *__p = (struct percpu_data *)~(unsigned long)(ptr); \ 
        (__typeof__(ptr))__p->ptrs[(cpu)];  \                                       
})
#define per_cpu(var, cpu) (*RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]))//为CPU选择一个每CPU数组元素,CPU由参数cpu指定,数组名称为var
原子操作
 
    
避免“读-修改-写”指令,引起的的竞争条件的最容易的办法,就是却把这样的操作在芯片级是原子的。
  • 进行零次或一次对齐内存访问的汇编指令是原子的
  • 如果在读操作之后、写操作之前没有其他处理器占用内存总线,那么从内存中读取数据、更新数据并把更新后的数据写回内存中的这些“读-修改-写”汇编指令是原子。
  • 操作码前缀是lock字节的“读-修改-写”汇编语言指令在多处理器系统中是原子的。
  • 操作码前缀是一个rep字节的汇编语言指令不是原子的
#define atomic_read(v)      ((v)->counter)
#define atomic_set(v,i)     (((v)->counter) = (i))
static __inline__ void atomic_add(int i, atomic_t *v)                               
{                                                                                   
    __asm__ __volatile__(                                                           
        LOCK "addl %1,%0"                                                        
        :"=m" (v->counter)                                                       
        :"ir" (i), "m" (v->counter));                                            
}
static __inline__ void atomic_sub(int i, atomic_t *v)                            
{                                                                                
    __asm__ __volatile__(                                                        
        LOCK "subl %1,%0"                                                        
        :"=m" (v->counter)                                                       
        :"ir" (i), "m" (v->counter));                                            
}
static __inline__ int atomic_sub_and_test(int i, atomic_t *v)                    
{                                                                                
    unsigned char c;                                                             
                                                                                 
    __asm__ __volatile__(                                                        
        LOCK "subl %2,%0; sete %1"                                               
        :"=m" (v->counter), "=qm" (c)                                            
        :"ir" (i), "m" (v->counter) : "memory");                                 
    return c;                                                                    
}
static __inline__ void atomic_inc(atomic_t *v)                                   
{                                                                                
    __asm__ __volatile__(                                                        
        LOCK "incl %0"                                                           
        :"=m" (v->counter)                                                       
        :"m" (v->counter));                                                      
}
static __inline__ void atomic_dec(atomic_t *v)                                   
{                                                                                
    __asm__ __volatile__(                                                        
        LOCK "decl %0"                                                           
        :"=m" (v->counter)                                                       
        :"m" (v->counter));                                                      
}
static __inline__ int atomic_dec_and_test(atomic_t *v)                           
{                                                                                
    unsigned char c;                                                             
                                                                                 
    __asm__ __volatile__(                                                           
        LOCK "decl %0; sete %1"                                                     
        :"=m" (v->counter), "=qm" (c)                                               
        :"m" (v->counter) : "memory");                                              
    return c != 0;                                                                  
}
static __inline__ int atomic_inc_and_test(atomic_t *v)                           
{                                                                                
    unsigned char c;                                                             
                                                                                 
    __asm__ __volatile__(                                                        
        LOCK "incl %0; sete %1"                                                  
        :"=m" (v->counter), "=qm" (c)                                            
        :"m" (v->counter) : "memory");                                           
    return c != 0;                                                               
}
static __inline__ int atomic_add_negative(int i, atomic_t *v)                    
{                                                                                
    unsigned char c;                                                             
                                                                                 
    __asm__ __volatile__(                                                        
        LOCK "addl %2,%0; sets %1"                                               
        :"=m" (v->counter), "=qm" (c)                                            
        :"ir" (i), "m" (v->counter) : "memory");                                 
    return c;                                                                    
}
static __inline__ int atomic_add_return(int i, atomic_t *v)                      
{                                                                                
    int __i;                                                                     
#ifdef CONFIG_M386                                                               
    if(unlikely(boot_cpu_data.x86==3))                                           
        goto no_xadd;                                                            
#endif                                                                           
    /* Modern 486+ processor */                                                  
    __i = i;                                                                     
    __asm__ __volatile__(                                                        
        LOCK "xaddl %0, %1;"                                                     
        :"=r"(i)                                                                 
        :"m"(v->counter), "0"(i));                                               
    return i + __i;                                                              
                                                                                 
#ifdef CONFIG_M386                                                               
no_xadd: /* Legacy 386 processor */                                              
    local_irq_disable();                                                         
    __i = atomic_read(v);                                                        
    atomic_set(v, i + __i);                                                      
    local_irq_enable();                                                          
    return i + __i;                                                              
#endif                                                                           
}                                                                                
                                                                                 
static __inline__ int atomic_sub_return(int i, atomic_t *v)                      
{                                                                                
    return atomic_add_return(-i,v);                                              
}
#define atomic_inc_return(v)  (atomic_add_return(1,v))                           
#define atomic_dec_return(v)  (atomic_sub_return(1,v))

下面是linux中的原子位处理函数
static inline int constant_test_bit(int nr, const volatile unsigned long *addr)
{                                                                                
    return ((1UL << (nr & 31)) & (addr[nr >> 5])) != 0;                          
}                                                                                
                                                                                 
static inline int variable_test_bit(int nr, const volatile unsigned long * addr)
{                                                                                
    int oldbit;                                                                  
                                                                                 
    __asm__ __volatile__(                                                        
        "btl %2,%1\n\tsbbl %0,%0"                                                
        :"=r" (oldbit)                                                           
        :"m" (ADDR),"Ir" (nr));                                                  
    return oldbit;                                                               
}
优化和内存屏障
编译的时候,编译器会重新安排汇编指令以使寄存器以最优的方式使用。现代CPU通常并行的执行若干条指令,可能重新安排内存访问,加速充许的执行。为了避免指令重新排序,使用内存屏障, 事实上,所有的同步原语起优化和内存屏障的作用。优化屏障原语保证编译程序不会 混淆放在原语之前的汇编指令和之后的汇编指令。linux中优化屏障就是barrier宏。
#define barrier() __asm__ __volatile__("": : :"memory")
volatile关键字禁止编译器把asm指令与程序中的其他指令重新组合。memory关键字强制编译器假定RAM中的所有内存单元已经被汇编语言指令修改。
内存屏障原语保证,在原语之后的操作开始之前,原语之前的操作已经完成。
80x86处理器中,下列汇编语言指令是“穿行的”,起到内存屏障的作用:
  • 对I/O端口进行操作的所有指令
  • 有lock前缀的所有指令
  • 写控制寄存器、系统寄存器或调试寄存器的所有指令
  • 在Pentium 4微处理器中引入的汇编语言指令lfence、sfence和mfence,有效的实现了读内存屏障、写内存屏障和读-写内存屏障。
  • 少数专门的汇编语言指令,终止中断处理程序或异常处理程序的iret指令
linux使用六种内存屏障原语,也被当作是优化屏障。
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#ifdef CONFIG_X86_OOSTORE
/* Actually there are no OOO store capable CPUs for now that do SSE,
   but make it already an possibility. */                                        
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)   
#else                                                                            
#define wmb()   __asm__ __volatile__ ("": : :"memory")                           
#endif
#ifdef CONFIG_SMP                                                                
#define smp_mb()    mb()                                                         
#define smp_rmb()   rmb()                                                        
#define smp_wmb()   wmb()
#endif
mb,rmb,wmb,适用于MP和UP的内存屏障,读内存屏障,写内存屏障。
smp_mb, smp_rmb, smp_wmb,适用于MP的内存屏障,读内存屏障,写内存屏障。

自旋锁
自旋锁是用来在多处理器环境中工作的一种特殊的锁。如果内核发现锁由另一个CPU上的内核控制路径锁着,就在周围"旋转“,反复执行一条紧凑的循环指令,知道锁被释放。
一般来说由自旋锁保护的每个临界区都是禁止内核抢占的,在单处理器系统上,这种锁本身并不起锁的作用,仅仅是禁止或启用内核抢占。在自旋锁忙等期间,内核抢占还是有效的,所以等待自旋锁释放的进程有可能被更高优先级的进程替代。
typedef struct {                                                                 
    volatile unsigned int slock;//表示自旋锁的状态,值为1表示“未加锁”状态,任何负数和0表示“加锁”状态
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned magic;                                                              
#endif                                                                           
#ifdef CONFIG_PREEMPT                                                            
    unsigned int break_lock;//表示进程正在忙等自旋锁(只在SMP和内核抢占的情况下使用这个标志)
#endif                                                                           
} spinlock_t;

自旋锁宏

#define SPIN_LOCK_UNLOCKED (spinlock_t) { 1 SPINLOCK_MAGIC_INIT }                
                                                                                 
#define spin_lock_init(x)   do { *(x) = SPIN_LOCK_UNLOCKED; } while(0)//把自旋锁置为1(未锁)
#define spin_lock(lock)     _spin_lock(lock)//循环,直到自旋锁变为1,然后把自旋锁置为0
#define _spin_lock(lock)    \                                                    
do { \                                                                           
    preempt_disable(); \/* 禁用内核抢占 */
    _raw_spin_lock(lock); \                                                      
    __acquire(lock); \                                                           
} while(0)
#define spin_unlock(lock)   _spin_unlock(lock)//把自旋锁置为1
#define _spin_unlock(lock) \                                                     
do { \                                                                           
    _raw_spin_unlock(lock); \                                                    
    preempt_enable(); \                                                          
    __release(lock); \                                                           
} while (0)
#define spin_unlock_wait(x) do { barrier(); } while(spin_is_locked(x))//等待,直到自旋锁变为1
#define spin_is_locked(x)   (*(volatile signed char *)(&(x)->slock) <= 0)//如果自旋锁巍被置为1,返回0,否则,返回1
#define spin_trylock(lock)  __cond_lock(_spin_trylock(lock))//把自旋锁置为0,如果原来锁的值是1,则返回1,否则,返回0
#define _spin_trylock(lock) ({preempt_disable(); _raw_spin_trylock(lock) ? \     
                1 : ({preempt_enable(); 0;});})
具有内核抢占的spin_lock宏

_raw_spin_lock,对自旋锁的slock字段执行原子性的测试和设置操作,该函数等价于下列汇编语言片段:

movb $0, %a1

xchgb %a1, slp->slock

xchg指令原子性的交换8位寄存器%a1和slp->slock指示的内存单元的内容。

读写自旋锁

这个锁的引入是为了增加内核的并发能力,只要没有对数据结构进行修改,读/写自旋锁就允许多个内核控制路径同时读同一数据结构。如果内核控制路径想对这个结构进行修改,必须首先获得写锁,写锁授权独占这个资源。

typedef struct {                                                                 
    volatile unsigned int lock;                                                  
#ifdef CONFIG_DEBUG_SPINLOCK                                                     
    unsigned magic;                                                              
#endif                                                                           
#ifdef CONFIG_PREEMPT                                                            
    unsigned int break_lock;                                                     
#endif                                                                           
} rwlock_t;
每个读写锁都是一个rwlock_t结构,lock字段是一个32位的字段,分为两个不同的部分:

  • 24位计数器,表示对受保护的数据结构并发的进行读操作的内核控制路径的书目。存放在字段的0~23位。
  • “未锁”标志字段,当没有内核控制路径在读或写时设置该位,否则清0.这个标志在第24位。
#define RW_LOCK_UNLOCKED (rwlock_t) { RW_LOCK_BIAS RWLOCK_MAGIC_INIT }           
                                                                                 
#define rwlock_init(x)  do { *(x) = RW_LOCK_UNLOCKED; } while(0)
#define RW_LOCK_BIAS         0x01000000
如果自旋锁为空,那么lock字段的值为0x1000000,如果写者已经获得自旋锁,那么lock字段为0x00000000,如果一个、两个读者获取了自旋锁,那么lock字段的值为读者个数,读者个数的二进制补码在0~23位。

为读获取和释放一个锁

read_lock宏实现为读获取锁,

为写获取和释放一个锁

顺序锁

读写自旋锁,读/写锁具有相同的优先级,读者必须等待写锁,同样,写者必须等待读锁。linux2.6引入了顺序锁,只是为写者赋予了较高的优先级。这样做的优点是写者永远不用等待读锁,缺点是读者不得不反复读相同的数据直到获取有效的副本

每个顺序锁是包括两个字段的seqlock_t结构:

typedef struct {                                                                 
    unsigned sequence;//顺序计数器,每个读者必须在读数据前后读顺序计数器,并检查值,如果不同,则暗示读者刚才读到的值是无效的
    spinlock_t lock;
} seqlock_t;
#define SEQLOCK_UNLOCKED { 0, SPIN_LOCK_UNLOCKED }                                  
#define seqlock_init(x) do { *(x) = (seqlock_t) SEQLOCK_UNLOCKED; } while (0)
通过seqlock_init把seqlock_t初始化为“未上锁”。

写者通过write_seqlock和write_sequnlock获取和释放顺序锁。

static inline void write_seqlock(seqlock_t *sl)                                     
{                                                                                   
    spin_lock(&sl->lock);                                                           
    ++sl->sequence;
    smp_wmb();                                                                   
}                                                                                
                                                                                 
static inline void write_sequnlock(seqlock_t *sl)                                
{                                                                                
    smp_wmb();                                                                   
    sl->sequence++;
    spin_unlock(&sl->lock);                                                      
}
write_seqlock和write_sequnlock都增加计数器,保证写者在写的过程中,计数器的值是奇数,并且在没有写者在改变数据的时候,计数器的值是偶数。

读者调用read_seqbegin返回顺序锁的当前顺序号,如果是奇数或seq的值与顺序锁的顺序计数器的当前值不匹配,read_seqretry就返回1。

static inline unsigned read_seqbegin(const seqlock_t *sl)                        
{                                                                                
    unsigned ret = sl->sequence;                                                 
    smp_rmb();                                                                   
    return ret;                                                                  
}
static inline int read_seqretry(const seqlock_t *sl, unsigned iv)                
{                                                                                
    smp_rmb();                                                                   
    return (iv & 1) | (sl->sequence ^ iv);                                       
}
注意,读者进入临界区时,不必禁用内核抢占;写者由于获取自旋锁,所以它进入临界区时自动禁用内核抢占

读-拷贝-更新(RCU)
这是为了保护在多数情况下被多个CPU读的数据结构而设计的一种同步技术。RCU允许多个读者和写者并发执行RCU是不使用锁的

RCU如何不使用动态数据结构而实现多个CPU同步?关键的思想包括限制RCU的范围,如下:

  1. RCU只保护被动态分配并通过指针引用的数据结构
  2. 在被RCU保护的临界区中,任何内核控制路径都不能睡眠。
当内核控制路径要读取RCU保护的数据结构时,执行rcu_read_lock,等同于preempt_disable()。接下来读者,引用该数据结构指针所对应的内存单元并开始读这个数据结构,最后用等同于preempt_enable的宏rcu_read_unlokc标记临界区的结束。

写者修改指针时,不能立即释放数据结构的旧副本。只有在CPU上的所有读者都执行完rcu_read_unlock宏之后 ,才可以释放旧副本。写者通过call_rcu来释放数据结构的旧副本

void fastcall call_rcu(struct rcu_head *head,                                        
                void (*func)(struct rcu_head *rcu))                                  
{                                                                                    
    unsigned long flags;                                                             
    struct rcu_data *rdp;                                                            
                                                                                     
    head->func = func;                                                               
    head->next = NULL;                                                               
    local_irq_save(flags);                                                           
    rdp = &__get_cpu_var(rcu_data);                                                  
    *rdp->nxttail = head;                                                            
    rdp->nxttail = &head->next;                                                      
    local_irq_restore(flags);                                                        
}
RCU是linux2.6中新加的功能,用在网络层和虚拟文件系统中

信号量

实际上,linux提供了两种信号量:

  • 内核信号量,由内核控制路径使用
  • system V IPC 信号量,由用户态进程使用
只有可以睡眠的函数才可以获取内核信号量,中断处理程序和可延迟函数不能使用内核信号量

struct semaphore {                                                               
    atomic_t count;//如果该值大于0,资源就是空闲的,如果是0,则信号量是忙的,但是没有等待进程;如果是负数,资源不可用,至少有一个进程等待资源
    int sleepers;//表示是否有一些进程在信号量上睡眠。
    wait_queue_head_t wait;//存放等待队列链表的地址,当前等待资源的所有睡眠进程都放在这个链表中。如果count大于或等于0,这个链表为空
};
static inline void sema_init (struct semaphore *sem, int val)                    
{
    atomic_set(&sem->count, val);                                                
    sem->sleepers = 0;                                                           
    init_waitqueue_head(&sem->wait);                                             
}                                                                                
                                                                                 
static inline void init_MUTEX (struct semaphore *sem)//初始化互斥访问所需的信号量为空闲
{                                                                                
    sema_init(sem, 1);                                                           
}                                                                                
                                                                                 
static inline void init_MUTEX_LOCKED (struct semaphore *sem)初始化互斥访问信号为忙
{                                                                                
    sema_init(sem, 0);                                                           
}
读/写信号量
与读/写自旋锁类似,不同的是, 在信号量打开之前,等待进程是挂起而不是自旋。内核以严格的FIFO顺序处理等待读/写信号量的所有进程。
struct rw_semaphore {                                                               
    signed long     count;//存放两个16位的计数器,高16位以二进制补码形式存放非等待写者进程的总数和等待内核控制路径数。低16存放非等待的读者和写者进程的总数。
#define RWSEM_UNLOCKED_VALUE        0x00000000
#define RWSEM_ACTIVE_BIAS       0x00000001
#define RWSEM_ACTIVE_MASK       0x0000ffff
#define RWSEM_WAITING_BIAS      (-0x00010000)
#define RWSEM_ACTIVE_READ_BIAS      RWSEM_ACTIVE_BIAS
#define RWSEM_ACTIVE_WRITE_BIAS     (RWSEM_WAITING_BIAS + RWSEM_ACTIVE_BIAS)
    spinlock_t      wait_lock;
    struct list_head    wait_list;//指向等待进程的链表。链表中的每个元素都是一个rwsem_waiter结构
#if RWSEM_DEBUG                                                                  
    int         debug;                                                           
#endif                                                                           
};
每个读/写信号量都是由rw_semaphore结构描述的。

struct rwsem_waiter {
    struct list_head list;
    struct task_struct *task;//指针指向睡眠进程的描述符
    unsigned int flags;//标志表示进程是为读需要信号还是为写需要信号量
#define RWSEM_WAITING_FOR_READ  0x00000001
#define RWSEM_WAITING_FOR_WRITE 0x00000002
};
static inline void init_rwsem(struct rw_semaphore *sem)                          
{                                                                                
    sem->count = RWSEM_UNLOCKED_VALUE;                                           
    spin_lock_init(&sem->wait_lock);                                             
    INIT_LIST_HEAD(&sem->wait_list);                                             
#if RWSEM_DEBUG                                                                  
    sem->debug = 0;                                                              
#endif                                                                           
}
init_rwsem函数初始化rw_semaphore结构,把count置为0,初始化wait_lock为未锁,把wait_list置为空链表。

down_read和down_write函数分别为读或写获取读/写信号量,同样,up_read, up_write为读和写释放以前获取的读/写信号量。down_read_trylock和down_write_trylock函数分别类似于down_read和down_write,不同的是,信号量忙的情况下,不会阻塞进程。downgrade_write把写锁转换成读锁。

补充原语
为了解决多处理器上,up和down在同一信号量的并发执行,实现了补充。completion结构体:

truct completion {                                                              
    unsigned int done;                                                           
    wait_queue_head_t wait;                                                      
};
与up对应的函数叫做complete()

void fastcall complete(struct completion *x)                                     
{                                                                                
    unsigned long flags;                                                         

    spin_lock_irqsave(&x->wait.lock, flags);//自旋锁调用,
    x->done++;//递增done字段
    __wake_up_common(&x->wait, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE,
             1, 0, NULL);//唤醒wait等待队列上睡眠的互斥进程
    spin_unlock_irqrestore(&x->wait.lock, flags);                                
}
与down对应的函数是wait_for_completion。

void fastcall __sched wait_for_completion(struct completion *x)                  
{                                                                                
    might_sleep();                                                               
    spin_lock_irq(&x->wait.lock);                                                
    if (!x->done) {//如果done的值大于0,就终止,说明complete已经在另一个CPU上运行。
        DECLARE_WAITQUEUE(wait, current);                                        
                                                                                 
        wait.flags |= WQ_FLAG_EXCLUSIVE;                                         
        __add_wait_queue_tail(&x->wait, &wait);                                  
        do {                                                                     
            __set_current_state(TASK_UNINTERRUPTIBLE);                           
            spin_unlock_irq(&x->wait.lock);                                      
            schedule();                                                          
            spin_lock_irq(&x->wait.lock);                                        
        } while (!x->done);                                                      
        __remove_wait_queue(&x->wait, &wait);                                    
    }                                                                            
    x->done--;                                                                   
    spin_unlock_irq(&x->wait.lock);                                              
}

=============================================

禁止本地中断

确保一组内核语句被当作一个临界区处理的主要机制之一就是中断禁止。在多处理器系统上,禁止本地中断经常与自旋锁结合使用。

保存和恢复eflags的内容是通过local_irq_save和local_irq_restore来实现的。

禁止和激活可延迟函数

宏local_bh_disable给本地的软中断计数器加1,而函数local_bh_enable从本地CPU的中断计数器减1,

对内核数据的同步访问

内核开发者采用下述由经验得到的法则:把系统中的并发度保持在尽可能高的程度

系统中的并发度取决于两个主要因素:

  • 同时运转的I/O设备数
  • 进行有效工作的CPU数
为了有效的利用CPU,应该尽可能避免使用基于自旋锁的同步原语。由于自旋锁对硬件高速缓存的影响而使系统的整体性能产生不利影响。

在自旋锁、信号量及中断禁止之间选择

一般来说,同步原语的选取取决于访问数据结构的内核控制路径的种类。记住,只要内核控制路径获得自旋锁,就禁用本地中断或本地软中断,自动禁用内核抢占

内核控制路径访问的数据结构所需要的保护
访问数据结构的内核控制路径 单处理器保护 多处理器进一步保护
异常 信号量
中断 本地中断禁止 自旋锁
可延迟函数 无或自旋锁
异常与中断 本地中断禁止 自旋锁
异常与可延迟函数 本地软中断禁止 自旋锁
中断与可延迟函数 本地中断禁止 自旋锁
异常、中断与可延迟函数 本地中断禁止 自旋锁
保护异常所访问的数据结构

异常处理程序,常见的产生同步问题的异常就是系统调用服务例程,竞争条件可以通过信号量避免,信号量工作方式在单处理器和多处理器系统上完全相同。只有在访问每CPU变量的情况下,必须显式的禁用内核抢占。

保护中断所访问的数据结构

============================================

在多处理器上,不能通过禁止本地中断来避免竞争,即使在一个CPU上禁止了中断,中断处理程序还可以在其他CPU上执行。linux使用了几个宏,把中断激活/禁止与自旋锁结合起来。
保护可延迟函数所访问的数据结构
在SMP上可延迟函数访问的数据结构所需的保护:
软中断 自旋锁; 一个tasklet 无保护;多个tasklet 自旋锁;
因为同一个软终端可以在多个CPU上并发执行。
保护由异常和中断访问的数据结构
 
避免竞争条件的实例
引用计数器
引用计数器只不过是一个atomic_t计数器,当内核控制路径使用资源时就原子的增加计数器,当引用计数器为0时,说明该资源未被使用,如果有必要,旧释放该资源。
大内核锁
在早期的linux版本中,大内核所(big kernel block,也叫全局内核锁或BKL)被广泛使用。在linux2.6版本中,用大内核锁来保护旧的代码(绝大多数与VFS和几个文件系统相关的函数)。
2.6.11开始,用kernel_sem的信号量来实现大内核锁。
每个进程描述符都有个lock_depth字段,这个字段允许同一个进程几次获取大内核锁。这个字段对中端处理器程序,异常处理程序和可延迟函数获得大内核锁都是至关重要的。
void __lockfunc lock_kernel(void)
{
	struct task_struct *task = current;
	int depth = task->lock_depth + 1;

	if (likely(!depth))
		/*
		 * No recursion worries - we set up lock_depth _after_
		 */
		down(&kernel_sem);

	task->lock_depth = depth;
}

void __lockfunc unlock_kernel(void)
{
	struct task_struct *task = current;

	BUG_ON(task->lock_depth < 0);

	if (likely(--task->lock_depth < 0))
		up(&kernel_sem);
}

linux允许一个持有大内核锁的进程调用schedule,从而放弃CPU,不过,schedule函数检查被替换进程的lock_depth,如果是0或是正数自动释放kernel_sem。
内核描述符读/写信号量
slab高速缓存链表的信号量
slab高速缓存描述符链表是通过cache_chain_sem信号量保护的,这个信号量允许互斥的访问和修改该链表。
索引节点的信号量
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值