Answers to Frequently Asked Questions About Kernel Contexts and Kernel Stacks: Gregory Kesden 11/16/00 Please note that this document presents things in reasonable detail, but does not present source code or source code level detail. Things may be left out or glossed over. This is by design -- we'd like you to understand the big picture and develop your own code. But, if you are having problems, please remember, "We're here to help." How do we create the first process? Let's assume that VM is already enabled. What this means is that the two page tables already exist. The Region 0 page table should correctly map the kernel's space. The Region 1 page table exists, but is empty. It is empty, because we haven't loaded the first process, yet. At this point, the kernel is operating in the context of some "Future Process" Remember that the kernel operates in the context of a particular process -- not in its own context. But at this point, the kernel is operating on credit -- it hasn't create any processes, yet. So, when we create our first process, we need to give it the kernel's current context, including stack. In some sense, they already belong to this future process, the kernel is just using them in advance. To create this "Future Process" we must initialize its state and make it runnable. We initialize its PCB. We copy the UserContext that is passed into KernelStart() as the initial context for this "Future Process." We associate the current kernel context and kernel stack with this process. We load the process from disk. To associate the current kernel context with the "Future Process" we need to copy the current KernelContext stack and associate the copy with the "Future Process", probably by storing it in the PCB. We also need to store the frame numbers associate with the current kernel stack and associate them with the "Future Process" -- probably by storing them in the PCB. These two actions ensure that the kernel will be operating in the context of this "Future Process." We can both copy the KernelContext and record the kernel stacks frame numbers using KernelContextSwitch() and a special KCSFunc_t(). Let's call this function ExtractContext(). ExtractContext() will be much like the function, func(), described in the handout, with a slightly different purpose: It will only use two parameters: the current KernelContext and the PCB for the "Future Process" It will return the old context, not a new context (there really isn't a new, different context, yet), so there won't actually be a context switch. It will copy the current KernelContext and associate it with "Future Process", probably by storing a copy in the PCB. It will associate the frame numbers of the current kernel stack with the current process, probably by storing them in the PCB. At this point, we have created "Future Process". So now, we can say without complication that the kernel is running in the context of the "Future Process." (Well, perhaps not so future, anymore). How do we create a second process from within KernelStart (or third, or fourth, ...)? Most groups will create two processes during KernelStart(). The "idle" and "init". Idle will be a tight loop around Pause(), whereas init will eventually start useful processes, such as a shell, and also wait for orphaned children. One of these two processes will be the "Future Process" -- probably init (Why start out running idle if there is real work that can be done?) So, Init is created exactly as above. The creation of the second process (or third, or fourth, ...) is much like the creation of the first, but there are some difference. To create "idle", we can't share the kernel stack. Instead, we need to copy it. For this we need to use a different KCSFunc_t function with KernelContextSwitch(). Let's call this function CopyContext(). This function will do basically the same thing as ExtractContext(), but it will copy the kernel stack to a new set of physical frames and then associate those frames with the new process, probably by storing them in the PCB. By copying the kernel stack, we ensure that the new process be able to provide the kernel with a unique environment within which to execute system calls and store other things associated with the process (perhaps its user context?) How does the first switch to user mode occur? Do we have to do anything special? Just because we've created processes doesn't mean that the kernel is no longer running. We will enter user-mode for the first time shortly after returning from KernelStart(). At that time, we will enter user mode in whatever process is defined by the "UserContext * frame" passed into the KernelStart() function. In other words, we select the first process to run by doing two things: a) Ensuring that the right page table is referenced by PTBR1/PTLR1 b) Overwriting the old UserContext reference by "frame" with the UserContext for the first process we want to run We have to be a bit careful here. We want to make sure that the first process to run in "Future Process". This is because the kernel is currently running in the context of "Future Process." How do we Fork() a new process? As you might imagine, when Forking() a new process, we want to do basically the same thing as we did when we statically created the second through Nth processes, as described above. Depending on the details, we probably want to call the same KCSFunct_t function, CopyContext(), as we described earlier. But there is something to consider. Is it always necessary or prudent to create an entirely new process space when forking? Or is there a way that we might be able to save all of the copying? Consider the common case of Fork() followed immediately by Exec(). This pays for a bunch of frame duplication that is never needed -- they get thrown away upon Exec(). After everything else works, you might want to consider this challenge. How do we switch from one process to another (context switch)? We may switch among processes due to blocking, timer interrupts, &c. When this happens, we need to call KernelContextSwitch() for its original purpose. Instead of using it to duplicate a context, we use it to switch between them (go figure). The handout distributed in class gives a good and detailed description of this process. The short version is this: We need another KCSFunc_T function. We'll call this one ContextSwitch(). This function should accept three parameters, not two: 1) the old kernel context 2) the old PCB 3) the new PCB This function should associate the current kernel context with the old process, probably by saving it in the PCB. This function should install the kernel stack mappings associate with the new process. (The old one doesn't need to be saved, because they don't change. The kernel stack doesn't grow or shrink -- storing them once is good enough). Return the new kernel context. You didn't mention the TLB. When do I flush it? Any time you change or remove a mapping, you must flush the TLB. Sometimes just one address, sometimes an entire region, and sometimes both. Why do we always return the kernel context and not the user context? (Isn't the user context more important?) The only time that a process is blocked is when it is in kernel mode. Because of this, when we restore it to execution, we return it to execution in the kernel context -- that's where it was when we stopped it. This way the trap or system call can finish, clean up if necessary, and then retun to user mode.