该篇文章是哈工大操作系统实验6的完成笔记,其中包含了详细的步骤和相关代码,并有截图说明。实验内容都成功通过了,但是因为内容较多,记录中难免会有疏忽,如有发现错误,欢迎大家留言和我联系。
不知不觉又肝了一周多,欢迎大家一键三连:点赞、关注加收藏,感谢大家的支持。
理论知识
理论部分建议参考:【经典进程同步问题】生产者消费者、读者写者、哲学家进餐、打瞌睡的理发师 | 信号量机制_哔哩哔哩_bilibili,这个视频第一部分讲解了生产者消费者的问题,由此可以对生产者,消费者模型有一个大概的认识。
因为信号量Linux0.11中没有实现,所以注释书籍中并没有信号量的说明,但是代码实现中涉及了文件操作和锁,推荐阅读其中:
- 第6章 块设备驱动程序(block driver),kernel/blk_drv/ll_rw_blk.c 中 lock_buffer 和 unlock_buffer 两个实现了加解锁操作。
- 第9章 文件系统(fs),文件操作相关。
其实信号量的理解可以归纳成一句话:信号量值可以理解为资源——资源大于0,就可以进入执行、等于0就要阻塞等待。对信号量值的加减需要加锁,Linux0.11中的锁是使用开关中断进行实现的。
实验的主要内容:
根据以上实验内容,本文主要是分成三个部分:
- 编写Ubuntu上运行的程序
- 内核实现信号量
- 在Linux0.11上运行信号量程序
编写Ubuntu上运行的程序
直接上代码,代码的主要内容就是:1个生产者进程不停产生0~500多的数字,4个消费者进程消费这些数字(在屏幕上打印)。
生产者和消费者进程使用文件进行通信,文件格式如下:
具体细节代码注释非常详细,就不多做说明了。
#include <unistd.h> /* 提供对 POSIX 操作系统 API 的访问,例如 fork(), pipe(), read(), write(), close() 等 */
#include <sys/types.h> /* 提供数据类型,例如 pid_t */
#include <stdio.h> /* 提供输入输出函数,例如 printf() */
#include <stdlib.h> /* 提供通用工具函数,例如 exit(), malloc(), free() */
#include <fcntl.h> /* 提供对文件控制选项的访问,例如 open(), lseek() */
#include <sys/stat.h> /* 提供对文件状态标志和模式的访问(但此代码中未直接使用) */
#include <semaphore.h> /* 提供对 POSIX 信号量的访问,例如 sem_open(), sem_wait(), sem_post(), sem_unlink() */
#include <sys/wait.h> /* 提供等待进程结束的函数,例如 wait(), waitpid() */
#define M 530 /* 打出数字总数 */
#define N 5 /* 消费者进程数 */
#define AVG M / N /* 每个消费者消费数字平均数 */
#define BUFSIZE 10 /* 缓冲区大小 */
int main()
{
sem_t *empty, *full, *mutex; /* 3个信号量 */
int fd; /* 共享缓冲区文件描述符 */
int i, j, k, child; /* 循环变量和子进程计数器 */
int data; /* 写入的数据 */
pid_t pid; /* 进程id */
int buf_out = 0; /* 记录上次从缓冲区读取位置 */
int buf_in = 0; /* 记录上次写入缓冲区位置 */
/* 1. 创建信号量 */
empty = sem_open("empty", O_CREAT | O_EXCL, 0644, BUFSIZE); /* 剩余空间信号量,初始化为 BUFSIZE */
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); /* 设置文件偏移量到缓冲区之后的位置(用于存储 buf_out) */
write(fd, (char *)&buf_out, sizeof(int)); /* 将上次读取位置buf_out存入buffer后的一个位置,每次从该位置获取上次位置,以便子进程之间通信 */
/* 2. 生产者进程 */
pid = fork();
if (pid < 0) /* 创建进程失败 */
{
perror("Fail to fork!\n");
return -1;
}
else if (pid == 0)
{
printf("I'm producer. pid = %d\n", getpid());
/* 生产多少个产品就循环几次 */
for (i = 0; i < M; i++)
{
sem_wait(empty); /* empty--,等待有空闲缓冲区, empty>0才能生产 */
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;
}
/* 3. 消费者进程,一共创建N个消费者进程 */
for (j = 0; j < N; j++)
{
pid = fork();
if (pid < 0) /* 创建进程失败 */
{
perror("Fail to fork!\n");
return -1;
}
else if (pid == 0)
{
/* 每个进程平均读取数字,即每个进程读取 M/N 个数字 */
for (k = 0; k < AVG; 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));
printf("%d: %d\n", getpid(), data); /* 打印消费者进程 ID和消费的数据 */
fflush(stdout); /* 确保数据送到终端 */
sem_post(mutex); /* mutex++,离开临界区 */
sem_post(empty); /* empty++,增加空闲缓冲区计数 */
}
printf("Child-%d: pid = %d end.\n", j, getpid());
return 0;
}
}
/* 4. 回收 */
/* 回收子进程资源 */
child = N + 1; /* 包括生产者在内的子进程总数 */
while (child--) /* 等待所有子进程结束 */
wait(NULL); /* 等待子进程结束 */
/* 释放信号量 */
sem_unlink("full");
sem_unlink("empty");
sem_unlink("mutex");
/* 释放文件资源 */
close(fd);
return 0;
}
在Ubuntu上编译这个程序和运行这个程序:
$ gcc -o pc pc.c -lpthread
$ ./pc > pc.txt
其中:-lpthread表示链接Ubuntu的线程库,线程库中包含了sem_xxx相关函数。
运行后查看输出文件pc.xt,内容如下图所示:
Ubuntu上编写成功后,因为Linux0.11并没有实现sem_xxx相关的函数,所以我们要自己编写相关的函数,并提供调用接口。
内核实现信号量
定义sem.h
新建头文件include/linux/sem.h,定义信号量的数据结构:
#ifndef _SEM_H
#define _SEM_H
#include <linux/sched.h>
#define SEM_TABLE_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 sem_table[SEM_TABLE_LEN]; /* 声明全局 */
#endif
实现sem.c
新建文件kernel/sem.c,实现信号量的相关函数:sys_sem_open、sys_sem_wait、sys_sem_post、sys_sem_unlink。
#include <linux/sem.h> // 信号量头文件,定义了信号量的数据结构。
#include <linux/sched.h> // 调度程序头文件,定义了任务结构 task_struct、第 1 个初始任务的数据。还有一些以宏的形式定义的有关描述符参数设置和获取的嵌入式汇编函数程序。
#include <unistd.h> // Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。如定义了__LIBRARY__,则还含系统调用号和内嵌汇编 syscall0()等。
#include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。
#include <linux/tty.h> // tty 头文件,定义了有关 tty_io,串行通信方面的参数、常数。
#include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。
#include <linux/fdreg.h> // 软驱头文件。含有软盘控制器参数的一些定义。
#include <asm/system.h> // 系统头文件。以宏的形式定义了许多有关设置或修改描述符/中断门等的嵌入式汇编子程序。
#include <asm/io.h> // io 头文件。以宏的嵌入汇编程序形式定义对 io 端口操作的函数。
#include <errno.h> // 错误号头文件。包含系统中各种出错号。(Linus 从 minix 中引进的)。包含了 EINVAL 错误码的定义。
sem_t sem_table[SEM_TABLE_LEN]; // 已创建的信号量表
int sem_len = 0; // 记录已创建的信号量个数
// 创建一个信号量。
// 参数:name表示信号量的名字,用于唯一标识信号量;value表示信号量的初始值。
sem_t *sys_sem_open(const char *name, unsigned int value)
{
char kernel_name[SEM_NAME_LEN]; // 用来存储应用程序传入的信号量名字
int exist_index = -1; // 用来存储信号量名字在信号量表中的索引值,-1表示不存在
int i = 0; // 循环索引变量
int name_len = 0; // 存储信号量名字的长度
sem_t *p = NULL; // 信号量结构体,最后需要返回的变量
// 1. 检测信号量名字长度
while (get_fs_byte(name + name_len) != '\0') // 需要通过get_fs_byte从用户空间获取数据
name_len++;
if (name_len > SEM_NAME_LEN) // 如果信号名字长度大于指定长度则退出
{
printk("The length of name is greater than \d!\n", SEM_NAME_LEN);
return NULL;
}
// 2. 将信号量名字从用户态复制到内核态,存入kernel_name
// 最后一个\0也是需要复制的,这个也是 i<=name_len,而不是 i<name_len 的原因。
for (i = 0; i <= name_len; i++)
kernel_name[i] = get_fs_byte(name + i);
// 3. 检查信号量名字是否已存在于信号量表sem_table中
for (i = 0; i < sem_len; i++)
{
// 先比较长度,再比较字符串,如果有一致的则exist_index置为对应的i值,并退出循环
if (strlen(sem_table[i].name) == name_len && !strcmp(kernel_name, sem_table[i].name))
{
exist_index = i;
break;
}
}
// 4. 若已存在,则直接使用已存在的信号量;不存在,则新建信号量。
if (exist_index >= 0) // 已存在
{
p = (sem_t *)(&sem_table[exist_index]);
printk("Sem %s is exist!\n", kernel_name);
}
else // 不存在
{
// 赋值信号量名字
strcpy(sem_table[sem_len].name, kernel_name);
// i = 0;
// for (i = 0; i < name_len; i++)
// {
// sem_table[cnt].name[i] = kernel_name[i];
// }
// 赋值信号值
sem_table[sem_len].value = value;
p = (sem_t *)(&sem_table[sem_len]);
printk("Creat new sem %s, value is %d !\n", kernel_name, value);
sem_len++;
}
return p;
}
// 该函数实现了信号量的P操作。
// 参数:sem指向要等待的信号量的指针。
int sys_sem_wait(sem_t *sem)
{
cli(); // 关中断,阻止调度,实现临界区保护
while (sem->value <= 0) // 使得所有小于0的进程阻塞(生产者没有缓冲可用empty<=0,则进入睡眠)
sleep_on(&sem->queue); // 睡眠当前进程
sem->value--;
sti(); // 开中断
return 0;
}
// 该函数实现了信号量的V操作。
// 参数:sem指向要释放的信号量的指针。
int sys_sem_post(sem_t *sem)
{
cli(); // 关中断,阻止调度,实现临界区保护
sem->value++; // 因为是释放操作,所以value要+1
// 若信号值大于0,则可以唤醒进程。
// 这里还可以增加一个判断:当前是否有进程在等待。有进程在等待才有必要唤醒进程。
// 但是因为 wake_up 函数已经加了判断了,所以这里不加也可以。
if (sem->value > 0)
wake_up(&sem->queue);
sti(); // 开中断
return 0;
}
// 删除信号量。
// 参数:name表示信号量的名字,用于唯一标识信号量。
int sys_sem_unlink(const char *name)
{
char kernel_name[SEM_NAME_LEN]; // 用来存储应用程序传入的信号量名字
int exist_index = -1; // 用来存储信号量名字在信号量表中的索引值,-1表示不存在
int i = 0; // 循环索引变量
int name_len = 0; // 存储信号量名字的长度
// 1. 检测信号量名字长度
while (get_fs_byte(name + name_len) != '\0') // 需要通过get_fs_byte从用户空间获取数据
name_len++;
if (name_len > SEM_NAME_LEN) // 如果信号名字长度大于指定长度则退出
{
printk("The length of name is greater than \d!\n", SEM_NAME_LEN);
return -EINVAL; // 置errno为EINVAL,返回“-1”。具体实现见include/unistd.h中的_syscalln宏。
}
// 2. 将信号量名字从用户态复制到内核态,存入kernel_name
// 最后一个\0也是需要复制的,这个也是 i<=name_len,而不是 i<name_len 的原因。
for (i = 0; i <= name_len; i++)
kernel_name[i] = get_fs_byte(name + i);
// 3. 检查信号量名字是否已存在于信号量表sem_table中
for (i = 0; i < sem_len; i++)
{
// 先比较长度,再比较字符串,如果有一致的则exist_index置为对应的i值,并退出循环
if (strlen(sem_table[i].name) == name_len && !strcmp(kernel_name, sem_table[i].name))
{
exist_index = i;
break;
}
}
// 4. 存在则删除信号量
if (exist_index >= 0)
{
int tmp;
// 从定位到的位置开始,后面的元素都往前移动一格,最后一个赋初值(避免数组溢出),实现删除。
for (tmp = exist_index; tmp < (sem_len - 1); tmp++)
{
if (tmp == (sem_len - 1)) // 最后1个只要赋初值即可。
{
sem_table[tmp].name[0] = '\0';
sem_table[tmp].value = 0;
sem_table[tmp].queue = NULL;
}
else
{
sem_table[tmp] = sem_table[tmp + 1];
}
}
sem_len--; // 信号量表长度减1
return exist_index;
}
return -ERROR; // 不存在则返回一个通用错误
}
新增4个系统调用
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
2)修改/kernel/system_call.s,需要修改总的系统调用数:
nr_system_calls = 76
3)修改/include/linux/sys.h,声明全局新增函数:
extern int sys_setregid();
/* 新增函数 */
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_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink
};
4)修改 linux-0.11/kernel/Makefile,添加sem.c编译规则:
# 添加 sem.o
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上运行信号量程序试试看。
在Linux0.11上运行信号量程序
修改信号量程序
前面Ubuntu上运行的信号量程序在Linux0.11不能直接运行,主要是创建信号量之前的代码需要进行调整。参考如下代码:
#define __LIBRARY__ /* 定义一个符号常量,见下行说明。unistd.h文件中会用到。 */
#include <unistd.h> /* 提供对 POSIX 操作系统 API 的访问,例如 fork(), pipe(), read(), write(), close() 等 */
#include <sys/types.h> /* 提供数据类型,例如 pid_t */
#include <fcntl.h> /* 提供对文件控制选项的访问,例如 open(), lseek() */
#include <stdio.h> /* 提供输入输出函数,例如 printf() */
#include <stdlib.h> /* 提供通用工具函数,例如 exit(), malloc(), free() */
#include <linux/sem.h> /* 提供对 POSIX 信号量的访问,例如 sem_open(), sem_wait(), sem_post(), sem_unlink() */
#define M 530 /* 打出数字总数 */
#define N 5 /* 消费者进程数 */
#define AVG M / N /* 每个消费者消费数字平均数 */
#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; /* 进程id */
int buf_out = 0; /* 记录上次从缓冲区读取位置 */
int buf_in = 0; /* 记录上次写入缓冲区位置 */
/* 1. 创建信号量 */
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); /* 设置文件偏移量到缓冲区之后的位置(用于存储 buf_out)。0~9是缓冲区,10记录上次是从哪个缓冲区获取数据的 */
write(fd, (char *)&buf_out, sizeof(int)); /* 将上次读取位置buf_out存入buffer后的一个位置,每次从该位置获取上次位置,以便子进程之间通信 */
/* 2. 生产者进程 */
pid = fork();
if (pid < 0) /* 创建进程失败 */
{
perror("Fail to fork!\n");
return -1;
}
else if (pid == 0)
{
printf("I'm producer. pid = %d\n", getpid());
/* 生产多少个产品就循环几次 */
for (i = 0; i < M; i++)
{
sem_wait(empty); /* empty--,等待有空闲缓冲区, empty>0才能生产 */
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;
}
/* 3. 消费者进程,一共创建N个消费者进程 */
for (j = 0; j < N; j++)
{
pid = fork();
if (pid < 0) /* 创建进程失败 */
{
perror("Fail to fork!\n");
return -1;
}
else if (pid == 0)
{
/* 每个进程平均读取数字,即每个进程读取 M/N 个数字 */
for (k = 0; k < AVG; 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));
printf("%d: %d\n", getpid(), data); /* 打印消费者进程 ID和消费的数据 */
fflush(stdout); /* 确保数据送到终端 */
sem_post(mutex); /* mutex++,离开临界区 */
sem_post(empty); /* empty++,增加空闲缓冲区计数 */
}
printf("Child-%d: pid = %d end.\n", j, getpid());
return 0;
}
}
/* 4. 回收 */
/* 回收子进程资源 */
child = N + 1; /* 包括生产者在内的子进程总数 */
while (child--) /* 等待所有子进程结束 */
wait(NULL); /* 等待子进程结束 */
/* 释放信号量 */
sem_unlink("full");
sem_unlink("empty");
sem_unlink("mutex");
/* 释放文件资源 */
close(fd);
return 0;
}
将文件拷贝到linux-0.11系统中
将已经修改的pc.c、unistd.h和sem.h文件拷贝到linux-0.11系统中,unistd.h和sem.h是用户程序运行也需要的头文件。
# 在 oslab 目录下
$ 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
运行信号量程序
编译及运行Linux-0.11:
$ cd ./linux-0.11
$ make all
$ ../run
编译及运行信号量程序:
$ gcc -o pc pc.c
$ ./pc > pc.txt
$ sync
在Ubuntu中挂载hdc,将linux0.11输出的pc.txt文件移动到Ubuntu中:
# 在 oslab 目录下
sudo ./mount-hdc
sudo cp ./hdc/usr/root/pc.txt ./
然后就可以在Ubuntu下查看pc.txt文件,一种可能的输出如下:
16: 0
16: 1
16: 2
16: 3
16: 4
16: 5
16: 6
16: 7
16: 8
16: 9
20: 10
20: 11
20: 12
20: 13
20: 14
20: 15
20: 16
20: 17
20: 18
20: 19
16: 20
16: 21
16: 22
16: 23
16: 24
16: 25
16: 26
16: 27
16: 28
16: 29
20: 30
20: 31
20: 32
20: 33
20: 34
20: 35
20: 36
20: 37
20: 38
20: 39
17: 40
17: 41
17: 42
...
20: 295
Child-4: pid = 20 end.
19: 296
...
16: 305
Child-0: pid = 16 end.
17: 306
...
17: 341
Child-1: pid = 17 end.
18: 342
18: 343
...
19: 507
Child-3: pid = 19 end.
18: 508
18: 509
18: 510
18: 511
18: 512
18: 513
18: 514
18: 515
18: 516
18: 517
18: 518
18: 519
18: 520
18: 521
18: 522
18: 523
18: 524
18: 525
I'm producer. pid = 15
Producer end.
18: 526
18: 527
18: 528
18: 529
Child-2: pid = 18 end.
结果分析:虽然0、1、2、3、4这些数字是按顺序输出的,前面的进程也会切换,但是切换的不太自然,比如前面几段进程16输出09、才切换到进程20,进程20输出1019、才切换到进程16,如果反复,似乎要一次性消费满10个才会切换。
初步想来原因可能是:生产者进程和消费者进程共享其中的mutex信号量,每次只能有1个进程进入运行,每个进程从获取到mutex信号量到最后释放,中间的运行过程占比较大。导致虽然有切换到其他进程,但是因为无法获取到mutex信号量,只能继续休眠等待,直到当前运行的进程消费完(生产完)后,进入睡眠才会切换到其他进程运行。
实验报告
完成实验后,在实验报告中回答如下问题:
1) 在 pc.c 中去掉所有与信号量有关的代码,再运行程序,执行效果有变化吗?为什么会这样?
答:执行效果会有变化,不会顺序输出了。因为没有锁的机制,进程交叉运行,导致各种输出可能性都有。
2) 实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:
Producer()
{
// 生产一个产品 item;
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
// 产品资源
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}
这样可行吗?如果可行,那么它和标准解法在执行效果上会有什么不同?如果不可行,那么它有什么问题使它不可行?
我感觉实验报告应该是弄错了,上面给的代码应该是标准解法,不合理的是下图这种。
答:根据上图可以回答不行,会产生死锁问题。例如:
- 执行生产者进程时,当mutex=1,empty=0时,申请以后变为:mutex=0,empty=0,P(Empty)这行产生阻塞;
- 此时调度到消费者进程运行,因为mutex=0,P(Mutex)这也发生阻塞了。
生产者和消费者都发生阻塞了,会产生死锁。
参考资料
从以下资料得到了不少帮助,特此表示感谢。
- 【经典进程同步问题】生产者消费者、读者写者、哲学家进餐、打瞌睡的理发师 | 信号量机制_哔哩哔哩_bilibili
- 【哈工大_操作系统实验】Lab6 信号量的实现和应用_哈工大操作系统 实验-优快云博客
- (浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验5-信号量的实现与应用_哈工大操作系统实验5 信号量的实现-优快云博客
完。