delorie.com/djgpp/doc/ug/interrupts/inthandlers2.html   search  
Interrupts and Handlers Part 2

       Any Questions/Comments/Criticisms/Praises/Threats, please feel free to E-Mail the author of this section, Frederico Jerónimo.
       And, why not visit his site as well where you can find other tutorials : Dark Realms
       This tutorial has been split in two due to its length. Check Part 1 as well.

Protected-Mode Considerations

       In Real-mode, which is a misnomer by the way since it should be called unprotected mode, your programs can only access the first Mb of memory (also called conventional memory). However, they can do so freely with no restrictions at all as no access checks are carried out for the code and data segments and the I/O address space. What this means is that your program can roam around unchecked and poke at dangerous memory locations which could result in disastrous consequences. Inadvertently, a wild loose pointer could erase everything that you have on your HD...

       All 80x86 CPUs up to Pentiums support the real mode for compatibility reasons. But, with the advent of the 80286 a new and advanced operating mode called protected-mode appeared, where the access of a task to code and data segments and the I/O address space is automatically checked by processor hardware. This allows the OS to run multiple programs at the same time, without letting them interfere with each other. Also, in protected mode you can access all the memory of your computer.

       In protected mode your program has complete and total freedom to roam around its address space, but when it wants to explore outside of its address space, it has to inform the OS that it is not about to do something harmful. Of course, DOS, being a 16-bit OS, adds additional complications since Djgpp programs run as 32-bit applications and the address generation in protected mode is incompatible with that in real mode. Therefore, in order to interact with DOS, a 32-bit programmer must perform a plethora of time-consuming tasks. The tradeoff is that, while in protected mode, programs can run quite a bit faster than in real mode (despite of what some say), manage memory much more efficiently, and allow the programmer to sleep at ease in the knowledge that a stray pointer won't accidentally wipe out his HD.

       I assume that if you are reading this, you're a programmer. And if you use Djgpp, you're a 32-bit programmer. And if you're a 32-bit programmer, then you need to know that interrupt handling and memory access require some special techniques in protected mode. And what are those techniques? Keep on reading.

       Note : All that follows applies to Djgpp, the freeware 32-bit compiler. However, most of the notions can be used with other DPMI compilers.


