18.进程同步与死锁——信号量的代码实现

1.生产者代码

//伪代码
Producer(item)
{
    P(empty);
    ...
    V(full);
}


//sem.c 进入内核
//信号量在内核中,包含 value 和 PCB,value的值能被多个进程看到
typedef struct{
    char name[20];// name是信号量的名字,比如empty
    int value; // value记录供进程判断的值
    task_struct * queue; // PCB队列
} semtable[20];

// sem_open的最终实现
sys_sem_open(char *name)
{
    在semtable中根据name寻找对应的元素;
    如果没找到就创建这个元素;
    返回对应的下标;
}
//代码实现 Producer.c
main()
{
    sd = sem_open("empty");//通过系统调用sem_open打开内核,共同使用empty这个信号量
    //执行5次,在文件中写出5个数字,每个数字占4个字节
    for(i = 1 to 5){
        sem_wait(sd); // 判断是否有空闲缓冲区
        write(fd, &i, 4);
    }
}

// sd是下标
sys_sem_wait(int sd){
    cli();
    // 根据下标取value,value-- < 0 说明没有空闲缓冲区了
    if(semtable[sd].value-- < 0){
        设置自己为阻塞;
        将自己加入semtable[sd].queue中;
        schedule();
    }
    sti();
}

// value是共享的,操作value要设置 开关中断,单CPU可以用cli() sti()
V(full)
{
    cli();
    if(semtable[sd].value++ >= 0)
    {
        从semtable[sd].queue中取出一个进程,设置为就绪态;
    }
    sti();
}

2.Linux 0.11 的实现

2.1 从磁盘读数据

// fs/buffer.c
/*
* bread() reads a specified block and returns the buffer that contains
* it. It returns NULL if the block was unreadable.
*/
/*
* 从设备上读取指定的数据块并返回含有数据的缓冲区。如果指定的块不存在
* 则返回NULL。
*/
// 从指定设备上读取指定的数据块
//用户程序发出read指令,就要进入内核,执行sys_read,最终执行的是bread
struct buffer_head *
bread (int dev, int block)
{
  struct buffer_head *bh;// 申请一块空闲缓冲区
  ...
// 启动读的命令,就要阻塞了
  ll_rw_block (READ, bh);
  wait_on_buffer (bh);//bh是信号量
  ...
}
// kernel/blk_drv/ll_rw_blk.c
// 锁定指定的缓冲区bh。如果指定的缓冲区已经被其它任务锁定,则使自己睡眠(不可中断地等待),
// 直到被执行解锁缓冲区的任务明确地唤醒。
static inline void
lock_buffer (struct buffer_head *bh)
{
  cli ();// 开关保护
  while (bh->b_lock)    // 如果缓冲区已被锁定,则睡眠,直到缓冲区解锁。
    sleep_on (&bh->b_wait);
  bh->b_lock = 1;// 立刻锁定该缓冲区。b_lock是信号量,1表示上锁,没读完。读完了,中断会解锁,其他进程要读,会判断 b_lock,如果是1,锁上了,进程会睡眠
  sti ();// 开中断。
}

我们之前使用的信号量是 if 判断的,上边用的是 while 判断的

// kernel\sched.c

// 把当前任务置为不可中断的等待状态,并让睡眠队列头的指针指向当前任务。
// 只有明确地唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。
// 函数参数*p 是放置等待任务的队列头指针。p是指向 task_struct结构体的指针的指针
void sleep_on (struct task_struct **p)
{
  struct task_struct *tmp; // tmp是一个局部变量
  ...
  //下边2句是最隐蔽的队列,1.将自己放到阻塞队列中
  tmp = *p; // tmp 指向已经在等待队列上的任务(如果有的话),tmp指向 task_struct,之前的队首,tmp保存在内核栈中,根据tmp可以找到下一个进程的内核栈
  *p = current; // 将睡眠队列头的等待指针指向当前任务。新的阻塞队列的队首是current

  current->state = TASK_UNINTERRUPTIBLE;    // 2.将当前任务置为不可中断的等待状态,阻塞态
  schedule ();          // 调度,切换到别的进程去执行
  // 只有当这个等待任务被唤醒时,调度程序才又返回到这里,则表示进程已被明确地唤醒。
  // 既然大家都在等待同样的资源,那么在资源可用时,就有必要唤醒所有等待该资源的进程。
  // 该函数嵌套调用,也会嵌套唤醒所有等待该资源的进程。然后系统会根据这些进程的优先条件,
  // 重新调度应该由哪个进程首先使用资源。也即让这些进程竞争上岗。
  // tmp 是被唤醒的进程的变量,是被唤醒的进程的下一个进程
  if (tmp)          // 若还存在等待的任务,则也将其置为就绪状态(唤醒)。
    tmp->state = 0;   // 如果有下一个进程,就把下一个进程唤醒
}

2.2 如何从Linux 0.11 的这个队列唤醒?

18.进程同步与死锁——信号量的代码实现_第1张图片

// kernel\blk_drv\hd.c
//// 读操作中断调用函数。将在执行硬盘中断处理程序中被调用。
static void read_intr (void)
{
  // 磁盘中断
  ...
  end_request (1);      // 若全部扇区数据已经读完,则处理请求结束事宜,
  do_hd_request ();     // 执行其它硬盘请求操作。
}

// 结束请求。
extern inline void
end_request (int uptodate)
{
     ...
     unlock_buffer (CURRENT->bh);   // 解锁缓冲区。
     ...
}

// 释放锁定的缓冲区。
extern inline void
unlock_buffer (struct buffer_head *bh)
{
  if (!bh->b_lock)      // 如果指定的缓冲区bh 并没有被上锁,则显示警告信息。
    printk (DEVICE_NAME ": free buffer being unlocked\n");
  bh->b_lock = 0;       // 否则将该缓冲区解锁。
  wake_up (&bh->b_wait);    // 唤醒等待该缓冲区的进程。
}
// kernel\sched.c
// 唤醒指定任务*p。
void
wake_up (struct task_struct **p)
{
  if (p && *p)
    {
      (**p).state = 0;      // 置为就绪(可运行)状态,唤醒队首
      *p = NULL;
    }
}

一个进程被唤醒,从上一次 切出去的地方:sleep_on 调用完 schedule后 继续执行

为什么用while?
wake_up 唤醒队首进程,队首执行,再唤醒下一个进程
下一个进程也从schedule开始执行,执行时 再把当前进程的 下一个进程唤醒
while 是逐渐将阻塞队列的进程 全部唤醒,if只能唤醒阻塞队列的第一个进程

假设 进程1要等待一个事件,阻塞了,进程2也等待这个事件,也阻塞了
用 if 唤醒,进程1 总是优先执行
while 是将所有的进程都唤醒,再由schedule决定执行哪个进程,优先级高的进程执行,而不是按照队列顺序,lock_buffer 中会 再执行 while(bh->b_lock), 如果没锁,bh->b_block = 1; 自己执行,上锁。
之前阻塞的队列中的进程 已经全部为就绪态,执行时,会判断 while(bh->b_lock),如果锁了,就会睡眠
while 不需要记录有多少进程在阻塞,每次都是全部唤醒,下次再判断,信号量不用累加

你可能感兴趣的:(操作系统_哈工大_李治军)