(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验5-信号量的实现与应用

操作系统实验5:信号量的实现与应用


实验基本内容:用信号量解决P-V问题,并在0.11实现信号量,用P-V程序检验之。

1.在Ubuntu 上编写应用程序pc.c然后复制到linux0.11上去,pc.c 中将会用到 sem_open()sem_close()sem_wait()sem_post() 等信号量相关的系统调用,解决经典的生产者—消费者问题,完成下面的功能:

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

2实现信号量的系统调用kernel/sem.c,修改makefile编译进去:

sem_t *sem_open(const char *name, unsigned int value); //创建或打开信号量。返回值为信号量唯一标识(比如内核地址或ID等)
int sem_wait(sem_t *sem); //P原子操作(不满足条件,线程等待在sem上)。返回0成功,-1失败
int sem_post(sem_t *sem); //V原子操作(满足条件,唤醒一个等待sem的线程)。返回0成功,-1失败
int sem_unlink(const char *name); //删除名为name的信号量。返回0成功,-1失败

注意:sem.c里面用了一个sem_t的数据结构,需要添加到/include/linux/sem.h

3.实验报告


零、思维导图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、pc.c,40%

代码如下,具体见注释:

#define __LIBRARY__

#include <stdio.h>
#include <linux/sem.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.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 BUFSIZE 10  /* 缓冲区大小,按照指导书要求设置为10 */

#define CONSUMER_NUM 4  /* 消费者进程数 */

#define MAX_NUM 500

int fd;
sem_t *empty; /* 空槽位的数量 */
sem_t *full; /* 满槽位的数量 */
sem_t *mutex;  /* 控制对文件互斥的访问 */

//注意,Linux 0.11中没有ftruncate、fsync和snprintf,后面两个都有可以凑合用的替代品,但是没有ftruncate的替代品,因此,通过修改fcntl实现了一个ftruncate
int ftruncate(int fd, unsigned long size)
{
    return fcntl(fd, F_CHGSIZE, size);
}

/**
 * 生产者在文件的最后添加数字
 */
void producer()
{
    int item_num = 0;
    while (item_num < MAX_NUM)
    {
        sem_wait(empty);
        sem_wait(mutex);
        if (lseek(fd, 0, SEEK_END) < 0)
            fprintf(stderr, "Error in producer lseek\n");
        write(fd, &item_num, sizeof(int));
        sync();
        sem_post(mutex);
        sem_post(full);
        ++item_num;
    }
    close(fd);
}

/**
 * 消费者在文件的开头读取数字,
 * 并且将读取过的数字删除(通过将后面的数字往前移实现)
 */
void consumer()
{
    int item;
    int file_len;
    int tmp_value;
    int j;

    do
    {
        sem_wait(full);
        sem_wait(mutex);
        if (lseek(fd, 0, SEEK_SET) < 0)
            fprintf(stderr, "Error in consumer lseek\n");

        if (read(fd, &item, sizeof(int)) == 0) // read不成功
        {
            sem_post(mutex);
            sem_post(empty);
            break;
        }

        printf("%d: %d\n", getpid(), item);
		fflush(stdout);  //刷新缓冲区
/* 使用标准 C 的文件操作函数要注意,它们使用的是进程空间内的文件缓冲区,父进程和子进程之间不共享这个缓冲区。因此,任何一个进程做完写操作后,必须 fflush() 一下,将数据强制更新到磁盘,其它进程才能读到所需数据。
	用 printf() 向终端输出信息是很自然的事情,但当多个进程同时输出时,终端也成为了一个临界资源,需要做好互斥保护,否则输出的信息可能错乱。
	另外,printf() 之后,信息只是保存在输出缓冲区内,还没有真正送到终端上,这也可能造成输出信息时序不一致。用 fflush(stdout) 可以确保数据送到终端。*/
        
        file_len = lseek(fd, 0, SEEK_END); 
        if (file_len < 0)
            fprintf(stderr, "Error when get file length\n");
        /* 通过将后面的数字前移来删除已经读取的数字 */
        /* 这种方式速度特别慢,不知道还有没有别的好办法 */
        for(j = 1; j < (file_len / sizeof(int)); j++) 
        { 
            lseek(fd, j * sizeof(int), SEEK_SET); 
            read(fd, &tmp_value, sizeof(int)); 
            lseek(fd, (j - 1) * sizeof(int), SEEK_SET); 
            write(fd, &tmp_value, sizeof(int)); 
        } 
        ftruncate(fd, file_len - sizeof(int));

        sem_post(mutex);
        sem_post(empty);
    }while(item < MAX_NUM - 1);

    sem_post(full);  /* 当第一个进程退出时通知另外一个阻塞在full上的进程,不然另一个进程永远不会退出了 */

    close(fd);
}

int main()
{
    char empty_name[64];
    char full_name[64];
    char mutex_name[64];
    int i;
    pid_t p_pid;  /* 生产者进程pid */

    /* 确保每个信号量有不同的名字 */
    /* from APUE */
    snprintf(empty_name, 64, "/%ld_empty", (long)getpid());    
    snprintf(full_name, 64, "/%ld_full", (long)getpid());
    snprintf(mutex_name, 64, "/%ld_mutex", (long)getpid());

    fd = open("share.file", O_CREAT | O_RDWR | O_TRUNC, 0666); //系统调用打开文件

    empty = sem_open(empty_name, BUFSIZE); //创建3个信号量
    if (empty == NULL)
    {
        fprintf(stderr, "Error when create empty\n");
        exit(0);
    }
    full = sem_open(full_name, 0);
    if (full == NULL)
    {
        fprintf(stderr, "Error when create full\n");
        exit(0);
    }
    mutex = sem_open(mutex_name, 1);
    if (mutex == NULL)
    {
        fprintf(stderr, "Error when create mutex\n");
        exit(0);
    }

    printf("Create semaphore OK!\n");

    /* 消费者进程 */
    for (i = 0; i < CONSUMER_NUM; i++)
    {
        if (!fork())
        {
            consumer();
            exit(0);
        }
    }

    /* 生产者进程 */
    if (!fork())
    {
        producer();
        exit(0);
    }
    /* 等待所有子进程结束 */
    while (waitpid(-1, NULL, 0) > 0)
        ;

    sem_close(empty_name);
    sem_close(full_name);
    sem_close(mutex_name);
    close(fd);
    
    return 0;
}

别忘了修改include/fcntl.h,增加一个宏定义:

在这里插入图片描述

然后修改include/fs/fcntl.c如下:

在这里插入图片描述
kernel/Makefile中添加如下依赖:

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

然后在全部编辑完代码后,重新编译内核即可。

二、kernel/sem.c,40%

现在我们添加四个系统调用:

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

先在include/linux文件夹中添加sem.h头文件,内容如下:

# ifndef _SEM_H_
# define _SEM_H_

#include <linux/sched.h> /* task struct */

#define MAX_NAME 20

typedef struct sem_t{
    char name[MAX_NAME];
    unsigned int value;
    struct task_struct *wait_queue;
}sem_t;

#endif

然后在kernel/文件夹中添加sem.c,内容如下:

#include <linux/sem.h>
#include <string.h> /* strcpy()  strcmp() */
#include <asm/segment.h>  /* get_fs_byte() */
#include <unistd.h>  /* NULL */
#include <asm/system.h>  /* cli()  sti() */
#include <linux/kernel.h>  /* printk() */

#define SEMS_SIZE 5

static sem_t sems[SEMS_SIZE] = {
    {"", 0, NULL},
    {"", 0, NULL},
    {"", 0, NULL},
    {"", 0, NULL},
    {"", 0, NULL},
};

sem_t* sys_sem_open(const char *name, unsigned int value)
{
    if (name == NULL)
    {
        printk("name == NULL\n");
        return NULL;
    }
    int i, index = -1;
    char temp_name[MAX_NAME];
    for (i = 0; i < MAX_NAME; ++i)
    {
        temp_name[i] = get_fs_byte(name+i);
        if (temp_name[i] == '\0')
            break;
    }
    if (i == 0 || i == MAX_NAME)
    {
        printk("name too long or too short, i = %d\n", i);
        return NULL;
    }

    for (i = 0; i < SEMS_SIZE; ++i)
    {
        if (strcmp(sems[i].name, "") == 0)   //信号量还未被初始化
        {
            index = index == -1 ? i : index;  //只要有一个信号量没使用,index=i而不等于-1
            continue;
        }
        if (strcmp(sems[i].name, temp_name) == 0) //名字符合,返回
            return &sems[i];
    }
    sem_t *res = NULL;
    if (index != -1)  //如果还有空位,那么占据他
    {
        res = &sems[index];
        // printk("before set: name is %s\n", res->name);
        // strcpy(sems[index].name, temp_name);  // 不能使用strcpy,是因为在内核态的原因吗?
        for (i = 0; temp_name[i] != '\0'; ++i)
                sems[index].name[i] = temp_name[i];
        sems[index].name[i] = '\0';
        // printk("after set: name is %s\n", res->name);
        res->value = value;
    }
    else
        printk("no empty slots: index = %d\n", index);
    return res;
}

int sys_sem_wait(sem_t *sem)
{
    if (sem == NULL || sem < sems || sem >= sems + SEMS_SIZE)
        return -1;
    cli();
    while (sem->value == 0)
        sleep_on(&sem->wait_queue);
    sem->value--;
    sti();
    return 0;
}

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

int sys_sem_unlink(const char *name)
{
    if (name == NULL)
        return -1;
    int i; 
    char temp_name[MAX_NAME];
    for (i = 0; i < MAX_NAME; ++i)
    {
        temp_name[i] = get_fs_byte(name+i);
        if (temp_name[i] == '\0')
            break;
    }
    if (i == 0 || i == MAX_NAME)
        return -1;
    temp_name[i] = '\0';

    for (i = 0; i < SEMS_SIZE; ++i) //释放信号量
    {
        if (strcmp(sems[i].name, temp_name))
        {
            sems[i].name[0] = '\0';
            sems[i].value = 0;
            sems[i].wait_queue = NULL;
            return 0;
        }
    }

    return -1;
}

系统调用总数,在kernel/system_call.s

nr_system_calls = 76

调用函数表,在include/linux/sys.h

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

//最后的sys_call_table[]加上
, sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink

以下添加都在linux0.11下,通过文件挂载进行。

sem.h移到linux0.11 bochs虚拟机的对应include文件目录下,以及include/fcntl.h

系统调用的编号要添加,在include/unistd.h

#define __NR_sem_open   72
#define __NR_sem_wait   73
#define __NR_sem_post   74
#define __NR_sem_unlink 75

编辑好代码后,将实验结果输入到文件中,执行以下命令:./pc > out.txt

等待后打开out.txt内容如下:

在这里插入图片描述

证明实验成功!

三、实验报告,20%

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

  • 有变化,输出的结果没有顺序,程序会产生奔溃。
  • 没有了信号量,进程之间无法进行同步或者协作,一种情况下是缓冲区满了,生产者还在写入数据;一种是缓冲区没了,消费者还在消费数据
  1. 实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:
Producer()
{
    P(Mutex);  //互斥信号量
    生产一个产品item;
    P(Empty);  //空闲缓存资源
    将item放到空闲缓存中;
    V(Full);  //产品资源
    V(Mutex);
}

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

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

  • 这样做是可不行的,有造成死锁的可能。不能先对Mutex加锁再对Empty加锁。
  • 假如mutex=1,当前缓冲区为空,full=0,Consumer执行P(Mutex)后又执行P(full),然后producer执行P(mutex),那么full和mutex 的值将永远得不到变化,生产者和消费者陷入死锁。

0.感谢高人的代码参考以及网友的精彩博文:

https://github.com/iLoveTangY/hit-oslab/blob/master/lab_2.md
https://blog.csdn.net/Cs_hnu_scw/article/details/80204038
1.实验楼(操作系统原理与实践)
https://www.shiyanlou.com/courses/115
2.网易云课堂:哈尔滨工业大学,国家级精品课程,操作系统
https://mooc.study.163.com/course/1000002004#/info
3.推荐markdown神器,本文由此写成
https://typora.io/
(完)

  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值