 All righty, whoops, holy, all righty, welcome back to operating systems, everyone's hopefully favorite course. So today, talking about page tables, this always breaks people a little bit. So glad you're here and we can suffer through it. So where we left off in this hot, hot room was that we had an issue. If we had a giant page table for a 39-bit virtual address, it was a gigabyte and you would have a page table per process. So that's no good. So what can we do about this? Any bright ideas for not having a gigabyte page table? Because well, most processes do not use all of their memory. Pretty much no process except, I don't know, maybe Chrome uses 512 gigabytes. So we don't need something that big. So what should we do? Possible to have some type of function, maybe, well, we're kind of writing a function, like very obvious solution. If I have a huge page table and I don't need all of it, what's something I should do? If it's this big, I want this big, just make it smaller, right? Fairly easy. So one nice property we might have, and this will get into your function idea, so we'll extend that a little bit to get where we're going. But first idea, we can have a cache. Cache will come into play later because having a cache won't reduce the size of that gigantic page table. It might make the lookups faster, but it's not going to help with that gig size. So first thing we could do is, well, pages seem to be what an operating system likes. It might be useful to just fit the page table on a single page. So if the page table fits on a single page, what does that mean? Well, we can calculate how many entries we could have knowing the page table size and how big an entry is. So, whoops. If our page size is 2 to 12 bytes and my page table entry, size is 2 to the 3 bytes, which is just another way to say 8 bytes, well, the number of entries I could fit on a single page would be something like 2 to the 9, right? So it's just how many entries will fit? What number would get me to 2 to the 12 if I multiply it by 2 to the 3? So if I just had a single page, I could fit 2 to the 9 entries on it. So one idea we could have is just, yeah, instead of that gigantic page table, make a small one. And then also, when the MMU needs to do the translation and everything, it just needs to know what page table to use for your process. So in this case, if your process just had a little tiny page table, well, then you would tell the MMU where that's located in memory, in physical memory. And then as part of the virtual address, it will know where to look up to actually find that page table entry that references its virtual address and do the same translation as we did before. So any problems with this? Yeah, so it's smaller. So what happens if there's not enough pages? Because this could fit 2 to the 9 entries, and then each of those points to a page. So at most, it would use like 2 megabytes we could actually access. So if a program needs more than 2 megabytes, it's flat out a luck. So ideally, we want something that's not quite as bad as a gig and not quite like this. So our solution, get ready for it, is just to have multiple levels. So this is how virtual memory actually works. So it takes that same 39-bit address, and the idea here is that each page table fills up an entire page. So that's where the 9 bits for each index comes from. And here there are 3 levels of page tables. So in this case, there would be an L0, an L1, and an L2. And that registered, which is the root page table, points to an L2 page table. And then how the translation would work is, given this L2 page table and the virtual address, well, the index we use for this L2 page table comes from these 9 bits, the upper 30, like bit 38 to 29 or whatever. So it comes from these upper 9 bits. And then the page table entry in that page table, instead of just directly having a physical page number we use for translation, it actually points to an L1 page table. And then in the L1 page table, it knows what index to look at from the virtual address. And then if that entry page table entry is valid, the physical page number will tell it where to go to to look up the L0 page table. And then mercifully in the L0 page table, it behaves exactly like we saw before. In that page table entry, we get the physical page number for the actual physical address we finally care about. And keep that the same. And again, we don't do anything with the offset. So any questions about this fun thing? Make sense? Yeah, so how this minimizes? Yeah, so that's a good question. Like, so before when we had a giant page table to translate a single address, we needed that giant page table. So we needed a whole gigabyte. In this case, how much space do I need to translate a single address? Well, this L2 page table is fits on a page. So I need one page for that. Then it points to an L1 page table, which fits on a page. So I need a page for that. And this points to an L0 page table, which fits on a page. So I need a page for that. So I need three pages. And each page is four kilobytes. So I only need 12 kilobytes for this to translate a single address instead of a gigabyte. So this is the only reason why we're doing this, because to translate a single address, we only need 12 kilobytes instead of a gigabyte. Otherwise, if we didn't have that benefit, it's just base savings. If we didn't have that benefit, there would be no way we would do this because this is a lot of work. Yeah, so in the L2 page table, it's just a page table, so it would have 512 entries in it. And it could be full. So it could point to two to the nine L0 or L1 page tables. And then in each L1 page table, it could point to two to the nine L0 tables. And then in our L0 tables, it could point to two to the nine things. So that way it kind of cascades. So we still can translate all the addresses. But if your program doesn't use that many, we save a lot of space doing this. But it does make it more of a pain. So remember requirements before that virtual memory should be really, really fast. So in this case, even if we just have a single page table, if we were just told to read some byte in memory, that would just be one memory access, right? So for this, in this scheme, how many memory accesses do we have to do to actually read the physical memory? How many? So if we look up an entry in the page table, is that an access? Yeah, that'd be a memory access. So we'd have to look it up and we'd waste one memory access just doing the translation. And then for this, you would also count the original access. So without virtual memory, it would be one memory access and then in this case, with just one page table, it's two memory accesses, which is twice as slow. What would it be in this case? Yeah, there'd be three additional ones, right? So I'd have a memory access to L2, then I have a memory access to L1, then I have a memory access to L0, and then I have my original memory access. So it is, this is twice as slow as raw memory and this is twice as slow as that where four times as slow. So it sounds like a bad deal, right? We'll get into the solution later, that's where your cache solution comes in. So you just throw a cache in front of it and it's all good. All right, but any questions about this translation? Cause if not, we'll just, we're talking about this all day. This is like, typically this is the second hardest thing in the course. Yeah, sorry? So it saves space because to translate a single address, I only need one of these, right? And then it points to one of these, which points to one of these, and that's all I need to translate a single address. Cause before, if I had, before when we had like that index was 27, that page table was a gigabyte, right? So we went from gigabyte to we split it off and we essentially made it more like pay for what you use. So this is only three pages to store the translation. So it's only 12 kilobytes. Yep, yes. So L2 is full of page table entries, same as before. It's just that the physical page number there is not for the translation. It's to, it's where to find the L1 page table. So you can think of it like a pointer if you want. It's the same idea, but it's just instead of storing the address of the translation, it stores the address of the next page table to use. Yeah, so, yeah. So instead of look it up directly, it's ask me, I can only answer 512 questions. Then I pawn it off to the next one. They can only answer 512 questions. They pawn it off to the next one. And then the next one's like, okay, I actually have the translation for you. So it is a much more roundabout way. And again, the only reason we're doing this is to save space because most programs do not use all the memory. So if every program on Earth used 512 gigabytes, our page table would have to be a gigabyte and there'd be nothing we can do about it. But most programs don't like your program. How much memory did your program use in lab two? Probably not that much. Hopefully, unless you, well, so even if you allocated a gigantic array, guess what? The offering system's lazy and it wouldn't have actually mapped it anyways. So it wouldn't have actually used any more memory. All right, so let's talk about this for the rest of the day then. So why we do this and why it's the same thing? Well, because allocating fixed size blocks of memory or pages is really, really simple. You do not need to have a fancy allocator or anything like that because everything's the same size. One block is as good as any other block. So when your kernel boots up, it can just divide all your physical memory in the pages and essentially just make a big link list of pages. So one page can point to the next one, can point to the next one, can point to the next one. It's all unused memory. So you can just throw the pointer somewhere on a page and you have a list of free pages. So if you need to allocate a page, really, really simple. You just, it's a link list. You just take whatever is at the beginning. That's your new page. You set the free list head to something else, whatever it was pointing to, that's it. And then if someone deallocates memory, you just add it to the front of the list. That's it, really simple operation. One block is as good as any other block. It can be reused later. So your memory allocator becomes real simple and we'll see more complicated memory allocators way later in the course. But for now, if you just deal with pages, just a link list, really easy. So here is our idea to use a page for each smaller page. And this is more of it in general. So this is again why we have two to the nine entries because of our entry size. And in general, for a page table entry, for multi-level page tables, level N will point to level N minus one. And eventually whenever you hit L zero, that's when you have your translation, the physical page number of the actual translation you actually care about. So for this, so this works for any number of page tables. It's just more work. So you can have four levels of page tables. You can have five levels of page tables. And that is also why if you look at the virtual memory supported by your machine, it is in multiples of nine. So if you wanted to have four levels of page tables, well, it would go from a 39-bit virtual address to a 48-bit virtual address because we're just adding another level. So it's just 39 plus nine. So the drawback is that, well, we have another level. So instead of four memory accesses, now we have five. So things are even slightly worse. So that is why you also want to limit it as much as you can. So if we only had two levels, right? That's only 30 bits, which is a gigabyte, which probably isn't enough memory. So that's why in today's day and age, we have three levels. So if Google takes over the world, we might have to go to 48-bit ones. So with that, some people get really confused about what a page table is. So another way to think of it, I think I said it before, but I will say it again, you can think of it as an array. So hopefully if I ask you this question, so let's say I have an array of 512-ints, how many bytes is that? Four times 512. Can we do math in our head? Yeah. Yeah, 2048. But you're right, too. You get used to powers of two, so I just multiply by two twice. So in that case, that is 2048 bytes. And then if you were to ask about the offset, well, each int is four bytes. So in that 2048 bytes, well, all the ints, the first one starts at byte 0, then the next one is at byte 4, then byte 8, then byte 12, so on and so forth. So that would be, remember, the offset is just how many bytes from the beginning. So what would be the offset of whatever the element at the second index is? 8, right? So at the 0th index, this is an array of integers. So it would be at 0. First index would be at byte 4. Then the next one would be at byte 8. So this would start at byte 8. So same thing with page tables. The only difference is, instead of an int, they store page table entries. Page table entries are at least the default system we're using right now are 8 bytes. So if the page table entry size is 8 bytes and we have 512 elements, well, then we have 4,096, which guess what? Same as a page size, which is what we were going for. So you should be able to double check yourself. So the size of the page table should be equal to the size of a page if we're doing this multi-level page table thing. And then the size of a page table is just like the number of elements or number of entries times whatever the size of an entry is. So should be able to do that math. Should all jive. Should be the size of a page. Should all make sense. You can double check yourself. All right, so let us go to an example and actually do a translation. So we will assume we only have, oh, was I a question? No? OK, just scratching. All right, let's assume we just have two levels of page tables. So we have a 30-bit virtual address. And let's say my virtual address is something like 3-f-f-f-f-0-0-8. So if this is my virtual address, well, I know when I translate it. This part, if I write it out in hex, this is my offset. It is 12 bits. And I do not have to translate it. So I just have to translate the rest of it. So if we look at our scheme from before, we would only have an L1 and an L0, and they would be 9 bits. So we would have 9 bits for our L1 index, then 9 bits for our L0 index. And then 3-f-f-f-f-f is basically just 18 ones. So this would all be 1, 1, 1, 1, 1, 1, 1, 1, 1. And why did I do this to myself? I don't know. But if we convert that to decimal, well, these are both at index 511. So they're the last element of the page table. So everyone good so far? That's where they would be. We're just getting the indexes. So to do the translation, you would need this register. And it would be set at the highest level page table, which in this case, since we only have two levels, it would point to an L1 page table. And it would just have a physical page number. So it could have something like physical page number 7. And remember, that is just the index of a physical page. So what that corresponds to is something like address 7000, because it would start at offset 0. And our page size is 4096, which corresponds to all three hex characters. So it would go from 7000 all the way to 7fff. That would be the last byte on the page. So our page spans all those addresses, which if you actually subtract them and figure out how many bytes are there, guess what? It's 4096, so that makes sense. So that would hold our page table, which is just like a giant array of numbers. So this would be our L1 page table. And in our L1 page table, it would be like a giant array. So there would be, if each row of these is a page table entry, how many of them would be in here? Yeah, there'd be 512 in here, right? So there'd be 512 entries. So it's just a giant array. So the index of the array wouldn't be here, so I could write the index to the side. So I'll put index 0 at the bottom and the last index at the top. So the last thing in this page table would be entry 511. And inside this page table, I'll just write it like it had a column. But remember, the two things we really care about in our page table entry is the physical page number and then the valid bit, whether or not this entry is valid. So I'll just write them at the top. But they would be, of course, in the page table entry. So for this element, it could have something like, I don't know, physical page number 08 in there and a valid bit. So if that is our entry 512 or 511, then for this address, we would use that entry. And it tells us that the next L0 page table is located on physical page 8. So that would correspond to addresses 8000 to 8FFF. And we'd have an L0 page table there. So it would look the exact same, have the same number of entries, except in this one, at entry 511, if I want to translate this address, it could have something like physical page number, I don't know, CAFE, just because I want to spell cafe, because I don't know. You don't get too many joys in this class, so this is a small little joy. So since this is the L0 page table, I'm mercifully done at this point. So if I need to translate this virtual address, well, same rules as before. I do not have to translate the offset. So when I write out my physical, well, I keep that 008 the same, because I don't have to translate it. And instead of that whole virtual page number, I just put in whatever I found in my L0 page table. So I would just get CAFE008, and that would be my physical address. So all good? Any questions about that at all? I thought we can play around with stuff, but that's basically how it works, yeah? Oh, so the question in the virtual address, what is the 3 for? So the 3 is just to make it fill up all 18 bits. So L1 plus L0 is 18 bits. So this is 4, 8, 12, 16. And then 3 is just two bits, which gives us the 18. So it's just to fill it up. Yeah, if I wrote out, like, if the address was something like this, well, then that would correspond to this, which is what, to the 7, 4 times slower. Yeah, so that would be entry was it 127 or something like that? So I just filled it all up. But it's just going to change what the index is. Any other questions? Other than please turn AC on, which I cannot do. All righty. So we get it. We're good, because this is essentially lab 3. OK? So there's what we had before, just so you have a record of stuff. And yeah, that's it. All right, so let's go play around with stuff then. So this is kind of what lab 3 looks like. Lab 3 will be less of a pain than lab 2. You won't have multiple processes or anything going around. You are essentially going to emulate whatever happens when you fork a process. So when you fork a process, that new process looks exactly like the parent. All the memory is the same. It would get a new page table. And then the silly thing to do, or the easy thing to do, to make them independent copies, well, you'd have to set a new page table for the new process you created, and then make it such that anything that the original process mapped to has to be valid in your process. And it needs to point to the same looking memory that has all the same values. So you would just have to copy, for every page it uses, you would have to copy the contents of that page and then make sure that it maps to the new copy of that page. So let's just see how the address would get translated. So for this program, I just ask if the kernel's page size is the page size I expect, which should be 4096, just as a Sandy check. And then I wrote this allocate page table function. All it does is return a new page that I can use for whatever I want. And it just plays with the type. So the type of it is it gives you just a 64-bit number to represent the page table entries. And there's just a bunch of functions that you don't have to access the bits in a page table entry yourself directly. You can just use the functions instead, and they'll play around with the bits for you. So all this does is return me a new page. And then the only thing my MMU needs to actually do a translation is it needs a root page table. In this case, I have three levels of page tables. So I have to give it an L2 page table. So here I create an L1 page table. And then in my L2 page table, I set entry zero to 0.2 my L1 page table. So this will essentially create a page table entry given the address of an L of a page table. So it just plays with the bits essentially to extract the physical page number out of it. So then I have to allocate a new page table for L0. And then in L1, at entry five, I make it point to that L0 table I just created. And then in L0, I create an entry at 188 that has a valid page table entry with the physical page number CAFE because, yeah, small joys, right? So then I have this MMU function. And that will actually walk the page tables. You don't have to write that part. So this will try and translate virtual address abcdef. And then also try to translate the address 1abcdef. So if I go ahead and run this, I get this address successfully translates, and then this address says page fault. What page fault means is that there's no valid translation. So that doesn't map to anything. That's valid. It can't walk the page tables and figure and get it to a physical page number. It's just invalid memory. And it's called a page fault which would trigger and interrupt to the kernel that the kernel has to deal with. And guess what? That's the source of your segfaults. So you accessing invalid memory, if you just guess, it will probably be invalid. And you'll get internally your CPU will have a page fault. Then your kernel is going to pass it up to you and say segfault. And you're dead. So everyone understand how abcdef got translated to c-a-f-e-f? Or should we go over that? Let's go over it. Good suggestion because, yeah, we have time. So the address I had was virtual address abcdef. And that's as far into the alphabet as I know. So it's hex, so I don't need to know any more. So that's good. So same way we translate it. So it would have an offset that we use because it's the same four kilobyte page size. So I do not have to translate that part. Def stays the same. Don't have to translate it. So for the rest of it, it's back to this. So I have to divide it into nine bits. Hex numbers don't play nice with nine bits. I unfortunately have to write it out in binary because that's just the way it is. So does anyone remember what a is in binary? What was our fun little thing to remember? 10, 10. A is 10. Binary 10, 10. So A is 1, 0, 1, 0. So what is B? Can we add 1 to that? Yeah, 10, 11. And then if we add 1 to that, we have to start carrying bits. It gets kind of hard, but we can do it. It's 1, 1, 0, 0. So that is abc. And then we have to go backwards and start grouping by nines. So from here, 1, 2, 3, 4, 1, 2, 3, 4, here to here. So this would be our L0 index. Well, that looks like a smiley face, kind of. So this would be our L1 index. And because we don't have anything else, it's like normal numbers. The rest are just implied leading zeros. So the rest of this would be 0, 0, 0, 0, 0. If you want to write it out. And then our L2 index, I'm not going to write 0 nine times. It's just going to be 0. So if that's the case, our L2 index is 0. What's our L1 index? 5, right? So the 1s, the 2s, the 4s, so 4 plus 1, 5. What's our L0 index? Probably 188. So if this worked, likely it's 188, because that's my entry that I had mapped. Whoops. So it's 188. So because my root page table pointed at L2, well, what happened? So because of that address, it would have tried to access the entry at 0, which would point to my L1 page table. Then my L1 page table for this address, the L1 index was 5. So I had to use 5, which led me to my L0 page table. And then what index did I have to use here? Well, I got it from my virtual address. So my L0 index was 188, and it had a valid entry. So I was able to translate it. And this address did not have a translation because let's see if we can figure out what that did. So let's go back. So if I had a 1 here, what changes? Yeah, the L1 index should change. So where we left off, like here. So this was C, this was B, this was A. If I just put a 1 in front of it, well, then that would just be 1. And then the rest of it would be 0s. So this would have changed from 5 to what? So this would have been an 8. We do some math, 13. Yeah, 13. So if it's 13, I could do something fun knowing that. So if I want to make that valid, well, I could do something like this. I could make it point to the same L0 page table or something like that. What's going to happen in this case? What do I expect to see? Should I see the same thing as before? Or what will I see in this case? Yeah, if I run it, they're the same address now, the same physical address. So these two different virtual addresses, which is what your process actually uses, might actually map to the same physical address. Wouldn't that be fun? Sorry? Yeah, the question isn't that not good, and depends who you are. So there are cases where this is useful. There are also many more cases where this is very bad. So if you change a variable and then it changes what looks like a completely unrelated variable, that's pretty bad. But if you start doing research or at your company, you have to be a computer wizard, you might find uses for this where it comes in handy. So if you want to essentially, you could use this if you want to share memory between two processes. So you could have it mapped to the same physical pages in both process, use them to communicate. You could also use it to protect your program if you want. So there's nothing to say that these two addresses could actually, in this case, they share page table entries, but it could be the case where, let's say I do something less silly. So if I do something like this, then the next address I make valid, let's see, would be go back. So in this case, if I change it to ABD, essentially all I'm doing is increasing this by one. And then this index is 189. So now if I go back here, these two addresses correspond to the same physical address. And that might be useful. You could do something like you could have this address because you can have different permissions for page table entries. You could make this one readable, or sorry, this one writable, and then this one only readable. And that might be beneficial. So you just let someone use the readable only one, and then you can change the writable one. And you won't have any problems because they just read it all the time. So that could be a very useful thing to do. And in fact, we'll get into it later, but that's what the kernel does for some things to make some things really fast. It will let your process have access to the kernel in read-only modes. And avoid system calls, be really fast. But this gets like too wizard territory. Yeah. So here they're mapped to the same physical address because that page table entry, in the page table entry, it says, whoops, what physical page number to use. But remember other things that are in the page table entry. So there's like permission bits. There's a whole bunch of other things. So this entry, I could set read write. And for this one, I could only set read or something like that. So it will suddenly change how the MMU actually uses it. So if it tries to write it for one address, it'll be OK. For the other one, it'll be like, no, thanks. So you could do something like that. But again, that's like super wizard territory. Although what you're doing in Lab 3 is like you're doing the kernel's job. So you're going to be wizard. So Lab 3, there's actually not going to be that much code. It's like 80 lines or something like that. If you write it somewhat sanely. But you'll have to think about it a good deal. Before you write those 80 lines, actually, probably less than 80 lines. You can probably do it in under 30 if you really want it. But you're probably going to, if you do the math on that, that's probably like a few minutes of line. So you're going to have to think. Yeah. So the MMU will detect if there's a page fault. So the MMU is what walks all those page tables. And to do a page fault, if any of the entries, the valid bits not set, it will just generate a page fault immediately. So if I had before when I had this address, well, the L2 index was 0. So it would have got a valid entry. And then found the L1 page table. But remember, that address used like 13 or something like that. And then by default, these page tables are just full of all zeros. So it would have looked at entry 13 and then found that the entry is not valid. So it would have just stopped right there. So you could page fault at any level. So if it was a bigger address that had a different L2 index, it might have died there. So it has to make it through all three levels in order to translate. And it could fail at any level. All right. Anything else? Yep. So question was about, see what does volatile and const mean? So const, OK. So const, overall, answers complicated because I got a PhD out of that one. So answering that question. So what const is supposed to do is make it so that you can only read from that variable. At least in C, it's simple. C++ is what I did my thing on. But makes it so you can only read to it, you can't write to it. And the compiler just enforces that. That's it. Volatile means that the compiler cannot do any optimizations because if I load a value from memory or something like that, the default assumption would be if I already load it and I access it again, I don't have to reload it because it's going to be the same value. So if I say, if I read something in variable x and x is 5, which corresponds to it must have read memory at some point. Well, two seconds later or in the next line, 5 won't change. It'll still be 5. So the compiler can do a bunch of optimizations where it doesn't have to load it every single time. And volatile just means you cannot make that assumption. So anytime you access x, it has to reload a fresh copy. And that's usually due to, it's supposed to be because that memory location represents hardware that you can't control and that memory can change at any point. So in this, so constant volatile only exists in C. That's it. So your kernel is not going to use that information. Just your compiler is going to use that information. So it just changes how things get compiled and what essentially just how it behaves. But yeah. And you also might think, hey, it's kind of silly that I just give it a virtual address and I have to fill out the page tables and try and make it valid. Well, guess what? This is a job the kernel actually has to do. It has to set up page tables for stuff. So remember in lab two, or sorry, not lab two, lecture two where I just had that little executable and I just had a starting address and you all asked me, why did I pick address whatever it was, like 10,000, whatever? And the answer to that was I could have picked whatever the hell I wanted to because that was a virtual address. And it is the kernel's job to set up the page tables for me so that that maps to that file I told it to load into memory. So it has to figure that out for me. So in lecture two, I could have picked any single address and the kernel has to figure it out for me. So when you have global variables and stuff in C, C just picks virtual addresses and the kernel has to figure it out. Yeah. Yeah. Yeah, so that's like if I had address ABC, whatever, then say my program, as part of the description of it, said, that's where my function is. Well, guess what? The kernel is going to have to figure out all this mapping and then set up the page table such that that actually represents your function. So it would load it into a page, like it would load that function on a page and then it needs to make sure that that address points to that page. So it would essentially be, it would have to follow these and then it would have to pick a page here and then that's where the actual memory is. And it would get this page from just the free list of pages. All good? All right, this is like the second hardest thing in this course historically. So next. So hardest is going to be threads. So you notice all your problems that some of you had in lab two that shouldn't have been things? Yeah, you get to fix that later. All right, so just remember, phone for you. We're all in this together.