"Restore the Truth About Operating Systems" Chapter 10 Input and Output System

The learning experience is better with videos!
Section 1: https://www.bilibili.com/video/BV18P411q7hg/?vd_source=701807c4f8684b13e922d0a8b116af31 Section 2: https://www.bilibili.com/video/BV19W4y1Q7yE/?spm_id_from=333.999.0.0&vd_source=701807
c4f8684b13e922d0a8b116af31th
Section 3: https://www.bilibili.com/video/BV1nX4y1t7Xz/?vd_source=701807c4f8684b13e922d0a8b116af31
Section 4: https://www.bilibili.com/video/BV1ZX4y1W75c/?spm_id_from=333.999.0.0&vd_source=70180 7c4f8684b13e922d0a8b116af31
fifth Section: https://www.bilibili.com/video/BV1Ch411P71C/?vd_source=701807c4f8684b13e922d0a8b116af31#reply171635664400

Code repository: https://github.com/xukanshan/the_truth_of_operationg_system

Previously, we could first turn off and then turn on interrupts to ensure that the printing code would not cause problems due to multi-thread scheduling. Multi-thread scheduling naturally leads to the issue of ordered access to public resources.

Here are some basic concepts:

Public resources: It can be public memory, public files, public hardware, etc. In short, it is a set of resources shared by all tasks.

Mutual exclusion: Mutual exclusion can also be called exclusive. It means that a public resource can only be used exclusively by one task at a certain time. When other tasks want to access the public resource, they must wait for the current visitor to the public resource to finish using the resource before starting to access it. .

Critical section: If a program wants to use certain resources, it must access these resources through some instructions. If multiple tasks access the same public resource, then the area composed of instruction codes for accessing the public resources in each task is called the critical section. It should be emphasized that the critical section refers to the instruction code in the program that accesses public resources. That is, the critical section is an instruction, not a static public resource that is accessed.

Competition conditions: Competition conditions refer to multiple tasks entering the critical section at the same time in a non-mutually exclusive manner. Everyone's access to public resources is carried out in parallel in a competitive manner, so the final state of the public resources depends on the critical sections of these tasks. micro-operation execution order.

Therefore, the core of the synchronization mechanism is to allow public resources to be accessed only by the critical section of one thread at a certain time, and to allow it to be executed to completion.

Focusing on the above purpose, we propose the concept of semaphore, which is essentially a counter with real meaning (such as the number of a certain resource). The common values ​​are 0 and 1. When a critical section of a process needs to access a public resource, it must query the semaphore of the public resource. Only when the semaphore is >0 (usually 1), that is, the resource is available, can the public resource be accessed. At this time, the public resource The semaphore is reduced to 0. Another process wants to access this public resource at this time and finds that the semaphore is 0, which means that the resources it needs are not available, but the process cannot continue to advance without the resources it needs, so the best way is not to let the process Instead of waiting here, you replace yourself with the processor and give up the processor. This means blocking yourself and waiting for the semaphore to reach 1 before you can be awakened to run. We call a semaphore that has only two conditions: 0 and 1 a binary semaphore.

Therefore, the core of the synchronization mechanism now becomes that only processes that have obtained the semaphore can run. Processes that have not obtained the semaphore block themselves. When the process that obtains the semaphore finishes running and releases the semaphore, it must block it. The process wakes up. To wake up is to move the pcb of the blocking process from the blocking queue of the semaphore into the ready queue.

In order to implement the synchronization mechanism, we first implement two functions, thread_block and thread_unlock. The former is used to block the process. The implementation principle is to modify the status field in the thread's pcb, and then use the schedule function (which needs to be modified later for the BLOCKED thread). Scheduling strategy); the latter is used to unblock the process. The principle is to modify the status field of the pcb, and then put the thread's pcb into the head of the ready queue (in order to schedule as soon as possible). Neither of these two functions involves the operation of the blocking queue of a certain semaphore, we implement it elsewhere.

**myos/thread/thread.c **

