代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
shell命令是由多个字符组成的,并且要以回车键结束,为了实现shell命令,必须要找个缓冲区把已键入的信息存起来,当凑成完整的命令名时再一并由其他模块处理。
缓冲区是多个线程共同使用的共享内存,线程在并行访问必须保证对缓冲区是互斥访问。
内存中的缓冲区就是用来暂存数据的一片内存区域,内存是按地址来访问的,因此内存缓冲区实际上是线性存储。
对于缓冲区的访问,我们提供两个指针,一个是头指针,用于往缓冲区中写数据,另一个是尾指针,用于从缓冲区中读数据。每次通过头指针往缓冲区中写入一个数据后,使头指针加1指向缓冲区中下一个可写入数据的地址,每次通过尾指针从缓冲区中读取一个数据后,使尾指针加1指向缓冲区中下一个可读入数据的地址,缓冲区相当于一个队列,数据在队列头被写入,在队尾处被读出。
当指针到达缓冲区的上边界后,想办法将指针置为缓冲区的下边界,从而使头尾指针形成回路,逻辑上实现环形缓冲区。
我们的环形缓冲区及其方法定义在ioqueue.h和ioqueue.c文件中
先看/device/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; //队尾,数据从队尾处读出
};
//函数声明
/*判断队列是否已满*/
bool ioq_full(struct ioqueue* ioq);
/*初始化io队列ioq*/
void ioqueue_init(struct ioqueue* ioq);
/*消费者ioq队列中获取一个字符*/
char ioq_getchar(struct ioqueue* ioq);
/*生产者往ioq队列中写入一个字节byte*/
void ioq_putchar(struct ioqueue* ioq,char byte);
#endifstruct ioqueue 结构便是环形缓冲区,它包括六个成员,其中:
- lock 是本缓冲区的锁,每次对缓冲区操作时都要先申请这个锁,从而保证缓冲区操作互斥。
- producer 是生产者,此项来记录当缓冲区满时,在此缓冲区睡眠的生产者线程。
- consumer是消费者,此项来记录当缓冲区空时,在此缓冲区睡眠的消费者线程。
- buf[bufsize]是定义的缓冲区数组,其大小为bufsize,在上面用define定义为64。
- head是缓冲区队列的队首地址,tail 是队尾地址。
对应/device/ioqueue.c
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
#define NULL 0
/*初始化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){ //书中是static,但是不能被keyboard.c引用,所以改为非static并声明
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);
}
}
ioqueue_init函数接受一个缓冲区参数ioq,用于初始化缓冲区ioq。此函数负责三样工作,先通过初始化io队列的锁,再将生产者和消费者置为NULL,最后再将缓冲区的队头和队尾置为下标0
next_pos函数接受一个参数 pos,功能是返回pos在缓冲区中的下一个位置值,它是将pos+1后再对bufsize 求模得到的,这保证了缓冲区指针回绕着数组 buf,从而实现了环形缓冲区。
ioq_full函数接受一个缓冲区参数ioq。功能是返回队列是否已满,若已满则返回true,否则返回false。“next_pos(ioq->head) =ioq->tail”,判断下一个队首位置是否和队尾位置相等,即是否会碰撞。虽然缓冲区大小bufsize是64字节,但其最大容量为63字节。
ioq_empty函数接受一个缓冲区参数 ioq。功能是返回队列是否为空,若空则返回 true。判断ioq->head 是否等于 ioq->tail,若头尾相等则为空。
ioq_wait函数接受一个参数waiter,它是pcb类型的二级指针,因此传给它的实参将是线程指针的地址,函数功能是使当前线程睡眠,并在缓冲区中等待。传给waiter的实参一定是缓冲区中的成员producer或consumer。在函数体内就做了两件事,将当前线程记录在waiter指向的指针中,也就是缓冲区中的producer或consumer,因此*waiter相当于ioq->consumer或ioq->producer。随后调用 “thread_block(TASK_BLOCKED)”将当前线程阻塞。
wakeup函数接受一个参数waiter,它同样也是pcb类型的二级指针,因此传给它的实参也是缓冲区中的成员producer或consumer。函数功能就是通过”thread_unblock(*waiter)”唤醒*waiter (生产者或消费 者),随后将*waiter 置空。
对缓冲区操作的数据单位大小是1字节,即生产者每次往缓冲区中放一字节数据,消费者每次从缓冲区中取一字节数据。
ioq_getchar函数接受一个缓冲区参数ioq,函数功能是从ioq的队尾处返回一个字节,这属于从缓冲区中取数据,因此ioq_getchar是由消费者线程调用的。函数体中,先通过”while(ioq-empty(ioq))”循环判断缓冲区ioq是否为空,如果为空就表示没有数据可取,只好先在此缓冲区上睡眠,直到有生产者将数据添加到此缓冲区后再被叫醒重新取数据。while循环体中先通过”lock_acquire(&ioq->lock) “申请缓冲区的锁,持有锁后,通过”ioq_wait(&ioq->consumer)”将自己阻塞,也就是在此缓冲区上休眠。这里传给ioq_wait的实参就是缓冲区的消费者&ioq->consumer,此项用来记录哪个消费者没有拿到数据而休眠。这样等将来某个生产者往缓冲区中添加数据的时候就知道叫醒它继续拿数据了。 醒来后执行”lock_release(&ioq->lock)”释放锁。 在while 循环判断中,如果缓冲区不为空的话,通过代码”byte=ioq->buf[ioq->tail]”从缓冲区队尾获取 1 字节的数据,接着通过“ioq->tail = next_pos(ioq->tail)”将队尾更新为下一个位置。在消费者读取了一个字节后,缓冲区就腾出一个数据单位的空间了,这时候要判断一下是否有生产者在此缓冲区上休眠。若之前此缓冲区是满的,正好有生产者来添加数据,那个生产者一定会在此缓冲区上睡眠。因此要判断ioq->producer是否不等于NULL,如果不等于NULL,这说明之前有生产者线程在调用函数ioq_putchar往缓冲区中添加数据时因为缓冲区满而休眠了,既然现在缓冲区已被当前消费者线程腾出一个数据单位的空间了,此时应该叫醒生产者继续往缓冲区中添加数据。因此调用”wakeup(&ioq->producer)”唤醒生产者。之后通过return byte返回获取数据。
ioq_putchar函数接受两个参数,一个是缓冲区参数ioq,另一个是待加入字节数据byte,函数功能是往缓冲区ioq中添加byte,这是由生产者线程调用的。在函数体中也是先通过while循环判断缓冲区ioq是否为满,如果满了的话,先申请缓冲区的锁 ioq->lock,然后通过调用”ioq_wait(&ioq->producer)”将自己阻塞并登记在缓冲区ioq的成员producer中, 这样消费者便知道唤醒哪个生产者了。随后释放锁。如果缓冲区不满的话,通过”ioq->buf[ioq->head]=byte”,将数据byte写入缓冲区的队首ioq->head。随后通过“ioq->head =next_pos(ioq->head)”将队首更新为下一位置。这里依然要判断是否有消费者在此缓冲区上休眠。若之前此缓冲区为空,恰好有消费者来取数据,因此会导致消费者休眠。现在当前生产者线程已经往缓冲区中添加了数据,现在可以将消费者唤醒让它继续取数据了。如果ioq->consumer不等于NULL,这说明之前已经有消费者线程因为缓冲区空而休眠,被登记在ioq->consumer,因此调用”wakeup(&ioq->consumer)”将消费者唤醒。
接下来就是在键盘驱动中处理的字符存入环形缓冲区当中。
/device/keyboard.c
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#include "ioqueue.h"
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
/*用转义字符定义部分控制字符*/
#define esc '\033' //8进制表示字符,也可以用16进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' //8进制表示字符,也可以用16进制'\x7f'
/*以上不可见字符一律定义为0*/
#define char_invisible 0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
/*定义控制字符的通码和断码*/
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
/*定义一下变量记录相应键是否按下的状态,ext_scancode用于记录makecode是否以0xe0开头*/
static bool ctrl_status,shift_status,alt_status,caps_lock_status,ext_scancode;
struct ioqueue kbd_buf; //定义键盘缓冲区
/*以通码make_code为索引的二维数组*/
static char keymap[][2] = {
/*扫描码未与shift组合*/
/*0x00*/ {0,0},
/*0x01*/ {esc,esc},
/*0x02*/ {'1','!'},
/*0x03*/ {'2','@'},
/*0x04*/ {'3','#'},
/*0x05*/ {'4','$'},
/*0x06*/ {'5','%'},
/*0x07*/ {'6','^'},
/*0x08*/ {'7','&'},
/*0x09*/ {'8','*'},
/*0x0a*/ {'9','('},
/*0x0b*/ {'0',')'},
/*0x0c*/ {'-','_'},
/*0x0d*/ {'=','+'},
/*0x0e*/ {backspace,backspace},
/*0x0f*/ {tab,tab},
/*0x10*/ {'q','Q'},
/*0x11*/ {'w','W'},
/*0x12*/ {'e','E'},
/*0x13*/ {'r','R'},
/*0x14*/ {'t','T'},
/*0x15*/ {'y','Y'},
/*0x16*/ {'u','U'},
/*0x17*/ {'i','I'},
/*0x18*/ {'o','O'},
/*0x19*/ {'p','P'},
/*0x1a*/ {'[','{'},
/*0x1b*/ {']','}'},
/*0x1c*/ {enter,enter},
/*0x1d*/ {ctrl_l_char,ctrl_l_char},
/*0x1e*/ {'a','A'},
/*0x1f*/ {'s','S'},
/*0x20*/ {'d','D'},
/*0x21*/ {'f','F'},
/*0x22*/ {'g','G'},
/*0x23*/ {'h','H'},
/*0x24*/ {'j','J'},
/*0x25*/ {'k','K'},
/*0x26*/ {'l','L'},
/*0x27*/ {';',':'},
/*0x28*/ {'\'','"'},
/*0x29*/ {'`','~'},
/*0x2a*/ {shift_l_char,shift_l_char},
/*0x2b*/ {'\\','|'},
/*0x2c*/ {'z','Z'},
/*0x2d*/ {'x','X'},
/*0x2e*/ {'c','C'},
/*0x2f*/ {'v','V'},
/*0x30*/ {'b','B'},
/*0x31*/ {'n','N'},
/*0x32*/ {'m','M'},
/*0x33*/ {',','<'},
/*0x34*/ {'.','>'},
/*0x35*/ {'/','?'},
/*0x36*/ {shift_r_char,shift_r_char},
/*0x37*/ {'*','*'},
/*0x38*/ {alt_l_char,alt_l_char},
/*0x39*/ {' ',' '},
/*0x3a*/ {caps_lock_char,caps_lock_char}
/*其他按键暂不处理*/
};
/*键盘中断处理程序*/
static void intr_keyboard_handler(void){
/*必须要读取输出缓冲区寄存器,否则8042不在继续响应键盘中断*/
/*这次中断发送前的上一次中断,一下任意三个键是否有按下*/
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
/*若扫描码scancode是e0开头的,表示此键的按下将产生多个扫描码,
所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if(scancode == 0xe0){
ext_scancode = true; //打开e0标志
return; //表示后面还有扫描码,返回
}
/*如果上次是以0xe0开头的,将扫描码合并*/
if(ext_scancode){
scancode = ((0xe000) | scancode);
ext_scancode = false;
}
/*判断扫描码是通码还是断码*/
break_code = ((scancode & 0x0080) != 0); //获取break_code
//若是断码break_code(按键弹起时产生的扫描码)
if(break_code){
/*由于ctrl_r和alt_r的make_code和break_code都是2字节,所以可以用下面的方法取make_code,多字节的扫描码暂不处理*/
uint16_t make_code = (scancode &= 0xff7f); //通码与断码的区别在于第8位是0还是1
//取得其make_code(按键按下时产生的扫描码)
/*若是任意下面三个键弹起,将其状态置为false*/
if(make_code == ctrl_l_make || make_code == ctrl_r_make){
ctrl_status = false;
}else if(make_code == shift_l_make || make_code == shift_r_make){
shift_status = false;
}else if(make_code == alt_l_make || make_code == alt_r_make){
alt_status = false;
}//由于caps_lock不是弹起后关闭,所以需要单独处理
return;
}
/*若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code*/
else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){
bool shift = false; //判断是否与shift组合,用来在一维数组中索引对应的字符
if((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) || (scancode == 0x1b) \
|| (scancode == 0x2b) || (scancode == 0x27) || (scancode == 0x28) || (scancode == 0x33) \
|| (scancode == 0x34) || (scancode == 0x35)){
if(shift_down_last) //如果同时按下shift键
shift = true;
}else{ //默认为字母键
if(shift_down_last && caps_lock_last){ //如果shift和capslock同时按下
shift = false;
}else if(shift_down_last || caps_lock_last){
shift = true;
}else{
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff); //将扫描码的高字节置零,主要针对高字节是e0的扫描码
char cur_char = keymap[index][shift]; //在字节中找到对应的字符
/*只处理ASCII码不为0的键*/
if(cur_char){
/*若kbd_buf未满并且待加入的cur_char不为0,则将其加入到缓冲区区kbd_buf中*/
if(!ioq_full(&kbd_buf)){
put_char(cur_char); //临时的
ioq_putchar(&kbd_buf,cur_char);
}
return;
}
/*记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键*/
if(scancode == ctrl_l_make || scancode == ctrl_r_make){
ctrl_status = true;
}else if(scancode == shift_l_make || scancode == shift_r_make){
shift_status = true;
}else if(scancode == alt_l_make || scancode == alt_r_make){
alt_status = true;
}else if(scancode == caps_lock_make){ //不管之前是否按下过caps_lock键,再次按下时状态相反
caps_lock_status = !caps_lock_status;
}
}else{
put_str("unkown key\n");
}
return;
}
/*键盘初始化*/
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");
}上面代码只有以下变化:
原来在intr_keyboard_handler函数,在将扫描码转换为字符之后,只是打印出来。

这里修改为先通过“if(!ioq_full(&kbd_buf))”判断缓冲区是否已满,如果未满则通过”ioq_putchar(&kbd_buf, cur_char)”将转换后的字符cur char加入到缓冲区中。

还有在 keyboard_init 函数中添加了对缓冲区初始化的调用 “ioqueue_init (&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");
}
对应/device/keyboard.h
#ifndef __DEVICE_KEYBOARD_H #define __DEVICE_KEYBOARD_H #include "stdint.h" //函数声明 /*键盘初始化*/ void keyboard_init(void); #endif
makefile修改
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/debug.o \
$(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o \
$(BUILD_DIR)/switch.o $(BUILD_DIR)/list.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \
$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h lib/kernel/print.h lib/stdint.h kernel/interrupt.h lib/kernel/io.h \
kernel/global.h device/ioqueue.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h kernel/interrupt.h kernel/debug.h \
kernel/global.h
$(CC) $(CFLAGS) $< -o $@
执行结果如下:
这里我一直输入,缓存区满后,就不再有反应了,因为没有消费者,这里之后实现shell时就有消费者了。

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


