Lecture 8: Introduction to I/O (by Trek Palmer) ============================================== I/O ==== The I/O devices are all hanging off of a single bus. Now, every bus is different and has its own idiosyncrasies, but all busses have three main components: address lines, data lines, and control lines. A generic view of an I/O device: Address: -------------------------------------------------- Data : ---^---------------------------------------------- Control: ---|------------------------------^--------------- | ^ | +---|-------------|----------------|-----------------+ | v v v | | +----------+ +-----------+ +------------------+ | | | Address | | Control | | Data,Control,and | | | | Decoder | | Processor | | Status Registers | | | +----------+ +-----------+ +------------------+ | +----------------------------------------------------+ The address decoder and control processor are for dealing with the bus, and are, for the most part, transparent to the CPU. What we're interested in are the Data and Control registers. These are registers or special memories on the I/O device itself that allow us to control the device, inspect its state, and read and write values from it. Although the specific layout and meaning of these registers is fairly device dependent there's some consistency. The status register(s) let you know what state the device is in, the control register(s) accept coded values that cause the device to do something, and the data register(s) store values just read in, or to be written out. A simple example, a brain-dead keyboard ---------------------------------------- A keyboard is basically the simplest sort of I/O device. The user hits the keys, the keyboard controller records them and notifies the CPU. The CPU then decides what to do about it. Also note that, for the most part, a keyboard is basically an input only device. Ex1: Single Character Buffer, +------------------+ | -Status Register | | -Character Buffer| | (1 byte) | +------------------+ So, if 'a' is pressed, the status register is set to the code indicating that a key was pressed and the character buffer is set to the value 'a'. The keyboard handling routine (usually part of the OS) would read the value out of the buffer and write it to some OS data structure in system memory (like an array). Note that if another key is pressed before the OS has a chance to read the first value out of the buffer that the old value will simply be overwritten. This is not the huge problem it may at first seem, because processors are so fast compared to humans it is very improbable that the processor would be so overloaded that it would be unable to process a simple keyboard interrupt in time. It is possible to have a multi-character buffer, but this complicates the status logic (it must now encode the number of unprocessed characters in the buffer, and also perhaps the position in the buffer of the first unprocessed character). Interrupts and Polling ------------------------ Up to this point, I have been unashamedly using phrases like, 'the keyboard controller notifies the CPU'. But how does the controller actually notify the CPU? And how does the CPU deal with this notification without damaging the control flow of the executing program? For the first question, there are a couple of ways. One is, the processor could, every few milliseconds, check up on all the I/O devices and see if any of their status(es) had changed. If so, it'd then run some I/O handling routine. This method, of checking over and over again is called polling and is basically a bad idea. But it's simple, easy to implement and it doesn't require any special hardware. Another way is to extend the hardware to allow for a new kind of control signal: an interrupt. When a processor receives an interrupt it stops executing the code it was, saves that code's state, and jumps into interrupt handling code. When the interrupt handling code is done execution in the previous code will resume. This has the advantage that there's not all that time wasted checking on unmodified I/O state, but it does require additional hardware. Most systems now support interrupts. Although this terse description still leaves questions unanswered. How does the CPU save the code's state? How can the CPU tell the difference between interrupts from different devices? What happens if another interrupt is triggered while the CPU is handling one? For the first question, this can be architecture specific. Some systems have a spare set of registers they use either to store the saved values in, or to do the interrupt handling in. Other architectures will enter special OS code that tells the CPU how to save the data (often by spilling the values to the app's stack and/or process structure). For the second question, interrupts are usually accompanied by an argument (or several). This argument, which could be some predefined number or perhaps the I/O address of the device, is used to decide who generated the interrupt. Now, depending upon the implementation, either the CPU can decide (that is, call a different interrupt handler), or some base interrupt handler will parse the argument and then make the call itself. The third question is the trickiest of the three. Many processors can handle having several outstanding interrupts waiting to be processed. Many processors even support different priorities of interrupts so that a higher priority interrupt can interrupt the execution of another interrupt! As you may guess, this can lead to a lot of additional hardware (you need to save low-priority interrupt state, etc.). When I say that they can process several outstanding interrupts, I don't mean hundreds. Remember this all requires hardware, which costs money, so what we're really talking about here are numbers hovering around ten. Syscalls and User-visible I/O ----------------------------- All of this messing around with status and data registers is a tedious and device-dependent process. Imagine how annoying it would be to have to write a keyboard driver in order for your application to be able to get keyboard input! This is one of those things that an operating system (OS) does for you. It abstracts away the device specifics and makes it look like a source of character data. As Java programmers, you're used to programming in user-space, above the operating system. For you, the OS interface is contained within the JVM and exposed to you through 'magical' classes like System and java.util.File. For assembly code, though, the OS interface is explicit. Most processors support two main operating modes: user and supervisor. The OS runs in supervisor mode and there are many system actions that are only allowed in supervisor mode (in user mode they usually just generate an error). But, because many of the things available in supervisor mode (like access to I/O devices) are useful for user code, systems have evolved a mechanism to allow user code "safe" access to supervisor functions: the system call. A system call can be regarded as a specialized sort of function call. It's a way for user code to jump into the operating system for a bit. A system call saves the state of the user-code, switches into supervisor mode, executes the supervisor code, restores the user state, flips back into user mode and resumes execution in the calling code. As you can imagine, this is something of a heavyweight operation and requires some special hardware to perform. Each platform has its own way of performing a system call, on the ARM, a special instruction, SWI is used. SWI stands for software interrupt, which is, in a way, what a system call is. SWI takes as an argument a constant. This constant specifies the system call number. On the ARM, the syscall number specifies which system call you want to make, and the arguments go in special registers. Which registers and how many is an OS-dependant thing. On Linux, R0-R2 are used to pass arguments to a system call. Additional arguments need to be placed on the stack (although 3 is usually more than enough). Many system calls return values, these are usually passed back to you in the same registers you used to pass arguments to the system call (from which we learn that we should copy those values we care about before making a system call). Up til now, your only exposure to a system call has been the mysterious line at the end of your programs, namely: swi #h00900001. This is actually the form of the exit system call for linux (and yes, there is a correlation between it and the System.exit() function in Java). Now, you're going to learn about two new system calls. These are for doing single-character I/O. SWI #h00F00001 ;getc SWI #h00F00002 ;putc getc gets a single character from standard input and puts it in R0. putc takes a character in R0 and writes it out to standard output. Ex. If "Hello, world" were typed into standard input, SWI #h00F00001 would place 0x48 in R0 (that's 'H'). And, if you were to execute the following code: MOV R0, #h48 SWI #h00F00002 It would append 'H' to standard output. Another thing to note is that getc will block until input becomes available. Using these two syscalls, you will now be able to do I/O! However, you'll have to get used to a different way of doing things. In Java you're used to passing around Strings, but here in assembly land you're just given one character at a time. There are several new problems here. First there's the problem that if you want to have all the characters together so that you can treat them as one string, you're going to have to aggregate them yourself. Second, how do you know when you've reached the end of the string? Remember, you're just getting these things one character at a time, unless you somehow know ahead of time how many characters to expect, you're out of luck. The solution used in many systems is to have null-terminated strings. In this system, there is a special character (null) whose presence signifies the end of the string. The value of the null character is 0. Now consider the following code: MOV R4, #0 readNext: SWI #h00F00001 ADD R4, R4, #1 CMP R0, #0 BNE readNext What does this code do? That's right, it reads a string from stdin and counts the number of characters.