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、兩者相互持有對方需要的資源,造成死鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章