写在最前的总结
下面的实验内容是在完整做完实验时候补充的,这里先把踩过的坑记录一下。
调试总结
- 先在Ubuntu上模拟生产者—消费者问题。这个实验分为两大部分,一个是实现信号量,另一个是验证信号量。对于第二个,建议先在Ubuntu上模拟生产者—消费者问题,确保自己的代码思路是正确的,之后再稍微修改一下代码就可以在linux-0.11上运行了。这样做的好处是减少问题,因为直接在linux-0.11上运行,一旦出现错误,不好确定是信号量的问题还是模拟生产者–消费者代码的问题。
- 在linux-0.11上运行pc.c的时候如果出现问题,可以用打印日志的方式来查看运行过程,因为在linux-0.11上不能使用GDB来调试。另外,运行信息直接打印在屏幕上在Linux-0.11上不方便看,可以保存到文件中用Ubuntu来查看。具体做法是在运行程序的时候在后面加上
>output
,其中output中保存了原本打印在屏幕上的信息。如果编译生成的可执行文件名为pc,则:
./pc>output
这样就把打印信息保存到文件了,退出Linux-0.11之前要用命令sync来保存文件到磁盘。
在Ubuntu上编译的时候会报错,需要连接pthread库,编译命令如下:
gcc -o test test.c -lpthread
其中的test.c就是在Ubuntu上实现的生产者–消费者问题的模拟。
- 减少数据量。实验要求的是生产者生产500个数据,在调试的时候改称100个就可以,在调试通过之后改回500个也一样能跑。实验数据太多,不方便调试。
代码注释方式和变量定义位置
在Linux-0.11中只支持/*…*/这样的注释方式,使用双斜杠注释会报错,这应该是和gcc的版本有关系。另外,在早期C标准中,函数内部的变量定义只能定义在函数最开头位置,否则会报错(也可能是gcc版本的原因)。
文件拷贝
将编写的pc.c、sem.h和unistd.h文件拷贝到Linux-0.11中,pc.c是编写的测试代码;sen.h是信号量实现的头文件,因为在ppc.c中会用到;unistd.h在实验过程修改过,也需要拷贝一份到Linux-0.11的用户空间中,否则会报错或者得不到正确的实验现象。
具体拷贝过程如下:
- 挂载。在lab5实现目录下,执行以下命令进行挂载:
sudo ./mount-hdc
这样就将Linux-0.11挂载到./hdc/目录下了。
2.拷贝。unistd.h文件的拷贝位置和原来的相同,直接覆盖原来的文件。sem.h文件放在/usr/include/linux/路径下,pc.c放在/usr/root/目录下。
阻塞后没有调度
在sys_sem_wait()函数中,如果信号量的值为0,则把当前进程添加到该信号量的阻塞队列中,然后进行调度,这里很好理解,只是写代码的时候忘了,导致程序卡死。
进程间通信问题
这里涉及到多个消费者进程,因此存在通信问题,即某个消费者进程要从共享缓冲区中读取数据时应该从哪个位置开始读呢?注意,这里不能使用全局变量的方式来标记读取位置,因为一个进程肯定是不能直接访问另一个进程的数据的。这里的解决办法是将这个标记位置写道共享缓冲区的最后,这样每个进程在读取共享缓冲区时先要从共享缓冲区的最后位置读取这个标记位置,然后再读取数据,读取完后再将下一个位置写回到该位置中,这样就能保证多个进程能正确读取数据。
内核态下不能直接使用用户态的函数
在创建信号量的函数里使用了字符串拷贝函数strcpy(),虽然没有报错,但是没有执行成功,搜了一下才知道在内核空间不能使用用户空间里的函数,因此可以自己写一个拷贝函数就可以了。
对队列的操作 pop 指向指针的指针
信号量的实现中会创建一个阻塞队列,用于保存阻塞在该信号量上的进程。原来的代码实现如下:
int sys_sem_post(sem_t* sem)
{
struct task_struct *p;
......
popQueue(&(sem->wait_queue),p);
......
}
int popQueue(waitQueue_t* queue,struct task_struct *p)
{
if(getQueueLength(queue) == 0) return -1;
p = queue->wait_tasks[queue->front];
queue->front = (queue->front+1) % (SEM_WAIT_MAX_NUM + 1);
return 0;
}
popQueue函数中传入了一个指向struct task_struct的指针,然后在函数内部给这个指针赋值。这个涉及到的是C语言传参问题!如果在函数内部要改变传入的参数,那必须传指针而不能传值!(c语言有传指针和传值两种传参方式)上面的实现用的是传值的方式,显然不可能在函数内部改变p的指向。修改之后的代码如下:
int sys_sem_post(sem_t* sem)
{
struct task_struct *p;
......
popQueue(&(sem->wait_queue),&p);
......
}
int popQueue(waitQueue_t* queue,struct task_struct **p)
{
if(getQueueLength(queue) == 0) return -1;
*p = queue->wait_tasks[queue->front];
queue->front = (queue->front+1) % (SEM_WAIT_MAX_NUM + 1);
return 0;
}
fork的使用
在实验中要创建1个生产者进程和5个消费者进程,进程的创建是通过fork()函数来实现的,fork()函数的使用可参照Linux中fork()函数的使用.
文件读写
实验中读写共享缓冲区其实就是读写文件,文件的读写可参照Linux 文件读写的简单介绍.
实验内容
本实验的目标有两个。
第一,在linux-0.11(没有实现信号量)上实现信号量有关的系统调用:
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是信号量的初始值,仅当创建信号量时,该参数才有效,其余情况下被忽略。在创建或打开成功时,返回值是该信号量的唯一标识。sem_wait()就是信号量的P操作,sem_post就是信号量的V操作。sem_unlink()的功能是删除名为name的信号量。
第二,利用上面实现的信号量系统调用,编写一个应用程序pc.c来模拟经典的生产者—消费者之间的同步。在这个程序中,要建立1个生产者进程和5个消费者进程,用文件建立一个共享缓冲区,生产者进程依次向这个缓冲区里写入正数0,1,2,3,…,499;每个消费者进程从缓存区中读取100个数,每读取1个数据就打印到标准输出上;缓存区文件最多只能保存10个数。
最终输出的效果应该是下面的样子,其中10:0中的10就是消费者进程的PID号,“:”后面的0就是从文件缓冲区中取出来的数据。不难看出,不论具体是哪个消费者进程取出了0~499中的哪个数,最终输出的结果都应该保持0,1,2,3,…,499这样的顺序。
10:0
10:1
10:2
10:3
10:4
11:5
......
11:498
11:499
实验过程
系统调用的添加在实验2的时候已经做过了,按照那个过程添加本实验的四个系统调用。
1.添加系统调用的编号
打开include/unistd.h
文件,在如下图所示位置添加系统调用编号:
2.添加IDT(中断描述符表)
打开include/linux/sys.h
文件,在文件中的sys_call_table[]的数组中添加sys_sem_open、sys_sem_wait、sys_sem_post和sys_sem_unlink,注意这里的前后顺序要和之前的系统调用编号的前后关系对应起来。同时,将sys_sem_open()、sys_sem_wait()、sys_sem_post()和sys_sem_unlink()声明全局函数。
3.修改系统调用数量
打开kernel/system_call.s
文件,将系统调用的数量由原来的72改为76:
4. 实现系统调用函数
在kernel/
目录下新建一个sem.c文件,在include/
目录下新建一个sem.h文件,用来保存新添加的4个系统调用函数。
sem.h文件
#ifndef __SEM_H
#define __SEM_H
#include <linux/sched.h>
#define SEM_NAME_MAX_LEN 32
#define SEM_WAIT_MAX_NUM 32
/* 阻塞队列 */
typedef struct{
struct task_struct *wait_tasks[SEM_WAIT_MAX_NUM+1];
int front;
int rear;
}waitQueue_t;
/*信号量*/
typedef struct{
char name[SEM_NAME_MAX_LEN];
unsigned int value;
int valid;
waitQueue_t wait_queue;
}sem_t;
#endif
因为会有队列阻塞在某个信号量上,因此这里需要定义一个队列,用来保存阻塞进程。
sem.c文件涉及的内容较多,这里分开介绍。
阻塞队列接口
void initQueue(waitQueue_t* queue)
{
queue->front = 0;
queue->rear = 0;
}
int getQueueLength(waitQueue_t* queue)
{
return (queue->rear - queue->front + SEM_WAIT_MAX_NUM + 1) % (SEM_WAIT_MAX_NUM + 1);
}
int pushQueue(waitQueue_t* queue,struct task_struct *p)
{
if(getQueueLength(queue) == SEM_WAIT_MAX_NUM)
return -1;
queue->wait_tasks[queue->rear] = p;
queue->rear = (queue->rear + 1) % (SEM_WAIT_MAX_NUM + 1);
return 0;
}
int popQueue(waitQueue_t* queue,struct task_struct **p)
{
if(getQueueLength(queue) == 0) return -1;
*p = queue->wait_tasks[queue->front];
queue->front = (queue->front+1) % (SEM_WAIT_MAX_NUM + 1);
return 0;
}
int isFull(waitQueue_t* queue)
{
return (queue->rear+1) % SEM_WAIT_MAX_NUM == queue->front;
}
上面的函数是队列的基本操作函数,如果对队列的操作不熟悉可以参考这篇文章:03_数据结构:栈与队列。
字符串拷贝和比较
int kstrcpy(char* des,char* src)
{
int index = 0;
while(src[index] != '\0'){
des[index] = src[index];
index++;
}
des[index] = '\0';
return index;
}
int kstrcmp(char* str1,char* str2)
{
int index = 0;
while(str1[index] != '\0' && str2[index] != '\0'){
if(str1[index] != str2[index])
return -1;
index++;
}
if(str1[index] != '\0' || str2[index] != '\0') return -1;
return 0;
}
因为在内核空间不能直接使用用户空间的函数,因此这里需要单独实现拷贝和比较函数。
sys_sem_open
//sys_sem_open()
sem_t* sys_sem_open(const char* name,unsigned int value)
{
int i;
char buf[SEM_MAX_NUM];
//从用户空间获取名字
for(i = 0; i < SEM_MAX_NUM; i++){
buf[i] = get_fs_byte(name+i);
if(buf[i] == '\0') break;
}
//判断name长度
if(i >= SEM_MAX_NUM){
printk("The semaphore's name is too long and fail to create the semaphore!\r\n");
return NULL;
}
//判断该信号量是否已经创建
for(i = 0; i < SEM_MAX_NUM; i++){
if(semaphores[i].valid == 1 && strcmp(buf,semaphores[i].name) == 0)
return &semaphores[i]; //存在则直接返回
}
//创建信号量
for(i = 0; i < SEM_MAX_NUM; i++){
if(semaphores[i].valid != 1) { //找一个无效信号量的位置
kstrcpy(semaphores[i].name,buf); //名字赋值
semaphores[i].valid = 1; //该信号量有效
semaphores[i].value = value; //赋值
initQueue(&(semaphores[i].wait_queue));
return (sem_t*)&semaphores[i];
}
}
return NULL;
}
其中的get_fs_byte()函数的作用是从用户空间addr地址处取出一个字节char,数组semaphores[]是用来存储信号量的一个数组,也就是说,系统支持的信号量的数量是有限的。sys_sem_open()的作用是创建一个信号量,如果该信号量存在就直接返回信号量地址。具体创建过程是在semaphores[]数组中找一个空位置,然后设置好信号量名称、数值、信号量状态以及初始化阻塞队列,最终返回该信号量的地址。
sys_sem_wait
//sys_sem_wait()
int sys_sem_wait(sem_t* sem)
{
struct task_struct *p;
if(sem == NULL) return -1;
cli(); //关中断,保护临界区
while(sem->value == 0){ //注意这里要使用while,而不能使用if
/*将当前进程加入到阻塞队列中*/
if(isFull(&(sem->wait_queue)))
{
printk("error!The semaphore wait queue is full!");
return -1;
}
//添加到阻塞队列
current->state = TASK_UNINTERRUPTIBLE; //阻塞当前进程
pushQueue(&(sem->wait_queue),current);
schedule();
}
sem->value --;
sti(); //开中断
return sem->value;
}
这里临界段的保护采用关中断的方式,如果信号量的值为0,则将该进程加入该信号量的阻塞队列中,然后阻塞该队列,并重新进行调度。特别注意,判断信号量数值是否为0那里要用while()循环,而不能用if判断,因为如果有多个进程阻塞在一个信号量上,当唤醒任务时的做法时唤醒所有任务,具体哪个进程能获得信号量由调度函数schedule()来决定,因为进程存在优先级,一般会先唤醒高优先级的任务。所以while()循环的意义是,当该进程被唤醒之后要再次判断能否获得信号量,如果不能则再次将自己阻塞。
sys_sem_post
//sys_sem_post
int sys_sem_post(sem_t* sem)
{
struct task_struct *p;
if(sem == NULL) return -1;
cli(); //关中断,保护临界区
sem->value++;
/*唤醒所有阻塞在该信号量上的进程*/
while(getQueueLength(&(sem->wait_queue)) > 0){
popQueue(&(sem->wait_queue),&p);
p->state = TASK_RUNNING;
}
sti(); //开中断
return sem->value;
}
这个函数将信号量的值加1,然后唤醒所有阻塞在该信号量上的进程,这里就对应于sys_sem_post()函数中的while()循环,这里一次唤醒所有队列,具体哪个任务能获得信号量由调度函数决定。
sys_sem_unlink
//sys_sem_unlink
int sys_sem_unlink(const char* name)
{
int i;
char buf[SEM_MAX_NUM];
//从用户空间获取名字
for(i = 0; i < SEM_MAX_NUM; i++){
buf[i] = get_fs_byte(name+i);
if(buf[i] == '\0') break;
}
//判断name长度
if(i >= SEM_MAX_NUM){
printk("The semaphore's name is too long and fail to delete the semaphore!\r\n");
return -1;
}
//查找该信号量
for(i = 0; i < SEM_MAX_NUM; i++){
if(semaphores[i].valid == 1 && kstrcmp(buf,semaphores[i].name) == 0){
semaphores[i].valid = 0;
break;
}
}
if(i == SEM_MAX_NUM){
printk("The semaphore does not exist\r\n");
return -1;
}
return 0;
}
这个函数的作用是在semaphores[]数组中找到要删除的信号量,然后改变该信号量的状态,这样就释放资源也就删除了。
5.修改Makefile(kernel/目录下)
修改两地地方:
第一处:
第二处:
6.编写用户程序pc.c
建议先在Ubuntu上实现生产者–消费者进程的模拟,在对函数稍作修改再在Linux-0.11上运行,如果直接在Linux-0.11上运行,出现错误不好确定是pc.c出现的问题还是信号量的实现出现的问题。
Ubuntu上的代码如下:
#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define TOTAL_ITEM_NUM 100 /*生产者生产的物品总数*/
#define CONSUMER_NUM 5 /*消费者的数量*/
#define BUF_SIZE 10 /*共享缓冲区的数量*/
int fd;
sem_t* empty;
sem_t* full;
sem_t* mutex;
void consumer();
void producer();
int main()
{
int buf_out = 0;
pid_t pid;
mutex = sem_open("mutex", O_CREAT | O_EXCL, 0644, 1);
full = sem_open("full", O_CREAT | O_EXCL, 0644, 0);
empty = sem_open("empty", O_CREAT | O_EXCL, 0644, BUF_SIZE);
fd = open("file.txt",O_RDWR | O_CREAT,0644);
lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
write(fd,(char*)&buf_out,sizeof(int));
if(!fork()){
producer();
exit(0);
}
for(int i = 0; i < CONSUMER_NUM; i++){
if(!fork()){
consumer();
exit(0);
}
}
while(pid = wait(NULL),pid != -1){
printf("pid %d process terminated!\r\n",(int)pid);
}
close(fd);
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
return 0;
}
void consumer()
{
int i;
int item;
int buf_out = 0;
for(i = 0; i < TOTAL_ITEM_NUM / CONSUMER_NUM; i++)
{
sem_wait(full);
sem_wait(mutex);
//读出buf_out
lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
read(fd,(char*)&buf_out,sizeof(buf_out));
lseek(fd,buf_out*sizeof(int),SEEK_SET);
read(fd,(char*)&item,sizeof(item));
printf("pid %d : consume item %d\r\n",getpid(),item);
fflush(stdout);
//写会buf_out
buf_out = (buf_out + 1) % BUF_SIZE;
lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
write(fd,(char*)&buf_out,sizeof(buf_out));
sem_post(mutex);
sem_post(empty);
}
}
void producer()
{
int i;
int buf_in = 0;
for(i = 0; i < TOTAL_ITEM_NUM; i++){
sem_wait(empty);
sem_wait(mutex);
lseek(fd,buf_in*sizeof(int),SEEK_SET);
write(fd,(char*)&i,sizeof(i));
buf_in = (buf_in + 1) % BUF_SIZE;
//printf("pid %d : produce %d\r\n",getpid(),i);
//fflush(stdout);
sem_post(mutex);
sem_post(full);
}
}
编译命令:
gcc -o pc pc_Ubuntu.c -lpthread
其中的pc_Ubuntu.c就是Ubuntu上的pc.c,编译时要链接pthread库,否则会报错。
编译后的运行结果(部分)如下:
在上面的代码上修改一下就可以在Linux-0.11上运行了,代码实现如下:
#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
static inline _syscall2(sem_t*,sem_open,const char*,name,unsigned int,value) /*sem_open*/
static inline _syscall1(int,sem_wait,sem_t*,sem) /*sem_wait*/
static inline _syscall1(int,sem_post,sem_t*,sem) /*sem_post*/
static inline _syscall1(int,sem_unlink,const char*,name) /*sem_unlink*/
#define TOTAL_ITEM_NUM 500 /*生产者生产的物品总数*/
#define CONSUMER_NUM 5 /*消费者的数量*/
#define BUF_SIZE 10 /*共享缓冲区的数量*/
int fd;
sem_t* empty;
sem_t* full;
sem_t* mutex;
void consumer();
void producer();
int main()
{
int i;
int buf_out = 0;
pid_t pid;
mutex = sem_open("mutex", 1);
/*sem_wait(mutex);
return 0;*/
full = sem_open("full", 0);
empty = sem_open("empty", BUF_SIZE);
fd = open("file.txt",O_RDWR | O_CREAT,0644);
lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
write(fd,(char*)&buf_out,sizeof(int));
if(!fork()){
/*printf("producer id created,pid = %d\r\n",getpid());*/
producer();
exit(0);
}
for(i = 0; i < CONSUMER_NUM; i++){
if(!fork()){
/*printf("consumer %d is created,pid = %d\r\n",i,getpid());*/
consumer();
exit(0);
}
}
while(pid = wait(NULL),pid != -1){
printf("pid %d process terminated!\r\n",(int)pid);
}
close(fd);
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
return 0;
}
void consumer()
{
int i;
int item;
int buf_out = 0;
for(i = 0; i < TOTAL_ITEM_NUM / CONSUMER_NUM; i++)
{
sem_wait(full);
sem_wait(mutex);
/*读出buf_out*/
lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
read(fd,(char*)&buf_out,sizeof(buf_out));
lseek(fd,buf_out*sizeof(int),SEEK_SET);
read(fd,(char*)&item,sizeof(item));
printf("pid %d : consume item %d\r\n",getpid(),item);
fflush(stdout);
/*写会buf_out*/
buf_out = (buf_out + 1) % BUF_SIZE;
lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
write(fd,(char*)&buf_out,sizeof(buf_out));
sem_post(mutex);
sem_post(empty);
}
}
void producer()
{
int i;
int buf_in = 0;
for(i = 0; i < TOTAL_ITEM_NUM; i++){
sem_wait(empty);
sem_wait(mutex);
lseek(fd,buf_in*sizeof(int),SEEK_SET);
write(fd,(char*)&i,sizeof(i));
buf_in = (buf_in + 1) % BUF_SIZE;
/*printf("pid %d : produce %d\r\n",getpid(),i);
fflush(stdout);*/
sem_post(mutex);
sem_post(full);
}
}
7.Linux-011上的测试
文件拷贝
将编写的pc.c、sem.h和unistd.h文件拷贝到Linux-0.11中,pc.c是编写的测试代码;sen.h是信号量实现的头文件,因为在ppc.c中会用到;unistd.h在实验过程修改过,也需要拷贝一份到Linux-0.11的用户空间中,否则会报错或者得不到正确的实验现象。
具体拷贝过程如下:
- 挂载。在lab5实现目录下,执行以下命令进行挂载:
sudo ./mount-hdc
这样就将Linux-0.11挂载到./hdc/目录下了。
2.拷贝。unistd.h文件的拷贝位置和原来的相同,直接覆盖原来的文件。sem.h文件放在/usr/include/linux/路径下,pc.c放在/usr/root/目录下。
编译
编译命令如下,注意不需要上面提高的链接库:
gcc -o pc_0.11.c pc
其中的pc_0.11.c就是Linux-0.11上的pc.c,由于报错提示文件名太长,由原来的pc_Linux-0.11.c改为pc_0.11.c。编译后就得到了pc可执行文件。
运行
由于打印信息直接显示在屏幕上会显示不完整,这里将显示信息保存到文件中,再用Ubuntu打开查看。
./pc>output
其中的>output
就表示保存到output文件,执行完成之后一定要执行sync命令将output保存到磁盘中再退出。退出之后,挂载,再打开output文件查看,命令如下:
sudo ./mount-hdc #挂载(lab5路径下)
sudo vim ./hdc/usr/root/output # 打开文件,如果没安装vim,可以替换为vi
打开文件的内容如下:
可以减少数据量,比如产生100个数,分析结果是否正确,修改文件中的宏定义即可修改数据量,分析发现结果是正确的。(当然,这是调试完成之后写下的,踩的坑和调试过程都记录在开头了)
终于是把这个实验认真地做完了,当然这里记录的还不太完整,后面有空再补充,这个实验不知道花了多长时间了,中间也是各种原因断断续续的,好在最终也是做出来了,也学会了很多东西,包括文件的简单读写,信号量的原理和使用,调试能力也得到了提升。总之,认真做试验收获很大。