UNIX内核(1):加锁解锁——等待事件及唤醒

原文转自:http://blog.chinaunix.net/uid-7471615-id-83756.html

概述
加锁和解锁的基本思想是,当某个进程进入临界区,它将持有一个某种类型的锁(UNIX里一般来说是semaphore,Linux里一般是信号量和原子量或者spinlock)。当其他进程在该进程没有释放该锁时试图进入临界区(加锁),它将会被设置成睡眠状态,然后被置入等待该锁的进程队列(某个优先级的)。当该锁被释放时,也就是解锁事件发生时,内核将从等待该锁的进程优先级队列中寻找一个进程并将其置为就绪态,等待调度(schedule)。

原理

在system v中,等待某一事件被称为sleep(sleep on an event),因此下文将统一使用睡眠(sleep)。等待某事件也可以成为等待某个锁。(注:本文中的sleep与sleep()系统调用不同)

系统的实现将一组事件映射到一组内核虚拟地址(锁);而且事件不区别对待到底有多少进程在等待。这就意味着两个不规则的事情:

一、当某个事件发生时,等待该事件的一组进程均被唤醒(而不是仅仅唤醒一个进程),并且状态均被设置成就绪(ready-to-run)。这时候由内核选择(schedule)一个进程来执行,由于system v内核不是可抢占的(Linux内核可抢占),因此其他的进程将一直在就绪状态等待调度,或者再次进入睡眠(因为该锁有可能被执行进程持有,而执行进程因为等待其他事件的发生而睡眠),或者等其他进程在用户态被抢占。

二、多个事件映射到同一个地址(锁)。假设事件e1和e2都映射到同一个地址(锁)addr,有一组进程在等待e1,一组进程在等待e2,它们等待的事件不同,但是对应的锁相同。假如e2发生了,所有等待e2的进程都被唤醒进入就绪状态,而由于e1没有发生,锁addr没有被释放,所有被唤醒的进程又回到睡眠状态。貌似一个事件对应一个地址会提高效率,但实际上由于system v是非抢占式内核,而且这种多对一映射非常少,再加上运行态进程很快就会释放资源(在其他进程被调度之前),因此这种映射不会导致性能的显著降低。

下面简单阐述一下sleep和wakeup的算法。

//伪代码

sleep(地址(事件),优先级)

返回值:进程能捕获的信号发生导致的返回则返回1,当进程不能捕获的信号发生时返回longjmp算法,否则返回0。

{

提高处理器执行等级以禁用所有中断;//避免竞态条件

将进程的状态设置为睡眠;

根据事件将进程放入睡眠哈希队列;//一般来说每个事件都有一个等待队列

将睡眠地址(事件)及输入的优先级保存到进程表中;

if (该等待是不可中断的等待)

//一般有两种睡眠状态:可中断的和不可中断的。不可中断的睡眠是指进程除了等待的事件外,

//不会被其他任何事件(如信号)中断睡眠状态,该情况不太常用。

{

上下文切换;//此处该进程执行上下文被保存起来,内核转而执行其他进程

//在别处进行了上下文切换,内核选择该上下文进行执行,此时该进程被唤醒

恢复处理器等级来允许中断;

返回0;

}

// 被信号中断的睡眠

if (没有未递送的信号)

{

上下文切换;

if (没有未递送的信号)

{

恢复处理器等级来允许中断;

返回0;

}

}

//有未递送的信号

若进程还在等待哈希队列中,将其从该队列移出;

恢复处理器等级来允许中断;

if(进程捕获该信号)

返回1;

执行longjmp算法;//这一段我也不明白

}

void wakeup(地址(事件))
{
禁用所有的中断;
根据地址(事件)查找睡眠进程队列;
for(每个在该事件上睡眠的进程)
{
将该进程从哈希队列中移出;
设置状态为就绪;
将该进程放入调度链表中;
清除进程表中的睡眠地址(事件);
if(进程不在内存中)
{
唤醒swapper进程;
}
else if(唤醒的进程更适合运行)
{
设置调度标志;
}
}
恢复中断;
}
在wakeup调用之后,被唤醒的进程并不是马上投入运行,而是让其适合运行,等到下次调度该进程才有机会运行(也可能不会运行)。

代码示例

由于没能找到UNIX的源码,因此用Linux的源代码替代。上述伪代码已经是比较简单的处理。Linux 0.01则更简单。
//Linux 0.01的实现:

//不可中断睡眠
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;

if (!p)
return;
if (current == &(init_task.task)) //current宏用来获取当前运行进程的task_struct
panic("task[0] trying to sleep");
tmp = *p; //将已经在睡眠的进程p2保存到tmp
*p = current; //将当前进程p1保存到睡眠进程队列中(实际上只有一个)
current->state = TASK_UNINTERRUPTIBLE; //p1状态设置为不可中断的睡眠
schedule(); //上下文切换,执行其他进程

//p1被唤醒后回到此处
if (tmp)
tmp->state=0; //将p2的状态设置为运行,等待调度
}

