Linux内核同步方法——信号量

信号量

    Linux中的信号量是一种睡眠锁

    如果有一个 任务试图获得一个不可用(已经被占用)的信号量是,信号量将会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列的哪个任务将被唤醒,并获得该信号量。

    举个门和钥匙的例子,当某个人到了门前,拿到钥匙,然后进入房间。最大的差异在于,当另一个人来到门前,但无法得到钥匙会发生什么情况。在这种情况下,这个人会把自己名字卸载一个列表中,然后打盹去了。当里边的人出房间后,首先会查看列表。如果列表上有名字,就对第一个名字仔细检查,然后叫醒那个人,让他进入房间。

在这种方式中,钥匙相当于信号量,确保一次只有一个人(执行线程)进入房间,这里的房间相当于临界区。


------------------------------------------------------------------

信号量和自旋锁在使用上的差异如下:

· 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。

· 如果锁被短时间持有时,就不适合使用信号量。因为睡眠、维护等待队列以及唤醒所花费的可能会比锁被占用的全部时间还要长。

· 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的

· 占用信号量的同时不能占用自旋锁。因为在你等待信号量是可能会睡眠,而在持有自旋锁时是不允许睡眠的。

扫描二维码关注公众号,回复: 1462061 查看本文章

· 信号量不同于自旋锁。信号量不会禁止内核抢占,所以持有信号量的代码可以被抢占。意味着信号量不会对调度的等待时间带来负面影响。

    如果需要在自旋锁和信号量中做选择,应该根据锁被持有的时间长短做判断。还有是否需要睡眠。

    信号量具有睡眠特性。而对于自旋锁,如果一个线程试图获得一个被已经持有(争用)的自旋锁,那么该线程就会一直进行忙等待——旋转——等待锁重新可用


-----------------------------------------------------------------------

计数信号量

    信号量中的计数信号量有一个特性,他可以允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。

    计数信号量不能进行强制互斥,因为它允许多个执行线程同时访问临界区。


互斥信号量

    在一个时刻仅允许一个锁持有者,即为计数等于1的信号量。因为它或者由一个任务持有,或者根本没有任务持有它。

    互斥信号量会强制进行互斥

    信号量在1968年由Dijkstra提出,是一种常用的锁机制。信号量支持两个原子操作P()和V(),这两个名字来自荷兰语Proberen(测试操作)和Vershogen(增加操作)。

    P操作又叫做down(),V操作是up()。

    down()操作通过对信号量计数减1来请求获得一个信号量。如果结果大于0或等于0,获得信号量锁,任务就可以进入临界区;如果结果是负数,任务就会被放入等待队列,处理器先处理执行其他任务。

     相反,当临界区中的操作完成后,up()操作用来释放信号量,该操作也称为提升,因为它会增加信号量的计数值。如果在该信号量上的等待队列不为空,那么处于队列中等待的任务在被唤醒的同时会获得该信号量。


-----------------------------------------------------------------------------------------------------------------

    在Linux-2.6.11.12版本中,信号量的具体定义实现在include\asm-i386\<semaphore.h>中。


基本数据结构

    信号量的类型为semaphore,内核信号量结构如下:

struct semaphore {
	/**
	 * 如果该值大于0,表示资源是空闲的。如果等于0,表示信号量是忙的,但是没有进程在等待这个资源。
	 * 如果count为负,表示资源忙,并且至少有一个进程在等待。
	 * 但是请注意,负值并不代表等待的进程数量。
	 */
	atomic_t count;
	/**
	 * 存放一个标志,表示是否有一些进程在信号量上睡眠。
	 */
	int sleepers;
	/**
	 * 存放等待队列链表的地址。当前等待资源的所有睡眠进程都放在这个链表中。
	 * 如果count>=0,那么这个链表就应该是空的。
	 */
	wait_queue_head_t wait;
};


创建和初始化信号量

static inline void sema_init (struct semaphore *sem, int val)
{
/*
 *	*sem = (struct semaphore)__SEMAPHORE_INITIALIZER((*sem),val);
 *
 * i'd rather use the more flexible initialization above, but sadly
 * GCC 2.7.2.3 emits a bogus warning. EGCS doesn't. Oh well.
 */
	atomic_set(&sem->count, val);
	sem->sleepers = 0;
	init_waitqueue_head(&sem->wait);
}
#define __SEMAPHORE_INITIALIZER(name, n)				\
{									\
	.count		= ATOMIC_INIT(n),				\
	.sleepers	= 0,						\
	.wait		= __WAIT_QUEUE_HEAD_INITIALIZER((name).wait)	\
}

#define __MUTEX_INITIALIZER(name) \
	__SEMAPHORE_INITIALIZER(name,1)

#define __DECLARE_SEMAPHORE_GENERIC(name,count) \
	struct semaphore name = __SEMAPHORE_INITIALIZER(name,count)

/**
 * DECLARE_MUTEX静态分配semaphore结构的变量,并将count字段初始化为1
 */
