哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用

本文介绍了如何在Linux环境下实现信号量,通过自底向上编写用户态pc.c和内核态sem.c,解决生产者消费者问题。涉及关键步骤包括sem_open, sem_wait, sem_post, sem_unlink的系统调用实现及用户进程对信号量的控制。
摘要由CSDN通过智能技术生成

实验5-信号量的实现和应用

实验内容请查看操作系统实验指导手册

绪论

在开始编写代码之前,先理一下这个实验需要做什么以及怎么做:

  • 编写用户态的程序pc.c,在这个程序中,需要创建一个生产者进程,多个消费者进程。创建进程会用到之前学过的fork,为了让这几个进程对同一块内存的读和写不发生错误,需要使用信号量进行控制;
  • 信号量的实现需要我们在内核态编写系统调用代码,在用户态通过int 0x80中断进入内核态执行相应的系统调用。需要编写的系统调用为:sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink

对于这种要写系统调用的实验,我们可以选择自顶向下从用户态的代码开始写,一直写到内核态的系统调用,也可以选择自底向上先写系统调用,最后再写用户态的调用系统调用的函数,也就是pc.c。本文选择自底向上的方式完成本次实验。

多看几遍李治军老师的视频,L16-L19的视频中的很多知识点,是本次实验要用上的。相信我,二刷甚至三刷视频,你不会后悔的。

一、实验内容

1.编写sem.c

(0) 头文件及其初始化部分

指导手册给了我们几个实现信号量的函数原型:

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);

挂载虚拟机,在(~/oslab/hdc/usr/include/linux)中创建sem.h,定义数据类型sem_t

/* 定义的信号量数据结构: */
# ifndef _SEM_H_
# define _SEM_H_

#include<linux/sched.h>

typedef struct semaphore_t 
{
	char name[20];/* 信号量的名称 */
	int value;    /* 信号量的值 */
	struct tast_struct *queue;/* 指向阻塞队列的指针 */
} sem_t;

#endif

在(~/oslab/linux-0.11/kernel/)中创建sem.c,并在其中添加如下内容:

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

//信号量最大数量
#define SEM_LIST_LENGTH 5

//信号量数组(都初始化为没有的状态)
sem_t sem_list[SEM_LIST_LENGTH] = {
	{"\0",0,NULL}, {"\0",0,NULL},{"\0",0,NULL},{"\0",0,NULL},{"\0",0,NULL}
};

(1)sys_sem_open()

/*
sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
*/

sem_t *sys_sem_open(const char * name,unsigned int value)
{
	if (name == NULL)
    {
        printk("name == NULL\n");
        return NULL;
    }
    /* 首先将信号量的名称赋值到新建的缓冲区中 */
    char nbuf[20];
    int i = 0;
    for(; i< 20; i++)
    {
    	nbuf[i] = get_fs_byte(name+i);
    }

    /* 然后开始遍历已有的信号量数组,如果有该名字的信号量,直接返回信号量的地址 */
    sem_t * result = NULL;
    i = 0;
    for(; i < SEM_LIST_LENGTH; i++)
    {
    	if(sem_list[i].name[0] == '\0')
    		break;
        if(!strcmp(sem_list[i].name, nbuf))
        {
            result = &sem_list[i];
            printk("sem %s is found\n",result->name);
            return result;
        }
    }
    /* 如果找不到信号量,就开始新建一个名字为name的信号量,值=value,队列指针=NULL,然后返回信号量的地址 */
    strcpy(sem_list[i].name, nbuf);
    sem_list[i].value = value;
    sem_list[i].queue = NULL;
    result = &sem_list[i];
    printk("sem %s is created , value = %d\n",result->name,result->value);
    return result;
}

(2) sys_sem_wait()

涉及到信号量的改变,为了防止放置进程的切换使得信号量的值被搞乱了,需要设置临界区来保护信号量:
不采用软件保护法(比如:轮换法\标记法\ peterson算法\ Lamport面包店算法),
采用硬件保护法,
由于是linux0.11运行在单cpu上(Bochs虚拟机提供单cpu环境),所以可以采用简单的开关中断的方法,
如果是多cpu环境,就使用硬件原子指令保护法(用硬件原子指令操控一个mutex信号量来保护临界区)