1) Interrupts in protected mode

       As I told you before, the 80286 and later processors can operate in protected mode (although the 80286 cannot switch back to real mode which is an important feature as we will soon find out), in which case the interrupt handling is somewhat different. First of all, the interrupt table consists of eight-byte descriptors instead of four-byte addresses and need not be located at physical address zero, nor contain the full 256 entries.

       Secondly, working with the interrupts themselves gained a whole new meaning. Let's take a look.

       - Software interrupts : Calling real-mode DOS or BIOS services from protected-mode programs requires a switch to real-mode, so the int86 family of functions should reissue the INT instruction after the mode switch. However, if the interrupt needs pointers to buffers the 'int86()' has to move data between your programs and low memory to transparently support these services. This means, it has to know the layout and the size of the buffers involved. While 'int86' supports most of these services, it doesn't support them all. For those it doesn't support, you'll have to use the '_dpmi_int()' function and set up or use an existing buffer (like the transfer buffer) in conventional memory.

       - Hardware interrupts : Hardware interrupts can occur when the processor is either in real mode (like when your program calls some DOS or BIOS service) or in protected mode. When you program runs under a DPMI host, all hardware interrupts are caught by the DPMI host and passed to protected-mode first; only if unhandled, are they reflected to real mode. How is this accomplished? When there is a switch to protected mode, the dpmi host ignores the normal interrupt vector table used in real mode that lies at address 0000:0000 and sets the 386+ IDT (Interrupt Descriptor Table) register to a vector table somewhere else in memory (and not necessarily at physical address zero). In fact, this is one of the main differences between the IVT and the IDT. Instead of being stuck at physical address 0, the protected-mode IDT can float around in the linear address space with absolute freedom (although it is possible to change the address of the IVT while in real-mode, it is incompatible with the implementation of the 8086 processor). The linear address of the IDT is determined by a value set into the IDTR register using the LIDT instruction. Each entry in the IDT is 8 bytes long (as opposed to the 4 bytes entries of the IVT) and can contain one of three gate descriptors :

       The DPMI host also keeps the addresses of the various protected-mode handlers in a 'virtual interrupt vector table'. When an interrupt comes in, the DPMI host scans this table and two things can happen:

       1) The interrupt number is claimed by a protected-mode function : The program switches to protected-mode (if need be) and the DPMI host simulates the interrupt.

       2) The interrupt number is not claimed by a protected-mode handler : The program switches to protected-mode (if need be) and the DPMI host's default handler after verifying that no protected-mode handler applies simply switches the CPU into real mode and re-issues the interrupt, so that it can be serviced by the original real mode owner of the interrupt.

       Want an example? Since I'm not feeling very imaginative right now, let's consider the example given by Alaric B. Williams in his document about interrupt handling in Djgpp (see the reference section) that deals with an interrupt that is vital to game programmers : The timer tick interrupt (IRQ 0, INT 8). Imagine that your application (let's say a game, shall we? ) is running in protected-mode and the timer tick interrupt goes off. This is the sequence of events:

  1. The CPU stores the protected mode state and looks up the address to jump in the IDT.
  2. As expected, it jumps to the DPMI host's interrupt wrapper for INT8.
  3. The wrapper checks to see if this interrupt has been claimed by a protected mode routine.
  4. It hasn't, so the wrapper switches to real mode, and jumps to the address stored in the interrupt vector table entry 8, thus executing the real BIOS interrupt handler.
  5. Upon return from the BIOS handler, the wrapper cleans up after itself, returning to protected mode and restoring the machine state so our protected mode application continues to run smoothly.

       Since all hardware interrupts go through the DPMI host first, you can always get away with installing only a protected-mode handler. However, as you can see by the previous example, a lot of switches between protected-mode and real mode can occur and if the interrupts happen at a high frequency (like for instance more than 10Khz), then the overhead of the interrupt reflection from real to protected-mode might be too painful, and you should consider installing a real-mode interrupt handler in addition or in replacement of the protected-mode one. Such a real mode handler will be called BEFORE the interrupt gets to the DPMI host and will treat the interrupt entirely in real-mode.

       How about software interrupts? Most software interrupts always require a switch to real mode. However, 3 software interrupts are special, in that they also get reflected to a protected-mode handler. Those interrupts are : 1Ch (the timer tick interrupt as presented in the previous example), 23h (keyboard break interrupt) and 24h (critical error interrupt). This means, that to catch these interrupts, you need to install a protected-mode handler only. Unlike hardware interrupts, it doesn't make sense to install dual PM and RM handlers for these software interrupts. Therefore, if you wanted to install a timer handler instead of using the BIOS default one as portrayed in the example, you must treat it as a protected-mode handler. Check the example file to see how it's done.



2) Handlers in protected mode

       Operating with handlers in protected-mode is, as you might expect, a bit trickier than in real-mode. First of all, an interrupt handler is always entered with the interrupts disabled (the interrupt gate is activated). This is quite important and often eludes some people. As with interrupts, let's differentiate two different types of handlers :

       - Software interrupt handlers : When you have a real-mode service that needs to call a real-mode function, you may have to build a user-defined software interrupt handler yourself. But since Djgpp runs in a protected-mode environment, you should wrap your protected-mode handler with a real-mode stub that'll be recognized by the previous service. To this end, create a '_go32_dpmi_seginfo' structure, assign its 'pm_offset' field to your handler as well as its 'pm_selector' field to '_my_cs()' and call either '_go32_dpmi_allocate_real_mode_callback_retf()' or '_go32_dpmi_allocate_real_mode_callback_iret()' as required by the real-mode service you want to hook. Now that you've called a small assembly routine to handle the overhead of servicing a real-mode interrupt, you can pass the "segment" (the 'rm_segment' of the 'go32_dpmi_seginfo' structure) and the "offset" ('rm_offset') it returns to the service you want by calling '__dpmi_int()'. Don't forget to restore the original handler on exit. A very useful application of this approach is when building mouse handlers using the mouse driver's 0Ch function. Want some example code? Please refer to my soon-to-be mouse handler tutorial. In the meantime, take a look at this very schematic example and modify it to fill your own needs :

       - Hardware interrupt handlers : Even more fun and "hair-consuming" (read as premature boldness...) than the previous. First of all, you'll have to consider how the handler you're about to build will affect the system (and vice-versa) because the optimal setup depends on several factors such as interrupt frequency, amount of processing required, raw desire for speed, and how much time and hard work you're willing to sacrifice. It's up to you to decide what best suits you but I'll be happy to provide a few pointers.

       As you recall, hardware interrupts can occur at any time, be it in real-mode or in protected-mode. We've also seen that this usually originates time-consuming mode switches. 'What?!? How do you expect me to build my ultra-hyper-mega blaster 3D engine that will really beat the crap out of quake3 and alikes if protected-mode is so SLOWWWWWW?' No need to start whining because you've several different solutions at your disposal to compensate that handicap. In the end, your protected-mode programs will run faster than any real-mode ones you've come up with. Do you doubt me? Well, if your program is really hungry for speed you can install a protected-mode interrupt handler, or place a real-mode handler in conventional memory that will be called BEFORE the interrupt gets to the DPMI host, depending on where your application spends the most time. If it's a DOS I/O bound program, the real-mode handler will run faster as no mode switch is necessary. On the other hand, if the application mostly swims in protected-mode waters, you can write a true protected-mode handler that, as you can easily guess, will execute sooner than its real-mode brother. Also, if you're feeling really adventurous or are forced by the circumstances, you can hook an interrupt with both protected-mode and real-mode handlers but be sure to hook the protected-mode one first as otherwise it will modify the real-mode one. Also, you should know that some DPMI hosts don't allow you to hook the real-mode interrupt and some call both handlers no matter what.

       Now for the serious stuff. Let's look at the various ways of installing a protected-mode hardware interrupt handler.


       - Installing a protected-mode hardware interrupt handler : The info docs included in Djgpp state that you should always write your handler in assembly to be bullet-proof. However, I don't quite agree with that. Most of the time, unless you need special control over the stack for instance, you can get away with a handler written entirely in C. That is, if you don't forget to lock all data that can be touched by your handler during interrupt processing. But more on that a little bit later. For now, let's see what steps to follow if you wish to install a C protected-mode handler :

  1. Initialization : Define 'new_handler' and 'old_handler' as '_go32_dpmi_seginfo' structure variables.
  2. Store the original handler you want to replace so it can be restored on program exit : This is done by calling '_go32_dpmi_get_protected_mode_interrupt_vector()'. This function puts the selector and offset of the specified interrupt vector into the 'pm_selector' and 'pm_offset' fields of a '_go32_dpmi_seginfo' structure pointed to by its second argument. You should save this data into the 'old_handler' structure variable so you can restore the original handler on exit.
  3. Create a small assembly routine to handle the overhead of servicing an interrupt : Set the 'pm_offset' and the 'pm_selector' field of the 'new_handler' structure variable to the address of your function and to '_go32_my_cs()' respectively. Pass this structure to '_go32_dpmi_allocate_iret_wrapper()'. The 'pm_offset' field will get replaced with the address of a small assembly function that handles everything an interrupt handle should do on entry and before exit (and what the code Djgpp generates for an ordinary C function doesn't include). The effect is similar to the 'interrupt' or '_interrupt' keyword found in some real-mode compilers.
  4. Rebound ? : If you merely want to chain into the interrupt vector and still return to the previous handler, call '_go32_dpmi_chain_protected_mode_interrupt_vector()' instead of the previous function. This will set up a wrapper function which, when called, will evoke your handler, then jump to the previous handler after your handler returns. Follow the same steps as above putting the address of your handler into the 'pm_offset' field and the value of '_go32_my_cs()' into the 'pm_selector' field of the 'new_handler' structure and pass a pointer to it to this function. Ignore the next step in this case as you've already called your handler and jump right in to step 6.
  5. Set your own protected-mode interrupt handler : If you don't want to chain to the previous handler, you need to call '_go32_dpmi_set_protected_mode_interrupt_vector()' with the address of the 'new_handler' structure variable. And you're ready to do some serious damage...
  6. Clean-up the mess : When you're done with the new handler, restore the old one by calling '_go32_dpmi_set_protected_mode_interrupt_vector()' with the 'old_handler' structure variable as its second argument. Also, remember to free the allocated IRET wrapper with the 'new_handler' structure variable as an argument to '_go32_dpmi_free_iret_wrapper()' if you didn't chain to the previous handler.

       As you've noticed, you can choose to chain back to a previous handler or simply install a new one over the original handler. A good example of chaining would be a timer routine handler since you only want to perform a set of tasks over a predefined period of time. On the other end of the line, you could have a user-defined keyboard handler that we want to take over the original and assume full control at all times.

       There might be situations where C isn't powerful enough to suit your purposes. If you want to play with stacks or want to create true reentrant functions, you have to invoke the language of the Gods : Assembly. To install an assembly protected-mode handler, you should do this :

       In your C code :

  1. Initialization : Define 'new_handler' and 'old_handler' as '__dpmi_paddr' structure variables.
  2. Allocate extra memory if necessary : If your interrupt handler is a real "memory monster eating", you should allocate memory for a user-defined stack.
  3. Retrieve the original handler you want to replace so it can be restored on program exit : Call 'dpmi_get_protected_mode_interrupt_vector()' and save the structure it returns in the 'old_handler' structure variable (passed as the second argument).
  4. Lock all the memory your handler touches and the code of the handler itself (and any functions it calls) : You've several methods at your disposal. Since this is an important issue, I devote an entire sub-chapter to it. Please check the locking section in this document for more information.
  5. Set your own protected-mode assembly interrupt handler : Finally, call '__dpmi_set_protected_mode_interrupt_vector()' passing it a pointer to the 'new_handler' structure variable filled with the value returned by '_my_cs()' in the selector field and the address of your assembly handler wrapper function in the 'offset32' field.

       Now that you've set everything you need in your C code to install your handler, it's time to create an assembly wrapper function that will perform all the chores required by the DPMI host and when that is done call the 'true' handler. Please keep in mind that it's the address of this wrapper function you should pass to '__dpmi_set_protected_mode_interrupt_vector()' and not the address of the handler itself.

       In your assembly wrapper code :

  1. Explicitly save all the segment registers : Segment registers and the stack pointer are not passed between modes. This means that the contents of the segment registers after a mode switch are undefined (the single most important exception is the cs register that is ALWAYS loaded with the code segment). This also means that you should push all segment registers (ds, es, fs, gs) into the stack, one by one, before messing with them in the next step. You should also push all other registers with a 'pusha' instruction.
  2. Assign a valid value to the previous registers : We don't want to play with handlers and have our segment registers undefined. We REALLY don't or we're in for some pain... Therefore, we should set each segment register to '__djgpp_ds_alias', a data selector that is always valid. And that's one less thing to worry about.
  3. Play with the stack, if you need or want to : The DPMI host automatically supplies a valid real mode stack but if necessary, you can set up your own stack at [SS:ESP] using the memory allocated in the C code. Just remember to save the original stack and restore it on exit.
  4. Call the assembly handler (at last!) : Finally, party time! Remember that the handler may return with an 'IRET' or a simple 'RET' depending on the type of service it calls. Be prepared to deal with both in your programming career.
  5. Restore the original stack if need be : If you used your own stack, be sure to restore the previous one or else...
  6. Restore the segment registers : Pop them from the stack. They are no longer needed. Ah, always restore the original stack (if you installed one yourself) BEFORE doing this if you want to avoid some unpleasant situations. Also, use the 'popa' instruction to retrieve the other registers.
  7. Explicitly issue the 'STI' instruction before 'IRET' : As in real-mode, hardware interrupt handlers are called with virtual interrupts disabled and the trace flag reset. In systems where the CPU's interrupt flag is virtualized, 'IRET' may not restore the interrupt flag. Therefore, you should execute a 'STI' before executing 'IRET' or else interrupts will remain disabled.

       In your assembly handler code :

  1. Just do it : Not much to say. All the hard work has already been done. Typically, you should preserve the stack caller's frame, disable interrupts, save all important registers, do what the handler is supposed to do, acknowledge the present interrupt, restore all the necessary registers, reenable interrupts, reset the stack caller's frame and return with either a 'IRET' or 'RET' as required by the service controlling your handler.

       Finally, one last issue needs to be addressed. The interrupt handler or the wrapper function needs to acknowledge the interrupt by sending an EOI. Usually, you only need to send a non-specific EOI to the PIC controller at the end of your routine with something like 'outportb(0x20,0x20)' (C) or 'out 0x20, 0x20' (Intel's asm). However, if priorities are shifted during the execution of the interrupt handler or if you're running in the special mask mode (see the section about the 8259A PIC for more information), you should send a specific EOI instead to set the right bit corresponding to the handler. This doesn't happen very often.

       - Installing a real-mode hardware interrupt handler : Relax. This a lot easier than the previous. There are only two things to consider in this case : If the CPU is running in real mode, your handler will be called before the interrupt gets to the DPMI host so it must be written in assembly and located in conventional memory ( below 1Mb mark ) which is something that hard-core real-mode programmers understand quite well... However, if the CPU is in protected-mode and you want to hook an interrupt with both RM and PM handlers you must hook the PM interrupt first, and then the RM one (because hooking the PM interrupt modifies the RM one). This means that you must make sure the interrupt won't be processed twice if both handlers are installed, using for instance some semaphore variable for mutual exclusion control.

  1. Initialization : Define 'new_handler' and 'old_handler' as '__dpmi_raddr' structure variables.
  2. Retrieve the original handler you want to replace so it can be restored on program exit : Call 'dpmi_get_real_mode_interrupt_vector()' and save the structure it returns in the 'old_handler' structure variable (passed as the second argument).
  3. Allocate some conventional memory : Call '__dpmi_allocate_dos_memory()' and allocate sufficient conventional memory to hold your handler. Save the segment the function returns in a variable.
  4. Set the handler in conventional memory : Put the code of your handler in recently allocated dos memory with the help of the 'dosmemput()' function. You could also call '__dpmi_allocate_real_mode_callback()' instead, but that would cause a mode switch on every interrupt, which is what we're trying to avoid. Otherwise, why bother installing a real-mode interrupt handler?
  5. Set the new handler : Use the variable that contains the address returned by '__dpmi_allocate_dos_memory()' to fill the 'new_handler' structure variable ( the lower 4 bits into 'offset16' field, the rest into 'segment' field), then call '__dpmi_set_real_mode_interrupt_vector()'.
  6. Clean-up the mess : When you're done with the new handler, restore the old one by calling '__dpmi_set_real_mode_interrupt_vector()' with the 'old_handler' structure variable as its second argument. Also, free the conventional memory allocated by calling '__dpmi_free_dos_memory()'.


3) Locking memory to prevent page faults

       With its 32-bit offset registers and 16-bit segment registers the i386 has a logical address space of 64Tbytes per task. Because the base address in the segment descriptors is 32 bits wide, these 64Tbytes are mapped to an address space with 4 Gbytes at most (2^32). The combination of the segment's base address and the offset within the segment leads to a so-called linear address. This means that memory is stored in a linear fashion. Thus, with a larger address you find the memory object higher up in memory. However, the segment selector, or if you prefer the segment number, doesn't indicate linearity. A selector larger in value may directly point to a segment at the lowest possible segment address and vice-versa.

       Because all segments must be present in physical memory, which is usually much smaller than 4Gbytes (unless you happen to own a massively parallel computer like the Connection Machine with its 65536 (!) processors), the base addresses of all the segments are also within the address space of the physical memory. With a main memory of, let's say 64 Mbytes, all base addresses are lower than 64Mbytes, which means that the complete linear address space between 64Mbytes and 4Gbytes has not been used. Actually, more than 99% of the linear address space remains unused. Loads of memory for many, many, many (you get the picture) segments...

       This is where paging comes in. Paging can be seen as the mapping of a very large linear address space onto the much smaller physical address space of main memory, as well as the large address space of an external mass storage unit (usually a hard disk). But mapping the entire i386 virtual address space onto a hard disk would be time-consuming not to mention space forbidding. Therefore, this mapping is carried out in "slices" of a fixed size called pages. In the case of the i386, a page size of 4kbytes is defined (this size is fixed by the processor hardware and cannot be altered). The Pentium chip also allows pages of 4Mbytes in addition to the previous ones. Thus the complete linear address space consists of one million 4kbytes pages (or one thousand 4Mbytes pages). In most cases, only a few are occupied by data. The occupied pages are either in memory or swapped to the hard disk. It's up to the operating system to manage, swap and reload the pages as the need arises as well as intercept the paging exceptions and service them accordingly.

       But what has this to do with locking memory? I was getting to that. But there is still one important concept called demand paging that I need to explain. Demand paging refers to the swapping of pages in main memory onto an external mass storage (usually a hard disk) if the data stored there is currently not needed by the program. If the CPU wants to access the swapped data later on, the whole page is transferred into memory again and another currently unused page is swapped. Therefore, as it was seen earlier on, a much larger virtual address space than that actually present physically can be generated.

       So, what seems to be the problem? Well, the problem can be the demand paging itself. If, during the process, the paging unit determines that the required page table or the page itself is externally stored (swapped), then the i386 issues an interrupt 14 (0eh). Normally, this offers no trouble whatsoever as the operating system (or in our case the DPMI host) can load the respective page or page table into memory and resume its operation. However, this should not be done during the execution of an interrupt handler because DOS is a non-reentrant operating system. If an interrupt handler, any other functions it invokes or any variable it touches is swapped out then a page fault (exception 14) might be issued and your program goes to the little fishes...

       The solution, as you may have guessed, is to 'lock' the memory regions that must be available, telling the DPMI host to keep them in active memory at all times. This can be done with the Djgpp library functions '_go32_dpmi_lock_code()' and '_go32_dpmi_lock_data()'. How do you use them? It depends on the circumstances.

       1) Locking data :

       If you only need to lock a static variable, it's pretty easy. Just use :

       If you want to lock a dynamically allocated memory block, you must know its size and you mustn't forget to lock the pointer variable, as well as the data itself :

       2) Locking code :

       Since we can't 'sizeof()' a function we'll have to use a trick :

       Just declare a dummy function after your handler routine and use pointer arithmetics to deduce the code size of your handler function.

       It would really be painful to write all this every time you need to lock code and data so I recommend you use the following set of convenient macros present in the Allegro library by Shawn Hargreaves :

       The first macro is used like so :

       This will create a dummy function named int_handler_end.

       LOCK_VARIABLE is pretty self explanatory.

       LOCK_FUNCTION(int_handler) expands to '_go32_dpmi_lock_code(int_handler, (long)int_handler_end - (long)int_handler);', thus saving you some time.

       Instead of locking the necessary memory regions, you might consider disabling virtual memory to make sure your program doesn't page. To accomplish this, either set the '_CRT0_FLAG_LOCK_MEMORY' bit in the '_crt0_startup_flags' variable, or use CWSDPR0 or PMODE/DJ as your DPMI host. This is a good way to start writing a program that hooks interrupts since it really eases out debugging. After you make sure your basic setup works, you can proceed to an environment where paging might happen. Please note that '_CRT0_FLAG_LOCK_MEMORY' is only recommended for small programs that run on a machine where enough physical memory is always available because the startup code in DJGPP currently doesn't test if memory is indeed locked, and if there is not enough physical memory installed service your program needs, you can end up with unlocked or partially unlocked memory, which will surely crash your program. If you want to make sure all memory is locked, use a DPMI server which disables paging (with all the inconveniences it brings).



