nullplan wrote:rdos wrote:The use of timer hardware also determines if timer queues are global (PIT) or per core (LAPIC).
If you don't have LAPIC, you also can't have SMP, so I don't think the distinction matters.
SMP is just a module in my system that implements synchronization, locks & interrupts in different ways. I still run the same scheduler on both SMP and non-SMP systems, and the timer functions are not dependent on having SMP or not, rather on which hardware that exists.
nullplan wrote:
Currently, I have three OS-defined IPIs in use: One is for rescheduling, and causes the receiving CPU to set the timeout flag on the current task (as would an incoming timer interrupt). The second is for halting, and it just runs the HLT instruction in an infinite loop. That IPI is used for panic and shutdown. And finally, the third is the always awful TLB shootdown. There is just no way around it. I can minimize the situations I need it in, but some are just always going to remain.
I use the NMI IPI for panic. It will always work regardless of what the core is doing.
nullplan wrote:
Basically, your failure here is to distinguish between parallelism and concurrency. Parallelism is a property of the source code to both support and be safe under multiple threads of execution. Concurrency is then an attribute of the hardware to allow for the simultaneous execution of these threads. Now obviously, source code that assumes that "CLI" is the same as taking a lock is not parallel. In order to be parallel, you must protect all accesses to global variables with some kind of synchronization that ensures only one access takes place at a time.
Maybe. In my experience, cli/sti often was used to synchronize variables that were shared with IRQs, and when going SMP, this needs to use spinlocks. Other than that, I don't see any reason to use cli or spinlocks. If you just protect code between various part of your software, semaphores, mutexes or critical sections (or whatever you call them) is the way to achieve locking, not cli or spinlocks. The synchronization primitives themselves might need to use cli or spinlocks or scheduler locks in their implementation, but this should not be known by users.
In drivers that need to use spinlocks to synchronize with an IRQ, I have a generic function for this that translates to cli/sti on non-SMP and a spinlock for SMP.
nullplan wrote:
I've made sure from day one that my kernel code was parallel. All accesses to global variables are behind atomic operations or locks. Spinlocks clear the interrupt flag, yes, but that is to avoid deadlocks from locking the same spinlock in system-call and interrupt contexts.
rdos wrote:Perhaps the toughest issue to solve is how to synchronize the scheduler, particularly if it can be invoked from IRQs.
As I've learned from the musl mailing list, fine-grained locking is fraught with its own kind of peril, so I just have a big scheduler spinlock that protects all the lists, and that seems to work well enough for now.
My kernel originally wasn't SMP aware, but it used a few synchronization primitives that were implemented in the scheduler. It also used cli/sti to synchronize with IRQs. When I switched to SMP, I just needed to modify the synchronization primitives to become SMP safe, and remove cli/sti and replace them with the new generic spinlock function.
I have some places which uses lock-free code (the physical memory manager) where I actually wrote code that is SMP safe without the use of spinlocks or mutexes, but it's generally too burdensome to write code in this way. Not to mention that it needs to be carefully evaluated for really being safe in all possible scenarios.
nullplan wrote:
See, this is precisely what I meant. Now you have a complicated memory model where the same kernel-space address points to different things on different CPUs. Leaving alone that this means you need multiple kernel-space paging structures, this means you cannot necessarily share a pointer to a variable with another thread that might execute on a different core for whatever reason, and this is a complication that was just unacceptable to me.
Not really. Each CPU allocates it's own GDT in linear address space, and then maps the first page to it's own page which links to the core data structure. All CPUs still use the same paging structures and the same linear address space. The only difference is that if you load GDT selector 40 it will map to the unique core structure. This structure is mapped to a unique linear address and selector, which are different for each core. So, it is GDT selector 40 that is mapped to this unique address that differs between cores. Therefore, when a core wants to do something in another core's data, it can do this by loading a linear address (or a selector) from a table of available cores. The core itself can also get the linear address (or selector) from the core data itself, and when loading this, can call code that needs to operate based on the unique addresses.
nullplan wrote:
Also, FS and GS aren't ordinary registers, they are segment registers, and their only job in long mode is to point to something program-defined. So in kernel-mode I make them point to CPU-specific data. That doesn't really consume anything that isn't there anyway and would be unused otherwise.
Then you need some way to do this (effectively) every time the processor switches (or potentially switches) to kernel mode, something that slows down code.
In long mode, this might work well, but not in protected mode, and particularly not when a segmented memory model is used which might pass parameters in fs or gs.