 All right. Good afternoon. So Lab 5 help. You've requested it. Here it is. We need help. Anyone want to start? Yep. OK, library, we went over all the different functions and what we're supposed to do. So first off, we did lecture 14 where we did the MMU simulator, where we essentially go back to it. So this is what's given to you and what we kind of did before setting up the page table so we can translate address A, B, C, 1, 2, 3. We can all see that, hopefully. So there are any questions about that before we start anything else because you should understand how that works first. So all good. We set up an L2 table. Make sure the index points to an L1. L1 points to L0. That has our actual mapping. That points to the data because we're using a three level page table here. So this is what it would look like just to set it all up that you don't have to do. This is just to set up L2 points to L1 points to L0 points to a valid entry that has, in this case, read, write, set. And then it sets the root page table, which is supposed to simulate that register that's used for the actual hardware MMU whenever it does a translation. This is just purely simulating it so you can get a chance to manage page tables yourself because the kernel does that and you're not allowed to do that. So we just simulate it in software. So this is simulating it changing what root page table the MMU uses because it just points to a single L2. So this points to an L2. And then whenever we do, so there's functions that are just used for testing that simulate actually writing to an address and reading from an address. So in this case, we write to a virtual address ABC because we set it up so that is actually a valid address. So we can see what we read to it. We can write to it. We can read to it to see that we actually wrote to it. And that's the default thing we're kind of given. So then the thing you could add is like, OK, well, let's test our fork copy. So what happens on a fork? Well, one thing that could happen on a fork is the new child has page tables that look exactly the same as the parents, but are completely independent in actual physical memory. So if the parent has an L1 or an L2 page table, I have an L2 page table. If it has a valid entry for index 0, I have a valid entry for index 0. And whatever index 0 points to that L1 page table, if it has a valid entry for index 5, I have a valid entry for index 5. And those L1 page tables are, again, completely independent. And same with the last step. So if it has a valid entry for index 188, I think ABC goes to. Then I have a valid index for ABC. And it points to another page that's a different physical page of memory. So if one process changes the value at that address, the other process doesn't see it, but it's valid. Because they're exact copies, they look exactly the same. So we're good with the kind of high level goal of the lab. So API functions that you will have to use and you will find useful. So there's this header file. It has everything that you don't have to use everything. So they're kind of organized here. All this MMU stuff is mostly used for testing. And this will be the only one you use. Because in the fork, you need to know what L2 table to copy. And it's just whatever it currently is. So I would use this to figure out which L2 page table I need to copy. And that would lead me to copy literally everything else. Aside from this, there are pages. So what this is simulating is when you boot up your system, you don't have any virtual addresses whatsoever. All you have is physical memory. So this is supposed to simulate all the pages. They're supposed to be physical pages. So if you ask for a new page, it will give you a new physical page. But of course, you're not managing the memory yourself. It's already done for you. But that gives you a new physical page. Free a page, just like new and free. If you want to deallocate something, well, you free it. And then you can use that page again for something else. You probably never even need to call this. This one's just used for testing to see how many pages you use just to sanity check. So if you do a copy, well, if there were, if you were doing the fork copy and copying everything, if there are four pages in use by the page table, well, if I do a copy of everything, then there should now be eight. Because I should have my own independent copy of it, and there should be eight pages now. This is git page table pointer. So these two are mostly used. You'll find useful when you're implementing the copy on right. So this will flatten up page. And essentially here, you give it a pointer to a page, and it will give you an index for it. And it will be, the first page will be index zero, and then it'll go up to how many pages there are, which is in here. So there's max pages. So you can keep track of the number of references to a page by just making an array, and then you can get an index. You can turn a page into an index. So if you want to count the number of references to a page, you can make an array of size max pages, and then use this function to get its index. And then this just does the opposite. If you give it an index, it'll give you a pointer to that page at the beginning of the page. So the page table ones, how they are all set up, our page table index, will, given a virtual address and then what level you want to access, it will give you the index at, it will give you that index for that level. So for this, let's go back to where is it, main. So if we did that for like A, B, C, D, or A, B, C, D, 1, 2, 3, well, if we split that between my L1, L2, L0, then let's go here, then it would look something like that. So if I give it the virtual address, A, B, C, 1, 2, 3, and ask for the index at 1, then it will spit out 5. If I ask for the index at 0, it'll spit out 188. If I ask it for the index at 2, it'll spit out 0. So that's a way to get that. And then this one will give you a PT entry and that will be a pointer to a page table entry. And a page table entry, remember, is that 8-byte thing and you don't know quite exactly where the data actually is in that. It's just 8-bytes and you have to use other functions to actually figure out the values because if you actually just print it out the number, it might not line up with what you expect, so you should use the other numbers. So this takes a page table, so a page and an index, and then it will just give you a pointer to that index. Basically, it's just doing an array access and giving you the pointer. This one will do those two things in one step, so if you give it a page table virtual address at its level, it will directly give you a page table index, so you don't have to go through the two-step process. It just uses one and then the other. And then these two things, ppn to page, will, given a ppn or a physical page number, it will just turn it into a page like a valid pointer to a page and it'll be aligned to a page. Basically, it's just going to shift everything over by 12 bits and make the offset zero. And then this will do the opposite. So given a page, it will convert that page to ppn number. And in this case, we're assuming like it's that risk five setup, so we're assuming a ppn is 44 bits, so it's a 64-bit number, but only the lower 44 bits are actually going to be used. And then all the rest of these functions take a page table entry and they let you play with the bits. So this one will set the valid bit to zero, given an entry which is just a pointer to a page table entry. This will set valid to one and this will return what valid is. So if I want to check if something's valid, I would call that if it returns zero, it's not valid. If it returns one, it is valid. And then the exact same thing for all the other permissions here. And then the last one is getting the ppn out of the entry because you're not quite sure where it is and the other ones to set it. And then after that, we just have our two forks. So any questions about that? OK, so let's start off a goal with the lab so we can start it a bit. And should I do this first? Yeah. So just to be clear with what we're doing, so we're kind of assuming that we're given a certain number of physical pages. So in this case, if I called these our physical pages, well, say my memory starts at byte zero, which is actually a valid address, and I have eight pages. Well, if I have eight pages, I could call them page zero, one, two, three, four, five, six, seven. And that page number I'm calling them, that's the ppn. That's just the physical page number. So if I broke it down into the address ranges because we're dealing with four kilobyte pages, page one will have valid addresses from zero to zero FFF. Then page one, one zero zero zero to one FFF, which is hopefully 4,096 bytes, so on and so forth. So any questions about those pages whatsoever? So our page table entry, what it contains, it's just a number, it's eight bytes, and it contains, what, five things. The ppn, which is 44 bits, which you don't know where it is in that 64-bit number, so that's why you have to use the helper functions. And then there's a custom bit that you can set at will, which you'll find useful for when you do copy on write. And then there's read write, which are actually checked when you do that VMS write or read. So it actually checks that permission to make sure you have it. Otherwise, it generates a page fault, which you can optionally handle. And I guess we can see that. And then a valid bit, so to say that this entry is valid. And for new page and everything, by default, they're all zero initialized, so everything is completely invalid. So any questions about that? Yeah. So the read and write bits, so if you're actually accessing memory, like from your L0 table, if you're trying to read to that, the MMU will check that it actually has read permission, so it's actually allowed to read. So if I tried to do here, let's go back to. So yeah, we can just do it here. So first thing I do is write. Well, here I set permissions on that page with what I'm allowed to do. So if I do something like this, where I just don't set it, by default, it will be zero. And if I do that, I'll say fatal page fault, because you tried to access ABC123. And it gives you some more information, like where the page table is. It'll say level zero, so the last access. And this is the page table entry itself. It's pointing to this page, which is valid. That's fine. And these are the flags. So read, it's read and valid, so write isn't valid. And I tried to write to it. Yeah, so that stuff's actually checked by the MMU. OK, any other questions? OK, well, I guess we can start looking at it more. All right, so let's get started with, wow, please close, with fork. So if I were to start fork from scratch, well, we don't want to return just the root page table, because that's essentially doing nothing. And you're making both processes share the same L1 page table. So if one changes memory, the other will change it. They'll see it. Nothing is independent anymore. That's really, really bad. So we want to use the parent one to copy from. So that's what's given by the new page table. So we can just name it better if we want. So that's the parent's L2 page table. So we want to create a copy. So if we want to create a copy, we can call it childL2. So here, we know we need to get a new page, because we want to make them completely independent. So we need a new page. So we would need something. So we need a new page. And right now, that new page is 0. So if we do something silly, like just return it, well, then it should generate a page fault because it has 0 valid entries. So let's go into main. So here, I just added a call to it at the end. So we can just play with it and test it. So if I go ahead and do this, so that should create a new L2 page table. And then if you're testing it to simulate context switching processes and actually swapping the address space, we would do something like the testing code you're given does something like VMS. Set a root page table. And we would set that to the child. Oops, child. Wow, why my root page table? So we would do something like that. So now, whenever we access memory, it's going to use the MMU, or the simulated MMU, is going to use the new page tables we gave it. So now, if we do something like, I don't know, let's try and write a 3 to that address. So if we do something like that, so we'll get a fatal page fault. And it'll say, hey, you're trying to write to this address. This is the page table. So that was the one we got from the new page. And it says it failed when it was trying to do the level 2 entry. So that was the first step it made. And saying our page table entry, well, it said that it points to the physical page number 0, which is not valid, because that's like a null pointer. So it's not actually mapped in your process. And there are no flags. It's not valid. It's not anything. So there's no way it could work. All right, everyone good? Questions about that at all? OK, well, I am not going to do this for you, but I will listen. So in this function, if I want to see all of the valid entries in the parent's L2 page table, how would I do that? Yeah, so I can do a for loop of all the entries and just check them out. So there's going to be 0, or we do a for loop. i starts at 0. It's indexed. And then i is less than. We have num pte entries, because there's a certain number of entries per page. There are all the pte entries fit on a page. It's already defined for you, but we've done this calculation like 1,000 times. So this would be 512, but it's given to you, so you don't even need to know that. So we could loop over every single pte entry. So the function we have to do that to get an entry is this one. So we could just get an entry from an index if we want to check it out, because we don't have a virtual address. We're just looking over the page table and just seeing what it has. So we would use this guy. And our page table we're looking at is the parent's L2, because that's currently what we want to copy from, and our index. So now every time, we will have a pointer to a pte entry, and then we can ask questions about it. So if we don't ask questions to it about it, I did a little helper function here that will just print the pte entry. So we can see. So what do we expect to happen if we actually run that now? So the comment was we set the read flags initially to 1 for all of them. So this is the parent's table here. So it's L2. It got an entry for whatever this virtual address is, and then set the physical page number to point to an L1 page table, and then we set the valid bit. So how many entries are in my L2 page table? 1. And I'm printing out 512 entries here. So I should probably expect 1 to be valid, and the rest to all be 0. So let's go ahead and Sandy check ourselves. So if I do that, yeah, there's a lot of 0s, a lot of 0s, a lot of 0s, and then at the very top, hey, there's a valid one, because the first index it uses was 0 in this case. So it's actually pointing to the L1 page table, and it's valid. So it would actually follow that it wouldn't generate a page fault immediately. Cool. So do, do, do. All right, so what would I do if I just didn't want to print all of the stupid entries that are not valid? Yeah. Yeah, just check. If it's valid, I can do something like if not VMS was a PTE valid, and then surprisingly, it takes an entry. So I can do something like this. So if it's not valid, I can go ahead and ignore it. So if I want to just skip anything that's invalid, because I don't have to do anything. If it's invalid, it doesn't lead anywhere. I wouldn't have to copy a page. I wouldn't have to do anything like that. So if I do that, whoops, yeah. So up here, I still have some other codes. So up here, it just prints one page table entry because I only have one valid entry now. All right, anywhere else I should go with this? Yeah, yeah, yeah. So here, we can kind of pseudocoder steps here. So if there is a valid entry here, well, so if I'm looking at L2 entries and I find a valid one, it means it points to an L1 entry. And there's not going to be any tricky things where two L2 entries points to the same L1 because that would be weird and make your lives harder. And that's not the purpose of this. So if it's actually valid, then it points to an L1 entry and yeah, we would have to make a new one because we want our own copy of that. So I need to make a new L1 and then I would point to it for this entry. And then I'd have to go through and then essentially do the same thing for L1. Any valid entries in L1 while they point to an L0, I have to make a new page for that, each one of those. Copy it, make sure it points to that. Copy, make sure the permissions are the same. In this case, it just needs to be valid. So not a big deal. And then when we get to the end, the L0 points to a natural page. We'd want to probably copy all the permissions and then make it point to a new page so that they're completely independent now. So yeah, the comment is three nested four loops. You could do that. I would suggest not doing that. Essentially, it'd probably be a bit cleaner if it was recursive. And you just change the level. The only thing that's different between them is the level, right? So your base case is 0. At 0, I know I'm pointing to actual physical memory. So you could probably do a recursive thing for this. You could do three, four loops, I guess, be really kind of ugly. And you'd be repeating yourself a lot. But I guess you could do that, which why would you need to do that? Yeah, so with the banks in we did to actually using threads for that makes it go slower. If I didn't, there's one thing that artificially slows it down, but these operations are so fast. The overhead of threads is too much. And there's data races and stuff. So by the time you prevent everything, it's just way slower than just running on a single CPU. Yeah. So here, let's go to this. So for ABC123, how it's kind of set up is that, well, it just grabs a page. It says, OK, I'll use page 0. Let's see. I'll use this for the parents L2. So it would grab that, make an entry for 0. And the ppn would point to an L1. And it would probably have a valid bit set. So the ppn would probably point to another one. This would be given as the L1 page table. It would have an entry for 5. ppn valid 1 points to another one. Maybe I use this page for L0. It has an entry for 180, whatever. And then this one actually matters for read, write, and all the permissions. So it would point to the actual page. We had write on, read on, valid on, something like that. And then our data is stored there. So if we actually tried to access ABC123, well, we would go whatever 123 bits in Texas into this page. And then there'd be four bytes there that we're going to treat as an int. All this does is just write ints to it. So if we write a value of 1 there, it would just be in there. So what you should do is when you do a fork and you're just doing the first copy one, well, you're making a new L2 page table. It should probably have an entry for 0 that points to a different one. L1 has that same entry, points to a new L0. And then you're saying that, well, why don't we keep all the permissions the same here or no valid? And the ppn we point here. OK, so if we do do that, say we are doing, so this would be in blue is the child, red is the parent. So right now, they point to the same data. And if the parent changes the value of, say, we called it x or whatever, so changed x after the fork from 1 to 2, then if we change back to the child, what's the value of x? 2, which is that what we want? No. So we need to do another copy here. So the only way this works is if we rewind and remember fork is supposed to be exact copies at the time of the fork. So whoops. So I would have to, if the value is 1 at the time of the fork, I have to copy all the data that is on this and put it here and then point to it. So now if I try and change the value of x in the parent, it changes its value of x. If I try to change it in the child, it changes my value of x. But where you actually start sharing the page, what you suggest is when you do copy on right. So that's the idea behind copy on right. So I try and share the page as long as I can, because if they're both reading, I don't have to actually make that physical copy of the page. I can just share it as long as they're both reading. I'm all good. And I delay the right to try and, yeah, I delay the right as long as possible. So now if one of them tries to do a right, the idea behind that is, well, when you actually make the copy, so what kind of what we alluded to in lecture, when I make the copy, well, I can do something like this and just set the right bit as zero. So now if either one of them tries to write to that page, it is going to go and generate a page fault. So go here. So it will generate a page fault and you are allowed to do things here. So it will, you can modify PTA entries here, you can allocate new pages, you can do whatever you want here. And whenever you return from the page fault handler, the MMU will retry the request once. And succeed. So at a really high level, so you'll get into the details of why you need that custom bit set. So your tools are, you have a custom bit and you can keep track of the number of references to a page. But basically what you would want to do is change that right permission to zero. Whenever a page fault happens in either the processes because either the parents can write that generates the page fault or the child, you don't know, you don't really care. You'll just be, the only page table you'll be able to modify is the one that currently generated the page fault. So let's just, for argument's sake, say the child did it. So the child tried to write, it generated a page fault and it wasn't allowed to write. So I have to make it allowed to write but I want to do it safely. So in the page fault handler, that's when you would stop pointing to this. You would get a new page, copy all the data. I guess you don't really have to copy the one you're about to modify but that would be a pain in the butt. So then you copy the page, you change where the PTE entry points to. So it points to its own new page and then you turn the right permission back on. And then whenever the MMU retries it, it can actually write to it and it's a different page. Yep. So the question is, can I go to the other page table and turn the right on as well? And yeah, that gets to be the trick of it because you won't. So whatever the page fault that gets generated is only for whatever, however it currently got there. So you'll have no way of knowing what other ones point to it unless you do like some really tricky stuff that you don't need to do that. So yeah, so my suggestion is to keep track of the number of references to a page to know whether or not you should do anything. So if there's only one reference to a page, you have a unique one. So here you wouldn't be able to modify it so its right permission would still be off. So one thing you wanna be careful of is if I write through the parent now, if I switch back to the parent and then I try and do it right to that address again, well, it's the only one pointing to that page now even though it doesn't have right permission because it got turned off before. Well, now it's the only one pointing to it so I can just actually turn the right permission back on and I don't have to do anything. So that's one of the tricks, yeah. Yeah, so the question is, does the copy on write just apply to the data pages or apply to the other page tables as well? So copy on write only applies to the data pages because if you actually shared page tables, then if you allocate new memory in one process, then another memory, the other process has it too, which would be weird. So it'd be a bit weird if you're running your program and then just new memories available that got mallocked by another process that's supposed to be independent and a whole bunch of other weird things can happen with that. So yeah, if they shared page tables too, not in this, but you could just, if you wanted to screw with another process and that was true, you could just free all your pages and then it's freed for them too so they crash immediately. Yeah, a page, so remember I said one thing you can do using, what is it? So using, what are the pages? Using this, whoops, that's my bad. So using this to get index from a page, I said one thing you can do is just create an array of the number of references to a page because there are max pages and it's supposed to represent physical memory so it's not gonna ever change. Like you don't just shove more RAM in your machine and you can use it, at least. Yeah, you have to reboot to use more memory. So you can just create an array that keeps track of the number of references to each pointer and then if you wanna turn it to an index, then you can use this. Yeah, as long as it's safe to do so, you copy the page, doesn't matter who started it. So the question is, like whenever I, wait, am I on this? Okay, so it was whenever I had this entry and then got the entry, it was all 000. That was your question? Yeah, here? So yeah, so when I do new page, it just returns the base pointer to the start of a page and it's all full of zeros. So it's just 4,996 bytes of zero which since valid being zero means it's invalid, they're all invalid entries. Yeah. Yeah, so you can do other things. So yeah, I guess you could do three, four loops unless if you really want, you might be repeating yourself a bunch. And then when you do copy and write, you do another three, four loops. Eh, you can probably do better than that but I guess I'm not your parents. Yeah, you could also just, instead of doing that, if you don't like the recursive solution, instead of doing like three, four loops, just how about doing two? So what about one for the, what level you're on and then one for the entries? So that would probably be a bit better. Also, I mean, if you want to do what every programmer does which is kind of just looking at code, reading code, well, the MMU has to go through all of them and you can see what it does. So it has a four loop that goes through the number of levels. Starts at two and it's defined and then essentially we'll actually do the correct index because this is for a specific entry but if you want to go over every entry, well, this is your loop per level and then you'd have an inner loop that goes over every entry. So you could do it with four or two, four loops if you don't like recursion. Any other questions, comments, concerns? So if you, like if you have multiple processes pointing all to the same page, no. So that's just describing what happens with the page fault handler. So the page fault handler, it will call it just on a single memory access. So on a single memory access, it will call it if it fails and generate a page fault and then it will let you do something, modify the page tables as you wish and then it will try that again and if it doesn't work because the permissions weren't set or whatever, it will just actually exit the process for you but each access is independent so you're allowed to fix it up one at a time. Like it doesn't happen just once. So for copy on write, like with this when you're originally sharing, let's see here. So when we're originally sharing and we turn the write permission off, whenever someone tries to write to it it will generate a page fault, right? Whenever it gets to the L zero. So whatever generates a page fault, you have one chance to fix it. So to fix it, that's when you like say the child did it. It would, you should copy the page and then make the child point to it, turn on the write permission so that when it tries doing that again, it would actually work this time. Yeah, and then if it switches to the parent and tries again, well the parent would now generate a page fault and wouldn't exit immediately because for every operation, you get one chance to fix it. So if it went back to the parent now and then tried to write to that same spot here, if it followed through everything, the parent's write is also off, that would generate a page fault and you can handle it. Yeah, yeah, so in this case, so you would still have the virtual address so when I went through and fixed it up here, so when the child tries to access it and I fixed it up and copied the page and set the write permission back, well, I didn't change the parents as part of that at all. Yes, well, so you would set the number of references like refs to one after that, but there's no way in your page fault handler, you can figure out every processes like L2 page table that eventually has an entry that points to it because this is an entry you would like just to turn the write permission back on, which is currently off. You can't just go and go change another process as page tables while you shouldn't because you can just wait, right? I can wait, let the page fault happen and then I'll notice that, hey, my write permission's off, but hey, I'm pointing to a page that I have the only reference to so I just turn the write permission back on and then it works. Yeah, so when the page fault happens, it immediately calls this page fault handler. No, this is all simulated, so the kernel doesn't care, right? I'm just writing a program that just allocates a bunch of memory and simulates an MMU, like the kernel has no way of knowing what the hell we're doing here, right? Yeah, yeah, so if you look at the code which is pretty much the MMU sim we went out except that I have a page fault, it calls a page fault. So one of the functions says that if it should generate a page fault and then if it does, if it hasn't faulted before, it will call page fault handler and if this is the second time calling it where it's faulted before, it will say, yeah, too bad and then exit. So this is, yeah, so this is where it happens. It literally just calls your function. But yeah, if you actually did this on real hardware, it would generate an interrupt. So there would be a page fault, which would be an interrupt, but this is just like a software interrupt. Well, it's not even really an interrupt because I'm calling it directly, but it's supposed to simulate like a page fault interrupt. Yeah, a page fault interrupt that you have no choice not to ignore. Any other questions or go over the test cases or whatever? So yeah, I added 12 test cases now based off feedback to make sure that we have done it correctly. So yeah, so for all this code, like you should just call that get root page table once because that's what you're copying from. That's it. Don't call set page table root page table at all because that's meant to simulate actually context switching. You're not supposed to do that when you're just setting up page tables and all that stuff. But most of the other functions you should know how to use. Yep, yep, yeah. For both of them, you're just returning the new child L2. Sorry, what do you mean? So you write the page fault handler just here. So this actually is called on a page fault like, so right now the main I have, right, generates a page fault because I create a new child and it's just full of nothing and I return it. So then I try and do a write to that same virtual address which won't work. So in here, for this it calls the page fault handler. So I just have right now we can fix it. I could change the entry or do whatever. So yeah, and yeah, that's a good point. So for this too, whatever you do the copy, you shouldn't have to fix up anything in the page fault handler because if a page fault happens and you made an exact copy of it, then a page fault should have happened, right? Didn't point to valid memory, it didn't have the right permissions, whatever. So this page fault handler fixing stuff up should only be applicable for the copy on write stuff. Okay, and yeah, sorry if any of this was unclear at the beginning. I thought I just kind of copy and pasted the MMU SIM and thought everyone was good with it, but yeah, sorry, I should have probably documented it more, but hopefully this helps. So yeah, and it's not a lot of code. You should be able, if you don't do like three levels of four loops should be like 100 lines of code. Any other last minute questions, concerns, anything? I assume everyone be working on it like Friday or over the weekend or Monday day? Yeah, Monday, yeah, okay. Yeah, I remember engineering undergrad, it was terrible. All right, anything else? All right, well, good luck, pulling for you. We're all in this together.