Hi,
jal wrote:As for a "pure message passing IPC", from your posts I understand this to mean a system that has no shared state among objects, basically one thread per process? That is, at least, a system I could see that locking would normally not be needed, but perhaps not very practical in terms of both performance and memory usage.
Slow code that scales better can be faster than fast code that doesn't scale!
Imagine a small motorbike that's capable of doing a 200 Km trip in 90 minutes using 1 litre of fuel. Now imagine a massive truck that can do the same 200 Km trip in 120 minutes using 50 litres of fuel. Which vehicle is faster, and which vehicle is the most fuel efficient?
If you want to visit a friend the small motorbike would be faster and more fuel efficient, and the truck would be a waste of time and a huge waste of fuel. However, what if you need to deliver 1000 computers? With the motorbike you'd only be able to take one computer at a time and you'd need to make 1000 trips - it'd take 180000 minutes and use 2000 litres of fuel (including return trips). The "slow and inefficient" truck might be able to take 100 computers at a time, and might take 24000 minutes and 1000 litres of fuel (including return trips). The "fast and efficient" motorbike would be a huge waste of time and a waste of fuel.
For a complex process with many threads, traditional locking is a nightmare - it's easy to get locking wrong and end up with random (timing dependant) faults that are extremely difficult to find/fix, and extremely hard to write software that scales well.
For "pure messaging passing" you'd break the complex process into many separate objects. You can't get the locking wrong (there is none) and it's relatively easy to make it scale (most large super-computers with thousands of CPUs use message passing because of this).
For RAM usage, there isn't too much difference - "pure messaging passing" uses more RAM for code, etc, but that's mostly insignificant because most RAM is used by data anyway. For example, for a specific piece of software, a single threaded process might have 100 KiB of code and use 100 MiB of data, traditional locking with multiple threads might have 120 KiB of code and use the same 100 MiB of data, and "pure message passing" might have 150 KiB of code and the same 100 MiB of data.
Basically (to put it in very general and over-simplified terms) traditional locking is good until you've got more than 4 or 8 CPUs, and then it starts to suck due to scalability problems; while "pure message passing" sucks when there isn't many CPUs but doesn't have scalability problems - it's better for 8 or 16 CPUs, and for hundreds or thousands of CPUs it's a clear winner.
Now, look back in history. In the past, most (80x86) computers have had less than 4 or 8 CPUs, and in the past traditional locking has been better. Now, look into your crystal ball and tell me what you see! Most new computers have 4 or 8 (or 6) CPUs now, both Intel and AMD are planning "8 cores wth SMT" (16 logical CPUs) for their next chips, and Intel was showing off a 48-core prototype a few weeks ago. My crystal ball says that era of traditional locking is coming to an end, and a major change in the way software is written/designed is on the way.
jal wrote:All subsequent file I/O operations (read, write, seek, flush, close, etc) use the file handle in the requests and the replies. This means you can have any number of file I/O requests/replies and know exactly what each message is for.
How would you handle a series of independent read operations, e.g. "read 500 bytes from offset 1024", "read 8412 bytes from offset 23556", "read 20 bytes from offset 53847"? Of course you could include all parameters in the reply, but this may not always be practical.
Sounds entirely practical to me..
jal wrote:My advice here is: don't use messaging for the kernel API. Instead use a normal API and normal system calls (e.g. call gate, software interrupt, SYSCALL/SYSENTER, etc) for all things the micro-kernel does itself. This includes "send_message()" and "get_message()"; but it may also include things like "alloc_page()", "spawn_thread()", "get_time()", etc if these things aren't done by user-level processes/services.
Well, I'm striving for having a really, really small kernel, so basically "send_message" is the only think the kernel API offers. There's a "services" component that runs in ring 0, but that's just for the lower-level user space services like the memory manager and scheduler. It'd probably be impractical to use messages for those things, I'm still not certain how to handle.
With asynchronous messaging, the sender creates a message, the message is stored somewhere, then (later on) the receiver gets the message. How is the RAM needed to create/store the message allocated? If you use messaging to allocate this RAM, then how is the RAM needed to create/store the "allocate some RAM" message going to be allocated?
A similar problem occurs with "scheduler runs as a separate task" - you can't send a message to the scheduler telling it to switch tasks, because the scheduler won't get CPU time until you do a task switch.
Both of these things do work with synchronous messaging (where sending a message implies an immediate task switch to the message receiver) - the message isn't stored for any length of time (and the scheduler doesn't have much control over when task switches occur).
jal wrote:The important thing is that if you do implement user-level locks, they aren't any different to user-level locks in any other OS (for e.g.
futexes).
Didn't know about futexes, but they indeed seem something worth looking at. The first question that comes to mind is what a user level thread would do when failing to obtain the lock: spinning until it can get it?
There's only 3 choices: spin until the lock becomes free, put the thread into a "waiting for lock" state (where the scheduler does a task switch and doesn't give the thread any CPU time until the lock becomes free), or a combination of both (e.g. if the conditions are right then spin, else enter a "waiting for lock" state).
Spinning is faster if the lock is held by another thread that's currently running on another CPU, and if the other thread will release the lock "soon". Otherwise it's a waste of time (and can lead to live-locks and lock contention problems - e.g. all CPUs spinning on lock/s that won't be freed until other tasks get CPU time).
Entering a "waiting for lock" state sounds like lots of overhead (switching to CPL=0, doing task switches, etc), but it's a little bit like the "motorbike vs. truck" thing: "lots of overhead" might be a lot less overhead.
Cheers,
Brendan