//将当前正在运行的线程pcb中的状态字段设定为传入的status,一般用于线程主动设定阻塞
void thread_block(enum task_status stat) {
    
    
/* stat取值为TASK_BLOCKED,TASK_WAITING,TASK_HANGING,也就是只有这三种状态才不会被调度*/
   ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING)));
   enum intr_status old_status = intr_disable();       //先关闭中断,因为涉及要修改阻塞队列,调度
   struct task_struct* cur_thread = running_thread();    //得到当前正在运行的进程的pcb地址
   cur_thread->status = stat; // 置其状态为stat 
   schedule();		      // 将当前线程换下处理器
/* 待当前线程被解除阻塞后才继续运行下面的intr_set_status */
   intr_set_status(old_status);
}
/* 将线程pthread解除阻塞 */
void thread_unblock(struct task_struct* pthread) {
    
    
   enum intr_status old_status = intr_disable();      //涉及队就绪队列的修改,此时绝对不能被切换走
   ASSERT(((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING)));
   if (pthread->status != TASK_READY) {
    
    
      ASSERT(!elem_find(&thread_ready_list, &pthread->general_tag));
      if (elem_find(&thread_ready_list, &pthread->general_tag)) {
    
    
	      PANIC("thread_unblock: blocked thread in ready_list\n");
      }
      list_push(&thread_ready_list, &pthread->general_tag);    // 放到队列的最前面,使其尽快得到调度
      pthread->status = TASK_READY;
   } 
   intr_set_status(old_status);
}

Update myos/thread/thread.h

void thread_block(enum task_status stat);
void thread_unblock(struct task_struct* pthread);

Next we need to implement the lock mechanism to achieve the orderly allocation of binary semaphores. First create data structures for semaphores and locks. The relationship between semaphore and lock: Semaphore is the management of a certain resource. It actually indicates how many resources there are and which threads are waiting for this resource. Locks are implemented on the semaphore mechanism. Compared with semaphores, there are more records of who caused the lock (that is, binary semaphores, or who the resources are allocated to).

myos/thread/sync.h sync takes the first letter of synchronization (synchronization)

#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"

/* 信号量结构 */
struct semaphore {
    
    
   uint8_t  value;              //一个信号量肯定有值来表示这个量
   struct   list waiters;       //用一个双链表结点来管理所有阻塞在该信号量上的线程
};

/* 锁结构 */
struct lock {
    
    
   struct   task_struct* holder;	    //用于记录谁把二元信号量申请走了,而导致了该信号量的锁
   struct   semaphore semaphore;	    //一个锁肯定是来管理信号量的
   uint32_t holder_repeat_nr;		    //有时候线程拿到了信号量,但是线程内部不止一次使用该信号量对应公共资源,就会不止一次申请锁
                                        //内外层函数在释放锁时就会对一个锁释放多次,所以必须要记录重复申请的次数
};

#endif

Next, implement a bunch of functions related to semaphores and locks, including initializing semaphores, initializing locks, pv operations on semaphores (down and up in the code), acquiring locks and releasing locks.

myos/thread/sync.c

#include "sync.h"
#include "list.h"
#include "global.h"
#include "debug.h"
#include "interrupt.h"

//用于初始化信号量,传入参数就是指向信号量的指针与初值
void sema_init(struct semaphore* psema, uint8_t value) {
    
    
   psema->value = value;       // 为信号量赋初值
   list_init(&psema->waiters); //初始化信号量的等待队列
}

//用于初始化锁,传入参数是指向该锁的指针
void lock_init(struct lock* plock) {
    
    
   plock->holder = NULL;
   plock->holder_repeat_nr = 0;
   sema_init(&plock->semaphore, 1); //将信号量初始化为1,因为此函数一般处理二元信号量
}

//信号量的down操作,也就是减1操作,传入参数是指向要操作的信号量指针。线程想要申请信号量的时候用此函数
void sema_down(struct semaphore* psema) {
    
    
   enum intr_status old_status = intr_disable();         //对于信号量的操作是必须关中断的

   //一个自旋锁,来不断判断是否信号量已经被分配出去了。为什么不用if,见书p450。
    while(psema->value == 0) {
    
    	// 若value为0,表示已经被别人持有
        ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
        /* 当前线程不应该已在信号量的waiters队列中 */
        if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
    
    
	        PANIC("sema_down: thread blocked has been in waiters_list\n");
        }
        //如果此时信号量为0,那么就将该线程加入阻塞队列,为什么不用判断是否在阻塞队列中呢?因为线程被阻塞后,会加入阻塞队列,除非被唤醒,否则不会
        //分配到处理器资源,自然也不会重复判断是否有信号量,也不会重复加入阻塞队列
        list_append(&psema->waiters, &running_thread()->general_tag); 
        thread_block(TASK_BLOCKED);    // 阻塞线程,直到被唤醒
    }
