Platform Independence
Platform Independence
I've been wondering about platform independence, and how to implement it.
For example, with the x86 archetecture you have to use segmented memory (even if it's just in the most limited way possible and then you implement paging) but it's not present on many (all?) other popular archetectures. There are many other features which are platform specific which are useful (i.e. CUPID) or nesscary to implement.
So, how do you support all of them?
Is it just a case of sprinkling preprocessor directives all over your code (I'm assuming you're using C) and/or having an interface to a set of functions (i.e. CPUID) and then having several different implementations for different platforms (and you then choose the one you need)?
For example, with the x86 archetecture you have to use segmented memory (even if it's just in the most limited way possible and then you implement paging) but it's not present on many (all?) other popular archetectures. There are many other features which are platform specific which are useful (i.e. CUPID) or nesscary to implement.
So, how do you support all of them?
Is it just a case of sprinkling preprocessor directives all over your code (I'm assuming you're using C) and/or having an interface to a set of functions (i.e. CPUID) and then having several different implementations for different platforms (and you then choose the one you need)?
-
- Member
- Posts: 132
- Joined: Wed Nov 03, 2004 12:00 am
- Location: Austria
- Contact:
Re: Platform Independence
If you take a look on other architectures you may see that paging and segmentation isn't a x86 only feature, a lot of other archs are capable of paging and segmentation (i. e. DEC Alpha 3 level page tables or MIPS, ...), you may divide your kernel up into a part which is platform independent, a few parts for every arch which brings the specific arch into the stage where the platform independent part can do it's work.
simple layer view:
everything else
------------------------------------------------------------------
platform independent kernel code
------------------------------------------------------------------
specific arch code, which sets up the arch specific things
simple layer view:
everything else
------------------------------------------------------------------
platform independent kernel code
------------------------------------------------------------------
specific arch code, which sets up the arch specific things
Re: Platform Independence
Hi,
I found this difficult to start with too, because so many things seem to be a mixture of arch-dependent and independent code.
First thing: Think about the interface between the "two portions" of your kernel. The architecture-dependent code is there to abstract away the details of the architecture. For example, take memory management: The architecture-independent part should only need to see linear memory - the architecture dependent part deals with paging (has the PFE handler on x86 and sets up a flat-model GDT). The arch-independent part mar for example have a kernel heap manager Class (assuming C++). When a PFE occurs, this class has a member declared:
If this returns true, the PFE handler knows it can page in the memory.
Similarly, the scheduler can be mainly architecture independent. On x86, you push all registers and then swap stacks. The representation of a thread can simply be a pointer to a task information structure, selected and returned by your architecture independent code. Your architecture-dependent code then knows that it takes this generic pointer, passes it to ESP and then pops all the thread registers. A task (rather than a thread) could, for e.g. be represented as a pointer to a page directory (which is converted to a generic task_t pointer).
Sorry to ramble as I'm typing in a bit of a hurry, but hopefully this has given you some ideas
Oh - and have a look at how Pedigree and other OSes deal with this...
Cheers,
Adam
I found this difficult to start with too, because so many things seem to be a mixture of arch-dependent and independent code.
First thing: Think about the interface between the "two portions" of your kernel. The architecture-dependent code is there to abstract away the details of the architecture. For example, take memory management: The architecture-independent part should only need to see linear memory - the architecture dependent part deals with paging (has the PFE handler on x86 and sets up a flat-model GDT). The arch-independent part mar for example have a kernel heap manager Class (assuming C++). When a PFE occurs, this class has a member declared:
Code: Select all
bool IsValidAddress(void *ptr);
Similarly, the scheduler can be mainly architecture independent. On x86, you push all registers and then swap stacks. The representation of a thread can simply be a pointer to a task information structure, selected and returned by your architecture independent code. Your architecture-dependent code then knows that it takes this generic pointer, passes it to ESP and then pops all the thread registers. A task (rather than a thread) could, for e.g. be represented as a pointer to a page directory (which is converted to a generic task_t pointer).
Sorry to ramble as I'm typing in a bit of a hurry, but hopefully this has given you some ideas
Oh - and have a look at how Pedigree and other OSes deal with this...
Cheers,
Adam
Re: Platform Independence
Hi,
Platform independence is difficult, and has to be thought about at the design stage and all the way through the project. The thing that I found helped me was actually bringing up multiple ports right from the start. That stresses your abstractions before you actually build stuff on top of them (i.e. when they're still easy to change!)
As far as memory management goes, we have several (C++) classes that abstract away details - VirtualAddressSpace and PhysicalMemoryManager. They do what it says on the tin, really - VirtualAddressSpace has members like "map", "demap", "isValidAddress", "isMapped" etc. The difference between "isValidAddress" and "isMapped" is that "isValidAddress" merely checks if an address is valid, canonically (i.e. on x86_64, that the top X bits are either 1 or 0 - proper 48-bit sign extension). Not all addresses are valid on all architectures. PhysicalMemoryManager has functions like "allocatePage", "allocateRegion" etc (Region stuff deals with DMA regions etc.)
Each architecture subclasses those classes to provide the implementation for that arch. You can see the effect this has on our tasking code - This is the code (common to all architectures) to create a new Thread:
That's it. StackFrame::construct and InterruptState::construct are different for each architecture, but that's hidden below an abstraction layer. This works for x86, x86_64, MIPS, PowerPC and soon ARM.
Any other questions just ask!
James
Platform independence is difficult, and has to be thought about at the design stage and all the way through the project. The thing that I found helped me was actually bringing up multiple ports right from the start. That stresses your abstractions before you actually build stuff on top of them (i.e. when they're still easy to change!)
As far as memory management goes, we have several (C++) classes that abstract away details - VirtualAddressSpace and PhysicalMemoryManager. They do what it says on the tin, really - VirtualAddressSpace has members like "map", "demap", "isValidAddress", "isMapped" etc. The difference between "isValidAddress" and "isMapped" is that "isValidAddress" merely checks if an address is valid, canonically (i.e. on x86_64, that the top X bits are either 1 or 0 - proper 48-bit sign extension). Not all addresses are valid on all architectures. PhysicalMemoryManager has functions like "allocatePage", "allocateRegion" etc (Region stuff deals with DMA regions etc.)
Each architecture subclasses those classes to provide the implementation for that arch. You can see the effect this has on our tasking code - This is the code (common to all architectures) to create a new Thread:
Code: Select all
Thread::Thread(Process *pParent, ThreadStartFunc pStartFunction, void *pParam,
void *pStack) :
m_State(), m_pParent(pParent), m_Status(Ready), m_ExitCode(0), m_pKernelStack(0), m_pInterruptState(0)
{
if (pParent == 0)
{
FATAL("Thread::Thread(): Parent process was NULL!");
}
// Initialise our kernel stack.
m_pKernelStack = VirtualAddressSpace::getKernelAddressSpace().allocateStack();
// If we've been given a user stack pointer, we are a user mode thread.
bool bUserMode = true;
if (pStack == 0)
{
bUserMode = false;
pStack = m_pKernelStack;
}
// Start initialising our ProcessorState.
m_State.setStackPointer (reinterpret_cast<processor_register_t> (pStack));
m_State.setInstructionPointer (reinterpret_cast<processor_register_t>
(pStartFunction));
// Construct a stack frame in our ProcessorState for the call to the thread starting function.
StackFrame::construct (m_State, // Store the frame in this state.
reinterpret_cast<uintptr_t> (&threadExited), // Return to threadExited.
1, // There is one parameter.
pParam); // Parameter
// Construct an interrupt state on the stack too.
m_pInterruptState = InterruptState::construct (m_State, bUserMode);
m_Id = m_pParent->addThread(this);
// Now we are ready to go into the scheduler.
Scheduler::instance().addThread(this);
}
Any other questions just ask!
James
Re: Platform Independence
(OT but sort of related - could be useful for implementing other arch independent stuff in C++!)JamesM wrote:As far as memory management goes, we have several (C++) classes that abstract away details - VirtualAddressSpace and PhysicalMemoryManager.
...
Any other questions just ask!
I am interested to see that you have a PhysicalMemoryManager class - I always use flat code for my physical memory manager. Presumably this class (and VirtualAddressSpace) is brought online before you call your global constructors, so that other constructors can use new and delete. How do you get around this? The way I see it, you either have to:
- Have empty default constructors but each class has to have an "init()" function which is manually called after MM is brought online (ugly?).
- Have the above classes declared as class pointers and manually initialise them before you call global constructors (less ugly, but still not much better).
- Use loads of static member functions (not much better than using non-oop code IMHO).
- Use the VirtualAddressSpace and PhysicalMemoryManager classes before their initialisers have been called (a bit hackish and not stricly "guaranteed" to work).
Adam
Re: Platform Independence
AJ wrote:(OT but sort of related - could be useful for implementing other arch independent stuff in C++!)JamesM wrote:As far as memory management goes, we have several (C++) classes that abstract away details - VirtualAddressSpace and PhysicalMemoryManager.
...
Any other questions just ask!
I am interested to see that you have a PhysicalMemoryManager class - I always use flat code for my physical memory manager. Presumably this class (and VirtualAddressSpace) is brought online before you call your global constructors, so that other constructors can use new and delete. How do you get around this? The way I see it, you either have to:Cheers,
- Have empty default constructors but each class has to have an "init()" function which is manually called after MM is brought online (ugly?).
- Have the above classes declared as class pointers and manually initialise them before you call global constructors (less ugly, but still not much better).
- Use loads of static member functions (not much better than using non-oop code IMHO).
- Use the VirtualAddressSpace and PhysicalMemoryManager classes before their initialisers have been called (a bit hackish and not stricly "guaranteed" to work).
Adam
We stick with the first solution. No dynamic memory is used in any constructor, almost all classes have an initialise() function.
Re: Platform Independence
In which case, sorry for calling your solution ugly
Cheers,
Adam
Cheers,
Adam
Re: Platform Independence
How about simply making sure that the PhysicalMemoryManager is the first global constructor called?
Every good solution is obvious once you've found it.
Re: Platform Independence
a) This is off topic.Solar wrote:How about simply making sure that the PhysicalMemoryManager is the first global constructor called?
b) How do you propose to enforce that? The .ctors section has no such ordering.
c) Many of our classes have several stages of initialisation before they're fully functional. An example of this is the Processor class. Therefore an "initialise" function is useful.
d) What if you don't /want/ the construction code to be run at boot time? What if you want to declare an object global but have its constructor / initialisation code called later, when something it relies on is functional?
e) If it ain't broke, don't...
Re: Platform Independence
I did let this rest, but it seems the original topic has expired anyway.JamesM wrote:a) This is off topic.Solar wrote:How about simply making sure that the PhysicalMemoryManager is the first global constructor called?
We're still talking "your own OS" here, not "normal" applications, do we? In that case, you could either fudge .ctors in the binary, or the code that does the .ctors processing. I'd prefer the former.b) How do you propose to enforce that? The .ctors section has no such ordering.
You can't help the constructor being called at instantiation. As for the init(), I know it's a useful and popular pattern, but I always feel bad when clients have to "know" too much about a class. I haven't really had that problem yet, but I'd probably ponder ways to make it initializing-on-first-call. A singleton is a nice pattern, but eats some performance on each call. Perhaps one could...d) What if you don't /want/ the construction code to be run at boot time? What if you want to declare an object global but have its constructor / initialisation code called later, when something it relies on is functional?
Sorry, if I go down that road I'll probably spend the whole day tinkering with it, instead of doing productive work.
That's of course correct.e) If it ain't broke, don't...
Every good solution is obvious once you've found it.
Re: Platform Independence
We use singletons too, but a pattern that doesn't eat performance at all. Just a simple private default constructor and a static const member variable, plus accessor. It does the trick, normally. I think bluecode is planning to rewrite it a little based on an implementation he saw by Candy, however.A singleton is a nice pattern, but eats some performance on each call. Perhaps one could...
Re: Platform Independence
But you're going through a pointer to access members, no?JamesM wrote:We use singletons too, but a pattern that doesn't eat performance at all. Just a simple private default constructor and a static const member variable, plus accessor.
Every good solution is obvious once you've found it.
Re: Platform Independence
How do you suggest to access member variables, if not through a pointer?Solar wrote:But you're going through a pointer to access members, no?JamesM wrote:We use singletons too, but a pattern that doesn't eat performance at all. Just a simple private default constructor and a static const member variable, plus accessor.
Code: Select all
MyClass::instance().doStuff();
Code: Select all
MyClass myClass;
myClass.doStuff();
Re: Platform Independence
It does? I mean, instance() contains at least a check whether the singleton has been initialized. If you want to be thread-safe, that check would have to be mutexed. I don't say it's impossible to optimize that away, but...JamesM wrote:compiles down to the equivalent of:Code: Select all
MyClass::instance().doStuff();
With -O3.Code: Select all
MyClass myClass; myClass.doStuff();
Ah well.
Every good solution is obvious once you've found it.