 All right, boom, there we go. OK, so going back. All right, so there are a bunch of functions already given to you for this. And they will help make your life easier because you can imagine doing like bitwise manipulations on like a PTE, kind of a pain in the ass. So I did it for you. Yay. So there are a bunch of functions. A lot of them are simulating like all these MMU functions are simulating what like the MMU and the CPU actually do. So like the CPU instructions would just be to like set and get that root page table. It's just in a register. So there are functions for that. And then the only code you would actually use in your implementation is like get root page table. So that gets you the current processes. So the process that allegedly called fork gets you their page table. And then you can work with that to figure out what you need to copy and everything like that. So the rest of these are just for testing. You're free to write your own tests if you want. Or there's a supplied file that we'll get into where you can go ahead, run, make sure you can sanity check some of your work. Other than that, we have some functions that deal with pages. A lot of them, the test handle for you. And the only two you actually need to use in your program are like new page. So that kind of simulates asking the kernel for just another physical page. So I would like a new page, please. And then you get to decide whether you need to use that for storage or use it for a page table up to you. Then the free page, it's there mostly for completeness. You're just doing what happens on a clone. We're not going to bother with managing pages after the fact, so you'll probably never use free page. Get use pages, so that's used by the testing to make sure you're not wasting space. Then there is this get page pointer. So just for ease of use, this library kind of just assumes that all the pages are in some giant array. So given a page pointer, you can get an index. And that index will be between 0 and there's a define called max pages. So if you want to use an array to keep track of something, you can go ahead and just ask, given a page, you can get whatever its index is. And you're guaranteed it's unique. Yep. Yeah. Yeah, so get new page, it will always be 4,096. So when you would need to use new page at bare minimum, when you fork and you need to copy all new page tables for the child processes, you need to make a new L2. So you need to get a new page. So you can use that for L2. And then assuming it has valid addresses, it probably has like a valid L1 entry. So you need to make a copy of that L1 table, make sure yours points to it, then L1, and then that new page and go on. So we'll have an example, we'll work through it too. All right, so after that, our page table functions. So basically they're just doing the array indexes for you and actually given like a virtual address, it will go ahead and figure out what part of that virtual address is used for the index. So like for page table index, wow, this is long. So page table index takes like a virtual address and then a level. And then it will give you the index at that level. So for example, if we have like the virtual address ABC123, so this is like the virtual page number. And we're assuming SV36, 39. So it would get split off until like an L, there's an L2 index, an L1 index, an L0 index. So to just get the index directly, you can just use this function. So get index for ABC123. So that's the whole virtual address. If you ask for the level one, you get that index. So questions about that or why we have that index for that? Because this was the whole thing where I had to like write that whole thing out in binary and then group it in terms of nines and all that stuff. So we use programming languages for a reason. So you don't have to do that by hand. All right, so another one, page table get PTE entry from the index. So it's basically just does a little bit of pointer arithmetic for you. So given a page table, so that starts at a page. If you give it an index, it will give you the PTE entry. And the entry is just a pointer to some 8-bit value. And then if you want to just get the entry directly without having to like go from the index. So this one takes like a page table and index. If you just want to skip the middleman, there is page table PTE entry. And that just saves you the step. So it just takes a virtual address, a level, and then a page table. And it gives you the page table entry that you want directly. Then there are some helper functions. So given a physical page number, it'll return a pointer to the corresponding page. So everything will be page aligned. So it will give you a pointer to start at the page. And then if you have a page and you want to make it a PPN, then you can use this function, takes a pointer to a page, gives you the PPN. Why would you need that? Well, because there are a lot of functions for dealing with the page table entries. So we are going for this lab to use, what is it, five bits? One, two, three, four, four. No, four bits. So we're going to use four bits. So there's going to be a valid bit. So any of these functions that say clear, that means it's set that bit to zero. Anything that says set means set that bit to one. So if I have a page table entry, if I want to say it's valid, I can just use valid set. Then there's one. And then there's the accessor ones. So that will say, OK, we'll just return one or zero. Is this true or false? So that is how you get the valid bit out of an entry. Again, this saves you from doing any bitwise manipulation or anything like that. So we have read. So there's a read permission. There is a write permission. And there is a custom permission. And you will need that. So you don't need to use the custom bit for implementing just a plain old copy. You'll probably only need this for copy on write because you'll probably need to keep track if you're sharing a page or something like that. Again, we'll go into an example. And then if you want to get the PPN out of an entry, you use this function. If you want to set it, then you use this function. Other than that, these are the two functions you are implementing. So you are implementing what happens on a fork if we're just blatantly copying everything and making a completely new independent copy that's not associated with the original process at all. And it always returns just a pointer to the new root page table for that process. And then after that, we should be able to switch back and forth between the old one and the new one in the test. And they should be independent. And then same idea, but we do it with copy on write. All right, any initial questions before we go into some more fun stuff? Yeah. Also, there's nothing new you have to learn for this lab. So my highly recommended thing to do is to start this immediately after the lecture and make sure you're done like before reading week. Otherwise, you will get behind. So this one is a lot of thinking. So you'll probably need to sleep because my whole implementation is under 100 lines. So it's not that much code. But that's only if you do it a smart way. If you do it a silly way, yeah, you can write a lot and you're going to waste your time. And like I said, you already know everything to technically do this lab anyway. So after this, this is even more help. So hopefully it will not be too bad. OK, so how it's also laid out is that there is this main.c file here. And that is just for you, just if you want to go ahead and test your thing and we'll use that today. Right now, all it does is takes a virtual address like abc123 and then makes it valid. So it sets up all the page tables for the parent process, prints some information about it, so we can just print off what we're actually doing. And we're just testing read and writing to that virtual address. So all the reads and writes just assume that they only operate on ints because I don't want to do anything more complicated than that. So there's change ints at a virtual address. So if I go ahead and I run, so you can go ahead, compile that using our normal steps. And then you'll have this buildVMS. And this is from that main file. This isn't graded for anything. This is only for you to play with if you want to do something outside just running the test. But of course, there's like, I forget how many tests there. There's like 18 tests or something like that or no. 14, something like that. All right. Anyways, if we do this and we run it, we can see that, OK, it works. So if I write a value 1 to some virtual address, I can go ahead and then print off whatever its value is. So it's a 1. And then we go ahead and we can say, oh, OK, let's change that value from a 1 to a 2 and then read it back again. And yeah, we get the updated value. This should not be surprising. So surprising thing is going to come from when we implement things. So this is the only file you will have to actually modify. It's VMS.C. And I also wrote some nice little helper functions for you if you need to print off information about PTE to save yourself some time. So there's this function called print PTE entry. So if you give it an entry, it will go ahead and print off what the PPN is and all the bits and what they're set to. So we'll get into the page fault handler in a little bit. First, we'll just talk about what happens with the fork copy. So this function, this is like a default stupid implementation. So get root page table. If I want to write it in a different way, I'm assuming that we're writing the code for the process that called fork. So if I call VMS get root page table, well, that would be the level 2 page table of the parent. So I can rewrite it a little bit differently. So let's just rewrite it and give it a name. All right, so if this was my implementation of fork, would this work? What bad things would happen here? What does this mean? Yeah, the child and the parent have the same root page table. So they have all the same mappings. So they're accessing all the same physical memory. So if this was my implementation, we can go back and even try it. So here, I'll just say like the forked L2. So whatever the result is of calling fork copy. So remember, what should happen is it returns the root page table of the new fork process that should be completely independent of the parent. So here, if I go ahead, and this is just for testing because this is like what the kernel would do. So this would simulate like context switching over to that new child process. And then if in that new child process, I use the same virtual address and I write the value 3, well, if I go ahead and read it, what value would I expect back out of it? 3, hopefully right if we haven't gone insane. Why did I just type compile? That's not this course. So yeah, we read 3, but that was in the new process. So let's say, let's make sure this is clear. So let's make sure we can keep track of what we're doing. So context switch to child and goes ahead. It updates that value at that virtual address to 3. And we go ahead and read it should be 3. So if we go ahead and what did I call it? I just called it L2. So we can go ahead and we can simulate doing a context switch back, oops, back to parent. And if I just print out the value at that address, what am I going to see here? 3, and is that good if I see 3? So are they independent there? No. So we have to make them independent. So also, any questions so far? Kind of slowly making sense what the goal is here. So to be, oh, yep, why was the last one printing 3? So the child and the parent, they're just using the same root page table. So they have all the same mappings for everything, right? If the MMU is trying to translate that virtual address to a physical address, it'd be translated the same to both. So if one changes it, changes the same physical address, and then the other would go back and read the same physical address. So any changes you make, you see them. All right, it's using the same physical memory because, well, I didn't do anything. They're using the same root page table. They have all the same translations. If you make a change in one, you see it in the other. Not what we want to happen for forks. All right, anything else? All right, so let's be clear what that looks like. So here, just as a reminder, so we divide memory up into pages. And this is the only unit of memory that the kernel actually cares about, or at least in terms of your processes. And be laid out something like this for here. I'll just simplify it a bit. So like page 0 would represent the memory addresses hex 0, 0, 0, 0, all the way up to 0, f, f, f, because that's exactly the size of a page. That's how many bytes are in a page 4096. And then similarly, it would start at 0 because everything's aligned to a page. Page 1 would be the next 4096 bytes. Then page 2, page 3, kind of looks like a big array, right? Yeah, so these are just pages in memory, and we get to choose what we use them for. So the question is, how do these relate to the L1, L2, L1, whatever page tables? So we can use these pages for whatever purposes we want. One of the purposes we can use them for is for page table. And then we can decide whether it's an L1, an L2, an L3, whatever. Or we can decide to use that page for actually keeping track of the memory of that process, like keeping track of the value of x or whatever. So this looks like a big array. I made it look like a big array for you using that index. So again, if you have to keep track of information about pages, use arrays. I don't know who tortured you because I've seen a lot of your lab 2 things, and you all like link lists for some god-awful reason, even though lab 2 was specifically engineered to push you towards arrays and be easy to do with arrays. So yeah, who hurt you? What? Maximum number of processes? Well, you don't know, right? Yeah, but if you don't know how many processes, you can dynamically resize the array. It's not hard. There's a function called realloc array. It does it for you. Wait, was no one actually aware of realloc array? This is too much, and your link lists are great? Yeah, yeah. So the typical thing for arrays, if it's out of bounds, you should be checking your bounds of the arrays. If it's out of bounds, just double its size. And yeah, doubling its size, for memory, that's how hash tables work. If a hash table's too full, just double its size. And if that fills up, it just doubles its size again. And because exponential growth is great, you don't have to do that many times. And yeah, that's also what will happen. That's a common networking thing too. You see your networking speed grow up linearly, and then when it gets slow, cuts it by half each time. Common thing to do, we like exponents. Where the hell was I after my rant with arrays? OK, yeah, what this would actually look like. So assume we have our parent process. It was using virtual address ABC123. So do you want me to write this out in binary and write out which index is which for everything? Let's save us the trouble. We have functions anyways. So if I save us the trouble, the L2 index, that is part of that virtual address, I believe would be 0. The L1 index would be 5, and the L0 index is 188. So in order for, like, in the code here, all the stuff before to actually make sure that that is a valid translation, all it does is it would do something. It would say like, oh, OK, well, I need a root page table. I need an L0 page table. So I would probably just ask for a new page. Oh, maybe I get this page. Maybe I get page 0. So let's say, OK, well, page 0, you're my new root page table. So I could say root. So my root page table would be using a whole address. It would just be 0, 0, 0, 0, 0. And then I'm going to use that as my L2 page table now. And in order to make this a valid translation, well, the entry in the L2 page table has to point to an L1. So I need to use a page for L1. So maybe I ask for a new page, and I get page 1. And I say, OK, well, I'll use that for my L1 page table. So if I want to make this valid in this L2 page table that I happen just to be using this page for, I need to make sure the page table entry at index 0 is first valid. And for walking the page tables, all we care is whether or not it's valid or not. We don't care about read, write, or the custom bit. We only use that in the last translation to check permissions. So here, in order to actually successfully decode this address, well, it needs to have a valid bit. And the ppn, if I wanted to point it to page 1, what ppn would I use here? The address, like the whole address? Yeah, without the last 12, right? Without that. Just 1. So page 1, because everything's aligned to the page. Otherwise, if I could just start a page anywhere, I would need to store the entire address. And I probably couldn't fit it. If my addresses can be up to eight bytes, well, I need to store other permissions if it can start anywhere. I have to waste a lot of space. And memory also doesn't like being not aligned. And computers really hate that. And you'll do it. I can't read the Discord message right now, but I'll look at that after. All right, so in here, this would be at index 0, 2. So at index 0, I would have a page table entry. The ppn would point to 1. And the valid bit would be 1. And that means if it tried to translate it, it would then hop over to L1. Then in L1, we would need an entry called 5. It would need, in that entry, it would need a ppn and then a valid bit. And oh no, I need to go ahead and get some page so I can use it as an L0 page table. So maybe I use this one. Ppn of that is just 2. And then here in my L0, well, that has the final translation, right? That has where, what physical page is am I supposed to use for that virtual address? So maybe I use page 3 just for my variables. So I'll just say this is my data. Actually, let's give this a name. Let's just say it's like process 2 data. And then this is going to be process 2. One of the L0s, process 2, one of the L1s, process 2, one of the L2s. So here it would be ppn3. So I'm deferring it to page 3. And we'd have index 188. And then here we would have permission. So read, write, execute, and then a custom one that you won't need to use for just a blatant copy one. Let's say I say the custom bit is 0. The write bit is 1. The read bit is 1. And the valid bit, whoops, that should be valid. Valid bit is 1. So then it can go ahead and then using the offset, it would actually use the memory on this page. So what I did when I forked is, OK, I tried this, and this is p2's root page table. So the implementation I had right now, all it did is it just returned the original processes root page table. So in this case, p3's root page table was also 1,000. So it would just do the exact same translation in order to get the new page. So what should processes 3's page tables probably look like? Any guesses with this or a good first step I should do? Yeah, at very minimum, we need a new L2. So maybe I do that. Maybe I just get a new page, say this one. And I'll say this one. Actually, let's use a different color. Red's kind of aggressive. Orange, sure. So we get a new page, and we can say it is process 3's L2. So here, we're at least returning a new L2. So this would be at address 4,000. OK. What's the next step I want to do to make them independent? Can I just get a new page and let's just say I copy the contents of process 2's L2 page table. But it's on a new page, right? So I'd have index 0. The ppn would be 1. Valid bit would be 1. So I just completely copied the page. I just mem copied it. I didn't want to think. I didn't want to do anything. So would that be valid? Technically, using different page tables now-ish. This is, come on. I'm doing your lab for you the more you ask. Yeah. Yeah, it'll eventually just point to the same address anyways, right? So if we were to translate that same address with p3, well, the entry in L2, even though the SL2 is independent, it's pointing to the same L1. So the translation would just use that. And at the end of the day, we'd get the same translation. So for the purposes of this lab, I'm also not going to do anything weird where two entries are pointing to the same page or anything weird like that. So do not worry about that. I could make your life a living hell with that. But I'm somewhat nice. This is already a thing. So nothing silly like that. So if I wanted to make them independent, well, I need a new L2 page table. But I should probably have a new independent set of page tables for this process, right? So it should have a new L2 page table. And it should also have new page tables for every L1 the original had, and new page tables for every L0 the original had. So full set of independent page tables. So here, let's just do it quickly. Let's just say we get page 5. It's valid. On page 5, we add index 5. We get a ppn. It goes to page 6. It's valid. At 188, we get a ppn. And then all of the bits. So I'll just write it on two lines read, write custom and valid. So this would be L3s. In this case, we only have one entry in L2. So we only have one L1 page table. So I don't have to draw any more. But if you had more, you'd have to get new pages. So this would be p3s L0. OK, so now we have a completely independent set of page tables, right? So if in here, if in p3s L0, I do O3 and then custom 0, read, write, valid, do I still have the same problem I had before? No? I'm good? Yeah? Yeah, it's the exact same issue. So I got closer, but at the end of the day, looks the same thing. So in order to translate the same address, I used completely different page tables. But remember, to do the final virtual page number to physical page number, that's the entry in L0. So if you screw up L0, it doesn't matter what you did. So turns out this is 90% of the way there, instead of 0% before. But we got the same result. But this is slightly better. So if I want to make it completely independent, well, in L0 for process 3, I should probably point to a new page, right, like this. And then if I do the translation, I'd go here, to here, to here, to here. Now is this OK if I just ask for a new page and also to note, when you get a new page, it's 0 initialized. So it's all full of 0s. So would this be OK if process 2, let's say it wrote x equals to 2, and then this whole page is just full of 0s? Would that be OK? Yeah. Yeah, I actually have to, in this case, if I'm making a completely new set, I have to make a exact copy of this, again, at the time of the fork. So now it would have a 2. And now if we go ahead and we modify the same virtual address using P2, it modifies this memory. So this 2 would change to 3 or 4 or whatever. And it wouldn't affect process 3. And now if we switch back to process 3, access the same virtual address, well, because we have to follow that, the new set of page tables to figure out what memory to change, it would change this, which is independent of the other one. Good? All right, let's look at some more code quick. So in here, so here, I'll even write some code for you. So if we want to get a new page to use as an L2 in the child, well, we know we always need to just, I'll just call it L2 child. And we can say, well, we just need to grab a new page because we need a new page to use for the L2 page table for this process. So I'll just grab a new page and then I'll return it. And I can even see what happens if I just do that. So if I do that and run the example, oh, fatal page fault. Oh, yay, aka, it's a seg fault, but it gives you more information. So we can see we context switched to child. We knew we tried to write to address ABC123. Here it tells you what page table we used, tells you what level the page fault would fail at, because we could fail at any individual level. So I could fail looking at the L2 entry, L1 entry, L0 entry. So it tells you what level it failed at. And then it gives you information about the PTE. So in this case, because that new page is full of zeros, that's aka, every single entry is invalid. So it's saying that, OK, well, there are no flags set. So if there was a valid flag, you'd see like a capital V here. And it says, oh, the PPN was zero. It had no flag set. That's why I had a fault here. So if I want to go ahead and look at every entry in the parent and look at all the PTE entries, I can do that. We can just write a for loop. So we can go I all the way up to, was it num PTE entries? So there's a define for you. But we should know this number. Anyone off the top of their head, what's this number? 512. Did I hear 512? 512, yeah. So 512. And we can mess over and see 512, because 4,096 divided by 8. Or page size divided by PTE size. All right. So now if I wanted to just look at all the page table entries, I can use that special function. So using the parent's page table and then using, actually, I have the index, sorry. So sorry, back up. So I don't have a virtual address here, because I'm just looking at the parent's page tables. So I could just say from index. So I'm using the parent's page tables. And I'm just going to iterate over every single index there. So if I do this, yeah. Zero index. Like if I do this, in this context, they mean the same thing. All right. OK, who taught you C? Jesus Christ. OK. Anyone quick know the difference between plus plus i and i plus plus? Yeah, so the only difference is essentially the result of that expression. If that doesn't mean anything to you, again, who taught you C? So if I do plus plus i, the effect of that is to always change i and increase its value by 1. And the result of this expression is going to be the updated value of i. So if i is 0, and I do plus plus i, the result of this expression is 1, and i gets updated to 1. If I do i plus plus, then the result of that expression, if i is initially 0, is 0. And i gets updated to 1. So in this case, in the for loop, the value of this doesn't get used for anything. The only thing we care about is that it updated i. So we can use plus plus i or i plus plus. But because of that dumb thing with i plus plus, like always giving you the old value as the result, it turns out that if you write i plus plus and you don't actually mean it, it can make your program go a lot, a lot slower due to the compiler having to do some magic to enable that getting the old value. So if you don't need to use the result of the expression, you should always use plus plus i. If someone has taught you to use i plus plus, give them that spiel and say that's stupid. I did a program for performance course just changing i plus plus i to i plus plus can potentially, under very certain circumstances, make things go a hell of a lot slower. So unless you mean it, always do plus plus i. All right, another sidetrack. OK, so if I go ahead and I run this now, whoops, I still page fault. But here I'm printing out all of the entries in my page table. So I'll probably see in my parent's page table. So I'm going to see a lot of them, because in this case, there's only one valid one. So what should I, so likely if you're going through all these page table entries and figuring out which ones you need to copy, you only really need to create new L0 or L1 page tables for any valid entry. So I can just check first if it is a valid entry. So I could use VMS. Sorry, let me just separate this out a little bit. All right, so this is me just getting the entry from the page table. And this will happen 512 times. And then I might be able to ask, oh, hey, VMS, PTE, are you valid or not? So I could just ask if it's valid or not. Well, if it's not valid, I probably just want to skip it, right? So I can just be like, hey, if you're not valid, then continue. And then after that, I can print the entry. And now this should show us every single valid page table entry in the original parent's L2 page table, right? So if I run that, I ignore the page fault. Then looks like this, I get the single entry in the parent. That should mean that, oh, OK, well, if the parent has an entry in L2 that points to an L1, I also need an entry at the same index in my L2 that points to my L1. And then you have to do the same thing here, right? Where, whoops, not that. Where we create a new one for each entry and go on through so that eventually, once we hit the end, we have pointing to the same or two different pages. Yeah? All of your programs start with a bunch of memory elements. Yeah. Yeah. So when you fork, right, when you fork their complete copies, they can access the same memory, same addresses, same everything, right? They're indistinguishable. OK. So OK with this. Oh, shit, I only afford it. OK, apparently I'm just swearing today. All right. So real quick, the hard part, so that part should be relatively easy. Maybe it takes you a little bit to get used to using the code. That is OK. But the hard part is going to go from using copy on right. So the idea behind that starts off the same where we make a completely new set of page tables for the new process. But initially, we have both of them sharing memory. And this is OK as long as they are both reading it, right? So as long as they're both reading it and haven't made a modification, they look exactly the same. So what I want to happen is that as soon as one process tries to modify that virtual address, let's say it was process three, it's trying to modify this, which is being shared between two processes. What you want to do is you want to detect that that's happening and then make a copy as late as possible. So if it tries to make a modification, then I go, OK, no you don't. I grab a new page, and then I make the modification on that page. And then I point p3's last entry to that new one so that they are otherwise independent. So I didn't screw up whatever process two thought. So you'd have to copy the entire page and then make the modification, because everything else needs to still exist. Oh, yeah. So when I did this, it was originally pointing to three. Because I'm trying to share memory. Yeah. Yeah, how do you know that it's connected? Is that your question? No, what's your question? Only the final one would be copy unwrite. The page table should be independent. Otherwise, it sucks for you. Yeah, copy all the page tables and make sure the memory's the same. All right, so real quick, how you would do this is basically this is what it's going to look like after you fork if you share pages. And you're going to need to keep track of which pages are being shared. And one of the typical things to do for copy unwrite is to go ahead and turn off this write permission in both the child and the parent. And then if either process tries to modify it, it will cause a page fault. And in this code, how the MMU works is it will generate a page fault. And then it will call your handler. And then in your handler, you are allowed to fix up that entry. So you're allowed to modify that entry and make it point to a new page. And then it's going to retry the translation and then use that as the final translation. If it fails the second time, then you're out of luck. So what should happen real quick, whoops, not that, is if it detects either process, it turns off the write. And it would generate a page fault and then say it was process three. That's when you would grab a new page. You would make a copy. You would update this ppn to point to page seven. And then you could turn on the write permission because it's the only process that's using that page. And it can go ahead and use it. And you should also detect that if process two goes back and tries to modify it, it's the only one using that page. So it doesn't need a bother to make a copy. It should just turn right to one. Yeah. So question is, how do I know the parent's the last one left using it? You need to keep track of it. So you should keep track of what pages are shared and by how many. And guess what? That's where an array, not a linked list is good for. That's why you have that index function and everything. And you also have a custom bit that you can see, hey, is this page possibly being shared or not? So you need to keep track of like the number of references to a page. And you should also make use of that custom bit because you probably need to tell yourself that, hey, this is a page that's subject to copy on right. Because it's also valid to just have a page that you're not allowed to write to. That's perfectly valid. So you have to keep track of, is this a copy on right page or not? All right, went over. All right, so other than that, just scored. Or I'll be around here until someone kicks me out. So just remember, pulling for you, we're on this together. All right, went.