互斥信号量内核代码的实现
当我们读取磁盘中的一些数据时,我们会调用read这个函数进行读取数据:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
接着这个read函数内部又调用了系统调用sys_read函数:
// linux-0.11/fs/read_write.c
int sys_read(unsigned int fd,char * buf,int count);
然后这个sys_read函数内部又调用了bread函数:
// linux-0.11/fs/buffer.c
struct buffer_head * bread(int dev,int block)
{...
struct buffer_head * bh;
ll_rw_block(READ,bh);
wait_on_buffer(bh);...
}
分析wait_on_buffer函数
1、上面sys_read函数内部调用了ll_rw_block函数,读取当前该资源的互斥锁队列,然后调用了wait_on_buffer函数,等待该互斥信号量释放。
// linux-0.11/fs/buffer.c
static inline void wait_on_buffer(struct buffer_head * bh)
{
cli(); // close the global interrupt
while (bh->b_lock)
sleep_on(&bh->b_wait);
sti(); // open the global interrupt
}
2、wait_on_buffer函数中的临界区内部调用了sleep_on:
while (bh->b_lock)
sleep_on(&bh->b_wait);
这句代码的意思是如果该互斥信号量资源没有释放,则调用sleep_on函数,阻塞当前任务。
分析sleep_on函数
// linux-0.11/kernel/sched.c
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;...
tmp = *p; // get the last blocked task
*p = current; // add the current blocking task
current->state = TASK_UNINTERRUPTIBLE; // set blocking
schedule(); // start schedule
if (tmp) // set it running when the last blocked task exists
tmp->state=0;
}
1、sleep_on函数的形参是一个task_struct的二重结构体指针变量,上述传递变量的是阻塞在队列中的任务栈指针。
2、在sleep_on函数内部的开始,是向当前进程的栈中放入一个任务栈结构体指针变量tmp,然后将最近的一个因为该资源堵塞的进程栈指针保存在tmp中。
3、接着将当前任务栈的指针变量保存在该资源的阻塞队列中,并设置当前任务栈进入堵塞状态,随后进入调度函数进行调度。
4、当该进程再次被唤醒时,继续设置该进程的上一个堵塞的进程状态为就绪状态(如果有的话)
如何唤醒堵塞的进程
上面是将进程堵塞的代码,当磁盘这个读资源被释放后,又是怎样唤醒那些因它而堵塞的资源呢?
1、当磁盘这一次读完后,会进入磁盘读中断read_intr:
// linux-0.11/kernel/blk_drv/hd.c
static void read_intr(void)
{
...
end_request(1);
...
}
2、read_intr内部调用了end_request:
// linux-0.11/kernel/blk_drv/blk.h
static inline void end_request(int uptodate)
{
...
unlock_buffer(CURRENT->bh);
...
}
3、end_request内部调用了unlock_buffer:
// linux-0.11/kernel/blk_drv/ll_rw_blk.c
static inline void unlock_buffer(struct buffer_head * bh)
{...
bh->b_lock = 0; // reset the mutex
wake_up(&bh->b_wait); // wake up the current blocking tasks
}
该函数内部释放了互斥信号量,然后唤醒因此阻塞的任务。
4、unlock_buffer中调用了wake_up:
// linux-0.11/kernel/sched.c
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
*p=NULL;
}
}
该函数内部将传传进来的任务栈进行唤醒,接着通过调度函数会唤醒最近一次堵塞的进程,然后进入此处继续运行:
// linux-0.11/kernel/sched.c
if (tmp) // set it running when the last blocked task exists
tmp->state=0;
连续唤醒所有被堵塞的进程
1、通过上面的tmp变量,将每一次堵塞的任务栈指针形成一个递归队列,当wake_up中将信号量释放后,会依次通过schedule唤醒这些进程
2、然后这些进程再通过优先级的调度方法依次进入读取磁盘,然后又会将该信号量锁住,等待释放,调度下一个进程进行读取磁盘。以此往复,直到全部读完磁盘为止。