操作系统项目:锁

为了操作系统项目,研读了一下Operating Systems: Three Easy Pieces中关于锁的那一章,记录笔记!

首先要明确,成为一个锁所需具备的条件有:
1.提供互斥功能
2.进程调度公平
3.使用锁时的性能

下面看一下关于锁的实现的迭代过程

1 禁用临界区中断

void lock() {
	DisableInterrupts();
}
void unlock() {
	EnableInterrupts();
}

是用在单处理器操作系统中的,一个进程在执行就禁止其他进程来打断。很简单,但是缺点也很多:
1.若贪心程序或恶意程序占着锁不放,唯一能做的就是重启系统了;
2.不适用于多处理器系统,不同进程运行在不同CPU上,若进程1和进程2都想进入同一个临界区,CPU1上的进程1禁用了中断,但CPU2上并没有被禁用,还是可以进入临界区;
3.长时间禁止中断,可能会丢失中断现场;
4.效率低下。

2 基于标志变量的锁

typedef struct __lock_t { int flag; } lock_t; 
void init(lock_t *mutex) {
	// 0 -> lock is available, 1 -> held
	mutex->flag = 0;
}
void lock(lock_t *mutex) {
	while (mutex->flag == 1) // TEST the flag ; 
	// spin-wait (do nothing)
	mutex->flag = 1;
}
void unlock(lock_t *mutex) { 
	mutex->flag = 0;
}

现在假设进程1进入了临界区,占用了锁。进程2此时也想进入临界区,只能在while循环中等待,直到进程1调用unlock把标志置为0,才跳出循环,进入临界区。
这种方法不能正确提供互斥功能,在效率上也不高。
1.互斥功能
从flag=0开始。进程1调用lock,在跳出循环,还没有把flag写成1的那一刻,被进程2打断;进程2调用lock(),把flag写为1,返回中断;进程1继续把flag写为1,这样两个进程都进入临界区了。
2.效率
CPU一直在执行循环,其他什么事也做不了。

3 Test and Set

int TestAndSet(int *old_ptr, int new) {
	int old = *old_ptr; // 获取标志的旧值
	*old_ptr = new; // 把新值写到内存里
	return old; // 返回旧值
}

typedef struct __lock_t { int flag;} lock_t; 
void init(lock_t *lock) {
	// 0: lock is available, 1: lock is held 
	lock->flag = 0;
} 
void lock(lock_t *lock) {
	while (TestAndSet(&lock->flag, 1) == 1) ; // spin-wait (do nothing)
}
void unlock(lock_t *lock) { 
	lock->flag = 0;
}

为什么叫Test and Set呢,是因为让你在Set新值的时候可以Test一下旧值!(注意:TestAndSet是属于硬件支持,以原子方式执行,这里只是通过C语言代码片段去解释TestAndSet的作用。)
从flag=0开始,进程1调用lock(),首先获取到旧值为0,再把新值1赋给标志变量,把旧值0返回,这样进程1就占用了锁。进程2想要获取锁,拿到的旧值是1,就会一直循环等待。
注意在单处理器操作系统中,自旋锁需要搭配抢占式的调度程序使用,不然的话单处理器会一直沉迷循环,无法自拔。

对自旋锁的特性进行分析

1.满足提供互斥功能
2.无法保证公平性(如果一个进程一直占着锁不放,会出现饥饿现象)
3.在单处理器系统下效率极低;在进程数和CPU数相同的情况下,效率还可以接受

4 基于睡眠唤醒的锁

typedef struct __lock_t { 
	int flag; 
	int guard; 
	queue_t *q;
} lock_t;

void lock_init(lock_t *m) {
	m->flag = 0; 
	m->guard = 0;
	queue_init(m->q); 
} 

void lock(lock_t *m) {
	while (TestAndSet(&m->guard, 1) == 1) ; // 以防下面的操作还没整完,就有新的进程来打断
	if (m->flag == 0) {
		m->flag = 1; // lock is acquired 
		m->guard = 0;
	}
	else { // 若锁已经被占用了
		queue_add(m->q, gettid()); // 加入等待队列
		/* 下面两句的先后顺序重要 */
		m->guard = 0; 
		park(); // 让等待进程睡眠
	} 
} 

void unlock(lock_t *m) {
	while (TestAndSet(&m->guard, 1) == 1) ;  // 作用同上
	if (queue_empty(m->q)) 
		m->flag = 0; // 没有进程在占用锁了
	else
		unpark(queue_remove(m->q)); // 唤醒睡眠进程
	m->guard = 0; 
}

在进程调度公平性方面,在此处通过等待队列来控制下一个获取锁的进程,以此避免了饥饿现象;在性能方面,一套操作做完了就把m->guard置零,CPU不会一直被自旋锁占用,但是无法完全避免自旋,如果你当前一整套拿锁的操作还没做完,就被另一个进程打断了的话,那个来打断的进程还是会在那里自旋的。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值