#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)
/**
 * DECLARE_MUTEX静态分配semaphore结构的变量,并将count字段初始化为0
 */
#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)


/**
 * 初始化semaphore将count字段初始化为1
 */
static inline void init_MUTEX (struct semaphore *sem)
{
	sema_init(sem, 1);
}

/**
 * 初始化semaphore将count字段初始化为0
 */
static inline void init_MUTEX_LOCKED (struct semaphore *sem)
{
	sema_init(sem, 0);
}

    可通过sema_init函数静态地声明信号量,其中name为信号量变量名,count是信号量的使用数量:

struct semaphore name;
sema_init(&name, count);

    创建一个更为普通的互斥信号量可以使用DECLARE_MUTEX

static DECLARE_MUTEX(name);


-----------------------------------------------------------------------------------------------------------

使用信号量


down()

     down()操作通过对信号量计数减1来请求获得一个信号量

    如果结果大于0或等于0,获得信号量锁,任务就可以进入临界区;如果结果是负数,任务就会被放入等待队列,处理器先处理执行其他任务。

   当进程希望获得内核信号量锁时,调用down()函数,

static inline void down(struct semaphore * sem)
{
	might_sleep();
	__asm__ __volatile__(
		"# atomic down operation\n\t"
		/**
		 * 首先减少并检查sem->count的值
		 * 如果为负(说明减少前就是0或负)就挂起
		 * 这里的减一操作是原子的
		 * 请注意当count<0时,此时-1是不正确的,因为调用进程会被挂起,而没有真正的获得信号量。
		 * 它恢复count值的时机,不在down中,在__down中。
		 */
		LOCK "decl %0\n\t"     /* --sem->count */
		"js 2f\n"
		"1:\n"
		/**
		 * 为负了,调用__down_failed
		 * __down_failed会保存参数并调用__down
		 */
		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");
}

    这里首先会调用might_sleep()一直睡眠,进行减少检查sem->count,如果当前sem->count的值是负数(说明减少前是0或者负数)就挂起,这时并没有真正获得信号量,会调用__down函数;

    如果检查没问题之后,在汇编上调用__down_failed,即指令call __down_failed。

fastcall void __down_failed(void /* special register calling convention */);

__down_failed会保存参数并调用__down

也就是当申请信号量失败时,调用__down使线程挂起。直到信号量可用。本质上,__down将线程设置为TASK_UNINTERRUPTIBLE并将进程放到信号量的等待队列。(并禁止本地中断)

fastcall void __sched __down(struct semaphore * sem)
{
	struct task_struct *tsk = current;
	DECLARE_WAITQUEUE(wait, tsk);
	unsigned long flags;

	/**
	 * 设置状态为TASK_UNINTERRUPTIBLE。
	 */
	tsk->state = TASK_UNINTERRUPTIBLE;
	/**
	 * 在将进程放到等待队列前,先获得锁,并禁止本地中断。
	 */
	spin_lock_irqsave(&sem->wait.lock, flags);
	/**
	 * 等待队列的__locked版本假设在调用函数前已经获得了自旋锁。
	 * 请注意加到等待队列上的睡眠进程是互斥的。这样wakeup最多唤醒一个进程。
	 * 如果唤醒多个进程,会扰乱sleeper和count的值。考虑下面注释中的第三种情况。
	 */
	add_wait_queue_exclusive_locked(&sem->wait, &wait);

	/**
	 * sleepers是__down函数的精髓。它既是准确的,同时还是高效的。
	 * 它并不是表示在此信号量上睡眠的线程数。它仅仅表示是否有线程在信号量上面等待。
	 * 请注意它与count的关系。
	 * 当信号量可用时,其COUNT>=1,sleeper=0,此时__down根本不会被执行。
	 * 当信号量不可用时,没有睡眠的进程,则count==0,sleeper==0
	 *     则down会将count设置为-1,且此时sleeper==0,进入本函数后面的for循环后,
	 *     atomic_add_negative执行原子加,但是加的值为0,atomic_add_negative变成检查count值是否为负。
	 *     如果为负,就将sleeper重新置为1。否则说明信号量可用,就将sleeper设置为0。并从循环退出。
	 * 当信号量不可用时,并且有其他进程在等待时,count==-1,sleeper==1。则进入时,count被减1,即count==-2,sleeper(暂时)==2
	 *     此时atomic_add_negative执行原子加,此时加的值是sleeper-1即1.
	 *     并且此时用的是临时变量,linux中经常需要这样将数据存到临时变量中,只有临时变量中的值才是可靠的。其他的都有可能被其他线程或者中断改变。
	 *     加sleeper-1是因为sem->sleepers++;一句后,到atomic_add_negative检查count前,count可能被其他进程加上1了。这样就可以检查出这种情况。
	 *     如果加1后count为负,说明信号仍然不可用,此时count被恢复成-1了,请记住,down中将count减1,此时将它补回去。因为线程并没有获得信号量,而count多减了1
	 *     如果加1后,count不为负,(xie.baoyou注:应该就是0,不应该是正值)。也把sleeper重新置为0。并唤醒另一个线程。
	 *     此时count==0,sleeper==0,看起来是错的。其实是正确的。因为新进程被唤醒了,新进程醒来时,sleeper==0,加sleeper-1就相当于是将count减去1。
	 *     新进程在调用schedule前,将sleeper又设置成1了。
	 */
	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;
	}

	/**
	 * 请注意上面循环中,spin_lock_irqsave和配对使用情况,运行到这里时,还是锁住的。
	 * 所以可以调用remove_wait_queue的locked版本。
	 */
	remove_wait_queue_locked(&sem->wait, &wait);
	/**
	 * 在获得信号量后,还需要唤醒等待队列上的下一个进程。只唤醒下一个,而不会是多个进程。
	 */
	wake_up_locked(&sem->wait);
	spin_unlock_irqrestore(&sem->wait.lock, flags);
	tsk->state = TASK_RUNNING;
}


