HIT OS2020 Spring Lab4

实验4 信号量的实现与应用

4.1 实验目的

  • 加深对进程同步与互斥概念的认识;
  • 掌握信号量的使用,并应用它解决生产者——消费者问题;
  • 掌握信号量的实现原理。

4.2 实验内容

本次实验的基本内容是:

  • 在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;
  • 在 0.11 中实现信号量,用生产者—消费者程序检验之。
  • 用信号量解决生产者—消费者问题
    在 Ubuntu 上编写应用程序 pc.c ,解决经典的生产者—消费者问题,完成下面的功能:
  • 建立一个生产者进程, N 个消费者进程( N>1 );
  • 用文件建立一个共享缓冲区;
  • 生产者进程依次向缓冲区写入整数 0,1,2,...,M, M>=500 ;
  • 消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和数字输出到标准输出;
  • 缓冲区同时最多只能保存 10 个数。
    一种可能的输出效果是:
10: 0
10: 1
10: 2
10: 3
10: 4
11: 5
11: 6
12: 7
10: 8
12: 9
12: 10
12: 11
12: 12
……
11: 498
11: 499

其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。
pc.c 中将会用到 sem_open() 、 sem_close() 、 sem_wait() 和 sem_post() 等信号量相关的系统调用,请查阅相关文档。
《UNIX环境高级编程》是一本关于 Unix/Linux 系统级编程的相当经典的教程。 电子版可在网站上下载,后续实验也用得到。如果你对 POSIX 编程感兴趣,建议买一本常备手边。

4.2.1 实现信号量

Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。 如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 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是信号量类型,根据实现的需要自定义。
sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
name是信号量的名字。不同的进程可以通过提供同样的name而共享同一个信号量。如果该信号量不存在,就创建新的名为name的信号量;如果存在,就打开已经存在的名为name的信号量。value是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID等),由另两个系统调用使用。如失败,返回值是NULLsem_wait()就是信号量的P原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量sem上。返回0表示成功,返回-1表示失败。
sem_post()就是信号量的V原子操作。如果有等待sem的进程,它会唤醒其中的一个。返回0表示成功,返回-1表示失败。
sem_unlink()的功能是删除名为name的信号量。返回0表示成功,返回-1表示失败。
kernel目录下新建sem.c文件实现如上功能。然后将pc.c从Ubuntu移植到0.11下,测试自己实现的信号量。

4.3 实验问题

4.3.1 在 pc.c 中去掉所有与信号量有关的代码,再运行程序,执行效果有变化吗?为什么会这样?

  • 会有变化
  • 可能造成如下的情况
    - 乱序输出:可能缓冲区已经满了,但是生产者仍然在持续写入其他数据,导致之前的数据被覆盖,从而导致无序输出;亦或者可能缓冲区已经为空,但是此时消费者仍然在读取数据,此时也会导致乱序输出,
    - 程序崩溃:可能出现多个进程因为没有MUTEX信号而出现越界访问代码的情况,这样会使得程序崩溃。

4.3.2 实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的,这样可行吗?如果可行,那么它和标准解法在执行效果上会有什么不同?如果不可行,那么它有什么问题使它不可行?

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

Consumer()
{
    P(Mutex);
    P(Full);
    // 从缓存区取出一个赋值给item;
    V(Empty);
    // 消费产品item;
    V(Mutex);
}
  • 不可以,这样会造成死锁的情况出现。当Empty信号为0,而Mutex为1的时候,各个进程就会相互等待,从而进入死锁状态。

4.4 实验过程

4.4.1 整体说明

在实验过程中,我们实现的多个函数都要以系统调用的形式在Linux0.11中进行使用,因此,需要根据实验2的内容对makefile、unistd.h、system_call.s等文件进行修改,修改的过程不在此赘述,可以参考另一篇讲实验2的博客。

4.4.2 sem_open()函数

sem_t *sys_sem_open(const char *name,unsigned int value) //sem_t的定义会在后边给出,name是信号量的名称,value信号量对应的初值
{
    char kernelname[100]; 
    int isExist = 0;
    int i = 0;
    int name_cnt = 0;
    while(get_fs_byte(name + name_cnt) != '')//进行信号量名称长度的读取
    {
        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]);
    }
    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]);
        cnt++;//并且将这个信号放入信号表中
     }
    return p;
}

