信号量&读写信号量&完成变量

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     自动把写锁转换为读锁

完成变量(补充原语)

引入补充原语是为了解决多处理器系统上发生的一种微妙的竞争条件,当A进程分配了一个临时变量,把它初始化为关闭的MUTEX,并把地址传递给进程B,然后再A之上调用down(),进程A打算一旦被唤醒就撤销该信号量。随后,运行在不同CPU上的进程B在同一信号量上调用up(),然而,目前up()和down()的实现允许这两个函数在同一个信号量上并发执行。因此,进程A可以被唤醒并撤销临时信号量,而进程B还在运行up()函数,结果,up()可能试图访问一个不存在的数据结构。

如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量(complection variable)是使两个任务得以同步的简单方法。如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务

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);
}

与up()对应的函数叫做complete(),该函数接收completion数据结构的地址作为参数,递增done字段,唤醒在wait等待队列上的睡眠的互斥进程。

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);
}

与down()对应的函数叫做wait_for_completion(),该函数接收completion数据结构的地址作为参数,并检查done标志的值。如果该标志的值大于0,wait_for_completion()就终止,因为这说明complete()已经在另一个CPU上运行。否则该函数把current作为一个互斥进程加到等待队列的末尾,并把current置为TASK_UNINTERRUPTIBLE状态让其睡眠。一旦current被唤醒,该函数就把current从等待队列中删除,然后,检查done标志的值:如果等于0函数就结束,否则再次挂起进程。

信号量&读写信号量&完成变量

时间: 2024-10-17 08:25:07

信号量&读写信号量&完成变量的相关文章

大话Linux内核中锁机制之信号量、读写信号量

大话Linux内核中锁机制之信号量.读写信号量 在上一篇博文中笔者分析了关于内存屏障.读写自旋锁以及顺序锁的相关内容,本篇博文将着重讨论有关信号量.读写信号量的内容. 六.信号量 关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程才能执行临界区的代码:不同的是获取不到信号量时,进程不会原地打转而是进入休眠等待状态.它的定义是include\linux\semaphore.h文件中,结构体如图6.1所示.其中的count变量是计数作用,通过使用lock变量实现对count变量的保

todo:读写信号量

读写信号量的相关API有: DECLARE_RWSEM(name) 该宏声明一个读写信号量name并对其进行初始化. void init_rwsem(struct rw_semaphore *sem); 该函数对读写信号量sem进行初始化. void down_read(struct rw_semaphore *sem); 读者调用该函数来得到读写信号量sem.该函数会导致调用者睡眠,因此只能在进程上下文使用. int down_read_trylock(struct rw_semaphore

linux内核同步之信号量、顺序锁、RCU、完成量、关闭中断【转】

转自:http://blog.csdn.net/goodluckwhh/article/details/9006065 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] 一信号量 信号量的概念 信号量的数据结构和相关API 数据结构 初始化 获取和释放信号量 读写信号量的概念 读写信号量的数据结构 二顺序锁 顺序锁的概念 数据结构 写操作 读操作 三Read-Copy Update RCU 写操作 读操作 释放旧的版本 四完成量Completions 完成量的概念 数据结构

20150518 Linux设备驱动中的并发控制

20150518 Linux设备驱动中的并发控制 2015-05-18 Lover雪儿 总结一下并发控制的相关知识: 本文参考:华清远见<Linux 设备驱动开发详解>—第7章 Linux 设备驱动中的并发控制,更多详细内容请看原书 一.并发与竞态 并发(concurrency)指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量.静态变量等)的访问则很容易导致竞态(race conditions). 在 Linux 内核中,主要的竞态发生于如下几种情况:

Linux内核设计与实现——内核同步

内核同步 同步介绍 同步的概念 临界区:也称为临界段,就是訪问和操作共享数据的代码段. 竞争条件: 2个或2个以上线程在临界区里同一时候运行的时候,就构成了竞争条件. 所谓同步.事实上防止在临界区中形成竞争条件. 假设临界区里是原子操作(即整个操作完毕前不会被打断),那么自然就不会出竞争条件.但在实际应用中.临界区中的代码往往不会那么简单,所以为了保持同步,引入了锁机制.但又会产生一些关于锁的问题. 死锁产生的条件:要有一个或多个运行线程和一个或多个资源,每一个线程都在等待当中的一个资源.但全部

《Linux内核设计与实现》笔记——内核同步简介

相关概念 竞争条件 多个执行线程(进程/线程/中断处理程序)并发(并行)访问共享资源,因为执行顺序不一样造成结果不一样的情况,称为竞争条件(race condition) 举例说明 #include<thread> using namespace std; int i = 0; void thread1(){ //for(int x=0;x<100000;x++) i++; } void thread2(){ //for(int x=0;x<100000;x++) i++; } i

Hasen的linux设备驱动开发学习之旅--linux设备驱动中的并发与竞态

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:linux设备驱动中的并发与竞态 * Date:2014-11-04 */ 1.并发与竞态 并发(concurrency)指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源(软件上的全 局变量,静态变量等)的访问则很容易导致竞态(race conditions). 主要的竞态发生在以下几种情况: (1)对称多处理(SMP)

《Linux内核设计与实现》读书笔记(十)- 内核同步方法

原blog:http://www.cnblogs.com/wang_yb/archive/2013/05/01/3052865.html 内核中提供了多种方法来防止竞争条件,理解了这些方法的使用场景有助于我们在编写内核代码时选用合适的同步方法, 从而即可保证代码中临界区的安全,同时也让性能的损失降到最低. 主要内容: 原子操作 自旋锁 读写自旋锁 信号量 读写信号量 互斥体 完成变量 大内核锁 顺序锁 禁止抢占 顺序和屏障 总结 1. 原子操作 原子操作是由编译器来保证的,保证一个线程对数据的操

Linux内核剖析 之 内核同步

主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核如何为不同的请求服务 哪些服务? ====>>> 为了更好地理解内核是如何执行的,我们把内核看做必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板.对于不同的请求,侍者采用如下的策略: 1.老板提出请求时,如果侍者空闲,则侍者开始为老板服务. 2.如果老板提出请求时侍者正在为顾客服务,那么侍者停