非常难的实验, 耗时将近一个半星期才做好. 提供了一种不同于其他博主的思路. 代码经过测试完全正确. 大家可以自行调试 (如果大家有问题可以在评论区留言, 我会尽量解答:-) )
首先是 kernel/sem.c 文件. 这里必须要先说一件事, linux0.11 的链式唤醒并不能同时唤醒所有该等待队列上的进程!!!(可在csdn搜索关键词查阅). 所以以 sleep() 实现的 调度实际上并不公平, 因为总是先唤醒最后加进来的进程. 所以这里我改用了以数组的形式存放等待队列进程. 这样子可以做到当要唤醒等待队列的进程时, 可以公平的全部唤醒, 由内核调度
信号量的结构:
typedef struct semaphore {
char name[MAX_CHAR_NUM];
unsigned int value;
struct task_struct * task[MAX_TASK]; //将等待队列改为由数组存放
}sem_t;
Note!!! 如果在调试时, 你设的子进程数量超过 MAX_TASK, 请增大 MAX_TASK ( 本来应该用链表的形式存放的, 但我不知道linux0.11有没有能申请内存的方法:-( )
于是引出 sem_wait 和 sem_post 的实现:
int sys_sem_wait(sem_t *sem){
int i;
cli();
while(sem->value == 0){
/* sleep_on(&sem->queue); */
for(i = 0; i < MAX_TASK; i++){
if(!sem->task[i]) break;
}
sem->task[i] = current; //将该进程加入等待队列尾部, 并将下一个位置置为NULL
sem->task[i+1] = NULL;
current->state = TASK_UNINTERRUPTIBLE; //睡眠该进程
schedule();
}
sem->value--;
sti();
return 0;
}
int sys_sem_post(sem_t *sem){
int i = 0;
cli();
sem->value++;
/* wake_up(&sem->queue); */
while(sem->task[i]) sem->task[i++]->state = TASK_RUNNING; //唤醒所有等待队列进程
sem->task[0] = NULL; //将等待队列置为空, 即首部置为空
sti();
return 0;
}
kernel/sem.c 完整代码:
#include <linux/sched.h>
#include <asm/segment.h>
#define MAX_SEM_NUM 20
#define MAX_CHAR_NUM 20
#define MAX_TASK 30
#define isused 1
#define noused 0
#define cli() __asm__ ("cli"::)
#define sti() __asm__ ("sti"::)
typedef struct semaphore {
char name[MAX_CHAR_NUM];
unsigned int value;
struct task_struct * task[MAX_TASK];
}sem_t;
sem_t sems[MAX_SEM_NUM] = {};
int sem_used[MAX_SEM_NUM] = {0};
/*
Copy the f_name in user address space to name in kernel address space.
*/
void get_name(const char* f_name, char* name){
int i = 0;
char c;
do{
c = get_fs_byte(f_name + i);
name[i] = c;
i++;
}while(c != '\0');
}
/*
Find the sem by the given name. Return the index in the array of sems.
*/
int find_sem_by_name(const char* f_name){
int i, j;
char name[MAX_CHAR_NUM];
get_name(f_name, name);
for(i = 0; i < MAX_SEM_NUM; i++){
if(sem_used[i]){
j = 0;
while((name[j] == sems[i].name[j]) && (name[j] != '\0') && (sems[i].name[j] != '\0'))
j++;
if((name[j] == '\0') && (sems[i].name[j] == '\0'))
return i;
}
}
return -1;
}
/*
Open a sem by the given name. If it is existed, just return the address of
the sem. If it is not existed, open a new sem and initialize it with the
given value, return the address of the sem.
*/
sem_t* sys_sem_open(const char* f_name, unsigned int value){
int i, j;
char name[MAX_CHAR_NUM];
if(!name) return NULL;
i = find_sem_by_name(f_name);
get_name(f_name, name);
if(i >= 0){
return &sems[i];
}else{
for(i = 0; i < MAX_SEM_NUM; i++){
if(!sem_used[i]){
j = 0;
while(name[j] != '\0'){
sems[i].name[j] = name[j];
j++;
}
sems[i].name[j] = '\0';
sems[i].value = value;
sems[i].task[0] = NULL;
sem_used[i] = isused;
return &sems[i];
}
}
return NULL;
}
}
/*
The P operation. if (sem->value == 0), add the current task to the waiting
queue, sleep the current task and schedule. Whenever return to this task,
it is necessary to check whether the sem->value is 0. If it is 0, sleep
the current task again. The return value is negligible.
*/
int sys_sem_wait(sem_t *sem){
int i;
cli();
while(sem->value == 0){
/* sleep_on(&sem->queue); */
for(i = 0; i < MAX_TASK; i++){
if(!sem->task[i]) break;
}
sem->task[i] = current;
sem->task[i+1] = NULL;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
}
sem->value--;
sti();
return 0;
}
/*
The V operation. Wake up all tasks which wait on the waiting queue.The
return value is negligible.
*/
int sys_sem_post(sem_t *sem){
int i = 0;
cli();
sem->value++;
/* wake_up(&sem->queue); */
while(sem->task[i]) sem->task[i++]->state = TASK_RUNNING;
sem->task[0] = NULL;
sti();
return 0;
}
/*
Delete the sem by the given name. If sucessed, return 0. Else return -1
*/
int sys_sem_unlink(const char* f_name){
int i;
i = find_sem_by_name(f_name);
if(i >= 0){
sem_used[i] = noused;
return 0;
}
return -1;
}
然后是 pc.c 的实现. 这个实现的方法各有不同, 这里我提供一种比较容易懂的方法(主流方法的双文件指针确实巧妙). 我用一个文件(var/num)来存放指示消费者进程读取指针, 最后的输出写入文件(var/write). 文件(var/buffer)用来做缓冲区的作用. 实现较为繁琐, 但容易理解
我在写入到最终输出文件时把生产者生产的生产顺序也写入进文件了, 读者可以自行删除相关代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/*#include <string.h>*/ //string.h可能会造成bug
#include <stdlib.h>
#include <sys/wait.h>
#define __LIBRARY__
#include <unistd.h>
#define BUFFER_SIZE 10 //缓冲区大小
#define ITEM_SIZE 500 //测试的项目大小
#define TASK_NUM 5 //子进程数量(Note!!!请不要大于信号量中的等待数组大小. 否则请自行修改)
_syscall2(long*, sem_open, const char*, name, unsigned int, value);
_syscall1(int, sem_wait, long*, sem);
_syscall1(int, sem_post, long*, sem);
_syscall1(int, sem_unlink, const char*, name);
int main(){
int i, status, f_pid, wpid;
int fd_buffer, fd_write, fd_num;
long *empty, *occupy, *mutex;
int temp = 0;
int temp_1 = 0;
int temp_2 = 0;
char* str1 = ": "; //用于写入文件的字符串, 不重要
char* str2 = "\n";
char* str3 = "(0)";
char string[20] = {0};
f_pid = getpid(); //记录父进程pid
fd_buffer = open("/var/buffer", O_RDWR|O_CREAT|O_TRUNC, 0666); //缓冲区
fd_write = open("/var/write", O_RDWR|O_CREAT|O_TRUNC, 0666); //输出文件
fd_num = open("/var/num", O_RDWR|O_CREAT|O_TRUNC, 0666); //指示指针
write(fd_num, &temp, sizeof(int)); //首先写入0, 表示目前没有产品, 缓冲区为空
empty = sem_open("empty", BUFFER_SIZE); //打开三个信号量
occupy = sem_open("occupy", 0);
mutex = sem_open("mutex", 1);
if(!fork()){
for(i = 0; i < ITEM_SIZE; i++){
sem_wait(empty);
sem_wait(mutex);
lseek(fd_buffer, 0, SEEK_END); //向缓冲区写入生产者产品
write(fd_buffer, &i, sizeof(int));
write(fd_write, str3, strlen(str3));
sprintf(string, "%d", i);
write(fd_write, string, strlen(string));
write(fd_write, str2, strlen(str2));
if(i == (ITEM_SIZE - 1)){ //如果已到达产品上限, 写入-1表示结束
temp = -1;
write(fd_buffer, &temp, sizeof(int));
}
sem_post(mutex);
sem_post(occupy);
}
}else{
for(i = 0; i < TASK_NUM; i++){
status = fork();
if(status == 0 || status == -1)
break;
}
if(status == 0){
empty = sem_open("empty", BUFFER_SIZE);
occupy = sem_open("occupy", 0);
mutex = sem_open("mutex", 1);
do{
sem_wait(occupy);
sem_wait(mutex);
lseek(fd_num, 0, SEEK_SET); // 读取指示的指针, 并将其乘4, 用以定位要读取
//的数据
read(fd_num, &temp, sizeof(int));
temp_1 = temp + 1;
temp = temp * sizeof(int);
lseek(fd_buffer, temp, SEEK_SET); //消费者进程读取产品
read(fd_buffer, &temp, sizeof(int));
if(temp >= 0){ //不为-1, 则写入输出文件
temp_2 = getpid();
sprintf(string, "%d", temp_2);
write(fd_write, string, strlen(string));
write(fd_write, str1, strlen(str1));
sprintf(string, "%d", temp);
write(fd_write, string, strlen(string));
write(fd_write, str2, strlen(str2));
lseek(fd_num, 0, SEEK_SET);
write(fd_num, &temp_1, sizeof(int));
read(fd_buffer, &temp, sizeof(int)); /* for the last -1*/
//再往下读一个, 用来判断是否要退出
}
sem_post(mutex);
sem_post(empty);
}while(temp >= 0);
sem_post(occupy); //这一部很关键!!!后面讲解
}
}
if(f_pid == getpid()){
while ((wpid = wait(NULL)) > 0); //父进程等待所有子进程退出
close(fd_buffer);
close(fd_write);
close(fd_num);
sem_unlink("empty");
sem_unlink("occupy");
sem_unlink("mutex");
}
}
这里还有要说一个非常关键的点. 那就是下面多出来的这句:
}
sem_post(mutex);
sem_post(empty);
}while(temp >= 0);
sem_post(occupy); //关键
}
}
因为: 当第一个读到-1的子进程退出后, 信号量occupy已经为0了!!!这就导致后面其他进程被唤醒后, 在判断条件 while( sem->value == 0) 后, 再次休眠.所以就会造成只有第一个读到-1的子进程退出, 其他子进程全部无法退出
所以这里, 每当一个消费者进程退出, 都要执行一次 sem_post(occupy), 让 occupy+1 = 1, 使得下一个消费者进程能够退出循环 while( sem->value == 0) { sleep(); }; 于是该消费者进程读取buffer, 发现为-1, 退出, 并再次执行 sem_post, 于是后面所有消费者都能退出. 这里是其他答案没有讲到的点
最后是完成这个实验所要修改的所有文件汇总, 总共六个文件:
1. sem.c, 放在 linux0.11/kernel. 代码在上面, 复制粘贴即可
2. pc.c, 在 mount hdc 之后, 放在 hdc/usr/root 下. 代码在上面, 复制粘贴即可.
3.Makefile, 放在 linux0.11/kernel/ 下.
在Makefile中修改两处
4.sys.h, 放在 linux0.11/include/linux/ 下.
增加四个函数原型, 并在函数指针数组中增加四个函数
5.unistd.h, 放在 linux0.11/include/ 下, 以及 hdc/usr/include/ 目录下!
增加四个系统调用号
6.system_call.s, 放在 linux0.11/kernel/ 下
增加最大系统调用号至76
最后, 在虚拟机中编译 pc.c 文件. 运行后打开 hdc/var/write 即可看到实验结果. 感谢你看到这里:-)
(别忘了退出虚拟机前用命令 sync 将数据写入磁盘!否则可能无法得到结果)
(0): xxx 表示的是生产者生产产品的顺序