写在最前的总结
下面的实验内容是在完整做完实验时候补充的,这里先把踩过的坑记录一下。
调试总结
- 先在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);