三、内核信号量(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函数。

(1)信号量结构semaphore

信号量用结构semaphore描述,它在自旋锁的基础上改进而成,它包括一个自旋锁、信号量计数器和一个等待队列。用户程序只能调用信号量API函数,而不能直接访问信号量结构,其列出如下(在include/linux/semaphore.h中):

struct semaphore {
            spinlock_t		lock;
            unsigned int		count;
            struct list_head	wait_list;
            };

(2)初始化函数sema_init

函数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),		\  //初始化等待队列
            }

(3)可中断获取信号量函数down_interruptible

函数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;
            }

(3)释放信号量函数up

函数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);  //唤醒等待此信号量的其他线程
            }

 


 

Logo

更多推荐