conybear@moncsbruce.oz (Roland Conybeare) (02/11/88)
I have received several responses to my posting about stack sniffer problems when multitasking on the Macintosh. This posting summarises the information I have learnt. Thank you to all the people who wrote to me, particularly Paul Mercer and Bryan Stearns from Apple, who entertained several exchanges of E-mail. Quick summary: there's more to getting tasking to work on the Mac than just disabling the stack sniffer. Normal operation of the Macintosh depends on the following model of the Mac's memory being correct: +----+ | | | | +----+ <-- SP | | +----+ <-- StkLowPt ($00000110) | | +----+ <-- HeapEnd | | | | +----+ The sniffer invokes a system error if SP < StkLowPt. Normally, StkLowPt will be moved by the Memory Manager when the heap expands. However, I understand that *if* StkLowPt is 0, the sniffer is *disabled* and the Memory Manager will no longer alter StkLowPt when the heap changes size. The sniffer can be re-enabled by putting -1 in StkLowPt. However, we are not out of the woods yet! QuickDraw expects HeapEnd to be less than SP. Some QuickDraw operations (I don't know the complete subset, but certainly CopyBits and region operations) divvy up memory between HeapEnd and SP into buffers. Note that this is not the same as just using lots of stack space. To call QD without error, *all* memory from HeapEnd to SP must be unused. A consequence of QD's behaviour is that we cannot blithely move SP around, even if we promise to keep it out of the heap, if we also want to call QD. Now for some implementations. There are a number of approaches I know of for making tasking work, as follows: Avoiding the Stack Sniffer: (1) we allocate space for each task's stack on the heap (this is known as a 'cactus' stack). Tasks may release stack space, leaving 'holes'. The memory in these 'holes' becomes available again when the lower limit of the stack moves up to expose the hole again. This approach avoids the sniffer entirely, but we can't call QD from such a stack organisation. (2) allocate each task's stack on the heap. Now the community of tasks can be dynamic, although the heap may well become fragmented, since it is difficult to make stacks relocatable. This scheme can be made to work by storing zero in StkLowPt, disabling the stack sniffer. We still need to call QD from the 'system' stack. Calling QD correctly: (3) we could have one process permanently residing in the system stack, said process taking responsibility for all QD calls. The trouble with this is it will also have to handle any other toolbox call which uses QD. We will pay a high price for this service, that is, two context switches of 20-30 instructions each, per toolbox call. (4) Even better, we reserve stack some stack space above HeapEnd for calling QD. To make a toolbox call, we just move the stack pointer to this reserved area, call the appropriate trap, and return the stack pointer to its original value. This approach works because every 68000 compiler I have seen uses A6 to refer to local variables, and thus doesn't even notice that we have moved the stack. We have to be careful to keep enough 'normal' stack space available in this way, or we can't call QD anymore. I recall around 3K being sufficient for CopyBits... The price of (4) is simply that our code gets bigger: several instructions per toolbox call of overhead, if we do the above stack finessing inline. BTW, while the disabling of the sniffer is not documented as far as I can tell, I would not expect it to break, since Apple is reportedly rewriting the toolbox with several goals including 'better support for multitasking'. I hope this means that traps will make fewer assumptions about the Mac's memory organisation. I hope this posting helps someone else avoid the stackfalls :-) of taking the Mac to task. Roland Conybeare (conybear@moncsbruce.oz)