操作系统真象还原实验记录之实验十八:环形缓冲区
1.实验代码
1.1 ioqueue.h
#ifndef __DEVICE_IOQUEUE_H
#define __DEVICE_IOQUEUE_H
#include "stdint.h"
#include "thread.h"
#include "sync.h"
#define bufsize 64
/* 环形队列 */
struct ioqueue {
// 生产者消费者问题
struct lock lock;
/* 生产者,缓冲区不满时就继续往里面放数据,
* 否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠。*/
struct task_struct* producer;
/* 消费者,缓冲区不空时就继续从往里面拿数据,
* 否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠。*/
struct task_struct* consumer;
char buf[bufsize]; // 缓冲区大小
int32_t head; // 队首,数据往队首处写入
int32_t tail; // 队尾,数据从队尾处读出
};
void ioqueue_init(struct ioqueue* ioq);
bool ioq_full(struct ioqueue* ioq);
char ioq_getchar(struct ioqueue* ioq);
void ioq_putchar(struct ioqueue* ioq, char byte);
uint32_t ioq_length(struct ioqueue* ioq);
#endif
producer和consumer都是记录的是running_thread当前线程PCB地址
lock意味着一个用于互斥的信号量
1.2 ioqueue.c
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock); // 初始化io队列的锁
ioq->producer = ioq->consumer = NULL; // 生产者和消费者置空
ioq->head = ioq->tail = 0; // 队列的首尾指针指向缓冲区数组第0个位置
}
/* 返回pos在缓冲区中的下一个位置值 */
static int32_t next_pos(int32_t pos) {
return (pos + 1) % bufsize;
}
/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {
ASSERT(intr_get_status() == INTR_OFF);
return next_pos(ioq->head) == ioq->tail;
}
/* 判断队列是否已空 */
static bool ioq_empty(struct ioqueue* ioq) {
ASSERT(intr_get_status() == INTR_OFF);
return ioq->head == ioq->tail;
}
/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {
ASSERT(intr_get_status() == INTR_OFF);
/* 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,
* 目的是将来生产者往缓冲区里装商品后,生产者知道唤醒哪个消费者,
* 也就是唤醒当前线程自己*/
while (ioq_empty(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
char byte = ioq->buf[ioq->tail]; // 从缓冲区中取出
ioq->tail = next_pos(ioq->tail); // 把读游标移到下一位置
if (ioq->producer != NULL) {
wakeup(&ioq->producer); // 唤醒生产者
}
return byte;
}
/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
ASSERT(intr_get_status() == INTR_OFF);
/* 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,
* 为的是当缓冲区里的东西被消费者取完后让消费者知道唤醒哪个生产者,
* 也就是唤醒当前线程自己*/
while (ioq_full(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte; // 把字节放入缓冲区中
ioq->head = next_pos(ioq->head); // 把写游标移到下一位置
if (ioq->consumer != NULL) {
wakeup(&ioq->consumer); // 唤醒消费者
}
}
/* 返回环形缓冲区中的数据长度 */
uint32_t ioq_length(struct ioqueue* ioq) {
uint32_t len = 0;
if (ioq->head >= ioq->tail) {
len = ioq->head - ioq->tail;
} else {
len = bufsize - (ioq->tail - ioq->head);
}
return len;
}
首先缓冲区有63个字节
生产者可以是键盘驱动程序,这是个内核线程
消费者目前没有。
我们假设缓冲区满,三个生产者线程1、2、3先后调用ioq_putchar
最后一个消费者调用ioq_getchar
首先,线程1被卡在ioq_wait(&ioq->producer);这一行
这个时候,看看ioq_wait函数就会明白,全局变量ioq内的produce被赋值为了running_thread即当前线程PCB地址,然后直接block阻塞当前线程,切换线程2。这个时候produce是唯一保存线程1,所以produce不可被覆盖
线程2显然被卡在lock_acquire(&ioq->lock);
lock_acquire这个函数将线程2阻塞,线程2保存在lock的信号量的阻塞队列上。
线程3同理。
综上,生产者线程1被阻塞,由缓冲区结构体ioq的produce保存,生产者线程2、3被阻塞,由ioq的lock的信号量的阻塞队列去保存。
之后消费者调用ioq_getchar,执行
if (ioq->producer != NULL) {
wakeup(&ioq->producer); // 唤醒生产者
}
唤醒生产者,显然,他只能唤醒生产者线程1。线程1回到就绪队列,切换到线程1时,线程1释放锁,在阻塞在lock的线程2、线程3挑一个线程3放入就绪,然后出循环,又向缓冲区写入一个字节。
如果往后切换到线程3时,线程3也会被卡在ioq_wait(&ioq->producer)。
注意:static void ioq_wait(struct task_struct** waiter) 是双指针。
调用时要取地址&
ioq_wait(&ioq->consumer);
这里如果设定的是单指针,你想修改全局变量ioq里的produce或者consumer,就必须传入ioq的指针,但是这里putchar,getchar都要调用ioq_wait,一个要用produce记录,另一个要用consumer记录
你不知道到底要修改produce还是consumer,所以你只传iqo,
ioq_wait函数内部是没有办法判断的,所以只能传produce或者consumer的地址。
1.3 keyboard.c 修改
struct ioqueue kbd_buf; // 定义键盘缓冲区
/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf);
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}
if (cur_char) {
if (!ioq_full(&kbd_buf)) {
//put_char(cur_char);
ioq_putchar(&kbd_buf, cur_char);
}
return;
}
2.结果
注意:这个实验里面只有一个内核线程在跑main函数里的while(1)死循环,如果一直敲击键盘,会不断往环形键盘缓冲区写字符,当然这里为了有显示,实际键盘中断处理程序中解析完键盘码后的put_char(cur_char);这句代码并未注释。所以理论上一直按键盘,键盘环形缓冲区写满后阻塞的线程是主线程,因为这里暂时没有消费者。
就目前的代码而言,一旦阻塞了主线程,就绪队列又为空。所以shcedule会报一个ASSERT提示就绪队列为空,所以目前一直按键盘理论上键盘缓冲区满了后操作系统会异常退出,打印ASSERT断言信息。
在未来的代码,init_all的thread_init会创建idle线程,当就绪队列为空的时候,操作系统会挂起而不是死掉,就绪队列不空后会调度新线程或进程上处理器,具体在第二十三篇博客硬盘驱动程序。