Other Stuff

       We covered a lot of ground already but we're not over the hills yet. Some issues still need to be addressed, even if in a light-hearted approach. The first thing we need to consider is that ill-conceived and mythical problem of reentrancy and all the consequences it brings about (and why it's so badly understood...)

       Another thing to take in consideration is efficiency. I've told you before that interrupt handling is really important (read fundamental). But don't take my word for it. We'll study a few situations where alternatives will be presented. You'll find out that in most cases an interrupt driven system is usually superior despite the added complexity. However, not always. For some systems, alternative methods provide better performance.



1) Reentrancy Problems

       What is reentrancy? In order to answer this question I'll dispel a myth. Reentrant functions, AKA "pure code", are often falsely thought to be any code that does not modify itself. Too many programmers feel if they simply avoid self-modifying code, then their routines are guaranteed to be reentrant, and thus interrupt-safe. Nothing could be further from the truth.

       In order to make this easier, think what would happen if you enable interrupts in the middle of an ISR and a second interrupt from that device comes along. Well, this would interrupt the ISR and then reenter the ISR from the beginning. Many applications do not behave properly under these conditions. Those that do are said to be reentrant. The others are called nonreentrant. Therefore, a function is reentrant if, while it is being executed, it can be re-invoked by itself, or by any other routine, by interrupting the present execution for a while.

       And now enters another deeply ingrowing thorn : "I don't need to worry about reentrancy—I'm not writing multithreaded code". Big mistake! As explained earlier, by reentrant we're considering code that reenters itself. It's true that reentrancy was invented for mainframes running in multithreaded environments, in the days when memory was a valuable commodity. But it's far from being restricted to that nowadays.

       How do I know if my function is reentrant? A function must satisfy the following two conditions to be reentrant :

       1. It never modifies itself. That is, the instructions of the program are never changed. Period. Under any circumstances. Never is never. Far too many embedded systems still violate this cardinal rule.

       2. Any variables touched by the routine must be allocated to a individual storage place. Thus, if reentrant function XPTO is called by three different functions, then XPTO's data must be stored in three different areas of RAM.

       Ok, by now you must be drooling for an example. Let's pick a certain type of functions that are really asking for reentrancy problems : Recursive functions. Getting past the traditional factorial function (n!), take a look at the following very simple recursive function, taken from an example in the Borland C++ Programmer's Guide :

       This function is reentrant. It can be interrupted at will. Now suppose, we'll be using in our program the following equivalent function, where exp is now defined as a public variable, accessible to many other functions :

       This function is no longer reentrant. Why? What happened? Imagine if this function was called by, say, main(), and then an interrupt which calls the same function came along while it is executing, it will return an incorrect result. The variable exp is fixed in memory; it is not unique to each call, and is therefore shared between callers, a disaster in an interrupting environment.

       "But a part of my program cannot absolutely, positively be interrupted!" Fear not. Here comes a critical region or critical section to the rescue. What is a critical region? A critical region is that section of the code where the program must not be reentered while executing. Actually, this is a rather simplistic definition, especially when it comes to mutual exclusion and semaphores in multitasking systems, but one that suits our immediate needs. How to avoid reentry in these regions? Simple, just disable interrupts (with a cli instruction for instance). "Wait a minute! Doesn't this mean that my program is no longer reentrant?" Not necessarily. Most programs, even those that are reentrant, have various critical regions. The key is to prevent an interrupt that could cause a critical region to be reentered while in that critical region. The easiest way to prevent such an occurrence is to turn off the interrupts while executing code in a critical section. Overall, a program, if reentrant, will retain its status throughout most of its code and in the critical regions, since interrupts are disabled, there's no fear of errors creeping in.