/* 若value为1或被唤醒后,会执行下面的代码,也就是获得了锁。*/
    psema->value--;
    ASSERT(psema->value == 0);	    
/* 恢复之前的中断状态 */
    intr_set_status(old_status);
}

//信号量的up操作,也就是+1操作,传入参数是指向要操作的信号量的指针。且释放信号量时,应唤醒阻塞在该信号量阻塞队列上的一个进程
void sema_up(struct semaphore* psema) {
    
    
/* 关中断,保证原子操作 */
   enum intr_status old_status = intr_disable();
   ASSERT(psema->value == 0);	    
   if (!list_empty(&psema->waiters)) {
    
       //判断信号量阻塞队列应为非空,这样才能执行唤醒操作
      struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
      thread_unblock(thread_blocked);
   }
   psema->value++;
   ASSERT(psema->value == 1);	    
/* 恢复之前的中断状态 */
   intr_set_status(old_status);
}

//获取锁的函数,传入参数是指向锁的指针
void lock_acquire(struct lock* plock) {
    
    
//这是为了排除掉线程自己已经拿到了锁,但是还没有释放就重新申请的情况
   if (plock->holder != running_thread()) {
    
     
        sema_down(&plock->semaphore);    //对信号量进行down操作
        plock->holder = running_thread();
        ASSERT(plock->holder_repeat_nr == 0);
        plock->holder_repeat_nr = 1;    //申请了一次锁
   } else {
    
    
        plock->holder_repeat_nr++;
   }
}

//释放锁的函数,参数是指向锁的指针
void lock_release(struct lock* plock) {
    
    
   ASSERT(plock->holder == running_thread());
   //如果>1,说明自己多次申请了该锁,现在还不能立即释放锁
   if (plock->holder_repeat_nr > 1) {
    
       
      plock->holder_repeat_nr--;
      return;
   }
   ASSERT(plock->holder_repeat_nr == 1);    //判断现在lock的重复持有数是不是1只有为1,才能释放

   plock->holder = NULL;	   //这句必须放在up操作前,因为现在并不在关中断下运行,有可能会被切换出去,如果在up后面,就可能出现还没有置空,
                                //就切换出去,此时有了信号量,下个进程申请到了,将holder改成下个进程,这个进程切换回来就把holder改成空,就错了
   plock->holder_repeat_nr = 0;
   sema_up(&plock->semaphore);	   // 信号量的V操作,也是原子操作
}


Update myos/thread/sync.h

void sema_init(struct semaphore* psema, uint8_t value); 
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_init(struct lock* plock);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);

Now, we use the lock mechanism to establish a console_lock (meaning terminal lock) to coordinate printing and encapsulate the original put_int, put_char, and put_str.

Brief analysis of myos/device/console.c code

#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
static struct lock console_lock;    // 控制台锁

/* 初始化终端 */
void console_init() {
    
    
  lock_init(&console_lock); 
}

/* 获取终端 */
void console_acquire() {
    
    
   lock_acquire(&console_lock);
}

/* 释放终端 */
void console_release() {
    
    
   lock_release(&console_lock);
}

/* 终端中输出字符串 */
void console_put_str(char* str) {
    
    
   console_acquire(); 
   put_str(str); 
   console_release();
}

/* 终端中输出字符 */
void console_put_char(uint8_t char_asci) {
    
    
   console_acquire(); 
   put_char(char_asci); 
   console_release();
}

/* 终端中输出16进制整数 */
void console_put_int(uint32_t num) {
    
    
   console_acquire(); 
   put_int(num); 
   console_release();
}


And create the header file myos/device/console.h for it