//可中断睡眠
void interruptible_sleep_on(struct task_struct **p)
{
struct task_struct *tmp;

if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp=*p; //正在等待的进程p2保存到tmp
*p=current; //将当前进程p1保存到睡眠进程队列中
repeat: current->state = TASK_INTERRUPTIBLE; //将p1状态设置为可中断睡眠
schedule(); //上下文切换
if (*p && *p != current) { //p2睡眠被中断
(**p).state=0;//p1设置为运行态
goto repeat; //回到repeat,继续让p2睡眠
}

*p=NULL;
if (tmp)
tmp->state=0; //将p2的状态设置为运行态,等待调度
}
这两个函数比较难以理解,主要是在最后两条语句。在schedule()之前,切换的是当前进程的上下文,但是,切换回来之后,却是将原先正在睡眠的进程置为就绪态。在执行schedule()之前,各指针如下图所示(不好意思,不会粘贴图片):

---

| p |

---

||

\/

---- Step 3 ---------

| *p |--------->| current |

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

|

X Step 1

|

\/

---------------- Step 2 -----

| Wait Process |<--------| tmp |

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

而在schedule()返回到这段代码之后,事情就不一样了。因为在step 3之后,current进程已经进入睡眠,tmp指向的睡眠进程的描述符也被保存下来。从schedule()返回之后,执行的代码仍然是current,而tmp指向的仍然是wait process,此时将其状态置为就绪,等待下一次调度。

与前两个函数相比,wake_up相当简单:

//被唤醒的进程并不是马上投入运行,而是让其适合运行
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0; //将要唤醒的进程状态置为就绪
*p=NULL; //将进程移出等待的进程
}
}

使用加锁机制

有了sleep_on()和wake_up()之后,就可以对资源加锁了,如(硬盘缓冲加锁、等待缓冲可用、唤醒等待进程):

//锁住bh

static inline void lock_buffer(struct buffer_head * bh)
{
if (bh->b_lock)
printk("hd.c: buffer multiply locked\n");
bh->b_lock=1;
}

static inline void unlock_buffer(struct buffer_head * bh)
{
if (!bh->b_lock)
printk("hd.c: free buffer being unlocked\n");
bh->b_lock=0;
wake_up(&bh->b_wait);
}

static inline void wait_on_buffer(struct buffer_head * bh)
{
cli(); //禁止中断
while (bh->b_lock)
sleep_on(&bh->b_wait);
sti(); //恢复中断
}

//Linux 0.99.15的sleep和wake_up的实现(支持等待队列):

static inline void __sleep_on(struct wait_queue **p, int state)
{
unsigned long flags;
struct wait_queue wait = { current, NULL };

if (!p)
return;
if (current == task[0])
panic("task[0] trying to sleep");
current->state = state;
add_wait_queue(p, &wait); //将当前进程加入等待队列
save_flags(flags); //保存中断掩码
sti(); //屏蔽中断
schedule(); //上下文切换
remove_wait_queue(p, &wait); //从等待队列中移除当前进程
restore_flags(flags); //恢复中断掩码
}

void wake_up(struct wait_queue **q)
{
struct wait_queue *tmp;
struct task_struct * p;

if (!q || !(tmp = *q))
return;
do {//将等待队列中唤醒队首进程
if ((p = tmp->task) != NULL) {
if ((p->state == TASK_UNINTERRUPTIBLE) ||
(p->state == TASK_INTERRUPTIBLE)) {
p->state = TASK_RUNNING;
if (p->counter > current->counter)
need_resched = 1;
}
}
if (!tmp->next) {
printk("wait_queue is bad (eip = %08lx)\n",((unsigned long *) q)[-1]);
printk(" q = %p\n",q);
printk(" *q = %p\n",*q);
printk(" tmp = %p\n",tmp);
break;
}
tmp = tmp->next;
} while (tmp != *q);
}

PS:对于内核的理解很肤浅,文章中应该有不少错误,而且Linux内核0.01版本身就有很多bug,因此,稳中引用的代码不一定完全正确。盼高手指正!!!

参考:

The Deisgn of The UNIX Operating System, by Maurice J. Bach

Understanding the Linux Kernel, 3rd, byDaniel P. Bovet,Marco Cesati

Linux内核源代码0.01,0.99.15及2.6.22

Copyleft (C) raof01.
本文可以用于除商业外的所有用途。此处“用途”包括(但不限于)拷贝/翻译(部分或全部),不包括根据本文描述来产生代码及思想。若用于非商业,请保留此权利声明,并标明文章原始地址和作者信息;若要用于商业,请与作者联系(raof01@gmail.com),否则作者将使用法律来保证权利。

时间: 2024-09-28 15:48:24

UNIX内核(1):加锁解锁——等待事件及唤醒的相关文章

UNIX操作系统的加锁解锁:等待事件及唤醒