2) Interrupt Driven I/O vs Polling

       In a polling system the CPU continually test the status of an I/O device in order to check whether an I/O operation is complete or necessary. In a direct contrast, with an interrupt-driven system the CPU can continue to process instructions and is notified when some I/O activity occurs. This is generally much more efficient than wasting CPU cycles polling a device while it is not ready.

       I would not like to dwell much longer on this subject but rather point you to two other documents I wrote that approach these issues in detail : The mouse poll and the mouse handler tutorials (this last one will be available soon).



3) Interrupt Latency

       As a start, some of the terminology which will be necessary for the next couple of sections will be explained :

       - Interrupt latency : Time that elapses between the point a device signals that it needs service and the point where the ISR provides the needed service.

       - Interrupt response time : Time that elapses between the occurrence of an interrupt and the execution of the first instruction of that Interrupt Service Routine (ISR) by the CPU.

       - Interrupt service time : Time it takes the ISR to service the interrupt.

       - Interrupt frequency : How many times per second (or any other time measurement) a particular interrupt occurs.

       Now that we're cleared on this, let's dirty our hands. In response to an interrupt, the following operations take place in a microprocessor system while in Real Mode as indicated below. However, a very similar procedure would be followed in protected mode except that the Task switch may be necessary and the privilege levels may cause the instruction executions to take longer to execute than in normal real mode operation.

  1. An interrupt request (INTR) is issued (start of interrupt latency & interrupt response time).
  2. Recognition of the INTR by the CPU (end of interrupt latency).
  3. The processing of the current instruction is completed.
  4. Micro-code of the CPU Core executes the INTR.
  5. Get INTR vector from the 8259A PIC.
  6. Branching to the ISR while saving the microprocessor state.
  7. First instruction of the ISR executed (end of interrupt response time & start of interrupt service time).
  8. Continue with interrupt service routine.
  9. Restore the saved status of the microprocessor and return to the instruction that follows the interrupted instruction (end of interrupt service time).

       Sometimes, interrupt latency is more important than interrupt service time. For example, consider a device that interrupts the CPU in 5 seconds intervals but can only hold data on its input port for about a millisecond. In theory, 5 seconds is more than enough for interrupt service time; but the incoming data must be read within 1 millisecond to avoid loss.

       Therefore, having a quick recognition of the interrupt by the CPU with an interrupt acknowledge cycle (that is, a low interrupt latency) is very important if not even critical in many applications. Indeed, in some applications the latency requirements are so strict that you have to use a very fast CPU or you have to abandon interrupts altogether and go back to polling with all its inconveniences.

       You can easily measure the best case interrupt latency using our friend the 8254 timer chip. Doing this is beyond the scope of this article, so consult a reference manual (see the reference section for further details) or give me a ring.



