Linux 0.11下信号量的实现和应用(李治军操作系统实验6)

生产者-消费者问题

从一个实际的问题:生产者与消费者出发,谈一谈为什么要有信号量?信号量用来做什么?

  • 为什么要有信号量?
    对于生产者来说,当缓冲区满,也就是空闲缓冲区个数为0时,此时生产者不能继续向缓冲区写数,必须等待,直到有消费者从满缓冲区取走数后,再次有了空闲缓冲区,生产者才能向缓冲区写数。
    对于消费者来说,当缓冲区空时,此时没有数可以被取走,消费者必须等待,直到有生产者向缓冲区写数后,消费者才能取数。并且如果当缓冲区空时,先后有多个消费者均想从缓冲区取数,那么它们均需要等待,此时需要记录下等待的消费者的个数,以便缓冲区有数可取后,能将所有等待的消费者唤醒,确保请求取数的消费者最终都能取到数。
    也就是说,当多个进程需要协同合作时,需要根据某个信息,判断当前进程是否需要停下来等待;同时,其他进程需要根据这个信息判断是否有进程在等待,或者有几个进程在等待,以决定是否需要唤醒等待的进程。而这个信息,就是信号量。

  • 信号量用来做什么?
    设有一整形变量sem,作为一个信号量。此时缓冲区为空,sem=0。
    消费者C1请求从缓冲区取数,不能取到,睡眠等待。sem=-1<0,表示有一个进程因缺资源而等待。
    消费者C2也请求从缓冲区取数,睡眠等待。sem=-2<0,表示有两个进程因缺资源而等待。
    生产者P往缓冲区写入一个数,sem=sem+1=-1<=0,并唤醒等待队列的头进程C1,C1处于就绪态,C2仍处于睡眠等待。
    生产者P继续往缓冲区写入一个数,sem=0<=0,并唤醒C2,C1、C2就处于就绪状态。
    由此可见,通过判断sem的值以及改变sem的值,就保证了多进程合作的合理有序的推进,这就是信号量的作用。

实现信号量

信号量有什么组成?
1、需要有一个整形变量value,用作进程同步。
2、需要有一个PCB指针,指向睡眠的进程队列。
3、需要有一个名字来表示这个结构的信号量。
同时,由于该value的值是所有进程都可以看到和访问的共享变量,所以必须在内核中定义;同样,这个名字的信号量也是可供所有进程访问的,必须在内核中定义;同时,又要操作内核中的数据结构:进程控制块PCB,所以信号量一定要在内核中定义,而且必须是全局变量。由于信号量要定义在内核中,所以和信号量相关的操作函数也必须做成系统调用,还是那句话:系统调用是应用程序访问内核的唯一方法。

和信号量相关的函数

Linux在0.11版还没有实现信号量,我们可以先弄一套缩水版的类POSIX信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:

sem_t *sem_open(const char  *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);

sem_t是信号量类型,根据实现的需要自己定义。

信号量的保护

使用信号量还需要注意一个问题,这个问题是由多进程的调度引起的。当一个进程正在修改信号量的值时,由于时间片耗完,引发调度,该修改信号量的进程被切换出去,而得到CPU使用权的新进程也开始修改此信号量,那么该信号量的值就很有可能发生错误,如果信号量的值出错了,那么进程的同步也会出错。所以在执行修改信号量的代码时,必须加以保护,保证在修改过程中其他进程不能修改同一个信号量的值。也就是说,当一个进程在修改信号量时,由于某种原因引发调度,该进程被切换出去,新的进程如果也想修改该信号量,是不能操作的,必须等待,直到原来修改该信号量的进程完成修改,其他进程才能修改此信号量。修改信号量的代码一次只允许一个进程执行,这样的代码称为临界区,所以信号量的保护,又称临界区保护。
实现临界区的保护有几种不同的方法,在Linux 0.11上比较简单的方法是通过开、关中断来阻止时钟中断,从而避免因时间片耗完引发的调度,来实现信号量的保护。但是开关中断的方法,只适合单CPU的情况,对于多CPU的情况,不适用。Linux 0.11就是单CPU,可以使用这种方法。

信号量的代码实现

