首页 > 代码库 > 信号量&读写信号量&完成变量
信号量&读写信号量&完成变量
Linux提供两种信号量:
1、内核信号量,由内核控制路径使用
2、System V IPC信号量,由用户态进程使用
从本质上说,它们实现了一个加锁原语,即让等待者睡眠,直到等待的资源变为空闲。
信号量
内核信号量类似于自旋锁,因为当锁关闭着的时候,它不允许内核控制路径继续运行。然而,当内核内核控制路径试图获取内核信号量所保护的忙资源时,相应的进程被挂起。只有在资源被释放时,进程才再次变为可运行的。
因此,只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟处理函数都不能使用内核信号量。
struct semaphore { atomic_t count; int sleepers; wait_queue_head_t wait; };count 为一个atomic_t类型的值。如果该值大于0,那么资源就是空闲的,也就是说,该资源现在可以使用。相反,如果count等于0,那么信号量是忙的,但没有进程等待这个被保护的资源。最后如果count为负数,则资源时不可用的,并至少有一个进程等待资源。
sleepers 存放一个标志,表示是否有一些进程在信号量上睡眠
wait 存放等待队列链表的地址,当前等待资源的所有睡眠进程都放在这个链表中。当然,如果count大于或等于0,等待队列就为空。
可以使用init_MUTEX和init_MUTEX_LOCKED函数来初始化互斥访问所需的信号量:这两个宏分别把count字段设置成1(互斥访问的资源空闲)和0(对信号量进行初始化的进程当前互斥访问的资源忙等)
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); }
注意,也可以吧信号量中的count初始化为任意的正数值n,在这种情况下,最多有n个进程可以并发地访问这个资源。
获取信号量
当进程希望获取内核信号量时,就调用down()函数。
/arch/i386/kernel/semaphore.c
fastcall void __sched __down(struct semaphore * sem) { struct task_struct *tsk = current; DECLARE_WAITQUEUE(wait, tsk); unsigned long flags; tsk->state = TASK_UNINTERRUPTIBLE; spin_lock_irqsave(&sem->wait.lock, flags); add_wait_queue_exclusive_locked(&sem->wait, &wait); sem->sleepers++; for (;;) { int sleepers = sem->sleepers; /* * Add "everybody else" into it. They aren‘t * playing, because we own the spinlock in * the wait_queue_head. */ if (!atomic_add_negative(sleepers - 1, &sem->count)) { sem->sleepers = 0; break; } sem->sleepers = 1; /* us - see -1 above */ spin_unlock_irqrestore(&sem->wait.lock, flags); schedule(); spin_lock_irqsave(&sem->wait.lock, flags); tsk->state = TASK_UNINTERRUPTIBLE; } remove_wait_queue_locked(&sem->wait, &wait); wake_up_locked(&sem->wait); spin_unlock_irqrestore(&sem->wait.lock, flags); tsk->state = TASK_RUNNING; } asm( ".section .sched.text\n" ".align 4\n" ".globl __down_failed\n" "__down_failed:\n\t" #if defined(CONFIG_FRAME_POINTER) "pushl %ebp\n\t" "movl %esp,%ebp\n\t" #endif "pushl %edx\n\t" "pushl %ecx\n\t" "call __down\n\t" "popl %ecx\n\t" "popl %edx\n\t" #if defined(CONFIG_FRAME_POINTER) "movl %ebp,%esp\n\t" "popl %ebp\n\t" #endif "ret" );
/include/asm-i386/semaphore.h static inline void down(struct semaphore * sem) { might_sleep(); __asm__ __volatile__( "# atomic down operation\n\t" LOCK "decl %0\n\t" /* --sem->count */ "js 2f\n" "1:\n" LOCK_SECTION_START("") "2:\tlea %0,%%eax\n\t" "call __down_failed\n\t" "jmp 1b\n" LOCK_SECTION_END :"=m" (sem->count) : :"memory","ax"); }分析一下down的实现
首先decl 递减sem->count,然后检查该值是否为负。如果count大于或等于0,当前进程获得资源并继续正常执行。否则,count为负,当前进程必须挂起。把一下寄存器内容保存在栈中,然后调用__down()
从本质上说,__down()函数把当前进程的状态从TASK_RUNNING改变为TASK_UNINTERRUPTIBLE,并把进程放在信号量的等待队列。该函数在访问信号量结构之前,要获得用来保护信号量队列的sem->wait.lock自旋锁(因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护以避免对其同时访问),并禁止本地中断。
最后,__down()函数的主要任务是挂起当前进程(通过调用schedule),直到信号量释放。要牢记如果没有进程在信号量等待队列上睡眠,则信号量的sleeper字段通常是0,否则被置为1。
只有异常处理程序,特别是系统调用服务历程,才可以调用down()函数。
JS Jump if sign (negative)
JMP Jump
LEA Load effective address
释放信号量
fastcall void __up(struct semaphore *sem) { wake_up(&sem->wait); } asm( ".section .sched.text\n" ".align 4\n" ".globl __up_wakeup\n" "__up_wakeup:\n\t" "pushl %edx\n\t" "pushl %ecx\n\t" "call __up\n\t" "popl %ecx\n\t" "popl %edx\n\t" "ret" ); static inline void up(struct semaphore * sem) { __asm__ __volatile__( "# atomic up operation\n\t" LOCK "incl %0\n\t" /* ++sem->count */ "jle 2f\n" "1:\n" LOCK_SECTION_START("") "2:\tlea %0,%%eax\n\t" "call __up_wakeup\n\t" "jmp 1b\n" LOCK_SECTION_END ".subsection 0\n" :"=m" (sem->count) : :"memory","ax"); }up()函数增加count字段的值,然后检查它的值是否大于0。如果count大于0,说明没有进程在等待队列上睡眠,因此什么事情也不做。否则调用__up()函数以唤醒一个睡眠进程。
Linux中提供信号量的宏
sema_init 初始化信号量
down 获取信号量
down_interruptible 获取信号量,该函数广泛应用在驱动设备程序中,因为如果进程收到了一个信号但在信号量上被阻塞,就允许进程放弃down操作。
如果睡眠进程在获得需要的资源之前被一个信号唤醒,那么该函数就会增加count字段的值并返回-EINTR。因此返回-EINTR时, 设备驱动程序可以放弃I/O操作
down_trylock 获取信号量,非阻塞接口,与down()不同之处在于系统资源繁忙时,该函数会立即返回,而不是让进程去睡眠
up 释放信号量
读/写信号量
读写信号量类似前面的读写锁,有一点不同:在信号量再次变为打开之前,等待进程挂起而不是自旋。
struct rw_semaphore { signed long count; #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; #if RWSEM_DEBUG int debug; #endif };count 存放两个16位的计数器。其中最高16位计数器以二进制补码形式存放非等待写者进程的总数(0或1)和等待的写内核控制路径数。最低16位计数器存 放非等待的读者和写者进程的总数。
wait_list 指向等待进程的链表。
wait_lock 一个自旋锁,用于保护等待队列链表和rw_semaphore结构本身。
Linux中提供读写信号量的宏
init_rwsem 初始化读写信号量
down_read 获取读信号量
down_read_trylock 尝试获取读信号量,非阻塞接口
down_write 获取写信号量
down_write_trylock 尝试获取写信号量,非阻塞接口
up_read 释放读信号量
up_write 释放写信号量
downgrade_write 自动把写锁转换为读锁
完成变量(补充原语)
struct completion { unsigned int done; wait_queue_head_t wait; };
void fastcall complete(struct completion *x) { unsigned long flags; spin_lock_irqsave(&x->wait.lock, flags); x->done++; __wake_up_common(&x->wait, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, 0, NULL); spin_unlock_irqrestore(&x->wait.lock, flags); }
void fastcall __sched wait_for_completion(struct completion *x) { might_sleep(); spin_lock_irq(&x->wait.lock); if (!x->done) { 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); }