 I assume most of you here have finished the subtle one. So how is it? Is it tough? Is it a piece of cake? That's just a start. So today, we're going to talk about, so first let's recap what we talked about last time. We went over the concept of CV, which we did in the blackboard. So CV is basically a mechanism for thread to temporarily give up the exclusive access. And then after wake up, we get access. So I think most of you have used CV in either RW log or the synchronization problems. So here's the example we went over last time, how to use CV to implement semaphore. Basically, the condition there is just to check if the count is zero. And we also talked about how to use CV to implement the producer-consumer problem, where the condition here is the state of the buffer, whether it's full or it's empty. And we also talked about reader and writer log and what's the conditions for reader and writer to enter the critical section and how to avoid a starvation by also stop the reader if there is a writer waiting. And so that was just some quick recap of last time. Today, we're going to talk about most about assignment two. I will give you an overview of what the assignment two looks like. And then we'll go to the details of the system core in COS 161, how it works in this particular system. And also a final I will give you some hint about what to talk about in the design document. So assignment two, as you already know, the design document is due this Friday. So I think both the code reading questions and the design document are due on this date. Is that correct? So you better get started very quickly before the deadline. And we have implementation, which is roughly, I would say, three weeks after that. And you have to implement all kinds of CIS calls. I mean, just to give you an overview. So don't panic at this point. I mean, you may see too many CIS calls, but once you get a handle of it, implement them is just keep doing the same thing basically, especially for the file CIS call. So it's really challenging to get started. But after that, you will find yourself more comfortable to when implementing the CIS calls. So let's go to the first part of this. What is a CIS call anyway? Anybody, what is a CIS call? We'll keep talking about a CIS call, a CIS call. What is it? There can be many different concepts or definitions. So ultimately, what is a CIS call? It's basically a service offered by the operating system. So it's a way for the user layer program to request certain kind of service from the operating system kernel. For example, the service could be hardware related. For example, access a certain block in the disk, or it could be software related like allocates a memory or create a new process and all that. So basically, a CIS call is an interface between the user process and the operating system. CIS call defines what kind of services is offered by the operating system. There are all kinds of mysterious stuff about a CIS call. For example, we should explain them today. For example, how does the user trap into the kernel space? We know that there are normally at least two different modes of the hardware. One is a normal mode where the user program is running. And another is privileged mode, which the kernel is running at. So how to transit between them? How the user program can trap into kernel? And how does the kernel know it's a CIS call? CIS call is just one small category of all the interruptions. It could be, for example, how the kernel differentiates if the interrupt is caused by divided by 0 or is a system call. And also, it could be a time interruption, for example, if the kernel wants to do scheduling. So whenever there is an interrupt, the kernel has to know what just happened, whether it's a system call or other exceptions. And also, how does the user tell the kernel what the user want to do? How does the kernel know whether the user want to open or read or write? Or for read and write, how does the kernel know where to read from and where to read to? How does the user tell the kernel all that information? And finally, after the CIS call is done, after the kernel has offered his service, how does the user get the result of the CIS call? For example, if user want to open a file, how does the user get the file descriptor? How should the kernel return the result of CIS call to users? So you can take these slides for a little bit, like one or two minutes, and think what's the way to do it. I think Jeff covered most of it in the class. Did he? I mean, maybe some high-level concept by not such details. So let's take a look at it by wanting through an actual CIS call example. In this case, I choose time, because it has return values. In the assigned to instructions, there is example of reboot CIS call, right? It is also a CIS call, but the reboot doesn't return you anything. You call reboot, and the system will reboot. Nothing will return. So here, I choose another example, which is time. So basically, you want to tell kernel to tell you what time is it, right? So if you go to this file, you will see a time function. It's a library function. So inside it, it just call unscored, unscored time. It's actually a system call. So you can see from the call signatures, the unscored, unscored time are supposed to take two parameters, right? One is a pointer to a time variable. Another is now, meaning we do just long care. And the unscored, unscored time are supposed to return a time t, right? So this is the signature of the CIS call. Actually, you can see that in the manual. Do you know that OS 161 source code shipped with the manual you can see? So it's in the main page, main directory, sorry. So in the source directory, there's a sub-direct call man which has all kind of sub-directory. It's bean, div, libc, and misc. I think it has also has a CIS call. So it's basically a bunch of HTML files describing the signature or the prototype of the CIS calls. You can find the prototype of unscored, unscored time here. So it is like as we have analyzed, it detects two parameters. One is a pointer to second. Another is pointer to nanosecond, right? It's supposed to return a time t. So you can see that if we call time like this, that means that I want the kernel to tell me the second. How many seconds since airport, which basically a fixed time point, some point in the past, and I don't care about nanosecond, right? And if you go to this source code and trying to find the definition of this unscored, unscored time, you will end up finding nothing. So let's, it's in user lib, libc, time, time.c, right? It's here. So if you have your C tags and jump, you will find there's actually another function called unscored, unscored time. But that's not where this function is used. Actually, this holds a compatible directory is not used at all. So this function is actually a system call. So let's take a look at how system call is defined in the user space. So as you can see in the slides, it's defined in this assembly files. Build user lib, libc, syscall.s, awful. Let me make the font lib smaller. Is this better? Can everybody see? Is it too small? Okay, good. So in this file, you can have, you will see a couple of interesting things. First of all, we have a macro here, which is called syscall. It has two arguments. The first is symbol, and the second is syscall number. So before we go into the detail of the syscall, let's first see how this macro is used. So here we define a syscall macro, right? And after that, you will see a bunch of syscalls defined using this macro. So we see the name is fork, for example, in this, the name is fork and number is zero. The name is V fork, the number is one, and so forth. You will see all kinds of, all the syscalls are defined here. And right below, you can see time. So we use this macro to define a function, and the symbol is on a scope on scope time, and number is 113, right? So let's go back to the syscall definition, see what this line is, what does this line want to do. So we go to syscall. Here you can see it define a label. The name is just a symbol. So in our case, the label is just on a scope, on a scope time, right? Remember that you see the function is just where the instruction starts, right? So this one defines a function. It's called onScope onScopeTime. And inside of the function, what we do is we first jump to another function, basically call another function, and it is a MIPS caveat that this instruction has to get executed before the first jump instruction. So the function first wrote the number into v0, so basically save the code number into v0, and jump to this another syscall function. So inside of this function, this is, here starts the instructions, right? This line is just a label. Give a name to the instruction. And the second line is actually the instructions. So first we have instruction called syscall. And there is a question, and the query in question asks you about this, right? So what happened when the CPU executed this instruction? Any idea? So this is one MIPS instruction. And if you Google it, I mean, I already did that, but if you Google it, you will find that this is, yeah. Is that complicated? I mean, it's one instruction, what does it do? More like it, yeah. So it's basically any other source before I jump into the authors. I mean, what does this instruction do? So if you Google around, you will find that this instruction is called a software interruption generator. So it generates a software interruption. So normally instruction on CPUs are generated by some unintentional cause. For example, I unintentionally divided the number by zero, I will trigger interruption, the kernel need to handle that. But this one, it means that I want to trap into kernel. By putting this instruction here, then the program wants to tell the kernel that, hey, now I want your intention, I want your service. So basically this instruction will trigger an interrupt. So that answers our first mystery, which is how does the user program trap into kernel? How does the user program tell the kernel that it needs service from kernel? It's by this instruction to raise the attention of the kernel. And just like normal interruption, so I think Jeff covered this in the class, when there is an interruption, what happened? Regardless of it's divided by zero or by Cisco, it's an interruption. After the interruption happened, what will happen? So the CPU is running, there's an interruption, then what happened? Nobody? Yeah, after the interruption, what happened? Continues? Then what's the point of the interruption? It's interrupt, right? After the interruption. No, yeah, so, okay, so interruption is two time point in the history, right? Interruption starts, interruption ends. After the interruption, it continued execution. But what happened just after the interruption was raised? Exactly, so first thing we do, we enter privileged mode. Remember we have two modes of the CPU, right? So first we are in normal mode, whenever the instruction, the first thing in the hardware we do is enter privileged mode, then what? What's that? Yeah, so the hardware need to record what just happened, right? Because there are various reasons for interruption, right? The hardware need to figure out what's the reason and save it somewhere, so later on kernel can figure out what just happened, right? So basically that's more about, that's more or less it. So on the instruction, the hardware will first enter the privileged mode and then record the state, basically what just happened, the course of the interruption. And then the hardware will set the instruction counter to a particular predefined location. So in this particular hardware, the MIPS hardware, this location is 0x8 million, right? So the hardware will set the program counter, the PC, to that location. And the CPU will start reading instructions from that location. This is all hard-coded, right? And then what's there in this address? What's that? Handler, yes. Or interruption service routines or whatever, whatever the name is. So basically when we boot up the kernel, the boot up process will first place some small piece of code in that particular address, right? So whenever there is an instruction, who gets the control? The user program or the kernel? Kernel, right? Kernel has to take control of the system and see what just happened. And let's go to this. This will be the last assembly code we need to look at. So kernel, arc, MIPS, local, exception, S. So you can read the comments here. You don't have to understand the format or the syntax of the assembly. You don't need to. So you can just look at the comments here to figure out what these assembly codes are supposed to do. So basically this explains the procedure. And we first save all the stuff, save all the context, all the user registers. And then finally, we jump to a function called MIPS trap, which is fortunately a C function. So let's go to the C function. It's in the same directory, but trap.c. So in this function, what do we do? So we have a trap frame, basically record what happened just before the interruption. And then we get the exception or interruption code. Basically represent what type of instruction it is. And we do all kinds of stuff. For now, just ignore them. So now we are checking the instruction type. If it's a hardware interruption, we do something. And if it's not, we continue. And here we check if the interruption is a syscall. There is another clue in question asking you how to determine the interruption is a syscall. So we check the interruption code. And we find it's syscall. So we enter here. And we basically call this syscall function. Then we go to the syscall, which is defined in the syscall.c file. So we have the call number. The call number is v0. Remember before in user space, what did we do just before call the syscall? We load some number into v0. Here you can see why we store that number in v0. So this is kind of a protocol saying that, hey, user, please put whatever the call number in this particular register. In this case, v0 is agreed by the kernel and the user. And then the kernel will examine that register to figure out what the user want to do. So what the user want to do is a first piece of information that user program should tell the kernel. And then what? So for example, the user will tell the kernel, go back to the example, I want to get a time. Time has a particular call number, which is 113. Now what? The kernel need to know where you want me to store the time. The time is two piece of information, second and nanosecond. Where do you want me to store them? That's basically, the user already passed that in the arguments. So if you go back to here, the user already tells, hey, store a second in this variable and discard the nanosecond. That's another second piece of information that the user need to tell the kernel. So we got the call number. The kernel figure out, OK, this is the time, this call. So I do a switch on this call number. For different calls, I have different handlers to handle them. If it's reboot, I do reboot. If it's time, I do time. So I basically call my handler here in this file. Now imagine you want to handle open. What do you need to do? Cast is open, I call my open handler. Cast is read, I call my read. Basically what I need to do in this assignment is add a bunch of more cast branches here and add your own handlers to handle them. So here we have a handler for time. It is this function. And interestingly, we pass a 0 and a 1 into the function. So now it all go back to the argument passing. So back to the second question, how does the user tell the kernel program where to store all this information? So if you take a look at the comments here, this whole chunk of text, it will tell you exactly what's the convention between the user program and kernel on how to pass the argument. So this slide basically summarizes the convention. So when the CIS call has, so different CIS calls have different number of arguments. No matter how many arguments you have, this is the rule to post them. If you have less than four 32-bit arguments, just put them in a 0, a 1, a 2, a 3. So if you have more, you say you have six arguments. The first two will be put in a 0, 2, a 3. What about the remaining two? The remaining two will be put on user stack starting at this address. That's what the comments said. And what about 64-bit argument? Right now, we're only dealing with 32-bit argument. So a 64-bit argument will occupy two registers. It will only be placed in aligned registers. By aligned, it will mean either it's a 0, a 1, or a 2, a 3 to aligned registers. It cannot be a 1 and a 2. They are not aligned. This is the convention for arguments. And we will discuss the return value later on. But this is the rule. Basically, E3 indicates if it's a success or not. And V0 is for a 32-bit value. And V0, V1 is for a 64-bit value. We'll come back to this later. So basically, the first bullet is the convention for the user to tell the kernel what are the detailed information they currently need to know. For example, where to store all these time variables. So let's take a few Cisco examples to get familiar with this convention. So is it as time is simple? So we have two arguments. Both are 32-bit. Where should we put them? It is a 0 and a 1. So a 0 will be the second. And a 1 will be the nanosecond. What about open? Open, we have three arguments, phonem, flag, and mode. All of them are 32-bit. So we put phonem in a 0, flags in a 1, and mode in a 2. What about this one? Elsic. You may think Elsic only has three arguments. So a 0, a 1, and a 2. But that's not true. Because the second argument position is offset t. If you go to the definition of offset t, it's a 64-bit variable. So here, we will put the fd, the file descriptor, in a 0. And because the second is a 64-bit, so a 1 is skipped. It's not used. So position will be put in both a 2 and a 3. Then we run out of all the registers. We only have four. For the final, yeah. We put the arguments one by one in order. So this is the special case. This is a special case where the third argument is happened to be an integer where you can put in the second register. What if the once is also a 64-bit argument? In that case, you want a principled way to find all those arguments. You would want to give this function, I find an argument this way. That function, I find arguments that way. That will not scale. So for all the functions, no matter what your argument looks like, we have one rule to do them all. We just found the argument sequentially. Yeah. Stack. So when you handle in ELSIQ, you know the interface of ELSIQ. So you know exactly how many arguments are there. So you know the first is fd. The second is position. And third is once. You know there will be only one 32-bit variable that put in a stack. And the starting address of that variable will be fixed at sp plus 16. Sp, yeah. Sp is the stack pointer. Characters? Final name? It's a pointer. Pointer is fix-sized. Do you want the address? Yeah, the address. The string or char array could be variable size. But the pointer is always 4 bytes, fix-sized. Any other questions? So everybody know how to at least get the arguments from the user space. So let's go back to the code and see here. Now you have a better idea of why we are passing E0 and E1 here, because these two contains the information passed by the user. So E0 is basically where the second should be. And E1 is where the nanosecond should be. So now let's go to the handler. Basically, you first get a time and then copy out this time to user space. Now another question is why do we use copy out? Why not just assigning? We know that this is the pointer. Normally what we do is we do this. Star user second pointer equals to second. That's what you may attempt it to do at first place. Why don't we do that? Why do we use all the trouble to call function to just assign a variable? You will need to think about it in the code ring questions. So here comes the whole topic of why use copy and copy out. So first of all, what is copy and copy out? So as a name suggests, copy in data from user space to kernel space or copy out data from kernel space to user space. So basically data transfer between user and kernel space. You will use them aloud in this assignment. And so like we said, why do we use copy and copy out? If we just want to assign a variable, why not just assign it? Or if we just want to copy a chunk of memory, why not just memory move? Why do we use a special function copy and copy out? Any idea? Yeah? You are changing the kernel code. Actually, that's what you do in this assignment or in all the assignments. You use the same method. Exactly. User can be naughty or can be bad. You have no idea what the user want passing in. You know where it is, but you don't know the value. Or you cannot expect the user is well-behaved and always give you the correct value. Either the user is malicious or passing some bad values on purpose or a user is just dumb. By mistake, passing some bad values. You don't know. So for example, I open a file. I suppose to give you a name. But instead, I give you a non-pointer. If you go ahead and access that non-pointer, what happened? You have interruption. At the kernel, you have interruption. And that's not good. I mean, you are not supposed to have interruption at all inside a kernel. And also, when the user want to read this file and put the content at a memory location, and that memory location happened to be your interruption handler. Remember, whenever there is an interruption happened, this whatever instruction there have control. Whatever user load something is on program on that location. And so later on, whenever the instruction, the user have control of the system instead of the kernel. Or similarly, as right, the user may dump whatever the memory is to the file and examine the content. So the bottom line is that you cannot trust the user program. Whatever you read from user space, it cannot be trusted. You can pre-assume it will be bad. So that's why we use copy and copy out. So this function will handle this case. So what this function does is that the function will first try to access that memory or that location that user specified. If something bad happened, it will regain control. It's more like a try, catch block in Java. I try to do this. Whenever something bad happened, I got control instead of just throw out some other interruption. I want to have control of the whole process. That's what this copy and copy out does. And you want to use them whenever you are dealing with the user space, especially the pointers. Remember in the time, the user tell us to store the time and nanosecond in that two location. By location, we mean pointers. Whenever you are dealing pointers, you always want to use copy and copy out, no matter what. So we copy out the second value to the pointer. Remember that if something bad happened here, the copy out will get a control. So the copy out will behave like a normal function. If it fails, it won't cause any interruption. Instead, it will just silently retain your error code. You check the error code. If anything's wrong, I'm done. Then similarly, you copy out the nanoseconds here. Here, because we call time reason now, what will happen here? So inside this copy out function, the kernel will try to access now. That will trigger an interruption. But internally, it handles that. So as a caller of copy out, we don't know if there is an interruption or not. We will notice that the result will be not zero, meaning there is some error. We'll just return that. But at this time, we don't care. I mean, the user passing it now because the user don't care about nanoseconds. So it's fine. And we go back, check the error code. So we break out from this switch. So now we know how does the user trigger or trap into kernel? And how does the user tell kernel what to do and how to do it? Now, we reach the third part, which is how does the kernel return the information to the user? Oh, I think there is a question about that in the coherent question. I can't remember exactly, but I think. So the idea is that you can name whatever you want to a type. You already see that in Simon Zero. Why do we call it unscrup, unscrup time t? You can call whatever you want. It's just a name. Deep down in the lowest level, it's just int or char. So this is just a fancy name for pointer to let you know that to remind you that this is a user pointer. That's it. Now the kernel has finished its service and the kernel want to return information to the user. So as we discussed before, for return values, the convention is what? It's really indicate if this is called success or not. v0 or v1 are supposed to contain the return values. So here, we check the error code. If there is an error, we set v0 to the error code and set a3 to 1, indicating that this is called failed. Otherwise, we set v0 to the return value and say a3 to 0, indicating this is called succeed. Then we return. And before we return, we advance the EPC, basically the program counter, so that the user won't get stuck in the Cisco instructions again. There is also another code ring question about this. And then they'll figure out which one. So any questions about the Cisco process so far? So everybody know how to get the information from user space, how to get the code number? Code number you don't need to worry about. This whole framework has been set up for you. All you need to do is add plug-in your own handlers. But it's helpful to understand the whole process. I mean, how does a Cisco finish or what's the whole lifetime of a Cisco? Something from the stack? I can't. You need to figure it out. So basically, in C, given the address, you can get the value at that address. So in this case, what's the address of the, for example, in ELSIQ? What's the address of the WANs, the third variable? SP plus 16. Where is SP? What does SP stand for? Stat pointer. It's all here, all trap frame. So if you go to the trap frame definition, you will find all kinds of registers. This is basically all the registers in the CPU. So when did we set this up? How do we magically get a trap frame? So whoever called this function have a trap frame. Where did we get that? Remember, whenever there is an instruction happen, what's the hardware we do? Save the context. What is the context? The context is basically all the information just before the interruption, which is all the registers, all the values of the register. So trap frame, inside trap frame, you will find this one, SP. So you know the value of SP. You plus it by 16. You know the address of that WANs variable. And you're about to fetch the value from there. Remember, this is also a user pointer. You don't know whether the stack pointer is. So what do you should do? Copy in. Copy in some value from user space. OK. So now we have finished everything as part of the kernel. We have get the syscall. We have served the syscall. We have set up the return values. Now as the user, how the users are supposed to get all those values. The kernel has already stored all the values in either the registers or in the registers. It's really a V0 with WAN. Now let's go back to the very initial assembly file we look at it today with here. So user used this instruction to trap into kernel. Before that, the user has already stored the code number into V0. So the kernel know what to do. Now the kernel returns. So as you said, as someone mentioned, we continue execution after the interruption as if nothing happened. So we get to this instruction what we do here. This actually means branch if equal. We check the value of a3. Why do we check the value of a3? Because that's where the kernel put the flag in it. If it's 1, it means fail. If it's 0, it means success. So we check the value of a3. So don't read the assembly code. Read the comments after that. If a3 is 0, core succeeded. So we jump into this location and we just return. If it's not a 0, then something bad happened. So we store the V0 in the variable called error number and load the routine value to be negative 1, we return. That's why in user space, whenever you call a function, when something bad happened, you will know that the routine value is negative 1 and the error code is stored in a global variable called error number. That's how it's accomplished in the library. Remember, this is all in user space. This assembly code is in user space. That's why it uses a syscall to trap into kernel and get that information or get the time information in our example. Return value, check these two files. So now what's the big picture of the syscall? What's the life cycle of the syscall? So first, as a user program, the user put all the information that currently to know in some pretty funny registers. In this case, A3 and A0 are putting arguments and V0 is a code number. Then the user uses this call instruction to trap into kernel. Then the hardware takes over because there is an interaction. So hardware will enter the previous mode and save the context, basically the trap frame. And then as a kernel, the kernel need to identify the reason of the interruption and dispatch the syscall to various handlers. Right now, there are two handlers, reboot and time. You are supposed to add more handlers to it. And then when you finish handling the syscall, the kernel will pick up the result in some pretty funny registers, again, in A3 and V0. Then we go back to the user mode. And the user mode will pick the result from these registers. So this is the whole picture of how a syscall happened. Any questions? I mean, I know this is maybe a lot to you to digest, but you need to understand this before you can be comfortable with all this syscall handling stuff. Otherwise, it's very easy for you to get lost. I mean, you don't have no idea what you're doing. You're confused whether you are writing user code or kernel code. One rule is that whatever you write is kernel code. For example, is this kernel arcmips syscall syscall.c? This is definitely kernel. Any questions? Let me see. OK, so this is just a tip of how to add a syscall. Like we said, you just create some file, c file, define your handler there, and add that handler in the switchcast branch. Basically, in that switchcast branch, you insert your own handler there. This is just some technical details of how exactly to enter the c file and the header file. Let's first jump back to the design documents. If we have time, we can go back and take a look at this. So for design document, it's a two-page PDF file. You need to describe all the important issues in this assignment. So basically, how do you design the file descriptor and the file table? And how do you synchronize the concurrent access? For a similar file, there will be two processes that concurrently access this file. How do you synchronize? And you need to describe, basically, for each syscall, open, read, and write what you need to do. You don't have to use code. I mean, you just have a board that they open. I will first do what, maybe find available file descriptor, then I will do what. So just use English, not code. And also, we require you to describe a second scheduler in the design document. And lastly, how your partners are planning to break down the work between you. Maybe one do file syscall, another do process syscall, or some other work breakdowns. So basically, when we're writing the design document, we have a checklist to see whether you have described this, whether you have described that. You want to make sure you covered everything. So let's go back to this. So you want to put your syscalls in separate C files. For example, one possible organization could be put all the file related syscalls in a C file named file syscall. And put all other process related syscalls in another C file called PROC syscall. And then implement your handlers in that C file. And the third step is very important, that whenever you add a new C file, you need to add that file entry to the configuration file. So that when you do BMAC, it will be compiled. And create a header file, declare your open prototype, and include that header files in syscall.c where you will insert your handlers. And dispatch for sysopen. So here, basically, you want to do CAS sysopen, arrow equals to my sysopen, something like that. And this function should be implemented by you and defined somewhere and break. You can take a look at the systime. You can see that systime is defined in another C file called kernel syscall times syscall.c, same thing. Just use this as a template and implement your own syscalls. So that's what I have today. And we can discuss afterwards if you have any questions. Thanks.