相对于自旋锁,信号量的最大特点是允许调用它的线程进入睡眠状态·这意味着试图获得某一信号的进程会导致对处理器拥有权的丧失,也即出现进程的切换。
信号量的定义
struct semaphore { raw_spinlock_t lock;//lock是个自旋锁变量,用于实现对信号量的另一个成员count的原子操作。 unsigned int count;//无符号整型变量count用于表示通过该信号量允许进入临界区的执行路径的个数。 struct list_head wait_list;//wait_list用于管理所有在该信号量上睡眠的进程,无法获得该信号量的进程将进入睡眠状态。 };
如果驱动程序中定义了一个struct semaphore型的信号量变量,需要注意的是不要直接对该变量的成员进行赋值,而应该使用sema_init函数来初始化该信号量。sema_init函数定义如下:
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); }
初始化主要通过__SEMAPHORE_INITIALIZER宏完成:
#define DEFINE_SEMAPHORE(name) \ struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
#define __SEMAPHORE_INITIALIZER(name, n) \ { \ .lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \ .count = n, \ .wait_list = LIST_HEAD_INIT((name).wait_list), \ }
所以 sema_init(struct semaphore *sem, int val)调用会把信号量sem的lock值设定为解锁状态,count值设定为函数的调用参数val,同时初始化wait_list链表头。
DOWN操作
信号量上的主要操作是DOWN和UP,在Linux内核中对信号量的DOWN操作有:
- void down(struct semaphore *sem) 获取信号量,不建议使用此函数,因为是 UNINTERRUPTABLE 的睡眠。
- int down_interruptible(struct semaphore *sem) 可被中断地获取信号量,如果睡眠被信号中断,返回错误-EINTR。
- int down_killable (struct semaphore *sem) 可被杀死地获取信号量。如果睡眠被致命信号中断,返回错误-EINTR。
- int down_trylock(struct semaphore *sem) 尝试原子地获取信号量,如果成功获取,返回0,不能获取,返回1。
- int down_timeout(struct semaphore *sem, long jiffies) 在指定的时间jiffies内获取信号量,若超时未获取,返回错误-ETIME。
上面这些函数中,驱动程序使用最频繁的是down_interruptible函数,本节将重点讨论该函数,之后再对其他入操作的功能作一概述性的描述。
int down_interruptible(struct semaphore *sem) { unsigned long flags; int result = 0; raw_spin_lock_irqsave(&sem->lock, flags);//保证对sem->count操作的原子性防止多个进程对sem->count同时操作 //可能引起混乱 if (likely(sem->count > 0)) sem->count--; else result = __down_interruptible(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); return result; }
函数首先通过spin_lock_irqsave的调用来保证对sem->count操作的原子性防止多个进程对sem->count同时操作
可能引起混乱。如果代码成功进入临界区,则判断sem->count是否大于0:如果count大于0,表明当前进程可以获得信号量,就将count值减I,然后退出:如果count不大于0,表明当前进程无法获得该信号量,此时调用down_interruptible,由后者完成一个进程无法获得信号量时的操作,在内部调用__down_common(struct semaphore *sem,long state,longt timeout),调用时的参数state=TASK_INTERRUPTIBLE,timeout=LONG_MAX所以当一个进程无法获得信号量时,最终调用的函数为__down_common:
__down_interruptible->__down_common
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout) { struct task_struct *task = current; /*通过对一个struct semaphore_waiter变量waiter的使用,把当前进程放到信号量sem的成员变量wait所管理的队列中*/ struct semaphore_waiter waiter; list_add_tail(&waiter.list, &sem->wait_list); waiter.task = task; waiter.up = false; for (;;) { if (signal_pending_state(state, task))//把当前进程的状态设置为TASK_INTERRUPTIBLE goto interrupted; if (unlikely(timeout <= 0)) goto timed_out; __set_task_state(task, state); raw_spin_unlock_irq(&sem->lock); timeout = schedule_timeout(timeout);//使当前进程进入睡眠状态,函数将停留在schedule_timeout调用上,直到再次被调度执行。 raw_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; }
函数的功能是,首先通过对一个struct semaphore_waiter变量waiter的使用,把当前进程放到信号量sem的成员变量wait所管理的队列中,接着在一个for循环中把当前进程的状态设置为TASK_INTERRUPTIBLE,再调用schedule_timeout使当前进程进入睡眠状态,函数将停留在schedule_timeout调用上,直到再次被调度执行。当该进程再一次被调度执行时,schedule_timeout开始返回,接下来根据进程被再次调度的原因进行处理:如果waiter.up不为0,说明进程在信号量sem的wati_list队列中被该信号量的UP操作所唤醒,进程可以获得信号量,返回0。如果进程是因为被用户空间发送的信号所中断或者是超时引起的唤醒,则返回相应的错误代码。因此对面down_interruptible的调用总是应该坚持检查其返回值,以确定函数是已经获得了信号量还是因为操作被中断因而需要特别处理,通常驱动程序对返回的非0值要做的工作是返回-ERESTARTSYS,比如下面的代码段:
//定义一个信号量 struct semaphore demosem; sema_init(&demosem,2); if(down_interruptiable(&demosem)); return -ERESTARTSYS;
然而对down_interruptible的调用最常见的可能还是返回0表明调用者获得了信号量。为了让讨论具体化,下面以一个例子来说明,假设一个信号量sem的count=2,说明允许有两个进程进入临界区,假设有进程A、B、C、D和E先后调用down_interruptible来获得信号量,那么进程A和B将得到信号量进入临界区,C、D和E将睡眠在sem的wait_list中,此时的情形如图2所示:
在接下来的UP操作中还会用到这里的例子,来讨论进程A和B结束临界区中的操作返回时执行UP操作对wait_list中进程C、D和E的影响。
在讨论完驱动程序最常使用的down_interruptible函数之后,再回过头来看看其他几种DOWN操作:
- void down(struct semaphore *sem);
与down_interruptible相比,down函数是不可中断的,这意味着调用它的进程如果无法获得信号量,将一直处于睡眠状态直到有别的进程释放了该信号量。从用户空间的角度,如果应用程序阻塞在了驱动程序的down函数中,将无法通过一些强制措施比如按Ctrl+D组合键等来结束该进程。因此,除非必要,否则驱动程序中应该避免使用down函数。 - int down_killable(struct semaphore *sem);
睡眠的进程可以因收到一些致命性信号(fatal signal)被唤醒而导致获取信号量的操作被中断,在驱动程序中极少使用。 - int down_trylock(struct semaphore *sem);
进程试图获得信号量,但若无法获得信号量则直接返回1而不进入睡眠状态,返回0意味着函数的调用者己经获得了信号量。 - int down_timeout(struct semaphore *sem, long jiffies);
函数在无法获得信号量的情况下将进入睡眠状态,但是处于这种睡眠状态有时间限制,如果在jiffies指明的时间到期时函数依然无法获得信号量,则将返回一错误码-ETIME,在到期前进程的睡眠状态为TASK_UNINTERRUPTIBLE。成功获得信号量的函数返回0。
UP操作
void up(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(list_empty(&sem->wait_list))) sem->count++; else __up(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); }
如果信号量的wait_list队列为空,则表明没有其他进程正在等待该信号量,那么只要把sem的count加1即可。如果wait_list队列不为空,则说明有其他进程正睡眠在wait_list上等待该信号量,此时调用__up(sem)来唤醒进程:
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 = true; wake_up_process(waiter->task); }
下面在图2的基础上讨论此处的操作。__up函数首先用list_first_entry取得sem->wait_list链表上的第一个waiter节点C,然后将其从sem->wait_list链表中删除,waiter->up=1,最后调用wake_up_process来唤醒waiter C上的进程C。这样进程C将从之前down_interruptible。
调用中的timeout=schedule_timeout(timeout)处醒来,waiter->up=1,down_interruptible返回0,进程c获得信号量,进程D和E继续等待直到有进程释放信号量或者被用户空间中断掉。即使不是信号量的拥有者,也可以调用up函数来释放一个信号量,这点与下节介绍的mutex是不同的。
在Linux系统中,信号量的一个常见的用途是实现互斥机制,这种情况下信号量的count值为1,也就是任意时刻只允许一个进程进入临界区。为此Linux内核源码提供了一个宏DECLARE_MUTEX,专门用于这种用途的信号量定义和初始化:
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 = true; wake_up_process(waiter->task); }
该宏定义了一个count=1的信号量变量name,并初始化了相关成员。所以接下来就可以使用信号量的DOWN和UP操作来实现互斥,比如下面的这个用DECLARE_MUTEX定义的信号量来实现互斥的代码段:
//先用DECLARE_MUTEX定义一个全局的信号量demo_sem DECLARE_MUTEX(demo_sem); //函数demo_write里面使用demo_sem作互斥用 int demo_write{ //打算进入临界区,调用down_interruptible获得信号量 if(down_interruptible(&down_sem)){ return -ERESTARTSYS } //成功获取信号量进入临界区 ..... //离开临界区,调用up释放信号量 up(&demo_sem) }