1、sem_open()
原型:sem_t *sem_open(const char *name, unsigned int value)
功能:创建一个信号量,或打开一个已经存在的信号量
参数:name,信号量的名字。不同的进程可以通过同样的name而共享同一个信号量。如果该信号量不存在,就创建新的名为name的信号量;如果存在,就打开已经存在的名为name的信号量。
value,信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。
返回值。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID等)。如失败,返回值是NULL。

由于要做成系统调用,所以会穿插讲解系统调用的相关知识。
首先,在linux-0.11/kernel目录下,新建实现信号量函数的源代码文件sem.c。同时,在linux-0.11/include/linux目录下新建sem.h,定义信号量的数据结构。
linux-0.11/include/linux/sem.h

#ifndef _SEM_H
#define _SEM_H
#include <linux/sched.h>
#define SEMTABLE_LEN    20
#define SEM_NAME_LEN    20
typedef struct semaphore{
    char name[SEM_NAME_LEN];
    int value;
    struct task_struct *queue;
} sem_t;
extern sem_t semtable[SEMTABLE_LEN];
#endif

由于sem_open()的第一个参数name,传入的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以要用get_fs_byte()函数获取用户空间的数据。get_fs_byte()函数的功能是获得一个字节的用户空间中的数据。同样,sem_unlink()函数的参数name也要进行相同的处理。

2、sem_unlink()
原型:int sem_unlink(const char *name)
功能:删除名为name的信号量。
返回值:返回0表示成功,返回-1表示失败
sem_wait()
原型:int sem_wait(sem_t *sem)
功能:信号量的P原子操作(检查信号量是不是为负值,如果是,则停下来睡眠等待,如果不是,则向下执行)。
返回值:返回0表示成功,返回-1表示失败。

3、sem_post()
原型:int sem_post(sem_t *sem)
功能:信号量的V原子操作(检查信号量的值是不是为0,如果是,表示有进程在睡眠等待,则唤醒队首进程,如果不是,向下执行)。
返回值:返回0表示成功,返回-1表示失败。

关于sem_wait()和sem_post()

我们可以利用linux 0.11提供的函数sleep_on()实现进程的睡眠,用wake_up()实现进程的唤醒。
但是,sleep_on()比较难以理解。我们先看下sleep_on()的源码。

void 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;
    *p = current;
    current->state = TASK_UNINTERRUPTIBLE;
    schedule();
    if (tmp)
        tmp->state=0;
}

还拿生产者和消费者的例子来说,依然是有一个生产者和N个消费者,目前缓冲区为空,没有数可取。
1、消费者C1请求取数,调用sleep_on(&sem->queue)。此时,tmp指向NULL,p指向C1,调用schedule(),让出CPU的使用权。此时,信号量sem处等待队列的情况如下:
在这里插入图片描述
由于tmp是进程C1调用sleep_on()函数时申请的局部变量,所以会保存在C1运行到sleep_on()函数中时C1的内核栈中,只要进程C1还没有从sleep_on()函数中退出,tmp就会一直保存在C1的内核栈中。而进程C1是在sleep_on()中调用schedule()切出去的,所以在C1睡眠期间,tmp自然会保存在C1的内核栈中。这一点对于理解sleep_on()上如何形成隐式的等待队列很重要。

2、消费者C2请求取数,调用sleep_on(&sem->queue)。此时,信号量sem处的等待队列如下:
在这里插入图片描述

从这里就可以看到隐式的等待队列已经形成了。由于进程C2也会由于调用schedule()函数在sleep_on()函数中睡眠,所以进程C2内核栈上的tmp便指向之前的等待队列的队首,也就是C1,通过C2的内核栈便可以找到睡眠的进程C1。这样就可以找到在信号量sem处睡眠的所有进程。

我们在看下唤醒函数wake_up():

void wake_up(struct task_struct **p)
{
    if (p && *p) {
        (**p).state=0;
        *p=NULL;
    }
}

从中我们可以看到唤醒函数wake_up()负责唤醒的是等待队列队首的进程。
当队首进程C2被唤醒时,从schedule()函数退出,执行语句:

if (tmp)
    tmp->state=0;

会将内核栈上由tmp指向的进程C1唤醒,如果进程C1的tmp还指向其他睡眠的进程,当C1被调度执行时,会将其tmp指向的进程唤醒,这样只要执行一次wake_up()操作,就可以依次将所有等待在信号量sem处的睡眠进程唤醒。

sem_wait()和sem_post()函数的代码实现