4) Interrupt Service Time

       Let's finish what we started. It's now time to brief you on interrupt frequency and interrupt service time (if you're unsure of these two definitions, please refer to the previous section). Usually, these two factors control most of the ISR's impact on system performance.

       Of course, the frequency is completely dependent on the source of the interrupt. For instance, the timer chip (8253 or its successor 8254) generates evenly spaced interrupts about 18 times per second. A serial port at 9600 bps generates more than 100 interrupts per second. When compared to the keyboard that generates at most 25 interrupts per second at an irregular rate, you can easily see there is no clear frequency pattern.

       And now we focus on the core of the issue : Interrupt service time. Since this represents the time it takes for the ISR to execute its instructions and since quite often it's up to the programmer to create those instructions (see the Handler sections of this document), it becomes quite a critical area of interest. One thing above all others affects the interrupt service time : The number of instructions being processed by the ISR (obviously). But the interrupt service time is also dependent on the particular CPU and clock frequency. This is quite logical since a powerful PC usually executes the same ISR faster than its slower brother.

       In order to measure the true impact an interrupt will have on system's performance you'll need to multiply the interrupt service time by its frequency. Remember, every CPU cycle spent in an ISR is one less cycle available for your application programs.

       So, what's your goal as a programmer? Yes, you guessed it. You'll want to build solid and fast ISRs that have minimum influence on system performance. This is one of the main reasons why handlers for DOS are still being written in Assembly.

       There's one last thing that has been left unsaid. Imagine that you've been assigned to an important software project that required the implementation of an interrupt handler. After careful deliberation and calculation you've come to the conclusion that no more than 15% could be spent on ISRs. You get to work and after many months of hard labour you produce a program where your handler consumes only 10% of the overall CPU cycles. A big and wild party follows and in the morning, while trying to survive a terrible hangover, you find yourself penniless, bewildered and without a job. What did I do to deserve this????? Well, you forgot one essential rule : Your ISR is probably not the only one running on the system. While your ISR is consuming only 10% of the CPU cycles, there may be another ISR that is doing the same thing; and another, and another, and… There goes the overall 15%! Of course, if you are designing an embedded system, one in which the PC runs only your application, you don't really give a damn whether your handler must coexist with other ISRs and applications.

       Ah... yes, the story was a bit far-stretched but you get the picture... :)