加锁和解锁的基本思想是,当某个进程进入临界区,它将持有一个某种类型的锁(UNIX里一般来说是semaphore,Linux里一般是信号量和原子量或者spinlock).当其他进程在该进程没有释放该锁时试图进入临界区(加锁),它将会被设置成睡眠状态,然后被置入等待该锁的进程队列(某个优先级的).当该锁被释放时,也就是解锁事件发生时,内核将从等待该锁的进程优先级队列中寻找一个进程并将其置为就绪态,等待调度(schedule). 在system v中,等待某一事件被称为sleep(sleep on a

UNIX内核(2):磁盘缓冲原理,缓冲分配、回收及用OO观点建模

本文将针对UNIX磁盘缓冲的原理及分配回收展开讨论,并在最后用OO观点来对缓冲进行建模. 概述 在UNIX家族的内核中,都有一个buffer cache,用于缓冲磁盘与文件系统之间的数据交换.这是个模块不仅包含内存管理功能,还提供了一套算法来管理该内存中缓冲的数据,如延迟写等等. 为了减少内存碎片,提高访问效率,减少系统调用的次数,在大多数复杂/庞大的软件系统中都会用到内存池或者对象缓冲等来管理内存.然而,内存是一片雷区,在管理内存时,需要特别小心,如果造成泄漏,就会导致系统内存耗尽.在应用程序

UNIX内核(4):inode及其相关操作

本文转自:http://blog.chinaunix.net/uid-7471615-id-83764.html 早期的UNIX系统最重要的两大功能是:文件存储/访问,任务/进程调度(多任务).由这两大功能衍生出了内存管理,设备管理,用户接口等功能.在这里就来说说其中第一个重要的功能:文件系统. 在UNIX系统上,所有一切都被当成文件来对待,包括设备.因此,就需要一个系统来管理这些文件,并提供一个统一的操作接口.inode就是用来管理文件的一个数据结构(或者说是模型.类).每个文件在磁盘上都对应

UNIX内核(7):super block管理inode和磁盘块

原文转自:http://blog.chinaunix.net/uid-7471615-id-83767.html UNIX内核系列已经写了5篇了.按照"The Design of The UNIX Operation System"给出的系统原型来看,file sub-system基本上已经覆盖到了--当然要除去设备驱动相关的部分,如下图所示: http://blogimg.chinaunix.net/blog/upfile2/071118215716.jpg --注意,file su

UNIX内核(3):磁盘读写以及磁盘缓冲的利弊

原文转自:http://blog.chinaunix.net/uid-7471615-id-83762.html "UNIX内核(2):磁盘缓冲原理,缓冲分配.回收及用OO观点建模"对UNIX磁盘缓冲的分配回收做了大致的说明,并给出了一些代码范例.本文将对磁盘的读写以及使用磁盘缓冲的利弊进行一下简要说明. 读取磁盘块 为了读取一个磁盘块,进程需要调用getblk来获取缓冲.如果能在hash Q上找到该缓冲,那么内核就能够立刻获得数据.否则,内核将向磁盘驱动提交一个读请求并且休眠,直到数

db file async I/O submit 等待事件优化

db file async I/O submit 等待事件优化   一.数据发生db file async I/O submit 我们从数据库awr报告中经常会看到很高db file async I/O submit的等待事件: SQL> select event,wait_class,wait_time from v$session_wait where wait_class<>'Idle' EVENT WAIT_CLASS WAIT_TIME --------------------

如何模拟产生CBC LATCH与buffer busy wait等待事件

数据库版本:Oracle 11.2.0.4.0 CBC latch出现的原因:   --只在逻辑读时产生 1.CBC latch保护不同的链表.不同BH   :同一LATCH下多个BUCKET被同时访问时,(一个LATCH对应多个BUCKET) 2.CBC latch保护同一链表下同一BH  :同一LATCH下同一BH被同时访问时 latch: cache buffers chains 解决方法: 1._db_block_hash_latches加大latch数量,作用是减少同一LATCH下多个

Oracle中常见的33个等待事件小结

在Oracle 10g中的等待事件有872个,11g中等待事件1116个. 我们可以通过v$event_name 视图来查看等待事件的相关信息   一. 等待事件的相关知识 1.1 等待事件主要可以分为两类,即空闲(IDLE)等待事件和非空闲(NON-IDLE)等待事件. 1). 空闲等待事件指ORACLE正等待某种工作,在诊断和优化数据库的时候,不用过多注意这部分事件. 2). 非空闲等待事件专门针对ORACLE的活动,指数据库任务或应用运行过程中发生的等待,这些等待事件 是在调整数据库的时候

oracle等待事件2——高速缓冲内等待事件

1.cache buffers lru chain 要想查看或修改工作组(LRU+LRUW)进程,始终要持有管理相应工作组的cache buffers lru chain 锁存器,若在此过程中发生争用,则要等待:latch:cache buffers lru chain 事件. 在下面两类情况下我们必须首先获得cache buffers lru chain锁存器: (1)进程想要读取还没有装载到内存上的块时,通过查询LRU列分配到所需空闲缓冲区,再次过程中需要cache buffers lru