由于我们要调用sleep_on()实现进程的睡眠,调用wake_up()实现进程的唤醒,我们在上面已经讲清楚了sleep_on()和wake_up()的工作机制,接下来,便可以具体实现sem_wait()和sem_post()函数了。

1、sem_wait()的实现
考虑到sleep_on()会形成一个隐式的等待队列,而wake_up()只要唤醒了等待队列的头结点,就可以依靠sleep_on()内部的判断语句,实现依次唤醒全部的等待进程。所以,sem_wait()的代码实现,必须考虑到这个情况。
参考linux 0.11内部的代码,对于进程是否需要等待的判断,不能用简单的if语句,而应该用while()语句,假设现在sem=-1,生产者往缓冲区写入了一个数,sem=0<=0,此时应该将等待队列队首的进程唤醒。当被唤醒的队首进程再次调度执行,从sleep_on()函数退出,不会再执行if判断,而直接从if语句退出,继续向下执行。而等待队列后面被唤醒的进程随后也会被调度执行,同样也不会执行if判断,退出if语句,继续向下执行,这显然是不应该的。因为生产者只往缓冲区写入了一个数,被等待队列的队首进程取走了,由于等待队列的队首进程已经取走了那个数,它应该已经将sem修改为sem=-1,其他等待的进程应该再次执行if判断,由于sem=-1<0,会继续睡眠。要让其他等待进程再次执行时,要重新进行判断,所以不能是if语句了,必须是while()语句才可以。
下面是我第一次实现sem_wait()的代码:

int sys_sem_wait(sem_t *sem)
{
    cli();
    sem->value--;
    while( sem->value < 0 )
        sleep_on(&(sem->queue))
    sti();
    return 0;
}

但是没有考虑到有一种特殊的信号量:互斥信号量。比如要读写一个文件,一次只能允许一个进程读写,当一个进程要读写该文件时,需要先执行sem_wait(file),此后在该进程读写文件期间,若有其他进程也要读写该文件,则执行流程分析如下:

进程P1申请读写该文件,value=-1,sleep_on(&file->queue)。
进程P2申请读写该文件,value=-2,sleep_on(&file->queue)。
原来读写该文件的进程读写完毕,置value=-1,并唤醒等待队列的队首进程P2。
进程P2再次执行,唤醒进程P1,此时执行while()判断,不能跳出while()判断,继续睡眠等待。此时文件并没有被占用,P2完全可以读写该文件,所以程序运行出错了。出错原因在于,修改信号量的语句,必须放在while()判断的后面,因为执行while()判断,进程有可能睡眠,而这种情况下,是不需要记录有多少个进程在睡眠的,因为sleep_on()函数形成的隐式的等待队列已经记录下了进程的等待情况。
正确的sem_wait()代码如下:

int sys_sem_wait(sem_t *sem)
{
    cli();
    while( sem->value <= 0 )        //
        sleep_on(&(sem->queue));    //这两条语句顺序不能颠倒,很重要,是关于互斥信号量能不
    sem->value--;               //能正确工作的!!!
    sti();
    return 0;
}

2、sem_post()的实现
sem_post的实现必须结合sem_wait()的实现情况。
还拿生产者和消费者的例子来分析。当前缓冲区为空,没有数可取,value=0。

消费者C1执行sem_wait(),value=0,sleep_on(&queue)。
消费者C2执行sem_wait(),value=0,sleep_on(&queue)。等待队列的情况如下:

生产者执行sem_post(),value=1,wake_up(&queue),唤醒消费者C2。队列的情况如下:

生产者再次执行sem_post(),value=2,wake_up(&queue)相当于wake_up(NULL)。队列情况如上。
消费者C2再次执行,唤醒C1,跳出while(),value=1,继续向下执行。
消费者C1再次执行,跳出while(),value=0,继续向下执行。
由此可以看出,sem_post()里面唤醒进程的判断条件是:value<=1。

sem_post的实现代码如下:

int sys_sem_post(sem_t *sem)
{
    cli();
    sem->value++;
    if( (sem->value) <= 1)
        wake_up(&(sem->queue));
    sti();
    return 0;
}

信号量的完整代码
linux-0.11/kernel/sem.c

#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>

sem_t semtable[SEMTABLE_LEN];
int cnt = 0;

