三、内核信号量(semaphore)
Linux的信号量是一种睡眠锁,这个不同于自旋锁.如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列(具体可以参考进程的活动状态),然后让其睡眠,此时处理器能重获自由,而去执行其他代码.当持有信号量的进程将信号量释放后,处于等待队列中的那个进程会被唤醒,并获得该信号量.
所以和自旋锁的区别是:
1)信号量适用于锁会被长时间持有的情况
2)持有信号量锁的线程可以睡眠,而持有自旋锁的线程是不允许睡眠的
3)信号锁不会禁止内核抢占
4)最重要的信号锁同时允许任意数量的锁持有者,而自旋锁在同一时刻最多只允许一个任务持有他
因为不受睡眠的限制,所以信号量锁用起来要方便一些.
信号量在无法得到资源时,内核线程处于睡眠阻塞状态,而自旋锁处于忙等待状态。因此,如果资源被占用时间很短时,使用自旋锁较好,因为它可节约调度时间。如果资源被占用的时间较长,使用信号量较好,因为可让CPU调度去做其它进程的工作。
信号量锁的持有者数目可以在声明信号量时指定.当信号量的持有者被声明为只允许一个持有者时,此时的信号量又被称为互斥信号量.
Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干。
信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。
信号量的API有:
操作信号量的API函数说明如表6。
函数定义 | 功能说明 |
sema_init(struct semaphore *sem, int val) | 初始化信号量,将信号量计数器值设置val。 |
down(struct semaphore *sem) | 获取信号量,不建议使用此函数。 |
down_interruptible(struct semaphore *sem) | 可被中断地获取信号量,如果睡眠被信号中断,返回错误-EINTR。 |
down_killable (struct semaphore *sem) | 可被杀死地获取信号量。如果睡眠被致命信号中断,返回错误-EINTR。 |
down_trylock(struct semaphore *sem) | 尝试原子地获取信号量,如果成功获取,返回0,不能获取,返回1。 |
down_timeout(struct semaphore *sem, long jiffies) | 在指定的时间jiffies内获取信号量,若超时未获取,返回错误-ETIME。 |
up(struct semaphore *sem) | 释放信号量sem。 |
样例:信号量的使用
DECLARE_MUTEX(name)
该宏声明一个信号量name并初始化它的值为0,即声明一个互斥锁。
DECLARE_MUTEX_LOCKED(name)
该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。
void sema_init (struct semaphore *sem, int val);
该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。
void init_MUTEX (struct semaphore *sem);
该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。
void init_MUTEX_LOCKED (struct semaphore *sem);
该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。
void down(struct semaphore * sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。
int down_interruptible(struct semaphore * sem);
该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。
int down_trylock(struct semaphore * sem);
该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。
void up(struct semaphore * sem);
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。
信号量在绝大部分情况下作为互斥锁使用,下面以console驱动系统为例说明信号量的使用。
在内核源码树的kernel/printk.c中,使用宏DECLARE_MUTEX声明了一个互斥锁console_sem,它用于保护console驱动列表console_drivers以及同步对整个console驱动系统的访问。
其中定义了函数acquire_console_sem来获得互斥锁console_sem,定义了release_console_sem来释放互斥锁console_sem,定义了函数try_acquire_console_sem来尽力得到互斥锁console_sem。这三个函数实际上是分别对函数down,up和down_trylock的简单包装。
需要访问console_drivers驱动列表时就需要使用acquire_console_sem来保护console_drivers列表,当访问完该列表后,就调用release_console_sem释放信号量console_sem。
函数console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要访问console_drivers,因此它们都使用函数对acquire_console_sem和release_console_sem来对console_drivers进行保护。
下面函数do_utimes利用信号量防止多个线程对文件系统节点inode同时进行访问。其列出如下(在fs/open.c中):
long do_utimes(char __user * filename, struct timeval * times) { struct inode * inode; …… down(&inode->i_sem); //获取信号量 error = notify_change(nd.dentry, &newattrs);//修改inode中值 up(&inode->i_sem); //释放信号量 …… }
下面说明信号量API函数。
信号量用结构semaphore描述,它在自旋锁的基础上改进而成,它包括一个自旋锁、信号量计数器和一个等待队列。用户程序只能调用信号量API函数,而不能直接访问信号量结构,其列出如下(在include/linux/semaphore.h中):
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };
函数sema_init初始化信号量,将信号量值初始化为n,其列出如下:
static inline void sema_init(struct semaphore *sem, int val) { static struct lock_class_key __key; *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val); /*初始化一个锁的实例,用于调试中获取信号量的调试信息*/ lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0); } #define __SEMAPHORE_INITIALIZER(name, n) { .lock = __SPIN_LOCK_UNLOCKED?lock), \ //初始化自旋锁 .count = n, \ //将信号量计数器赋值为n .wait_list = LIST_HEAD_INIT((name).wait_list), \ //初始化等待队列 }
函数down_interruptible获取信号量,存放在参数sem中。它尝试获取信号量,如果其他线程被允许尝试获取此信号量,则将本线程睡眠等待。如果有一个信号中断睡眠,则它返回错误-EINTR。如果成功获取信号量,函数返回0。
函数down_interruptible列出如下(在kernel/semaphore.c中):
int down_interruptible(struct semaphore *sem) { unsigned long flags; int result = 0; spin_lock_irqsave(&sem->lock, flags); //获取自旋锁,关闭中断,将状态寄存器值存放在flags /*如果信号量计数器值大于0,说明有多个空闲资源可访问,可以成功获取信号量了*/ if (likely(sem->count > 0)) //likely表示成功获取的概率大,通知编译器进行分支预测优化 sem->count--; else result = __down_interruptible(sem); //进入睡眠等待 spin_unlock_irqrestore(&sem->lock, flags); return result; } static noinline int __sched __down_interruptible(struct semaphore *sem) { return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); }
函数__down_common进入睡眠等待,其列出如下:
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout) { struct task_struct *task = current; struct semaphore_waiter waiter; list_add_tail(&waiter.list, &sem->wait_list); //加入到等待队列 waiter.task = task; waiter.up = 0; for (;;) { if (state == TASK_INTERRUPTIBLE && signal_pending(task)) goto interrupted; if (state == TASK_KILLABLE && fatal_signal_pending(task)) goto interrupted; if (timeout <= 0) goto timed_out; __set_task_state(task, state); spin_unlock_irq(&sem->lock); timeout = schedule_timeout(timeout); //调度 spin_lock_irq(&sem->lock); if (waiter.up) return 0; } timed_out: list_del(&waiter.list); return -ETIME; interrupted: list_del(&waiter.list); return -EINTR; }
函数up在没有其他线程等待使用信号量的情况下释放信号量,否则,唤醒其他等待线程。其列出如下:
void up(struct semaphore *sem) { unsigned long flags; spin_lock_irqsave(&sem->lock, flags); /*判断是否有线程等待在此信号量上,即判断等待队列是否为空*/ if (likely(list_empty(&sem->wait_list))) /*没有线程等待此信号量,释放信号量,将信号量计数器加1,表示增加了1个空闲资源*/ sem->count++; else __up(sem); /*将本线程从等待队列删除,唤醒等待此信号量的其他线程*/ spin_unlock_irqrestore(&sem->lock, flags); } static noinline void __sched __up(struct semaphore *sem) { struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list); list_del(&waiter->list); //将本线程从等待队列删除 waiter->up = 1; wake_up_process(waiter->task); //唤醒等待此信号量的其他线程 }