#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
void console_init(void);
void console_acquire(void);
void console_release(void);
void console_put_str(char* str);
void console_put_char(uint8_t char_asci);
void console_put_int(uint32_t num);
#endif


Encapsulate the console_init function into init_all

myos/kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"

/*负责初始化所有模块 */
void init_all() {
    
    
   put_str("init_all\n");
   idt_init();   //初始化中断
   mem_init();	  // 初始化内存管理系统
   thread_init(); // 初始化线程相关结构
   timer_init();  
   console_init();
}

Now let's test whether the synchronization mechanism can run normally and write the test code myos/kernel/main.c

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

void k_thread_a(void*);
void k_thread_b(void*);

int main(void) {
    
    
   put_str("I am kernel\n");
   init_all();

   thread_start("k_thread_a", 31, k_thread_a, "argA ");
   thread_start("k_thread_b", 8, k_thread_b, "argB ");

   intr_enable();
   while(1) {
    
    
      console_put_str("Main ");
   };
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    
         
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
   char* para = arg;
   while(1) {
    
    
      console_put_str(para);
   }
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    
         
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
   char* para = arg;
   while(1) {
    
    
      console_put_str(para);
   }
}

Next, we will do a small experiment to write a keyboard driver and complete the preparation of the input and output system. The content of the experiment is that when we press any key on the keyboard, the screen displays k

Inside the keyboard, there will be an 8048 chip, and there is an 8042 chip inside the motherboard. When we press/release a certain key, the 8048 will report the pressing and releasing of the key to the 8042, and then the 8042 will interrupt The controller sends the signal.
Insert image description here
The process after pressing the key on the keyboard:

1. When a key is pressed (does not pop up)
2. 8048 monitors which key is pressed, and 8048 sets the pass code corresponding to the key (the code used to describe a key press, the break code is to describe a key The released code) is sent to 8042. If it is not released, then it will continue to send.
3. After 8042 receives the pass code, it will know which key has been pressed, process it, and then save the pass code to itself. Register
4. 8042 then sends an interrupt to the interrupt agent 8259A. If it is not released, it will continue to send interrupts.
5. After an interrupt occurs, the processor executes the corresponding interrupt handler

The process of the key popping up is the same as the process of pressing it.

The scan code (the pass code and the break code are collectively called the scan code) are determined by the keyboard encoder. Different keyboard encoders will produce different encoding schemes. Currently, there are three sets:

scan code set 1, application: XT keyboard
scan code set 2, application: AT keyboard
scan code set 3, application: keyboard used by IBM PS/2 series high-end computers

Nowadays, most of them use the second set, so what most keyboards send to 8042 is the second set of scan codes. For the sake of compatibility, no matter which set of encoding scheme we use, when the keyboard sends the scan code to 8042, , processed by 8042 and converted into the first set of scan codes, which is also one of the reasons for the existence of 8042. Therefore, we only need to process the first set of scan codes in the keyboard's interrupt handler.

p465, p466, p467 analysis code kernel.S, interrupt.c, main.c, keyboard.c:

1. Code function

After pressing any key on the keyboard, the character k will be printed on the screen. If you keep pressing it and do not release it, it will print continuously.

2. Implementation principle

Keyboard signals are finally processed by the interrupt processor, and finally are processed by the corresponding interrupt processing function.

3. Code logic

A. Establish an interrupt processing function for keyboard interrupt

B. Set the interrupt controller and turn on the keyboard interrupt signal

4. How to write code?

A. kernel.S: Add assembly interrupt function processing entry defined using assembly template

B. kerboard.c: Write the keyboard interrupt processing function intr_keyboard_handler; write the keyboard initialization function keyboard_init (call the interrupt registration function to register the keyboard interrupt processing function), and encapsulate it into init_all

C. interrupt.c: Modify the number of supported interrupts; modify the interrupt controller initialization code pic_inic to only enable keyboard interrupts (the keyboard interrupt signal is connected to the IR1 pin of the main chip)

D. Write the test code main.c, which is an infinite loop.

5. The code is implemented as follows:

Modify myos/kernel/kernel.S in one step and register all 8259A interrupts (there are 16 IR pins in total, so 16 interrupts are also registered)

