BigEd wrote:
Some interesting choices ahead! As ever, which solution is best depends on what you're trying to do.
Ultimately, I'd like to run a UNIX-like operating system. Needless to say, to support multiple processes I have to be able to sandbox them, which means hardware memory protection is
de rigeuer.
Quote:
You mention protection between tasks (in a multi-task OS) and you mention protection of I/O space from user tasks (in an OS which has a supervisory state) - for both of those, it's clear that the '816 doesn't provide it as-is, so you need some additional hardware between the address bus and the enable pins, if you're set on having those features.
In the banked form of the system, I envision having logic in the CPLD detect when a non-privileged process tries to address RAM above $BFFF. If detected, the CPLD would immediately toggle ABORT to prompt the kernel to take action. This condition is easy to detect, as the simultaneous assertion of A15 and A14 would only occur with addresses equal to or higher than $C000.
The concept would extended by designating one bank as privileged, which means it wouldn't trigger the protective mechanism if A15 and A14 were simultaneously asserted. The real UNIX kernel never allows user space to touch hardware, which in POC V2, is at $D000. As hardware requires device drivers to operate, any process that wants to access hardware in some way would have to do so via the kernel. Hence making user space access to anything above $BFFF verboten provides the required protection.
Further to this, due to the nature of the banked architecture, an instruction such as STA $1A2B3C wouldn't actually write to that address because the bank address emitted by the '816 isn't used. So all that would happen is the MPU would write to $2B3C in the bank from which the instruction was executed. Thus the possibility of one process writing into another's space is eliminated. Kernel calls can be used for inter-process communication (e.g., semaphores or shared memory) using part of the common RAM area ($C000 and higher).
Quote:
It might be that one could use the '816 more natively - using the 24-bit address space and the bank registers - and get these features. For example, you'll probably decode much of the address bus to provide your I/O decoding, so comparing Bank 0 addresses against a 7-bit task identifier could allow you to provide two 256-byte pages in Bank 0 for each task.
That's a good idea. It's only limitation is that a relatively small number of tasks could be defined. At 512 bytes per task, and discounting RAM needed for the I/O block and code executed from the MPU vectors (both of which need to be in bank 0, since that's the bank that will be selected when an interrupt occurs), no more than 96 tasks could be defined at any given time—this is based on using RAM from $000000 to $00CFFF for zero pages and stacks. The alternative would be to start swapping out sleeping processes to reclaim the space they are occupying in bank 0.
Quote:
But, you're also interested in having larger stacks. So you'd need some address ranges for each task, and that's probably out of scope for your CPLD.
Most modern languages are stack oriented, so having more than one page of RAM for a stack seems to be a good idea. In pure assembly language, it's rare to use a lot of stack, so initially I could get by with a smaller (say, one page) stack.
Quote:
Amongst your choices, I see these possibilities:
- use your banking scheme, which limits the 16-bit space for each task to 48K lumps, but allows stack to be more than 512 bytes
It also is the easiest for which to provide memory protection.
Quote:
- use linear addressing, the tasks can have much larger allocations for program and data, but limit direct+stack to 512 bytes (or more, if you allow fewer tasks)
More efficient use of RAM, at the expense of fewer tasks in core (requiring a virtual memory system to handle more tasks) and more difficult memory protection—the MMU would have to watch where each task writes in bank 0, as well as in other parts of the memory map.
Quote:
- use linear addressing, have large stacks in Bank 0 but don't worry about inter-task protections in Bank 0
Probably not a good solution with a UNIX-like OS. How would you keep user space from touching the kernel or I/O hardware?
Quote:
- perhaps some hybrid, where you map alternate banks into Bank 0 - perhaps at 32k size to allow the OS to have the rest - so a task has one or more linear 'high' banks and a privately-mapped half-bank in 0.
That's a variation on the banking I proposed.
Quote:
My suspicion is that 'large stacks' is fairly optional: you're fond of them, but probably you don't need them.
As I said, required stack size is ultimately a function of the language(s) chosen to write programs. One of the goals of this project is to eventually implement Lee Davison's EhBasic, with code added to support I/O features suitable for a multitasking environment. That means byte range locks on files, among other things. BASIC is one of those languages that makes heavy use of a stack—vidi FOR-NEXT, WHILE-WEND, etc.
Quote:
There will be many more ways of doing this. I suspect that implementing protections will be complex: you need to invent, document, implement hardware and then write an OS which makes good use of them.
I'm thinking that if I plan this right, most of the memory protection grunt work can be done inside a CPLD, and the kernel's job will be to react to an ABORT interrupt should a process try to stray from its confines. Exactly what the kernel would do when ABORT comes calling is something I've yet to formulate.
Quote:
The nice thing about programmable glue logic is that, with a bit of foresight and a bit of luck, you can build something simple in the first instance and have room for something more complex in the same board at a later point.
I suspect by the time I bring this to fruition it'll take more than one CPLD to handle the logic. The relatively simple logic I've written for the POC V2 all but maxed out an ATF2500—I've used every pin on that device. It might make sense to use one CPLD to handle the usual decoding tasks, wait-state generation, etc., and second one strictly to monitor memory accesses and step in when something goes astray.