But if you started with (e.g.) Linux, and turned off all of the compile-time features that you could, isn't the end result very similar? Say you run your application as PID 1, etc., etc.
My point is that a fully "generalized" unikernel is actually almost the same as a standard monolithic kernel. The technical differences are 1. memory protection, 2. scheduler (maybe), 3. single binary? 4. different and less flexible tooling around runtime debugging/admin.
I mean, we all want a fully featured unikernel that boots instantly, consumes no memory, etc. But there's no reason to believe that a unikernel with all of the features necessary to run, say, a database, won't have overhead similar to Linux tuned for that same role.
(Not trying to worship Linux here, I'm just holding it up as an example that has had a lot of eyeballs over the years.)
-- "context switch" (expensive) is the switch among active (running on this CPU) tasks/threads/processes.
-- switching between kernel and userspace would be "mode switch", much less expensive and of "constant" cost (see, for example, http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-ma...).
Your phrase already carries this contradiction: "__can__ be fairly expensive" points to something depending on what is being executed, rather than a property of the mode switch itself. Then, arguably, the same need of that costly execution should also be present with unikernels?
If any people from the mentioned project are here, I'd love to hear your answer to this.
1. A Unikernel: It is supposed to be small and simple. Only containing the components it needs. And the security model is custom for the application itself.
2. A frame work to build Unikernels. This might include multiple implementations of file systems, schedulers, network stack, etc.
The framework does need to be extensive (large) in order to give Unikernel developers the most ergonomics/reuse while developing. However, each Unikernel would be "small" as you describe. Hope that helps.