Conclusion

      We've gone through quite a while indeed. We covered interrupts and their meaning and with this knowledge in our baggage we proceeded to handler land, where ISRs could hide their secrets no longer. Tired with so much software information, we turned our minds to the more earth to earth components of our PC and their direct connection with interrupts. With this extra motivation, we ventured into the almost arcane mysteries of protected-mode interrupt programming and resisted the onslaught of endless examples. Finally, we discussed such important concepts as interrupt latency and reentrancy. Not too bad, don't you agree?

      This was a quite generic tutorial and we covered a lot of ground here. You can be sure that at least some of this knowledge will come in handy if you spend enough time programming. Interrupts and handlers will keep on confusing programmers for quite some time, mostly because of the debugging difficulty they originate (you can never be too sure where the error occurred). So, if you must fight them, take some time to study your enemy and victory shall be yours!

      This is a very long document, and, despite careful proofreading, errors are eagerly waiting to show their ugly little faces. Great care has been taken to prevent them but if you spot any mistakes, or wish to provide some feedback, please email me. Thanks in advance.



Examples

      The following zipped file contains practical and useful examples of how to manipulate interrupts and handlers. In fact, this download represents the third part of this article, and if you've read this far, you must get it as soon as possible. I'll even throw in an added bonus : An entire section covering memory handling in Djgpp. All examples require Djggp and/or NASM and were compiled using Djgpp version 2.03 and NASM version 0.98.

      Just download the zip file and extract all files to a directory of your choice. Then, follow the instructions present in the readme.txt file.

Examples V1.00


Acknowledgements

      I would like to thank the Djgpp and Allegro source code, the Info docs and the Djgpp faq for all the insight they provided. Gratitude is also expressed to Peter Johnson for graciously allowing me to inspire from some of his code, to Shawn Hargreaves for building such a great programming library (Allegro) and for answering my various questions and to Eli Zaretskii for maintaining an amazing Djgpp faq list and for providing precious help and feedback.



References

[ Go to Part 1 ]


By Frederico Jerónimo, Copyright 1999-2000

Click here to view this tutorial's revision history

  webmaster     delorie software   privacy  
  Copyright © 2000     Updated Dec 2000