sem_t *sys_sem_open(const char *name,unsigned int value)
{
    char kernelname[100];   /* 应该足够大了 */
    int isExist = 0;
    int i=0;
    int name_cnt=0;
    while( get_fs_byte(name+name_cnt) != '\0')
    name_cnt++;
    if(name_cnt>SEM_NAME_LEN)
    return NULL;
    for(i=0;i<name_cnt;i++)
    kernelname[i]=get_fs_byte(name+i);
    int name_len = strlen(kernelname);
    int sem_name_len =0;
    sem_t *p=NULL;
    for(i=0;i<cnt;i++)
    {
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
                if( !strcmp(kernelname,semtable[i].name) )
                {
                    isExist = 1;
                    break;
                }
        }
    }
    if(isExist == 1)
    {
        p=(sem_t*)(&semtable[i]);
        //printk("find previous name!\n");
    }
    else
    {
        i=0;
        for(i=0;i<name_len;i++)
        {
            semtable[cnt].name[i]=kernelname[i];
        }
        semtable[cnt].value = value;
        p=(sem_t*)(&semtable[cnt]);
         //printk("creat name!\n");
        cnt++;
     }
    return p;
}


int sys_sem_wait(sem_t *sem)
{
    cli();
    while( sem->value <= 0 )        //
        sleep_on(&(sem->queue));    //这两条语句顺序不能颠倒,很重要,是关于互斥信号量能不
    sem->value--;               //能正确工作的!!!
    sti();
    return 0;
}
int sys_sem_post(sem_t *sem)
{
    cli();
    sem->value++;
    if( (sem->value) <= 1)
        wake_up(&(sem->queue));
    sti();
    return 0;
}

int sys_sem_unlink(const char *name)
{
    char kernelname[100];   /* 应该足够大了 */
    int isExist = 0;
    int i=0;
    int name_cnt=0;
    while( get_fs_byte(name+name_cnt) != '\0')
            name_cnt++;
    if(name_cnt>SEM_NAME_LEN)
            return NULL;
    for(i=0;i<name_cnt;i++)
            kernelname[i]=get_fs_byte(name+i);
    int name_len = strlen(name);
    int sem_name_len =0;
    for(i=0;i<cnt;i++)
    {
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
                if( !strcmp(kernelname,semtable[i].name))
                {
                        isExist = 1;
                        break;
                }
        }
    }
    if(isExist == 1)
    {
        int tmp=0;
        for(tmp=i;tmp<=cnt;tmp++)
        {
            semtable[tmp]=semtable[tmp+1];
        }
        cnt = cnt-1;
        return 0;
    }
    else
        return -1;
}
实现信号量的系统调用

应用程序包含的宏定义和头文件
由于系统调用是借助内嵌汇编_syscall实现的,而_syscall的内嵌汇编实现是在linux-0.11/include/unistd.h中,所以必须包含#include <unistd.h>这个头文件,另外由于_syscall的内嵌汇编实现是包含在一个条件编译里面,所以必须包含这样一个宏定义#define LIBRARY

1、修改unistd.h
添加我们新增的系统调用的编号。
添加的代码如下:

#define __NR_sem_open   72  /* !!! */
#define __NR_sem_wait   73
#define __NR_sem_post   74
#define __NR_sem_unlink 75

2、修改system_call.s
由于新增了4个系统调用,所以需要修改总的系统调用的和值。
修改代码如下:

nr_system_calls = 76    /* !!! */

3、修改sys.h
要在linux-0.11/include/linux/sys.h中,声明这4个新增的函数。
修改代码如下:

extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace,  sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk,     sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct,     sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid,sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink };

4、修改linux-0.11/kernel目录下的Makefile
修改代码如下:

......
OBJS  = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o sem.o
......

###Dependencies:

sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h

在0.11环境下的/usr/include目录下,将修改过的unistd.h文件拷贝覆盖那里原有的unistd.h文件。

5、基本要求

1、 建立一个生产者进程,N个消费者进程( N>1 )
2、用文件建立一个共享缓冲区
3、生产者进程依次向缓冲区写入整数0,1,2,…,M,M>=500
4、消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程ID和+ 数字输出到标准输出
5、缓冲区同时最多只能保存10个数

