实验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
等),由另两个系统调用使用。如失败,返回值是NULL
。sem_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个数字。