这个函数实现的是打开信号量的操作。

4.4.3 sem_wait()函数

int sys_sem_wait(sem_t *sem)
{
    cli();//关中断
    while(sem->value <= 0) //进程等待直到信号量的值大于0
    {        
        sleep_on(&(sem->queue));   
    }
    sem->value--;
    sti();//开启中断
    return 0;
}

这个函数实现了进程的等待过程。

4.4.4 sem_post()函数

int sys_sem_post(sem_t *sem)
{
    cli();
    sem->value++;
    if((sem->value) <= 1)//唤醒在信号量上等待的进程
    {
        wake_up(&(sem->queue));
    }    
    sti();
    return 0;
}

这个函数用于唤醒对应的进程。

4.4.5 sem_wait()函数

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) != '')
    {
            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;
}

这个函数用于删除名为name的信号量。

4.4.6 sem.h

这个文件定义了sem_t这个数据类型。

#ifndef _SEM_H
#define _SEM_H

#include <linux/sched.h>

#define SEM_NAME_LEN 50
#define SEMTABLE_LEN 20
typedef struct sem_t{
    char name[SEM_NAME_LEN];//信号量名称
    unsigned int value;//初值
    struct task_struct *queue;//等待信号量的进程指针
}sem_t;
extern sem_t semtable[SEMTABLE_LEN];
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);

#endif

4.4.7 pc.c

#define __LIBRARY__
#include <unistd.h>
#include <stdio.h>
#include <linux/sem.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.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)

#define NUMBUFFER 550 /*打出数字的总数*/
#define NUMPRO 5 /*消费者进程个数*/
#define MAXSIZE 10 /*缓冲区大小*/

sem_t *empty, *full, *mutex;

int main()
{
    int i, j, k;
    FILE *fp = NULL;
    int buf_out = 0;/*从缓冲区读取的位置*/
    int buf_in = 0;/*写入缓冲区的位置*/
    empty = (sem_t *)sem_open("EMPTY", MAXSIZE);/*打开对应的信号量*/
    full = (sem_t *)sem_open("FULL", 0);
    mutex = (sem_t *)sem_open("MUTEX", 1);
    fp = fopen("/var/buffer.dat", "wb+"); /*存在文件中。*/
    fseek(fp, MAXSIZE * sizeof(int), SEEK_SET);
    fwrite(&buf_out, 1, sizeof(buf_out), fp);
    fflush(fp);
    if(!fork())
    {
        for(i = 0;i < NUMBUFFER;i ++)
        {
            sem_wait(empty);
            sem_wait(mutex);
            fseek(fp, buf_in * sizeof(int), SEEK_SET);
            fwrite(&i, 1, sizeof(i), fp);
            fflush(fp);
            buf_in = (buf_in + 1) % MAXSIZE;
            sem_post(mutex);
            sem_post(full);
        }
        return 0;
    }

    for(i = 0;i < NUMPRO; i++)
    {
        if(!fork())
        {
            for(j = 0;j < NUMBUFFER / NUMPRO; j++)
            {
                int cost;
                sem_wait(full);
                sem_wait(mutex);
                fflush(stdout);
                fseek(fp, MAXSIZE * sizeof(int), SEEK_SET);
                fread(&buf_out, sizeof(int), 1, fp);
                fseek(fp, buf_out * sizeof(int), SEEK_SET);
                fread(&cost, sizeof(int), 1, fp);
                printf("%d:	%d
", getpid(), cost);
                fflush(stdout);
                buf_out = (buf_out + 1) % MAXSIZE;
                fseek(fp, MAXSIZE * sizeof(int), SEEK_SET);
                fwrite(&buf_out, 1, sizeof(buf_out),fp);
                fflush(fp);
                sem_post(mutex);
                sem_post(empty);
            }
            return 0;
        }
    }

    wait(NULL);
    sem_unlink("EMPTY");
    sem_unlink("FULL");
    sem_unlink("MUTEX");
    fclose(fp);
    return 0;
}

4.4.8 实验结果

....
17:	543
17:	544
17:	545
17:	546
17:	547
17:	548
17:	549

最终可以输出550个数字。

原文地址:https://www.cnblogs.com/winston8086/p/13258591.html