down_interruptible()

    函数down_interruptible()试图获取自定的信号量

    如果信号量不可用,他将调用进程置成TASK_INTERRUPTIBLE状态——进入睡眠。如果进程在等待获取信号量的时候接收到信号,那么进程就会被唤醒,而函数don_interruptible()会返回-EINTR。

    如果得到信号量,返回0。


static inline int down_interruptible(struct semaphore * sem)
{
	int result;

	might_sleep();
	__asm__ __volatile__(
		"# atomic interruptible down operation\n\t"
		LOCK "decl %1\n\t"     /* --sem->count */
		"js 2f\n\t"
		"xorl %0,%0\n"
		"1:\n"
		LOCK_SECTION_START("")
		"2:\tlea %1,%%eax\n\t"
		"call __down_failed_interruptible\n\t"
		"jmp 1b\n"
		LOCK_SECTION_END
		:"=a" (result), "=m" (sem->count)
		:
		:"memory");
	return result;
}
     函数down()会让进程在TASK_UNINTERRUPTIBLE状态下睡眠。当然不希望这种情况发生,因为这样一来,进程在等待信号量的时候就不再响应信号了。因此,使用down_interruptible()比使用down()更为普遍(也更正确)。

down_trylock()

    函数down_trylock()可尝试以堵塞方式来获取指定的信号量。

    在信号量已被占用时,立刻返回非0值;否则,它返回0,而且会成功持有信号量锁。

static inline int down_trylock(struct semaphore * sem)
{
	int result;

	__asm__ __volatile__(
		"# atomic interruptible down operation\n\t"
		LOCK "decl %1\n\t"     /* --sem->count */
		"js 2f\n\t"
		"xorl %0,%0\n"
		"1:\n"
		LOCK_SECTION_START("")
		"2:\tlea %1,%%eax\n\t"
		"call __down_failed_trylock\n\t"
		"jmp 1b\n"
		LOCK_SECTION_END
		:"=a" (result), "=m" (sem->count)
		:
		:"memory");
	return result;
}


------------------------------------------------------------------------

  

up()

     当临界区中的操作完成后,up()操作用来释放信号量,该操作也称为提升,因为它会增加信号量的计数值。如果在该信号量上的等待队列不为空,那么处于队列中等待的任务在被唤醒的同时会获得该信号量。


static inline void up(struct semaphore * sem)
{
	__asm__ __volatile__(
		"# atomic up operation\n\t"
		/**
		 * 首先增加count的值
		 */
		LOCK "incl %0\n\t"     /* ++sem->count */
		/**
		 * 测试count值,如果当前小于等于0,那么有进程在等待,跳到2f,唤醒等待进程。
		 */
		"jle 2f\n"
		/**
		 * 运行到这里表示count>0,不用做任何事情,返回。
		 * 注意1后面的代码在单独的段中。此处就是函数的结束处。
		 */
		"1:\n"
		LOCK_SECTION_START("")
		/**
		 * 调用__up_wakeup,注意它是从寄存器传参的。
		 * 它最终调用的是__up,再调用wakeup。
		 * eax寄存器中传递的是第一个参数sem
		 */
		"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");
}

    首先,会增加并测试count的值,如果当前小于等于0,表示有进程在等待,唤醒等待进程,函数结束;

否则,会通过寄存器传参调用__up_wakeup。其实也就是调用__up,函数__up接着调用wakeup。

fastcall void __up(struct semaphore *sem)
{
	wake_up(&sem->wait);
}


读-写信号量

    读-写信号量相比普通信号量更具优势。

    所有的读-写信号量都是互斥信号量——它们的引用计数等于1,虽然它们只对写者互斥,不对读者。

只要没有写者,并发持有的读锁的读者数量不限。相反,只有唯一的写者(无读者),可以获得写锁。

    所有的读-写锁的睡眠都不会被信号打断。


    这里没有对读-写信号量的源码进行研究,后期再看。



参考资料:

《Linux内核设计与实现》

《Linux内核源代码情景分析》

    博客: Linux内核中的互斥操作(1)——信号量






猜你喜欢

转载自blog.csdn.net/hhhanpan/article/details/80556498