1)文件IO函数
由于要用文件建立一个共享缓冲区,同时生产者要往文件中写数,消费者要从文件中读数,所以要用到open()、read()、write()、lseek()、close()这些文件IO系统调用。
应用程序实现的难点在于,消费者进程每次读一个数,要将读出的数字从缓冲区删除,这几个文件IO系统调用函数中,并没有可以删除一个数字的函数。解决办法是,当消费者进程要从缓冲区读数时,首先调用lseek()系统调用获取到目前文件指针的位置,保存生产者目前写文件的位置。由于被消费者进程读过的数都被删除了,所以同时最多只能保存10个数的缓冲区已有的数,一定是消费者进程未读的,也就是说每次消费者要从缓冲区读数时,要读的数一定是缓冲区的第一个数。这样,让消费者进程每次都从缓冲区读10个数出来,取读出的10个数中的第一个数送标准输出显示,再将后面的9个数再次写入到缓冲区中,这样,就可以做到删除读出的那个数。最后,再调用lseek()系统调用将文件指针定位到之前保存的文件指针减1的位置,这样,生产者进程再次写缓冲区时,也能正确定位删除了一个数字的缓冲区的写位置。

2)终端也是临界资源

用printf()向终端输出信息是很自然的事情,但当多个进程同时输出时,终端也成为了临界资源,需要做好互斥保护,否则输出的信息可能错乱。
另外,printf()之后,信息只是保存在输出缓冲区内,还没有真正送到终端上,这也可能造成输出信息时序不一致。用fflush(stdout)可以确保数据送到终端。

3)伪代码描述:

Producer()
{
    生产一个产品item;
    P(Empty);
    P(Mutex);
    将item放到空闲缓存中;
    V(Mutex);
    V(Full);
}

Consumer()
{
    P(Full);  
    P(Mutex);  
    从缓存区取出一个赋值给item;
    V(Mutex);
    V(Empty);
    消费产品item;
} 

5)新建pc.c文件,代码如下:

#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>

_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)

const char *FILENAME = "/usr/root/buffer_file";    /* 消费生产的产品存放的缓冲文件的路径 */
const int NR_CONSUMERS = 5;                        /* 消费者的数量 */
const int NR_ITEMS = 50;                        /* 产品的最大量 */
const int BUFFER_SIZE = 10;                        /* 缓冲区大小,表示可同时存在的产品数量 */
sem_t *metux, *full, *empty;                    /* 3个信号量 */
unsigned int item_pro, item_used;                /* 刚生产的产品号;刚消费的产品号 */
int fi, fo;                                        /* 供生产者写入或消费者读取的缓冲文件的句柄 */


int main(int argc, char *argv[])
{
    char *filename;
    int pid;
    int i;

    filename = argc > 1 ? argv[1] : FILENAME;
    /* O_TRUNC 表示:当文件以只读或只写打开时,若文件存在,则将其长度截为0(即清空文件)
     * 0222 和 0444 分别表示文件只写和只读(前面的0是八进制标识)
     */
    fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222);    /* 以只写方式打开文件给生产者写入产品编号 */
    fo = open(filename, O_TRUNC| O_RDONLY, 0444);            /* 以只读方式打开文件给消费者读出产品编号 */

    metux = sem_open("METUX", 1);    /* 互斥信号量,防止生产消费同时进行 */
    full = sem_open("FULL", 0);        /* 产品剩余信号量,大于0则可消费 */
    empty = sem_open("EMPTY", BUFFER_SIZE);    /* 空信号量,它与产品剩余信号量此消彼长,大于0时生产者才能继续生产 */

    item_pro = 0;

    if ((pid = fork()))    /* 父进程用来执行消费者动作 */
    {
        printf("pid %d:\tproducer created....\n", pid);
        /* printf()输出的信息会先保存到输出缓冲区,并没有马上输出到标准输出(通常为终端控制台)。
         * 为避免偶然因素的影响,我们每次printf()都调用一下stdio.h中的fflush(stdout)
         * 来确保将输出立刻输出到标准输出。
         */
        fflush(stdout);

        while (item_pro <= NR_ITEMS)    /* 生产完所需产品 */
        {
            sem_wait(empty);
            sem_wait(metux);

            /* 生产完一轮产品(文件缓冲区只能容纳BUFFER_SIZE个产品编号)后
             * 将缓冲文件的位置指针重新定位到文件首部。
             */
            if(!(item_pro % BUFFER_SIZE))
                lseek(fi, 0, 0);

            write(fi, (char *) &item_pro, sizeof(item_pro));        /* 写入产品编号 */
            printf("pid %d:\tproduces item %d\n", pid, item_pro);
            fflush(stdout);
            item_pro++;

            sem_post(full);        /* 唤醒消费者进程 */
            sem_post(metux);
        }
    }
    else    /* 子进程来创建消费者 */
    {
        i = NR_CONSUMERS;
        while(i--)
        {
            if(!(pid=fork()))    /* 创建i个消费者进程 */
            {
                pid = getpid();
                printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i);
                fflush(stdout);

                while(1)
                {
                    sem_wait(full);
                    sem_wait(metux);

                    /* read()读到文件末尾时返回0,将文件的位置指针重新定位到文件首部 */
                    if(!read(fo, (char *)&item_used, sizeof(item_used)))
                    {
                        lseek(fo, 0, 0);
                        read(fo, (char *)&item_used, sizeof(item_used));
                    }

                    printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used);
                    fflush(stdout);

                    sem_post(empty);    /* 唤醒生产者进程 */
                    sem_post(metux);

                    if(item_used == NR_ITEMS)    /* 如果已经消费完最后一个商品,则结束 */
                        goto OK;
                }
            }
        }
    }
    OK:
        close(fi);
        close(fo);
        return 0;
  }

