【哈工大_操作系统实验】Lab6 信号量的实现和应用

October 11, 2024

Github代码仓库链接

本节将更新哈工大《操作系统》课程第六个 Lab 实验 信号量的实现和应用。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。

实验目的:

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

实验任务:

1、在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题; 2、在 0.11 中实现信号量,用生产者—消费者程序检验之。

文件名 介绍
hit-操作系统实验指导书.pdf 哈工大OS实验指导书
Linux内核完全注释(修正版v3.0).pdf 赵博士对Linux v0.11 OS进行了详细全面的注释和说明
file1615.pdf BIOS 涉及的中断数据手册
hit-oslab-linux-20110823.tar.gz hit-oslab 实验环境
gcc-3.4-ubuntu.tar.gz Linux v0.11 所使用的编译器
Bochs 汇编级调试指令 bochs 基本调试指令大全
最全ASCII码对照表0-255 屏幕输出字符对照的 ASCII 码
x86_64 常用寄存器大全 x86_64 常用寄存器大全

一、编写生产者-消费者模型

1、在编写生产者-消费者模型之前,先进行一些知识准备:

lseek() 函数可以改变文件的文件偏移量,所有打开的文件都有一个当前文件偏移量,用于表明文件开始处到文件当前位置的字节数。成功返回新的偏移量,失败返回-1。

off_t lseek(int filedes, off_t offset, int whence);
// 参数 filedes:  文件描述符
// 参数 offset:   位移量
// 参数 whence:   指定初始位置
	// 	SEEK_SET 将读写位置指向文件头后再增加offset个位移量。
	//	SEEK_CUR 以目前的读写位置往后增加offset个位移量。
	//	SEEK_END 将读写位置指向文件尾后再增加offset个位移量。

2、实现生产者、消费者模型,编写pc.c文件:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>          
#include <sys/stat.h>        
#include <semaphore.h>
#include <sys/types.h>  
#include <sys/wait.h>

#define M 530    		/*打出数字总数*/
#define N 5       		/*消费者进程数*/
#define BUFSIZE 10      /*缓冲区大小*/

int main()
{
	sem_t *empty, *full, *mutex;  /*3个信号量*/
	int fd;                       /*共享缓冲区文件描述符*/
    int  i,j,k,child;
    int  data;                    /*写入的数据*/
    pid_t pid;
    int  buf_out = 0;             /*记录上次从缓冲区读取位置*/
    int  buf_in = 0;              /*记录上次写入缓冲区位置*/
	/*创建信号量*/
	empty = sem_open("empty", O_CREAT|O_EXCL, 0644, BUFSIZE); /*剩余资源,初始化为size*/
    full = sem_open("full", O_CREAT|O_EXCL, 0644, 0);         /*已生产资源,初始化为0*/
	mutex = sem_open("mutex", O_CREAT|O_EXCL, 0644, 1);       /*互斥量,初始化为1*/
    /* 在文件中存储buf_out(因此生产者只有一个进程,所以buf_in不用存在文件中)*/
    fd = open("buffer.txt", O_CREAT|O_TRUNC|O_RDWR,0666); 
    lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);/*修改地址偏移在10个数字之后,即40字节位置*/
    write(fd,(char *)&buf_out,sizeof(int));/*将上次读取位置buf_out存入buffer后的一个位置,每次从该位置获取上次位置,以便子进程之间通信*/
    
    /*生产者进程*/
    if((pid=fork())==0)
    {
		printf("I'm producer. pid = %d\n", getpid());
		/*生产多少个产品就循环几次*/
        for( i = 0 ; i < M; i++)
        {
			/*empty大于0,才能生产*/
            sem_wait(empty);  /* empty-- */
            sem_wait(mutex);  /* mutex-- */
            
            /*从上次位置继续向文件缓冲区写入一个字符*/
            lseek(fd, buf_in*sizeof(int), SEEK_SET); 
            write(fd,(char *)&i,sizeof(int)); 
            /*更新写入缓冲区位置,保证在0-9之间,缓冲区最大为10*/
            buf_in = (buf_in + 1) % BUFSIZE;
            
            sem_post(mutex);  /* mutex++ */
            sem_post(full);   /* full++,唤醒消费者线程 */
        }
        printf("producer end.\n");
        fflush(stdout);/*确保将输出立刻输出到标准输出。*/
        return 0;
    }
	else if(pid < 0)
    {
        perror("Fail to fork!\n");
        return -1;
    }
    
	/*消费者进程*/
    for( j = 0; j < N ; j++ )
    {
        if((pid=fork())==0)
        {
	        /* 每个进程读取 M/N 个数字 */
			for( k = 0; k < M/N; k++ )
            {
	            /* full大于0,才能消费 */
                sem_wait(full);  /* full-- */
                sem_wait(mutex); /* mutex-- */
				
                /*从文件第11个位置获得上次读取位置*/
                lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);
                read(fd,(char *)&buf_out,sizeof(int));
                /*从上次读取位置继续读取数据*/
                lseek(fd,buf_out*sizeof(int),SEEK_SET);
                read(fd,(char *)&data,sizeof(int));
                /*在文件第11个位置写入下次应读取位置*/
                buf_out = (buf_out + 1) % BUFSIZE;
                lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);
                write(fd,(char *)&buf_out,sizeof(int));

                sem_post(mutex);  /* mutex++ */
                sem_post(empty);  /* empty++,唤醒生产者进程 */
                /*消费资源*/
                printf("%d:  %d\n",getpid(),data);
                fflush(stdout);
            }
			printf("child-%d: pid = %d end.\n", j, getpid());
            return 0;
        }
		else if(pid<0)
		{
			perror("Fail to fork!\n");
			return -1;
		}
	}
	/*回收线程资源*/
    child = N + 1;
    while(child--)
        wait(NULL);
    /*释放信号量*/
    sem_unlink("full");
    sem_unlink("empty");
    sem_unlink("mutex");
    /*释放文件资源*/
    close(fd);
    return 0;
}

