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

来源:互联网 发布:网络即时通讯工具问卷 编辑:程序博客网 时间:2024/05/23 20:00

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.cmain(){    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,最终执行的是breadstruct 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 voidlock_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 的这个队列唤醒?

这里写图片描述

// kernel\blk_drv\hd.c//// 读操作中断调用函数。将在执行硬盘中断处理程序中被调用。static void read_intr (void){  // 磁盘中断  ...  end_request (1);      // 若全部扇区数据已经读完,则处理请求结束事宜,  do_hd_request ();     // 执行其它硬盘请求操作。}// 结束请求。extern inline voidend_request (int uptodate){     ...     unlock_buffer (CURRENT->bh);   // 解锁缓冲区。     ...}// 释放锁定的缓冲区。extern inline voidunlock_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。voidwake_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 不需要记录有多少进程在阻塞,每次都是全部唤醒,下次再判断,信号量不用累加

0 0
原创粉丝点击