/*
 sem_wait()就是信号量的P原子操作。
 如果继续运行的条件不满足,则令调用进程等待在信号量sem上。
 返回0表示成功,返回-1表示失败。
 */
int sys_sem_wait(sem_t * sem)
{
    /* 判断:如果传入的信号量是无效信号量,P操作失败,返回-1 */
    if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
    {
        printk("P(sem) error\n");
        return -1;
    }
    /* 关中断 */
    cli();
    while(sem->value == 0)
    {
        sleep_on(&(sem->queue));
    }
    sem->value--; 
    /* 开中断 */
    sti();
    return 0;
}

(3)sys_sem_post()

/*
sem_post()就是信号量的V原子操作。
如果有等待sem的进程,它会唤醒其中的一个。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_post(sem_t * sem)
{
    /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
    if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
    {
        printk("V(sem) error\n");
        return -1;
    }
    /* 关中断 */
    cli();
    sem->value++;
    /* 如果有等待sem的进程,它会唤醒其中的一个。 */
    if(sem->value <= 0)
    {
    	wake_up(&(sem->queue));
    }
    /* 开中断 */
    sti();
    return 0;
}

(4)sys_sem_unlink()

/*
sem_unlink()的功能是删除名为name的信号量。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_unlink(const char *name)
{
    if (name == NULL)
        return -1;
    /* 首先将信号量的名称赋值到新建的缓冲区中 */
    char nbuf[20];
    int i = 0;
    for (; i < 20; i++)
    {
        nbuf[i] = get_fs_byte(name + i);
        if (nbuf[i] == '\0')
            break;
    }
    i = 0;
    for (; i < SEM_LIST_LENGTH; i++)
    {
        if (strcmp(sem_list[i].name, nbuf))
        {
            sem_list[i].name[0] = '\0';
            sem_list[i].value = 0;
            sem_list[i].queue = NULL;
        }
    }
    /* 没找到该名字的信号量,删除失败,返回-1 */
    if (i == SEM_LIST_LENGTH)
        return -1;

    return 0;
}

(5)修改系统文件

挂载虚拟机!!!在~/oslab/hdc/usr/include/unistd.h中添加系统调用号:
/* ... */
#define __NR_setregid 71
/* 添加的系统调用号 */
#define __NR_sem_open 72
#define __NR_sem_wait 73
#define __NR_sem_post 74
#define __NR_sem_unlink 75
在linux-0.11/kernel/system_call.s中改写系统调用数:
/* ... */
nr_system_calls = 76
在include/linux/sys.h中添加系统调用的定义:
/* ... */
extern int sys_setregid();
/* 添加的系统调用定义 */
#include<linux/sem.h>
extern sem_t * sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();

/* 在sys_call_table数组中添加系统调用的引用: */
fn_ptr sys_call_table[] = 
{ sys_setup, sys_exit, sys_fork, sys_read,……, sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink}
修改 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/kernel.h ../include/unistd.h \
  ../include/linux/sem.h ../include/linux/sched.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

至此,我们就完成了系统内核sem.c编写,并修改了系统文件让四个函数能正常运行。

使用make all指令重新编译linux0.11内核,如果有错误出现的话,按照系统提示依次修改,直至编译成功。

接下来,我们继续编写用户态的pc.c。

2.编写pc.c

/*
建立一个生产者进程,N个消费者进程(N>1);
用文件建立一个共享缓冲区;
生产者进程依次向缓冲区写入整数0,1,2,...,M,M>=500;
消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程ID和数字输出到标准输出;
缓冲区同时最多只能保存10个数。
提示:建议直接使用系统调用进行文件操作,使用标准C的fopen等文件读写函数需要额外的操作,稍显麻烦
*/
#define __LIBRARY__
#include <stdio.h>
#include <linux/sem.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.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);

int consumerNum = 5;	/* 消费者个数 */
const int maxNum = 500; /* 写入的数据量 */
const int bufSize = 10; /* 缓冲区大小 */
int data = -404;		/* 存放读取的数据*/
pid_t p_pid[5];			/* 进程号数组*/

sem_t *empty, *full, *mutex; /*三个信号量 */