3、在Ubuntu下编译执行

  • 注意:此处代码头文件使用的是Ubuntu系统下的semaphore.h中的信号量,故无需下文定义的信号量即可编译通过。
gcc -o pc pc.c -lpthread
./pc > 1.txt

打开1.txt文件即可见到输出如下: 在这里插入图片描述 在这里插入图片描述

二、信号量的实现

1、新建头文件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

2、新建文件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;
    // 计算信号量名字长度,需要通过get_fs_byte从用户空间获取数据
    while( get_fs_byte(name+name_cnt) != '\0')
	    name_cnt++;
	// 大于则退出
    if(name_cnt>SEM_NAME_LEN)
	    return NULL;
	// 将信号量名字从用户态复制到内核态,存入kernelname
    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;
        // 往信号量表cnt位置记录
        p=(sem_t*)(&semtable[cnt]);
         //printk("creat name!\n");
        cnt++;
     }
    return p;
}

// 信号量P原子操作
int sys_sem_wait(sem_t *sem)
{
	// 关中断,阻止调度,实现临界区保护
    cli(); 
    // 使得所有小于0的进程阻塞(生产者没有缓冲可用empty<=0,则进入睡眠)
    while( sem->value <= 0 )        
        sleep_on(&(sem->queue));    // 睡眠当前进程
    sem->value--;
    // 开中断
    sti();
    return 0;
}

// 信号量V原子操作
int sys_sem_post(sem_t *sem)
{
	// 关中断,阻止调度,实现临界区保护
    cli();
    sem->value++;
    // 加完后小于等于1,说明加之前是没有可消费的,则唤醒一个进程
    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;
    // 计算信号量名字长度,需要通过get_fs_byte从用户空间获取数据
    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;
    // 判断是否存在,并定位赋值给cnt
    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;
        // 从定位到的cnt位置往后开始向前覆盖,实现删除
        for(tmp=i;tmp<=cnt;tmp++)
        {
            semtable[tmp]=semtable[tmp+1];
        }
        cnt = cnt-1;  // 信号量个数减一
        return 0;
    }
    else
        return -1;  // 不存在则返回
}

3、新增上述四个系统调用

  1. 修改/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
  1. 修改/kernel/system_call.s,需要修改总的系统调用数
nr_system_calls = 76
  1. 修改/include/linux/sys.h,声明全局新增函数
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_setreuid,sys_setregid,sys_whoami,sys_iam,
sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink
};
  1. 修改 linux-0.11/kernel/Makefile,添加sem.c编译规则
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 who.o sem.o
// ...
### Dependencies:
sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h

三、在linux0.11环境下编译运行

前面编写的pc.c能够在Ubuntu下运行通过,但在linux0.11系统下不行,需要进行修改,我们复制pc.c,创建一个适合在linux0.11环境下编译的`pc2.c'

  1. 在linux0.11系统的应用程序中,注释不能写//,必须要写/* */
  2. 修改头文件,包含上面新建的sem.h
  3. 修改sem_open()的创建方法
#define   __LIBRARY__
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sem.h>

#define M 530            /*打出数字总数*/
#define N 5               /*消费者进程数*/
#define BUFSIZE 10      /*缓冲区大小*/

_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 main()
{
    sem_t *empty, *full, *mutex;  /*3个信号量*/
    int fd;                       /*共享缓冲区文件描述符*/
    int  i,j,k,child;
    int  data;                    /*写入的数据*/
    pid_t pid;
    int  buf_out = 0;             /*记录上次从缓冲区读取位置*/
    int  buf_in = 0;              /*记录上次写入缓冲区位置*/

    if((mutex = sem_open("mutex",1)) == NULL)
    {
        perror("sem_open() error!\n");
        return -1;
    }
    if((empty = sem_open("empty",10)) == NULL)
    {
        perror("sem_open() error!\n");
        return -1;
    }
    if((full = sem_open("full",0)) == NULL)
    {
        perror("sem_open() error!\n");
        return -1;
    }

    /* 在文件中存储buf_out(因此生产者只有一个进程,所以buf_in不用存在文件中)*/
    fd = open("buffer.txt", O_CREAT|O_TRUNC|O_RDWR,0666); 
    lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);/*修改地址偏移在10个数字之后,即40字节位置*/

//  .....(以下内容不变)

注意:本人实验过程中发现,如果几个头文件前后放的顺序不同,将导致找不到四个sem函数,血的教训,具体原因不知。

将已经修改的pc.cunistd.hsem.h文件拷贝到linux-0.11系统中

cd oslab_Lab5
sudo ./mount-hdc
cp ./pc.c ./hdc/usr/root/
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/ 
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
sudo umount hdc

编译及运行Bochs

cd oslab_Lab5/linux-0.11
make all
../run

在linux0.11的Bochs中编译生产者-消费者程序

gcc -o pc pc.c
./pc > 1.txt
sync

在Ubuntu中挂载hdc,将linux0.11输出的1.txt文件移动到Ubuntu中

sudo ./mount-hdc
sudo cp ./hdc/usr/root/1.txt ./

打开1.txt文件即可见到输出如下: 在这里插入图片描述 在这里插入图片描述


Profile picture

Written by JokerDebug who works at Southeast University, Nanjing, China You can follow me on Github