Project Goal Set IV - Interrupt Driven IO and Kernmon
This will be a big week for you and your operating system! You will be setting the stage of the creation of system calls, and you will also make your first venture into a real user interface for your operating system (albeit one that is essentially just a machine code monitor).
The sequence you should follow to pull this off are as follows:
- Create a set of buffers for message passing and IO.
- Create an interrupt and buffer based scheme for input and output.
- Make read and write functions that use your kernel buffers.
- Write a kernel monitor (kernmmon) to interact with your system.
The primary focus of how UNIX kernels interact with the world is through a series of unsigned character buffers. These are analogous to the files for a process in that read and write calls are simply reading and writing to these buffers. Internally, the operating system will maintain a large list of these, and they are typically of fixed size. They are also typically circular buffers. See the wikipedia article at https://en.wikipedia.org/wiki/Circular_buffer to get a good understanding of how to construct circular buffers.
Our system will be a microkernel, and that means that for us these buffers are practically the only thing our OS will do. There will be some raw in and out commands that we will process, but really we are just going to move data around in these buffers. I implemented the circular buffers to be a two dimensional array of unsigned characters each row is one kilobyte (1024 bytes) in size, and I allocated 1024 rows for a total of 1MB of buffer space. This should fit in the same 4MB page that your kernel currently occupies, but if it does not you will need to make another PTE to allocate space for your buffers in the entry page table (and eventually in the kernel page table allocations).
In addition to the raw character buffers, you also must maintain either two indexes or two pointers to indicate the start and end of the buffer. This operation is a little easier if you use integers, as then you can use simple arithmetic to move them through memory. I implemented my buffers using functions to read and write characters from them, and use that as my primary means of accessing them. I created two files, buffer.h and buffer.c, to do this. This section concludes with some hints about what are in these files.
Ok, this will be a little bit more than a hint! Here is what is in this file:
int buffer_write(int fd, const unsigned char *buf, int n); int buffer_read(int fd, unsigned char *buf, int n); int buffer_peek(int fd); int buffer_getchar(int fd); int buffer_putchar(int fd, int c); #define TTY_IN 0 #define TTY_OUT 1 #define BUF_SIZE 1024 #define BUF_COUNT 1024
The basic rundown of these functions will be given in the next file, however I should point out that the two constants are there to reserve buffer 0 and buffer 1 (as indicated by the fd numbers) for the TTY input and output respectively.
buffer.cpp implements all the buffer commands. Notice that I did not provide raw reference to the buffers, but rather I am forcing myself to access them via the functions. This also means that I have allocated the buffers as static variables inside of the buffer.cpp file. So at the top of my file, I have something like this:
static unsigned char kbuf[BUF_COUNT][BUF_SIZE]; //buffers static int kbuf_start; //start indexes static int kbuf_end; //end indexes
Now, for the rest, I will give you no more code!
Here's a rundown of what all these functions do:
- buffer_write(fd, buf, n)
- We will append n characters to buffer fd from buf. It stops writing if the buffer would be full. It returns the total number of characters written.
- buffer_read(fd, buf, n)
- Read up to n characters from buffer fd. This one does not stop at newlines! It stops only after reading n characters or exhausting the buffer. This also does not block, it returns either the number of characters it read, or it returns -1
- Return the character at the start of the buffer (but do not consume it). Returns -1 if no input is available.
- Return and consume the character at the start of the buffer. Return -1 if buffer is empty.
- buffer_putchar(fd, c)
- Write a character to the end of the buffer. Return 1 on success, 0 if the buffer is full.
Something worth contemplating is why we use integers instead of characters for peek, getchar, and putchar. For an answer to this, perhaps you could consult the sacred text of the prophets Kernighan and Ritchie.
It should be noted that all of these behaviors are non blocking. They immediately fail if there is nothing available when reading. For writing, they simply fail if the buffer runs out of space.
As a final hint, I used my buffer_getchar and buffer_putchar to implement my buffer_read and buffer_write functions.
Interrupt Handling and IO
The next step to make your system work is to implement interrupt driven input and output. To do this, study XV6 and set up your IVT, your trap handler, and your C function that handles routines. For now, we are going to leave off handling system calls, so don't copy that part. We will, however, be using the interrupts from the UART to populate the UART's input buffer. To do this, the following change need to be made:
- Your UART should enable interrupts.
- Have a UART interrupt handler that processes input by putting characters into the appropriate buffer.
- Output will be written to the output buffer, and then flushed whenever a newline is written. This should also use some sort of interrupt driven scheme.
In addition to this, console read and write functions should be updated to use the new buffer scheme. I will let you contemplate how this should work. Finally, your console interrupt handler should echo the characters as they are typed.
The time you have been waiting for is upon us! We are going to make your OS kinda useful! Your task is to write a monitor program which will function like the legendary wozmon (http://www.sbprojects.com/projects/apple1/wozmon.php). wozmon was a 6502 ROM monitor which ran on the Apple I. You should copy its interface commands, but of course we will be inserting i386 instructions into memory. This monitor will serve a couple of purposes. First and foremost, it will let us examine and alter memory. Secondly, it will allow us to key in programs and run them.
Look at the sbprojects page to get a good handle on how wozmon behaves, and then get to work coding it! Eventually, kernmon will run in a separate process, and will be invoked during kernel panics. For now, though, just create a function called "kernmon", which should probably live in its own .h and .c file pair. Think of kernmon as the main function for the monitor.
When you integrate this into your main function, you should remove the spinlock we wrote a few weeks ago. Instead, your kernel should consist of:
Initialize Interrupts console_init Print a welcome message. Call kernmon
It will remain in kernmon from then on.
To test kernmon, why not work out a "Hello, world" program? Write the assembly out on paper, then look up the opcodes to translate it into hex. Then key it in to your monitor and run it.
There are a couple of ways you can pull this off. The easiest way would be to just use the in and out instructions, but you could also try calling your console functions from within your asm code. I'll let you work out how to do that!
Either way, if you can key in a machine code i386 Hello, world during your next code review, I will be very impressed!