void producer()
{
	int i, fo;
	int endpos_produce = 0; /* 记录消费者进程的消费次数*/
	fo = open("report.txt", O_WRONLY | O_TRUNC, 0666);
	if (!fo)
	{
		perror("打开文件失败!\n");
		return;
	}
	/* 这一步是提前在文件中写入初始的endpos_consumer的值 */
	i = 0;
	lseek(fo, bufSize * sizeof(int), SEEK_SET);
	write(fo, &i, sizeof(int));
	
	for (i = 1; i <= maxNum; i++)
	{
		sem_wait(empty); /* 如果empty为0,意味着缓冲区已满,阻塞生产者*/
		sem_wait(mutex); /* 互斥控制 */

		lseek(fo, endpos_produce * sizeof(int), SEEK_SET);
		write(fo, &i, sizeof(int));

		endpos_produce = (endpos_produce + 1) % bufSize;

		sem_post(mutex); /* 出了临界区,需要mutex++,以便下一次可以进入 */
		sem_post(full);	 /* 看是不是需要唤醒阻塞 */
	}
	close(fo);
}
void consumer()
{
	int cnt, endpos_consumer, fi;
	fi = open("report.txt", O_RDONLY, 0666);
	if (!fi)
	{
		perror("创建文件缓冲区失败!\n");
		return;
	}
	for (cnt = 0; cnt < maxNum / 5; ++cnt)
	{

		sem_wait(full);
		sem_wait(mutex);
		/* 从文件中读取消费者进程的文件指针位置 */
		lseek(fi, bufSize * sizeof(int), SEEK_SET);
		read(fi, &endpos_consumer, sizeof(int));
		/* 将文件中对应位置的数据读取出来 */
		lseek(fi, endpos_consumer * sizeof(int), SEEK_SET);
		read(fi, &data, sizeof(int));
		
		printf("\nC,%d: %d...\n", getpid(), data);
		fflush(stdout);
		/* 移动endpos_consumer的位置,下一次的消费者进程将读取新的数*/
		endpos_consumer = (endpos_consumer + 1) % bufSize;
		lseek(fi, bufSize * sizeof(int), SEEK_SET);
		write(fi, &endpos_consumer, sizeof(int));

		sem_post(mutex);
		sem_post(empty);
	}
	close(fi);
}

int main()
{
	/* 建立这三个信号量 */
	empty = sem_open("empty", bufSize);
	if (empty == NULL)
	{
		perror("empty create falied!\n");
		return -1;
	}
	full = sem_open("full", 0);
	if (full == NULL)
	{
		perror("full create failed!\n");
		return -1;
	}
	mutex = sem_open("mutex", 1);
	if (mutex == NULL)
	{
		perror("mutex create failed!\n");
		return -1;
	}
	if (empty && full && mutex)
	{
		printf("create semphore successed!\n");
	}

	/* 调用生产者进程,往文件缓冲区中写入数字 */
	if (!fork())
	{
		printf("producer is running!\n");
		producer();
		exit(0); /* 生产者任务完成后,杀死该子进程 */
	}

	while (consumerNum--)
	{
		p_pid[consumerNum] = fork();

		if (!p_pid[consumerNum])
		{
			printf("consumer %d is running!\n", getpid());
			consumer();
			exit(0);
		}
	}

	wait(NULL);
	/* 关闭信号量 */
	sem_unlink("empty");
	sem_unlink("full");
	sem_unlink("mutex");
	return 0;
}

pc.c程序中,首先是调用sem_open函数创建了三个信号量empty, mutex, full

然后创建了一个生产者进程,调用producer函数向report.txt的文件写入数据。

之后用for循环创建了5个消费者进程,调用consumer函数从文件中读取数据,并控制文件指针的位置移动。5个消费者进程之间通过我们之前编写的信号量函数进行调度,保证一个时刻只会有一个进程对内存进行读写操作。

下面展示程序的运行结果:
==pic==

调试了五天,终于出结果了!!

二、回答问题

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

    1. 有变化。输出的结果没有顺序,程序会产生奔溃。
    2. 没有了信号量,进程之间无法进行同步或者协作,一种情况下是缓冲区满了,生产者还在写入数据;一种是缓冲区没了,消费者还在消费数据
  2. 实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:

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 的值将永远得不到变化,生产者和消费者陷入死锁。

参考文献:
1.Linux 文件编程(第一阶段主要是对 open和read write函数的详细demo)
2.(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验5-信号量的实现与应用
3.哈工大操作系统实验—lab5:信号量的实现与应用
4.信号量的实现和应用(二)

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值