Functions can be re-entered if some event causes execution of the function to stop, and causes another thread of execution to call the same function from the beginning.
Functions can be re-entered via
Reentrancy is best explained by an example:
For CPUs without an FPU, floating-point operations must be handled by a floating-point library. Typically, this library is not reentrant: if you use floating-point math in both the foreground routine and an interrupt-service routine (ISR), the system will either crash or give garbage results.
|
These evil effects occur because the floating-point library uses
static data. Each time the library code is called, a single
region of memory is used, either as temporary scratchpad memory, or to
simulate the registers of the missing FPU. The foreground routine starts
a floating-point operation, which puts intermediate results into the
global memory. Then an interrupt occurs, and one or more floating-point
operations in the ISR overwrite the intermediate results calcuated for
the foreground routine. When the interrupt routine returns, the
floating-point calcuations made for the foreground routine have been
corrupted.
If memory is needed by the floating-point library on a temporary basis only, it could be allocated from the stack instead. Every time the library is called, a new region of temporary memory would be created. The floating-point operation could be interrupted, and a new floating-point operation begun and run to completion, without harmful effects. |
To avoid problems with non-reentrant floating-point math:
|
In general, any routine that modifies global data (or, in C, static local data) is not reentrant. Many functions in the standard C library are not reentrant.
In software for desktop systems, where reentrancy is a concern because of multitasking, the term thread-safe is used instead.
The C functions that require an OS or BIOS can be difficult to spot. Most functions in io.h that access files fall into this category, as well as most of the stream I/O functions in stdio.h (though sprintf() is usually a happy exception). If you have access to source code for your C library, you can try looking for the macros, functions, or software interrupt instructions that are used to access OS syscalls or BIOS services.
Functions in the Watcom C/C++ standard library that require OS services. Note that many of these functions are also in the list of non-reentrant functions above.
Almost all compilers perform optimization, to make the machine-language code they generate smaller and/or faster. A common optimization is to store variables in CPU registers, rather than in memory. In embedded systems, this can be disasterous. Consider a function that writes one byte to parallel EEPROM memory:
int writeEEPROMByte(unsigned char Data, unsigned Offset)
{ unsigned char *Dst;
unsigned Await;
Dst=EEPROM_SEG + Offset;
/* if the EEPROM already has this value in it, skip this byte (saves wear) */
if(*Dst != Data)
{ *Dst=Data;
/* ... else write it and read it repeatedly until it verifies (D7 polling) */
for(Await=EEPROM_TIMEOUT; Await != 0; Await--)
{ if(*Dst == Data) break; }
if(Await == 0) return(-1); } /* timeout (failure) */
return(0); } /* success */
When compiled with DJGPP and heavy (-O2) optimization, the inner
for loop gets converted to code like this:
00001568 <L6>:
1568: 38 ca cmpb %cl,%dl
156a: 74 04 je 1570 <L4>
156c: 66 48 decw %ax
156e: 75 f8 jne 1568 <L6>
The compiler has cached the byte at Dst in one of the registers (dl).
Nothing in this loop changes either the value of dl or cl, therefore,
the first comparison will always fail. The write will ALWAYS timeout.
The compiler created this dumb code because it doesn't know any better. Tell it that the byte at Dst is volatile; that an external agent (hardware in this case: the EEPROM chip) can change the value there:
{ volatile unsigned char *Dst;
With this fix, the compiled inner loop code becomes
0000156c <L6>:
156c: 8a 01 movb (%ecx),%al
156e: 38 d8 cmpb %bl,%al
1570: 74 04 je 1576 <L4>
1572: 66 4a decw %dx
1574: 75 f6 jne 156c <L6>
Now the byte at Dst is no longer being cached in a register. Rather, it
is being reloaded from memory each time it's needed. When the EEPROM
write cycle ends, and the byte read back from the chip verifies, the
loop will exit successfully.
Aside: there are many good reasons to look at the output of your compiler and this is one of them. Others are to see how efficient the compiler is, or to see if it's generating bad code.