6)我们先将虚拟硬盘挂载,将文件pc.c拷贝到虚拟硬盘下:

cd workspace/oslab/
sudo ./mount-hdc
cp pc.c hdc/usr/root/

7)编译运行linux-0.11:

cd linux-0.11
make
../run

8)在linux-0.11中,编译运行pc.c:

gcc -o pc pc.c
./pc > sem_output    # 这里我将输出重定向到文件sem_output,因为输出的内容比较多,而linux-0.11终端不能滚屏

一定要记得把修改的数据写入磁盘:

sync

9)关闭linux-0.11,挂载虚拟磁盘,查看我们的文件:

cd ..
sudo ./mount-hdc
sudo less hdc/usr/root/sem_output
回答问题

实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:

Producer()
{
    P(Mutex);  //互斥信号量
    生产一个产品item;
    P(Empty);  //空闲缓存资源
    将item放到空闲缓存中;
    V(Full);  //产品资源
    V(Mutex);
}

Consumer()
{
    P(Mutex);  
    P(Full);  
    从缓存区取出一个赋值给item;
    V(Empty);
    消费产品item;
    V(Mutex);
} 

这样可行吗?如果可行,那么它和标准解法在执行效果上会有什么不同?如果不可行,那么它有什么问题使它不可行?

A:不可行。

1、假设Producer刚生产完一件商品,释放了Mutex,Mutex为1,此时缓存区满了,Empty为0;
2、然后OS执行调度,若被Producer拿到CPU,它拿到Mutex,使Mutex为0,而Empty为0,Producer让出CPU,等待Consumer执行V(Empty);
3、而Consumer拿到CPU后,却要等待Producer执行V(Mutex);
4、两者相互持有对方需要的资源,造成死锁。

  • 14
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《操作系统 治军 pdf》是一本由作者治军撰写的关于操作系统的书籍的电子版PDF文件。操作系统是计算机系统中的一个核心组成部分,它负责管理和控制计算机的硬件和软件资源,使得计算机能够高效地运行各种应用程序。 在这本书中,治军详细介绍了操作系统的基本概念、原理和设计原则。他从操作系统的起源和发展历程出发,讲解了多道、分时和实时操作系统等不同类型的操作系统,并深入解析了其内核结构和功能。此外,书中还讨论了进程管理、内存管理、文件系统、输入输出控制等重要的操作系统主题,为读者提供了全面了解和深入学习的机会。 这本书的PDF版本使得读者可以更加便捷地获取其中的内容,无论是在电脑、平板还是手机上都可以进行阅读。通过阅读这本书,读者可以深入了解操作系统的基本原理和相关技术,有助于他们提升对计算机系统的理解和应用。对于学习计算机科学或相关专业的学生和从事软件开发工作的技术人员来说,这本书是一本宝贵的参考资料。 总之,《操作系统 治军 pdf》是一本全面介绍操作系统的书籍的电子版PDF文件,读者可以通过阅读它来深入了解操作系统的原理和技术。这本书的出现为学习和研究操作系统提供了便利,对于对计算机系统有兴趣的人士来说是一本值得阅读的优秀教材。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值