Hello, in this section we will implement processes. As we have talked about it before,
a process is basically a program in execution.
It contains program instructions,
the data the program needs, heap and stack within the address space.
Each process has its own address space.
As you can see, we have two processes in this example.
The operating system kernel resides in the kernel space of each process.
So we map the kernel space to the same physical pages where the kernel is located.
However, the user spaces of these two processes will map to different physical pages.
So the process user space saves its own program instructions and data.
The kernel space is the same among all the processes.
Ok, let’s create our first process.
We add two new files process.c and process.h. In the header file,
we define a structure process which is like process control block. The structure is used to
store the essential data of the process in the system.
So it is saved in the kernel space and user program is not allowed to access it.
The pid is the identification number of a process.
The state indicates the status of the process, such as used, initialized, etc.
Page map saves the address of page map level 4 table, when we run the process, we will switch to the current vm
.
The stack is used for the kernel code. A process has two stacks, one is for user mode
and another is for kernel mode. The stack here is used when we enter the kernel mode.
The last one is trap frame, which we will discuss in a moment.
The structure tss is used only for setting up stack pointer for ring0.
We also add attribute packed so that the items in the structure are stored without padding in it.
Then we define some constants. The stack size specifies the size of the kernel stack which is 2m in this example.
The number of processes is set to 10.
So we could have a total of 10 processes running in the system.
The next two ones are the process states. As we move along, we will add more states in the file.
OK, let's see the process.c file.
The variable process stores the important info about all the processes and most of the operations in this module
is related to this structure. Since we allow 10 processes running in the system, we define
the process table array with 10 items.
The pid num is used to allocate a new process with a identification number.
Alright, the first function we are going to talk about
is function initialize process. In this function, we will find an unused process slot in the process table
by calling function find unused process
.
We use assert to make sure that the process is the first item in the table because we just initialize
the process right now.
Ok let’s see the implementation of find unused process function.
This function is simple. All it does is loop through the process table and check the state of the process.
If it is unused, we will copy the address of the process to the variable process
and exit out the for loop.
Because we have initialized process with NULL. If there is no process available to use,
the value of the process remains NULL. Return the process and we are done.
After we find a process, the next thing we are going to do is we are going to set the process structure
by the function set process entry.
The argument is the unused process we just retrieved.
This function sets each member of the process structure. The process state is set to proc initialized
and the pid is set with value pid num. Then we increment the variable pid num for later use.
Now we get to member stack. We allocate a new page for the kernel stack. So you can see each process
has its own kernel stack.
After we check the return value, we zero the page and add the page size to the base of the new page.
This is because the stack grows downwards.
So we adjust the top of the stack to the base of next page.
It will decrement the stack pointer when pushing data on the stack.
The next few statements are for the trap frame. Remember in the trap.c file, we used this structure.
Let's open trap.c file.
The reason we have the trap frame structure in the process is that in our system, we have two entry points
when we switch from ring3 to ring0. One is through interrupts,
another is through exceptions.
Since we have handled them in the same function handler, this is actually the only entry point.
Which means the function will be called when we jump from ring3 to ring0.
In the section interrupts and exceptions handling, we have tested the code for getting to ring3
and set up tss for kernel stack pointer which is used when we jump to ring0.
In our system, the top of the kernel stack is set to the rsp0 in tss. Meaning that
when the interrupt or exception handler is called,
the stack used in this case is actually the kernel stack we set up in the process.
In order to easily reference the data, we define the trap frame structure just as we did
in the handler function, such as trap number we referenced here.
Ok let’s back to the process file.
Let's move on. The top of stack - the size of trap frame, we will get the address of the trap frame.
As you see, the trap frame is located at the top of the kernel stack.
What we are going to do next is we are going to set the data in trap frame.
We have done it before.
Remember when we get from ring0 to ring3, we push the cs
rip, ss, etc, to fabricate a scenario where we return from ring0 to ring3.
The difference is that we did that using assembly language, and now we implement it using c language.
The rip is set to 400000 and rsp is 400000 plus page size. So the code and stack of the program
are in the same page.
Next we setup kvm which creates a new kvm for this process.
This is the first process,
so we assume that it doesn’t fail.
The page map stores the address of page map level 4 table. Then we setup uvm, the arguments we pass to it
are pml4 table, the start address of the program instructions we want to copy
and the size of the program.
In this example, the program is actually this simple main function.
Ok at this point, we have one process ready to run. The function launch
will start the process.
As you see it calls set tss. The argument is the address of process. So let's take a look.
You see set tss just assign the top of the kernel stack to rsp0 in the tss. So when we jump from ring3 to ring0,
the kernel stack is used. The tss is defined in the kernel file.
So let's open
the kernel file.
You can see it is defined here. We declare it global so that we can reference it in the process file.
In the c file, we extern tss. The structure tss is what we have defined in the header file.
Alright, let’s move on.
After we set tss, the next thing we are going to do is we are going to switch vm,
the pml4 table is the table we just set up in the process structure.
At this point, we are at the process virtual space and we have copied the main function in the address 400000
.
The only thing left to do is jump to trap return to get to ring3 and run the main function.
Before we implement pstart function,
let’s check the current state of the stack.
As you see, in the set process entry function, the member tf is set to the start of trap frame structure.
And we also set these fields. As their names imply, they will be popped to the segment registers
or general-purpose registers when we execute instruction iretq.
So in the trap assembly file,
let’s focus on trap return.
As we have seen it before, we pop 15 general-purpose registers and add rsp 16 to skip the trap number and error code.
and then the stack pointer is pointing to these 5 values.
We have set them up to the desired value which means when we execute interrupt return,
we will be jumping to address 400000 and running in ring3. The top of the stack we use in the process is set to
600000,
so if we push data on the stack, the first one will be pushed on the top address of the same page and so on
.
Ok, with this in mind, what we need to do is just change rsp register to point to
the start of the trap frame when we at trap return.
Alright, we define pstart
and copy the value of rdi to rsp.
Then jump to trap return.
That’s it. Because we use it in the c file, we declare it global.
OK, back to a process file.
We call pstart and pass tf to the function. Tf is set to the address of trap frame
and now we are good to go.
Before we build the project, let’s talk a little about main function.
Normally we should print message on the screen using print function. But in this example,
it is not an option.
We know that we are running in ring3 when we get to main function.
The printk function is mapped to the kernel space which cannot be accessed in ring3.
So here we just do a test, we pick a memory address which is located in the kernel space, and write 1 to it.
This operation will fail and generate cpu exception.
So let’s open trap.c file.
In the function handler, the exception will be processed in the default here.
We print the message on screen. So we print
we print error
the trap number,
at ring the number
Remember the lower 2bits of cs register stores the current privilege level,
so here we and cs value with 3 which will preserve the lower 2bits of the value.
Then we print error code, which gives us information about the error.
The virtual address we try to access which causes the exception. This virtual address is stored
in register cr2.
So we write
read cr2,
the last one is the address of the instruction which causes the exception.
So we print rip register.
Since we use printk function to print message on screen,
we add print.h
Now, let's write read cr2 function.
We add it in trap assembly file. This function is simple,
all we need to do is mov rax,cr2 and return.
So the return value is what we want. Don’t forget to
global read cr2.
In the trap.h file,
we also add the declaration of read cr2.
The last thing we need to do is call the init process function in the c file.
So we open the main.c file and include process.h
call function init process.
Then we start the process by calling function launch.
Since we add a new module process,
we add it in the build script.
And add the process.o
to the linker command.
Ok let’s build the project and test it out.
As you see, exception 14 is generated at ring3. This is page fault exception.
The error code is 7
that is the lower three bits are all set. The bit 1(not 2) means that the exception is caused by writing operation.
And bit 2(not 3) indicates that the exception is generated when we are in ring3.
The value in cr2 register is this virtual address,
you can see this is the exact address we try to access in main function.
The address of the instruction is at 40002e which is at the page we allocated for main function.
The base address is 400000,
so the offset is 2e in the main function.
If you want to see the instruction which causes the exception, you can disassemble the kernel file
in the terminal by typing objdump
-d kernel, press enter.
So let's check the main function.
As you see, this is the start address of the main function. Remember the offset of the instruction
is 2e in the main function.
So we add the offset 2e which produces the result here. The assembly code is in the at&t format,
what it does is copy the value 1 to the address saved in rax which is this value.
So this is what we did in the c file.
OK, that's it for this lecture.
See you in the next video.