VECTOR 0x20,ZERO	;时钟中断对应的入口
VECTOR 0x21,ZERO	;键盘中断对应的入口
VECTOR 0x22,ZERO	;级联用的
VECTOR 0x23,ZERO	;串口2对应的入口
VECTOR 0x24,ZERO	;串口1对应的入口
VECTOR 0x25,ZERO	;并口2对应的入口
VECTOR 0x26,ZERO	;软盘对应的入口
VECTOR 0x27,ZERO	;并口1对应的入口
VECTOR 0x28,ZERO	;实时时钟对应的入口
VECTOR 0x29,ZERO	;重定向
VECTOR 0x2a,ZERO	;保留
VECTOR 0x2b,ZERO	;保留
VECTOR 0x2c,ZERO	;ps/2鼠标
VECTOR 0x2d,ZERO	;fpu浮点单元异常
VECTOR 0x2e,ZERO	;硬盘
VECTOR 0x2f,ZERO	;保留

myos/device/keyboard.c

#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"

#define KBD_BUF_PORT 0x60	   // 键盘buffer寄存器端口号为0x60

/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
    
    
   put_char('k');
//每次必须要从8042读走键盘8048传递过来的数据,否则8042不会接收后续8048传递过来的数据
   inb(KBD_BUF_PORT);
   return;
}

/* 键盘初始化 */
void keyboard_init() {
    
    
   put_str("keyboard init start\n");
   register_handler(0x21, intr_keyboard_handler);       //注册键盘中断处理函数
   put_str("keyboard init done\n");
}

Support code myos/device/keyboard.h

#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void); 
#endif

Modify kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"

/*负责初始化所有模块 */
void init_all() {
    
    
   put_str("init_all\n");
   idt_init();   //初始化中断
   mem_init();	  // 初始化内存管理系统
   thread_init(); // 初始化线程相关结构
   timer_init();  
   console_init();
   keyboard_init();  // 键盘初始化
}

Modify myos/kernel/interrupt.c

#define IDT_DESC_CNT 0x30	   //支持的中断描述符个数48

/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
    
    

   /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI

   /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
   //outb (PIC_M_DATA, 0xfe);
   //outb (PIC_S_DATA, 0xff);

   /* 测试键盘,只打开键盘中断,其它全部关闭 */
   outb (PIC_M_DATA, 0xfd);   //键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了
   outb (PIC_S_DATA, 0xff);

   put_str("   pic_init done\n");
}

Test code myos/kernel/main.c

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

void k_thread_a(void*);
void k_thread_b(void*);

int main(void) {
    
    
   put_str("I am kernel\n");
   init_all();

//   thread_start("k_thread_a", 31, k_thread_a, "argA ");
//   thread_start("k_thread_b", 8, k_thread_b, "argB ");

   intr_enable();
   while(1); //{
    
    
      //console_put_str("Main ");
  // };
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    
         
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
   char* para = arg;
   while(1) {
    
    
      console_put_str(para);
   }
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    
         
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
   char* para = arg;
   while(1) {
    
    
      console_put_str(para);
   }
}

6. For detailed explanations of other codes, see book p465

Now, let's write the keyboard driver. The essence is to improve the keyboard interrupt processing function

p469 analyzes the keyboard.c code:

1. Code function

Supports the most basic input functions and displays the results on the screen

2. Implementation principle

The keyboard signal will eventually be processed by calling the keyboard interrupt processing function. We can take out the incoming make code and break code information from 8042 to restore the key press and release information for targeted processing.

3. Code logic

Get the code value from the 0x60 register of 8042, and then determine the character for targeted processing

4. How to write code?

Insert image description here
5. The code is implemented as follows: ( myos/device/keyboard.c )

#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"

#define KBD_BUF_PORT 0x60   //键盘buffer寄存器端口号为0x60

#define esc '\033'		    //esc 和 delete都没有\转义字符这种形式,用8进制代替
#define delete '\0177'
#define enter '\r'
#define tab '\t'
#define backspace '\b'

#define char_invisible 0    //功能性 不可见字符均设置为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


