本节将更新哈工大《操作系统》课程第六个 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、新增上述四个系统调用
- 修改
/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
- 修改
/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();
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
};
- 修改
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’
- 在linux0.11系统的应用程序中,注释不能写
//
,必须要写/* */
- 修改头文件,包含上面新建的
sem.h
- 修改
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.c
、unistd.h
和sem.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
文件即可见到输出如下: