 We have a YouTube page I think, I'm pretty sure. Okay, hi everyone. Now today is our fourth recitation and we will review what we've covered last week and then we will go or start with assignment two. So last week we've talked about condition variables and we all should now know that condition variables basically allow threats that are waiting for some condition to become true to go to sleep and release the luck and then once that condition becomes true they wake up and get the luck back. We've also presented two examples or use cases for condition variables. One is how to implement semaphores using condition variables and we've mentioned that the condition here going to be the semaphore count. We've also presented another example which is the producer-consumer problem where you implement now two condition variables and each one of them the producer and consumer will signal the other one and we've said also that here the condition is the buffer state whether it is full or empty. We've also presented the reader-writer locks and we've said that reader-writer locks are just simply another synchronization primitive that allow multiple threats to enter the critical section as readers while if a thread wants to enter the critical section as a writer he needs to get an exclusive lock which means nobody is in the critical section. And once that thread which enters the critical section as a writer enters the critical section then all other thread will be blocked from entering the critical section whether they are waiting to enter the critical section as a reader or writer. And we've mentioned that you need to make sure that no starvation happens in your reader-writer locks which means no writer or reader thread waits indefinitely for the critical section or for the lock to enter the critical section and one of the ways to avoid this is basically for example we allow as many readers to enter the critical section as long as we don't have a writer waiting for the critical section. Once we have a writer we then start blocking the readers, the incoming readers and put them to sleep up until all the readers in the critical section release the locks and gets out of the critical section and then we let the writer to enter the critical section and this way we avoid the writer from starving. So I assume everybody now is done with assignment one or at least locks and CV so you should be able with these two primitives to continue solving the upcoming assignments. Today we will go over the assignment two and then we will start talking about system calls in OS 161 specifically. What is a system call? We will give an existing system call which is the time. We will go through the lifecycle of that system call from the point that it is called to the point that we receive the return value and then we will also explain the interruption handling process and also argument passing because now we will have two spaces which is user space, kernel space and we need to know how should we pass arguments from user space to kernel and back to the user space. Also we will also talk about the copy and copy out which is the functions which will handle copying values from kernel to, or user to kernel and kernel to user and then we will summarize the system call lifecycle and then I will tell you how should you add a new system call. And lastly we will just go through briefly design document which is an important document from now on especially when you want to come to the office hours because now just like the assignment one the assignment two is not as simple as assignment one where the ATAs once they see your lock implementation or CV implementation they will figure out what's the problem. Since you're going to edit multiple places then it is very difficult for them to figure out how you're designing your system calls. So we ask you to design these or to write the design document and bring it with you into the office hours so this way the ATAs will know how you are approaching the problem. So assignment two overview, assignment two basically talks about system calls. You need to implement system calls and also exception handling. So these system calls if something goes wrong they need to return an error value and you need to not only implement the system call but also handle all kind of errors that could happen with that system call. So you have two types of system call that needs to be implemented. One is file system support and the other is process support. The file system support basically you need to implement open read write, LC close, W2, change directory, get current working directory and for process support you have the get process ID, fork, exec v and wait, PID and exit. So it looks a lot but you don't need to freak out because once for each type once you get how to implement one or two of them just like you will get, it will be easy for you to implement the rest of them. So it's only the first step which you need to take and figure out how to implement them. So the deadline going to be two and a half weeks from today so please start. Now operating system. One of the reasons that we have operating systems is that we need to provide abstractions to the user. That means users should be able to execute just like commands as easy as it possible. For example I want to open a file, I'll just call a function called open and the operating system should handle all the stuff from figuring out where is the file at which block and bringing that file into the memory and everything else. So the reason for having an operating system is to provide abstraction. So as we can see from the graph that the top level what we have is the users and then beneath them is the applications which is the application programs or user programs and those programs will basically call library functions. That's all what they need to do and the library functions are the ones that will deal with the operating system or call the system calls. As we can see the system call is basically the interface between the user and the kernel. So the C library for example when you call printf what happens is just like the C library will handle all the formatting but for writing into the standard output it will call the right system call for the kernel to put that string onto the screen. So as we can see the system call is the interface between the user and the kernel and from the system call and below this is what is called the kernel space and from the system call and up we have that is what is called the user space. So there are several reasons why a user or a user program would need a kernel to step in. One of them would be for example unintentional error handling for example just like dividing by zero. At this point the kernel had to step in and handles that exception. There are intentional interrupts that will call or will let the kernel step in something like the system calls or hardware interrupt or software interrupt or timers for example if we have a scheduling. These concepts will be much clear to you once we proceed. So now let's see what is a system call. A system call basically is a set of services that a user program could request from the kernel to provide. So for example the user program could ask for example the kernel to access the hard disk and retrieve some kind of information from the disk or this is what is called the hardware related services or we have software related services like create a process for me or allocate a memory. So basically as we said the system call is the interface between the operating system and the user program. And one of the reasons of providing this level of indirect communication between the user and the kernel is for the security reasons. So if we just like let the user program to directly call a system call that sometimes could make just like a security breach or just like could let the users to just like manipulate or just like maybe execute instructions or just like doing stuff that they are not supposed or not privileged to do. So this is a way to provide a security for the kernel and since the kernel when it takes control it has full control of the system as opposed to the user. User as just like we can call it a normal mood while the kernel mood is the privileged mood. So the privileged mood mean that you have full control over the system. So if you allow the user to directly have a full control of the system that could just like make trouble for you or just like security problems. So that's why we let the user first interact with the library and then the library will call the system call directly. That doesn't mean you cannot in your program call the system calls directly. No you can and this is one of the ways that you can for example after you implement your system calls you can check if your system calls are working or not by writing a program and directly calling the system calls. But the system call interface usually if you want to write a normal program it should go through the library that you are using. So there are several questions that we need to answer to understand how the system calls work. One of them is how can we trap or how can we transfer or switch context from the user mood into the kernel mood. And that's what we use the term trap for it. So we can say the user is a trap into the kernel mood. So one of the question is how we can change the normal mood into or change moods from normal to privileged from user space to kernel space. The other question is how does the kernel know that the interrupt that happens is a system call. So basically how does the user request services or ask the kernel to step in is by issuing or triggering an interrupt. That interrupt could be of different types. We could have exception interrupt that for example divide by zero. We could have hardware interrupt like pressing a keyboard. We could have a timer interrupt that is scheduling or we can have we could have system call. So system call is a type of intentional interrupt that I as a program user I want you kernel to step in and take control and for example provide me with that information. But or we have other interrupts like for example exception or divide by zero that is an unintentional interrupt that once something goes wrong then we need to let the kernel to step in and take actions. Two more questions we have also to answer is how should we pass argument into the kernel and how should we get we get the values back. For example if we call open how should we pass the file name into the kernel and once we open a file once the kernel opens the file how should we get the file descriptor back. The file descriptor if you recall from the lectures that each process has a file table from that the figure in the lectures there is a file table. So each index from that file table is called the file descriptor. It is basically an index. So each index in that file table points to a file. So when a kernel opens the file it will basically returns the file descriptor. And so this is one of the questions that we need to answer how can the kernel return a value to the user. So let's go through an example. We have currently what you have an OS 161 you have only two system calls implemented for you. One is time and one is reboot. We choose time to go through it because the reboot doesn't provide you with a return value but the time will provide you so we will take time example. And basically this is a step by step of how this system call is going to proceed. So once I call it and from that point in the library and then trapping into the kernel dispatching the system call handling that system call and then returning the value back to the user. So we will go through these files one by one. So the first file as we can see is the library function. So that's what the user will. So this is the time C so all the path are given for you here you can just like if you want you can check it now or later on. So this is the library function the time function this is what the user will have in his program so it will just call time and pass a pointer to get the time. And what the function will do the library function is calling the system call underscore underscore time and providing the function and none. Now you need to differentiate between now the user space and kernel space. So this file as we can see it's under user land lib lib C time time dot C. That means this file is in the user space but so how should the user space figure out how to handle that system call which is the time. Basically we have the build folder so this folder maybe most of you doesn't have it because what you currently compiled is only the kernel. But this build the build folder should be generated whenever whenever you compile a user land. Folder or write you write a user program and you compile it this file or this folder will be generated. Yes. So the library function user land. Oh no so they are different. So if you go into the source directory you have so what you've been dealing with is the current folder right under the source directory. Now there is another folder called user land and that's the one that has the library call and there is another folder that is generated once you compile the user land. That will be generated for you which is the build folder. How should you compile a user land. Basically what you've been compiling right now is just like going into the current compile DOM VM and then you run the be make depend be make be make install. But to compile the user land you just keep to care you just need to keep into the root of the source directory. So don't go into the current. If you run be make and be make install this file will be generated for you. So as we can see I can show it to you now. So this is the source folder. We what we used to do is just like go to current and then compile and then we go into that folder and that's what we've been using for assignment one. And then we run be make depend be make be make install. But what we need to do here is just like keep into the source folder without going into current and run build run be make and then be make install. That should now generate a file for you a folder for you which is the build folder. As you can see here so I think this is so this is the build folder that needs to generate will be generated for you. If the be make and be make install didn't work for you under the source basically just run the the command which is configure that we did and then you need to specify that option os3. And then you put the root because it needs to have the root directory which is the one that has the kernel to compile it. So just run this and then be make be make install and you should have the build directory appears in that in the source directory. So now let's continue. Okay, so now the library function makes the system call. What will happen next is the system call definition. So sorry. So as we said we're still in the user space. Now the library function makes the system call and the user space again there is a file assembly file that is called syscall.s which we'll go through now. This basically has all the system calls. So there is in that file there is a macro defined for you here that takes the the name of the system call and it will just like map it to a number to the syscall number. And all these this is all the system calls available and as mapped to their number. So for example, the time is over here. It will map it to 113. So that macro basically will call a function using the system call name as we can see that label. And then it will jump to another function call underscore underscore syscall. But before that there is an assembly instruction that will basically add the system call number into register v0 and then jump into the syscall function. So at this point this is the function that will jump here and then we will make the system call. So what will happen here? Does anybody know? Yep. No, no, no. So we are in the user space. We are executing some instructions and then at some point we will trigger and interrupt. And then, yes. It checks it. No. So basically system call that will trigger the interrupt. And that will just like trigger the kernel. Okay, kernel, please step in and take over. So this system call now. So here comes the interruption handling process. So when an interrupt is triggered, basically this is what you've covered already in the class. Three stuff that will happen. The processor will enter the privilege mode, which is the kernel mode. And then it will record all the state or the context of the last instruction as it happens in the user space. It needs to just like save all that state and then jump into a predetermined memory location, which is 0x80 million. 0x80 million, this is a memory location that from this location and on it is the kernel mode. This is where the kernel reside and it's just like whatever instruction executed at that location has a full control. So what is there in that address? 0x80 million. Basically it's an, so we triggered an interrupt. So what should be there? Something like interrupt handler. So this is where the address that it will handle the interrupt. And it is, so now as you can see, we will go into the kernel mode. So we're not anymore into the user land folder, we will move into the kernel folder. So we'll see that exceptionmips.s, this is the second or the last assembly file that we'll see today. So this is the interrupt handler. At this point what it will do is just like record the state into the, or just like you create a trap frame. The trap frame basically has all the registers, all the processor registers, and these are the ones that save the state. And once it saved the state, it starts executing. So as you can see, we are all, we are just saving or storing to the registers. And once we're done, we will call MIPSTRAP. So now we are trapped into the kernel mode. MIPSTRAP fortunately is a C function. And it is implemented in that file. Let's go back. It is implemented in general trap exception handling function for MIPS, which is under the kernel folder, architecture MIPS, local trap.c. Let's go to that file. So MIPSTRAP, if we go to trap.c, we see that this function is defined for us here. MIPSTRAP. Okay. Now we've triggered an interrupt. We saved the state. We entered the kernel mode. And now what should we do? We should know, now the kernel needs to identify what type of interrupts has been issued. Is it an exception? Is it a system call? Is it a hardware interrupt or a software interrupt? How it will do that? So as you can see, the MIPSTRAP has a pointer to a trap frame, which is a structure, as I said, has all the structure and has all the state in it. It will put it in that code variable. And then we will start checking. If code equal and hardware interrupt, then do the following. If not, then just skip and continue. If the code equal system call, then we will execute that branch of a statement. And it is equal to a system call. So what will happen is that we will call another function that's called syscall, and we will send or pass the trap frame to it. What will happen next is now we will call the system call dispatcher. This is the one that will handle the system call. Now we know that the interrupt that has been triggered is a system call. So now we need to know what type of system call it is. And this is what the system call dispatcher will do. And this is defined in syscall.c, which is that file under syscall folder, syscall.c. So this is the function that we've called in trap.c to a syscall. And now it is here. Now we are at the syscall function. So now what we will do is we will get, if you remember in the assembly code, we've stored the system call number in a register called v0. Now we will retrieve that system call number from the v0 register and put it in the call number variable. And what will happen here is basically a switch statement. It will check the system call number and based on the number it will branch. So what is implemented for us here is the reboot, as I said, and the time, because we only have two system calls implemented currently in OS 161. And here is where you need to add stuff. So all that things that happened that I mentioned, you're not going to just like implement or no. What you're going to implement is only the system call handler, how to handle a system call. And that's what you need to be done here. By adding, for example, more case statement into the switch and handling, for example, open, write, read, and exit. So now it will check the system call number. It will see that it is a time system call. And then it will call that function. So now comes the third question that I mentioned that we need to consider for system calls is how to pass arguments from user to kernel. So argument passing convention. This is all mentioned in the comments in that file, which is the syscall.c. So if you have the first four arguments that you have in the function or the library function or the system call, that are passed through the four registers, which are a0, a1, a2, a3. And these four arguments has to be 32-bit, of size 32-bit. But if the argument is 64-bit, then that means you need to use aligned registers. So you have four registers, a0, a1, a2, a3. Aligned register means if you have the first, let's say the first argument is 64-bit, then you need to use a0, a1 for that first argument. You cannot use a1, a2. It's either going to be a0, a1, or a2, a3. So just like you run out of registers, what you should do next, you need to put the remaining arguments into the user-level stack, which is sp, starting from sp plus 16. And then this is for passing arguments. And then on successful return, then we will put the return value in v0, if the return value is 32-bit, or v0, v1, if the return value is 64-bit. And the a3 register, we will use it just like to set it to zero if the system call succeed, or set it to one if the system call doesn't succeed. So let's go through examples. Now the system call time will take seconds and nanoseconds as argument. So that means, and if we see the definition for time underscore t, we will see that this is a 32-bit variable. So that means we will put seconds into a0 and nanosecond into a1. And then we will put the return value into v0, which is this time. And we will just set a3 to zero. That means success. Let's say we have three arguments open. So it will be almost the same. So we will use a0. Since all the arguments are 32-bit size, then we will use a0, a1, a2. And the same goes for the return value. Now lc takes three arguments. But the off underscore t, this is a 64-bit variable or type. Then the size of that type is 64-bit. That means so we have three arguments. The second one is 64. We will put the first one, which is 32. We will put it in register a0. And since we should use aligned registers for 64-bit variables, then the a1 will be unused. And we will place the position into a2, a3. So the high 32-bit going to be into a2. We will place the low 32-bit into a3. And the once has to be in sp plus 16. That means on the user stack, because we run out of registers. The same thing goes for return. Since the return type is of type off underscore t, which is 64-bit, then we need to place the return type in v0, v1 registers. Any questions? So what we will do next is calling that function. And this is going to be in that file, which is kernsyscall timesyscall.c. This is the sys underscore underscore underscore time. This is the function in kernel mode that we will call to get the time. So it will get the time, first it will get the time, and then it will use a copy out function. Now this is, here's another question. Why should we use copy and copy out functions? Just like indirect memory or moving values from the user space to the kernel space. Why can't we just call the function move, which moves the memory, directly to move values from the user space to the kernel and back from the kernel to the user? Anybody knows? Any idea? Exactly. So we're dealing with the user. And the user, we don't know what kind of user he is. Is he a trusted user? Does he know what he's doing? We have all these different kinds of users. So that means before moving a value from user space to the kernel space, we need to check that value. For example, let's say the user calls open and sends a null for the file name. So if we just move the null into the kernel, that means the kernel cannot handle it. So the copy and copy out functions will basically handle these cases. Just like if we have a null pointer or let's say the user calls read and then he sends the file descriptor of the file that he wants to read and he sends the address 0x80 million as the address where, after you read the file, you need to put the data into that address. And this is very dangerous. Why? Because at this address, whoever, whatever instruction you put at this address, that means that instruction or that code has a full control over your system. So if the user copies a code into that address, he has full control. So we need to prevent such cases. Or for example, let's say the user wants to read some data from an unauthorized memory location and write it into a file and then he can't just like look at it. So for example, he can't just like issue a write system call and then pass that memory location that he's not authorized to access and write the content of that memory location into the file, the FD, which is the file descriptor, which is points an index into file table that points to the file. It will save the content of that memory location into that file and then he can look at it. So to prevent such cases, we have the copy and copy out functions that will detect such relations. So whenever you have pointers, you need to use copy and copy out. The values or just like the primitive values, just like integers, these are directly moved into the register when we save the state. But pointers, you only, when the user pass a pointer, he's passing the address. He's not passing the value. So in order for you, now you are in the kernel mood and you need to get the value that that pointer is pointing to, then what you need to do is just like use the copy and copy out to get that value into the kernel mood and be careful about it because that's why you're using copy and copy out. To be careful and make sure that value that you're bringing from the user mood is the right value that should be or that you should get or need to deal with. So basically copy and copy out both to prove your kernel. Okay, let's go back. So now the time, it will copy out the time from the kernel mood and to the user mood. So you might ask, okay, when we want to copy some values from the user mood into the kernel mood, we need to be careful because maybe the user passes just like unwanted values. But why do we use copy out to copy from the kernel into the user mood? Basically because that value resides now into the kernel memory location, you need to copy it back into the user mood location. So it will copy and it will return. Let's go back. Okay, now it will return the value into the syscall which is that file. This is called .c, the switch statement and it will save it in the error variable. So next we will check that if the error is set, we will set the v0 into the error value and the a3 into 1. If not, let's say the system call succeeded, then we will set the v0 into the return value as you can see here and the a3, we will set it to 0, that means the system call succeeded. And from the system call we will go back into the simply file, the place where we issued the interrupt and here we will continue now. Now the next instruction tells you branch on equal. That means if the a3 is equal to 0, that means the system call succeeded. But if not, then it will just put the error number that it received from the register v0, it will save it into the error node variable and then set the v0 and v1 to minus 1 and return. So with this, this completes the system call cycle. So basically this is how it goes. In the user space, we will save the arguments of the system call into the registers and save the system call into v0 register, then we will issue or call the syscall to trap into the kernel and then now the kernel will step in, we'll go into the kernel mode, save the context, the kernel will identify what kind of interrupt it is, it will see that it is a system call and then it will dispatch that system call based on the system call number and then put back the result of the system call in the registers a3 and v0 and go back to the user mode. So any question up to now? This is a lot to digest, I understand, but as you read the assignment, go through this and also read the lecture slides. It should be very easy for you. So this framework has already implemented for you. What you need to implement is only the system call handler. How you should do this? So this is how to add a system call. These are the steps that you can go through. These are the standardized steps of defining a system call. Which is basically by creating, let's say the more organized way, by creating a new file for each system call and then you need to add these files into the configuration file for it to be compiled when you run bmake. And then you need to define another branch into the switch statement that will handle the system call. There is an easier way which is less more organized but just like it's only three steps, which is what you need to do is go to the syscall.c. So here is where the switch statement is. Now in that file you can define a function for your system call, let's say open. So you define a function sysunderscall open. You handle the open syscall and then you add a branch into the switch statement here that will handle just like, let's say, here you just write a case and sysunderscall open and then here you will call the function that you wrote. So the first step would be write a function into that file which is your system call handler. Add that function, add that case statement into the switch statement. The third thing is just like put the signature of the file into system call.h and that should be basically it. And it should work fine. So this is the less organized way but if you want to do it in more organized way then go through these steps that I've mentioned here. So the last thing to explain today is the design document. So as I said, assignment two is not as easy as assignment one and it's not going to be easy for TAs to understand what you're doing or what you have done designing or solving the assignment. So we require a design document that basically just like a two-page document you would need to explain how you're designing your file table, file descriptor, how you're handling synchronization. So let's say you have two processes, one access the same file, one of them want to read, one of them want to write, how you're going to handle this. You need to describe each system call. You don't need to include code in that, just use English. And error handling, you can also include that. And also you can tell us who did what. For example, if one of you want to work on the file system support and the other one work on the process system support. So this is basically the design document has to be and that's what we're planning to require from each of you. Whoever of you comes into the office hours, you will require that design document. It will not be graded. We're only going to use it to understand what you've done. So that's basically what I have for now. Thanks for coming. If you have any questions, just let me know.