//二维数组,用于记录从0x00到0x3a通码对应的按键的两种情况(如0x02,不加shift表示1,加了shift表示!)的ascii码值
//如果没有,则用ascii0替代
char keymap[][2] = {
    
    
/* 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}
};

int ctrl_status = 0;        //用于记录是否按下ctrl键
int shift_status = 0;       //用于记录是否按下shift
int alt_status = 0;         //用于记录是否按下alt键
int caps_lock_status = 0;   //用于记录是否按下大写锁定
int ext_scancode = 0;       //用于记录是否是扩展码

static void intr_keyboard_handler(void)
{
    
    
    int break_code;     //用于判断传入值是否是断码
    uint16_t scancode = inb(KBD_BUF_PORT);  //从8042的0x60取出码值
    if(scancode == 0xe0)	//如果传入是0xe0,说明是处理两字节按键的扫描码,那么就应该立即退出去取出下一个字节
    {
    
    
    	ext_scancode = 1;   //打开标记,记录传入的是两字节扫描码
    	return;     //退出
    }
    if(ext_scancode)    //如果能进入这个if,那么ext_scancode==1,说明上次传入的是两字节按键扫描码的第一个字节
    {
    
    
        scancode =( (0xe000) | (scancode) );    //合并扫描码,这样两字节的按键的扫描码就得到了完整取出
        ext_scancode = 0;   //关闭记录两字节扫描码的标志
    }

    break_code =( (scancode & 0x0080) != 0);  //断码=通码+0x80,如果是断码,那么&出来结果!=0,那么break_code值为1
    if(break_code)  //如果是断码,就要判断是否是控制按键的断码,如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序
    {
    
    
    	uint16_t make_code = (scancode &= 0xff7f); //将扫描码(现在是断码)还原成通码
    	if(make_code == ctrl_l_make || make_code == ctrl_r_make) 
            ctrl_status = 0;           //判断是否松开了ctrl
    	else if(make_code == shift_l_make || make_code == shift_r_make) 
            shift_status = 0;   //判断是否松开了shift
    	else if(make_code == alt_l_make || make_code == alt_r_make) 
            alt_status = 0;         //判断是否松开了alt
    	return;
    }
    //来到这里,说明不是断码,而是通码,这里的判断是保证我们只处理这些数组中定义了的键,以及右alt和ctrl。
    else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make))
    {
    
    
    	int shift = 0; //确定是否开启shift的标志,先默认设置成0
    	uint8_t index = (scancode & 0x00ff);    //将扫描码留下低字节,这就是在数组中对应的索引
    
	    if(scancode == ctrl_l_make || scancode == ctrl_r_make)  //如果扫描码是ctrl_l_make,或者ctrl_r_make,说明按下了ctrl    	
	    {
    
    
            ctrl_status = 1;
            return;
        }
	    else if(scancode == shift_l_make || scancode == shift_r_make)
        {
    
    
            shift_status = 1;
            return;
        }
	    else if(scancode == alt_l_make || scancode == alt_r_make)
        {
    
    
            alt_status = 1;
            return;
        }
	    else if(scancode == caps_lock_make) //大写锁定键是按一次,然后取反
        {
    
    
            caps_lock_status = !caps_lock_status;
            return;
        }

		if ((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) || \
			(scancode == 0x1b) || (scancode == 0x2b) || (scancode == 0x27) || \
			(scancode == 0x28) || (scancode == 0x33) || (scancode == 0x34) || (scancode == 0x35)) {
    
      
			/*代表两个字母的键 0x0e 数字'0'~'9',字符'-',字符'='
                           0x29 字符'`'
                           0x1a 字符'['
                           0x1b 字符']'
                           0x2b 字符'\\'
                           0x27 字符';'
                           0x28 字符'\''
                           0x33 字符','
                           0x34 字符'.'
                           0x35 字符'/' 
            */
         	if (shift_status)// 如果同时按下了shift键
            	shift = true;
      	} 
      	else {
    
    	  // 默认为字母键
			if(shift_status + caps_lock_status == 1)
            	shift = 1;   //shift和大写锁定,那么判断是否按下了一个,而且不能是同时按下,那么就能确定是要开启shift
      	}

		put_char(keymap[index][shift]); //打印字符
	    return;
    }
    else 
		put_str("unknown key\n");
    return;
}

/* 键盘初始化 */
void keyboard_init() {
    
    
   put_str("keyboard init start\n");
   register_handler(0x21, intr_keyboard_handler);
   put_str("keyboard init done\n");
}


6. For detailed explanations of other codes, see book p470

Next, we implement a circular input buffer for keyboard input, which is to implement consumer consumption and producer production on a logically circular buffer. Their access to the buffer is mutually exclusive. This is for the future implementation of the shell. In the future, we will enter the complete command in the buffer and then take the command from the buffer to execute. Since we are a buffer for keyboard input, there is only one producer and one consumer.

Insert image description here
First implement the buffer data structure struct ioqueue in myos/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;			        // 队尾,数据从队尾处读出
};

#endif

Implement a lot of small functions related to ring buffers myos/device/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;
}

/* 判断队列是否已空 */
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);          // 唤醒消费者
   }
}


Add function declaration myos/device/ioqueue.h

void ioqueue_init(struct ioqueue* ioq);
bool ioq_full(struct ioqueue* ioq);
bool ioq_empty(struct ioqueue* ioq);
char ioq_getchar(struct ioqueue* ioq);
void ioq_putchar(struct ioqueue* ioq, char byte);

Modify myos/device/keyboard.c to add the definition of the keyboard buffer, put the keyboard input into the buffer, and add the initialization of the buffer in the keyboard initialization function.

#include "ioqueue.h"

struct ioqueue kbd_buf;	   // 定义键盘缓冲区

        char cur_char = keymap[index][shift];
        if (cur_char) {
    
    
            /*****************  快捷键ctrl+l和ctrl+u的处理 *********************
             * 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为:
             * cur_char的asc码-字符a的asc码, 此差值比较小,
             * 属于asc码表中不可见的字符部分.故不会产生可见字符.
             * 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/
	        if ((ctrl_status && cur_char == 'l') || (ctrl_status && cur_char == 'u')) {
    
    
	            cur_char -= 'a';
	        }
            if (!ioq_full(&kbd_buf)) {
    
    
                put_char(cur_char);	    // 临时的
                ioq_putchar(&kbd_buf, cur_char);
            }
	        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");
}

Then we modify myos/keyboard.h to globalize the keyboard buffer so that external functions can access it.

extern struct ioqueue kbd_buf;

The effect that the test function myos/main.c function wants to achieve is: I press the keyboard and then write data to the buffer. Two consumer processes compete for this data. Whoever grabs it will print out the data. and tell who you are

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

/* 临时为测试添加 */
#include "ioqueue.h"
#include "keyboard.h"

void k_thread_a(void*);
void k_thread_b(void*);

int main(void) {
    
    
   put_str("I am kernel\n");
   init_all();
   thread_start("consumer_a", 31, k_thread_a, " A_");
   thread_start("consumer_b", 31, k_thread_b, " B_");
   intr_enable();
   while(1); 
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    
         
   while(1) {
    
    
      enum intr_status old_status = intr_disable();
      if (!ioq_empty(&kbd_buf)) {
    
    
         console_put_str(arg);
         char byte = ioq_getchar(&kbd_buf);
         console_put_char(byte);
      }
      intr_set_status(old_status);
   }
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    
         
   while(1) {
    
    
      enum intr_status old_status = intr_disable();
      if (!ioq_empty(&kbd_buf)) {
    
    
         console_put_str(arg);
         char byte = ioq_getchar(&kbd_buf);
         console_put_char(byte);
      }
      intr_set_status(old_status);
   }
}

Then enable the clock interrupt

myos/kernel/interrupt.c

/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
    
    

   /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI

   /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
   //outb (PIC_M_DATA, 0xfe);
   //outb (PIC_S_DATA, 0xff);

   /* 测试键盘,只打开键盘中断,其它全部关闭 */
   //outb (PIC_M_DATA, 0xfd);   //键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了
   //outb (PIC_S_DATA, 0xff);

   //同时打开时钟中断与键盘中断
   outb (PIC_M_DATA, 0xfc);
   outb (PIC_S_DATA, 0xff);

   put_str("   pic_init done\n");
}

Guess you like

Origin blog.csdn.net/kanshanxd/article/details/131301731