实验要求与实验指导见 实验楼。
实验环境为 配置本地实验环境。
根据实验要求,pc.c
程序需打开一个文件作为共享缓冲区、创建一个生产者进程和多个消费者进程,其中生产者进程向缓冲区写入连续的整数,消费者进程从缓冲区依次读取数字并输出。
对于涉及到的文件操作,这里使用 open()
、write()
、read()
系统调用来实现,并使用 lseek()
修改文件读写指针来控制读写的位置。为了达到删除读出的数字的要求,本程序进行循环读写缓冲区的前 10 个数字的空间,生产者在第 10 个位置写入数字后,又回到第 1 个位置继续写入;消费者读取了第 10 个位置的数字后,也回到第 1 个位置。于是各个消费者进程间需要记录各自读取的位置,这里使用另一个共享的文件 share.txt
来实现:每次消费者先从该文件处获取上一个文件记录的位置,然后从缓冲区读取下一个字符,最后将新的位置记录回去。
限制缓冲区的最大保存数量需要两个信号量来共同控制:sem_empty 视为缓冲区剩余的空间资源、sem_full 视为缓冲区已被使用的资源;另外对文件的读写和消费者的整个“消费”过程都是一个原子操作,不可以打断,于是使用两个信号量进行限制。
pc.c
程序的完整代码如下:
#include
#include
#include
#include
#include
#define SIZE 10 // 缓冲区大小
#define N 5 // 消费者数量
#define M 510 // 写入的最大整数
int main()
{
int fd, pt;
int val, current_point;
pid_t pid;
sem_t *sem_empty, *sem_full, *sem_fd, *sem_pt;
fd = open("buffer.txt", O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR); // 用作缓冲区
pt = open("share.txt", O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR); // 记录消费者读取位置
val = 0;
lseek(pt, 0, SEEK_SET);
write(pt, &val, sizeof(val)); // 初始化位置为 0
sem_empty = sem_open("empty", O_CREAT|O_EXCL, 0644, SIZE);
sem_full = sem_open("full", O_CREAT|O_EXCL, 0644, 0);
sem_fd = sem_open("fd", O_CREAT|O_EXCL, 0644, 1);
sem_pt = sem_open("pt", O_CREAT|O_EXCL, 0644, 1);
pid = fork();
if(pid == -1) {
printf("fork error!\n");
exit(-1);
}
else if(pid == 0) {
// 生产者进程
printf("I'm producer. pid = %d\n", getpid());
int count = 0;
while(count <= M) {
sem_wait(sem_empty);
sem_wait(sem_fd);
current_point = count % SIZE;
lseek(fd, current_point * sizeof(current_point), SEEK_SET);
write(fd, &count, sizeof(count));
sem_post(sem_fd);
sem_post(sem_full);
count++;
}
printf("producer end.\n");
fflush(stdout);
return 0;
}
int i;
for (i = 0; i < N; i++) {
if((pid=fork()) == 0) // 创建 N 个消费者进程
break;
}
if(pid == -1) {
printf("fork error.\n");
exit(-1);
} else if(pid == 0) {
// 消费者进程
while(1) {
// 从 share.txt 获取读取位置
sem_wait(sem_pt);
lseek(pt, 0, SEEK_SET);
read(pt, &val, sizeof(val));
if (val >= M) { // 读取完毕,退出
sem_post(sem_pt);
break;
}
sem_wait(sem_full);
sem_wait(sem_fd);
current_point = (val + 1) % SIZE;
lseek(fd, current_point*sizeof(current_point), SEEK_SET);
read(fd, &val, sizeof(val)); // 从缓冲区读取数字
printf("%d:%d\n", getpid(), val);
fflush(stdout);
sem_post(sem_empty);
sem_post(sem_fd);
// 将新的位置记录在 share.txt
lseek(pt, 0, SEEK_SET);
write(pt, &val, sizeof(val));
sem_post(sem_pt);
}
printf("child-%d: pid = %d end.\n", i, getpid());
return 0;
}
int child = N + 1;
while(child--)
wait(NULL);
close(fd);
close(pt);
sem_unlink("full");
sem_unlink("empty");
sem_unlink("fd");
sem_unlink("pt");
return 0;
}
在 Ubuntu 中编译运行此程序,结果如下:
信号量的信息由一个名为 sem_t
的结构体保存,至少需要保存名字和值两个属性,根据信号量的定义,还需要一个等待进程的队列。该结构体在 include/sem.h
中定义:
typedef struct {
char name[_SEM_NAME_MAX];
int value;
// 待添加队列属性
} sem_t;
sem_open()
函数的功能是新建一个信号量或打开一个已存在的信号量,首先需要将参数字符串从用户空间复制到内核空间,然后对比已经存在的信号量,如果名字相同则直接返回该信号量,否则创建新的信号量。sem_unlink()
函数的功能是删除名为 name 的信号量,为了保证系统的多个信号量能在有限长度的数组内都正常工作,当删去信号量时数组后面的元素往前移动,腾出空间。代码如下:
#include
#include
// #include // 有问题,见实验楼指导
#include
sem_t sems[_SEM_MAX]; // 信号量数组
int count = 0; // 信号量数量
sem_t *sys_sem_open(const char *name, unsigned int value)
{
char sem_name[_SEM_NAME_MAX];
int len = 0, i = 0;
// 从用户态复制到内核态
do {
sem_name[i] = get_fs_byte(name+i);
} while(sem_name[i++]!='\0');
if (i == _SEM_NAME_MAX) // 判断长短
return NULL;
len = i - 1;
// 比较是否同名
for (i = 0; i < count; i++)
if(!strcmp(sem_name, sems[i].name))
return &sems[i];
// 创建新信号量
for (i = 0; i < len; i++)
sems[count].name[i] = sem_name[i];
sems[count].value = value;
count++;
return &sems[count-1;
}
int sys_sem_unlink(const char *name)
{
char sem_name[_SEM_NAME_MAX];
int len = 0, i;
// 从用户态复制到内核态
for (i = 0;; i++) {
sem_name[i] = get_fs_byte(&name[i]);
if(sem_name[i] == '\0' || i == _SEM_NAME_MAX) {
break;
}
}
int flag = 0, j;
for (i = 0; i < count; i++) {
if(!strcmp(sem_name, sems[i].name)) {
flag = 1;
break;
}
}
if(flag) {
for (j = i; j < _SEM_MAX; j++)
sems[j] = sems[j + 1];
count--;
return 0;
} else
return -1;
}
sem_wait()
函数是 P 原子操作,功能是使信号量的值减一,如果信号量值为 0 就阻塞当前进程;sem_post
函数是 V 原子操作,功能是使信号量的值加一,如果有等待该信号量的进程,则唤醒其中一个。阻塞和唤醒进程由 kernel/sched.c
中的 sleep_on()
、wake_up
函数实现。
sleep_on()
函数参数获取的指针 *p 是等待队列的头指针,每次执行该函数时,指针 *tmp 指向原本的等待队列,*p 则指向当前进程,即将当前进程插入等待队列头部,然后将当前进程设为睡眠状态,执行 schedule()
进行调度。wake_up()
函数将唤醒 *p 指向的进程。某个进程被唤醒后,回到 sleep_on()
继续执行,它将 *tmp 指向的进程继续唤醒,即唤醒等待队列的上一个进程。依次执行下去,等待队列的所有进程都会被唤醒。
因此,每个信号量需要维护自己的进程等待队列,当阻塞时将进程放进等待队列,唤醒时将其移出。根据 sleep_on()
的特点,信号量结构体中只需设置一个 PCB 指针即可在该函数中形成隐式等待队列。
typedef struct {
char name[_SEM_NAME_MAX];
int value;
struct task_struct *queue;
} sem_t;
需要注意的是,当等待队列中有多个进程时,信号量值加一只能唤醒其中一个进程,而 wake_up()
函数会唤醒队列中的所有进程,所以需要在 sem_wait()
中使用 while 循环,当信号量的值小于等于 0 时保证其他进程一直阻塞,由于有这层限制,sem_post()
中也就不需要对 wake_up()
函数的使用进行条件限制了。代码如下:
int sys_sem_wait(sem_t *sem)
{
if (sem == NULL || sem < sems || sem > sems + _SEM_MAX)
return -1;
cli(); // 关中断
while(sem->value <= 0)
sleep_on(&(sem->queue));
sem->value--;
sti(); // 开中断
return 0;
}
int sys_sem_post(sem_t *sem)
{
if(sem==NULL || sem < sems || sem > sems+_SEM_MAX)
return -1;
cli();
sem->value++;
wake_up(&(sem->queue));
sti();
return 0;
}
完整的 include/sem.h
的代码如下。该头文件也需要放进 Linux-0.11 的 /usr/include/
路径下。
#include
#define _SEM_MAX 24
#define _SEM_NAME_MAX 100
#define sti() __asm__ ("sti"::)
#define cli() __asm__ ("cli"::)
typedef struct {
char name[_SEM_NAME_MAX];
int value;
struct task_struct *queue;
} sem_t;
extern sem_t sems[_SEM_MAX];
修改 kernel/Makefile
,将新增的程序一块编译进 Image:
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 # 末尾添加
# 最后一行添加:
sem.s sem.o: sem.c ../include/linux/kernel.h ../include/asm/segment.h \
../include/string.h ../include/sem.h
然后对系统调用的实现做后续补充。在 include/linux/sys.h
中添加:
extern int sys_sem_open(); // 不能是 sem_t,会无法编译
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
fn_ptr sys_call_table[] = { ..., sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink };
在 include/unistd.h
及 Linux-0.11 中的 usr/include/unistd.h
添加:
#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
这样信号量就添加完成了。
接下来修改 pc.c
,从而能够在 Linux-0.11 运行此程序:
#define __LIBRARY__ /* add */
#include
#include
#include /* change */
#include
#include
#define SIZE 10
#define N 5
#define M 50
/* add */
_syscall2(int,sem_open,const char*,name,unsigned int,vaule)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
int main()
{
...
/* change */
sem_empty = sem_open("empty", SIZE);
sem_full = sem_open("full", 0);
sem_fd = sem_open("fd", 1);
sem_pt = sem_open("pt", 1);
...
}
在 Linux-0.11 下编译运行,结果如下:
在 pc.c 中去掉所有与信号量有关的代码,再运行程序,执行效果有变化吗?为什么会这样?
有变化,输出的数字顺序完全混乱。
没有了信号量,进程之间无法同步工作,而且无法保证对文件的读写是原子操作,无法保证顺序,工作异常。
实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:
Producer()
{
// 生产一个产品 item;
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
// 产品资源
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}
这样可行吗?如果可行,那么它和标准解法在执行效果上会有什么不同?如果不可行,那么它有什么问题使它不可行?
从信号量的设计来看,Mutex 信号量保证了对缓冲区的互斥读写,Empty 和 Full 两个信号量保证了缓冲区的数量限制,并且不会出现死锁。与上述
pc.c
的逻辑基本一致。(可能实验楼题目把两个信号量顺序弄反了。。。