 Oh, whoops. Alrighty, welcome back to our operating systems. So today, we are talking about the page table implementation. So after this, well, you could have started Lab 3 last lecture. But after this, this is the last thing that will help you with Lab 3. Lab 3 is like 30 lines of code, if that. Most of it is thinking and reading code. And other than that, it should be really short. So don't worry if you spend a week thinking of it. And you should be able to finish the first part of it relatively quickly. The copy on right might take a bit more. All right, so this is where we left off last time. So we have something that looks like this. There are three levels of page tables. There's an L1 or L2, L1, L0. And there's just this root page table register. Something like SATP could be called something different on different architectures. And that points to the L2 page table. And then given that L2 page table and a virtual address, it knows how to walk, where to access everything in each page table to walk to the next one, to walk to the next one, to eventually get the entry in the L0 page table and do that physical address translation. So how did I get the nine bits there? Well, that should correspond to the number of bits for each level, right? That nine bits. There are two tables based on entry bits on the bits. Yeah, so there are two. Yeah, so in this case, there are two to the nine entries in each table. Remember, it was because we wanted to make each of these page tables fit exactly on a page. So that's where we got that from. Because, OK, something is in my eye. That's lots of fun. So how we got that was the page size was 4 kilobytes. So that's like our default page size. So it was 4,096, or 2 to the 12. And then our page table entry size was 8 bytes. Or in other words, 2 to the 3. Whoops, I can write that better. 2 to the 3. So if you want to figure out how many entries I can fit on a single page, I mean, I just divide those two, right? 2 to the 12 divided by 2 to the 3, which is the same as 2 to the 12 minus 3, which is 2 to the 9. Yeah, so each of those entries can point if they're in an L2 page table. Those can each point to a different L1 page table. And then as soon as you get to L0, they can each have that's how many entries there are. Yep. So a page table entry is essentially, let me go back, page table entry, the main part of it we care about is it just stores a physical page number of the translation. Other than that, there'll be some permission bits, like whether or not this is a valid translation, read, write, something like that. So for a page table entry, the only thing is there's a lot of other information. The only things we need for the course is a page table entry stores the physical page number, which in this case is 44 bits, and then some permissions, read, write, valid. Yep. Yeah, so if it's valid, we'll go through the translation. So if it's valid, it means that translation can continue. If it's invalid, it just stops right there and says I can't do this translation, it's not valid. Yeah, so not all page tables will be filled all the way, so they just contain invalid entries. So if you wanted a page table for a process where you can't do any translation, I could just have my L2 page table here and just fill it full of invalid entries. So if it tries to translate any address, it will fail immediately because there'll be no valid entries there. Yep. Yeah, so the question about the dirty bit and we will not be super concerned about it. Once we get into hard drives and stuff, we'll see what we could use it for. It's basically for creating the illusion that once you run out of memory, it can start using the disk and essentially move memory to disk and it needs that bit to keep track of stuff to make things consistent. But we'll get to that much, much, much later. Yep. Yes. Yeah, so the L0 page table is the only one with the actual physical address of the thing we're translating. The rest are just following page tables along. So yeah, so once we get to the L0 page table, it's the same idea as when we had one giant page table that just has the translation, we just use it, we're done. But aside from that, L2 points to L1, L1 points to L0. Yep. So these could be virtual or physical for the purpose of this course to just assume they're all physical. So all these pages just live in physical memory. But the kernel could use virtual memory too, but the simplest thing is that the kernel just uses physical memory. But we won't concern ourselves with little details like that. For the purposes of this course, page tables, everything just lives directly in physical memory. Okay, so yeah, keyword about alignment, why this all works too. So if you haven't heard the word alignment, it basically just means everything is nicely divisible. Computers like when things are divisible. So if pages are 4096 byte aligned, that means they always start when, like the offset zero is the same thing as when all of the lower 12 bits are all zero. And in computing, we like alignment because it makes our lives a lot easier. So for example, if I had a weird page start, where like the page started at address 7C00, if we add a page size to this to figure out what the last byte would be, the last byte would be like address A, B, F, F, which is just really, really ugly. So instead, everything is page aligned. So it starts at zero and all the offsets are multiples of the page size. So it will either start at zero, 4096, 8192, so on and so forth, I'm out of math at that point. So that means the pages start at where all these three hex characters are zero. So if it started at physical page number seven, that would be addresses 7,000 all the way to seven FFF. You know where you are within a page. Nice and easy to compute everything. Computers really like that. So here's a question. So is address EC8 byte aligned? I see some head shaking. Yes, no, yeah, a bunch of nose. So the easy way to tell if something is eight byte aligned, it means that the address should be a multiple of zero, eight, 16, 24, da, da, da, da, da. So if you write it out in, if you write it in in hex, you can see if it's a power of two because computers like powers of two. So it's either gonna be eight byte aligned if that hex character is zero or eight, and that's it. Yep, yeah, yeah. So that's another way to figure out. So eight would be three bits, so the last three bits have to be zero. So same thing for anything else, right? So because it's all powers of two, any higher bit, if it's on, it's still multiple of that because powers of two do that, right? Yep, yeah, C is 12, but it's not a power of eight. So this is asking if it's eight byte aligned. Okay. So yeah, because C is 12, it's not a multiple of zero, eight, 16, 24, da, da, da. So this address is not eight byte aligned, but this would be four byte aligned because 12's a multiple of four. So if it's four byte aligned, it would either end with a zero, a four, an eight, or a C, so four things. So any questions about alignment just makes our life a lot easier. We don't have to do a weird computation or fiddle with anything. Computers love alignment, yep. So I don't have to like, so if things weren't aligned and this is where my page started, in my page table entry, I couldn't just store the physical page number because things aren't aligned. I need to know where it starts and it doesn't not, if not everything starts at the same boundary, then I have to store that. That's more information I have to store. It's just a waste of space, right? And it makes it harder to add to because if everything's aligned, well, it doesn't really need to do any hard addition or anything. It just takes the offset, slaps it in there. That's it. Yep. So 4096 is just the page size. That's a hardware question. Someone thought that would be a good page size to choose in the 80s. And we still have it today. And hey, seems to work, hey? All right, so we simulated an MMU before, so if we have any more questions, we can go back to it at the end. So here's a question for making sure we understand the different levels of page tables. So say our program, when it was running, it actually uses 512 pages worth of memory. So in that case, how many, what's the minimum number of page tables I need and what is the maximum number of page tables I need? So I'll give you a second to think about that and then you can let me know. Can I do it with one page table? If I have, you know, assume, whoops, wrong way. Assuming our three level page table, I'm assuming this is our system. So can I do it with a single page table? Yes, oh, okay. How would I do it with a single page table? Two to the nine. So if I have a single L2 page table, it can point to two of the nine things, but it points to an L1 page table. So it would have to point to an L1 page table and then if I only have one page table, I don't have anything. Yeah, three minimum, right? Everyone agree with that? Why three minimum? Yeah, so exactly. So in this case, if I'm using 512 pages in my program, the minimum I need is, minimum I need is I need one L2 page table, which points to a single L1 page table, which points to a single L0 page table and then in my L0 page table, these are all pointing to 512 different pages. So if my program uses 512 pages, I only need three page tables and yeah, I can do 512 translations with just three page tables. Oh no, so this is with multi-level page tables because that's what your system actually uses. So that three-level page table is what we actually use. So in this case, if this is my minimum, what is my maximum if I get super, super unlucky? Yeah, so I might need 512 L0 page tables because each of them just points to one page. So if I need 512 L0 page tables, worst case, how many L1s do I need? Yeah. Also 512, right? Because every one of these L1 page tables could just point to that L0 page table. So in that case, I would also have 512 L1s and then for L2, you only have one L2 page table because that's your root page table. So your process only has one L2 page table and this one would be full. So, oops, every entry in this L2 page table would point to a different L1 page table. So it would be full of entries that each point to an L1 page table. Yep, so this is worst case. So I need 512 of them because my program uses 512 pages and each of those only has a single entry in it. So only one entry that's valid. Yeah, only one entry that's valid that's pointing to a page that the program's actually using. So like this page table would be pointing to, I don't know, page one that it uses and the last one would point to page 512 or something like that. So in this case, how many page tables do I have in the worst case? 1,025, it's pretty bad. So this is also a reason we like contiguous addresses because the way we chose which bits were used for what, well, guess what? All the L0 bits are the least significant bits. So if all the addresses are contiguous, it means all their entries will also be beside each other in the L0 page table because if I was crazy, I could flip the indexes and use the current L2 bits for L0 if I was insane, but I don't because I want everything that's contiguous to be located beside each other. I don't want to just create massive amounts of page tables when I don't need to. And yeah, so what can make you get this lucky? Well, it would mean that for every page I use, all the L2 pages or all the L2 bits are different. So it would be like, I guess I can't write on it. So like your virtual addresses would span every single L2 bit and then pass that, it wouldn't matter. So it would have to use every index in the L2 page table. So your addresses would be spread out and that's the only reason why you would get it. So another reason why to keep memory contiguous and as it turns out, one of the primary reasons. All right, so another thing, another fun question you might get asked is like, oh, hey, multi-level page tables, how do I calculate how many levels of page tables do I actually need for some architecture? So in this case, this actually describes the architecture of 32-bit CPUs, which were used, I guess when you were born, wait, when, what year were you guys born? Like 2000? Oh, three, okay, so they had, so 32-bit CPUs are before your time, I guess. Yikes, all right, cool, don't feel old at all. So 32-bit CPUs, ye old ancient hardware, back when we didn't have gigabytes of memory. So this is what the system actually looked like. So you had 32-bit virtual addresses and your page size was the same thing we used today, 4096, but the page table entry size was only four bytes because the physical addresses were also 32 bytes. So it doesn't need that much room for the page table entry because it's only a 32-bit address. So on this system, we want to figure out, well, how many levels of page tables do I need for this old ancient system? So in this case, we have a 32-bit virtual and physical, and then we're also given the page size, so that is 4096, so that is a byte page size, and our page table entries are four bytes. So in that case, same idea, everything we're gonna do, we want to fit our page tables exactly on a page for all the same reasons we wanted to do it before. So in this case, we have to calculate how many page table entries we can fit on a single page and the number will be slightly different. So my page size is again, two to the 12. A four byte PTE is two to the two, so therefore if I divide them, I get two to the 10. So this is number of PTEs per page table. So in that case, I would need how many index bits to select an entry in a page table? 10, all right, and it's just this number. So this is how many index bits I need. So my address would look something like offset, which is still 12, and then some number of L zero and then a bunch of other levels until we actually have virtual addresses. So the equation, which isn't much of an equation on the sheet is the number of levels you need is equal to the ceiling of our virtual bits minus offset bits divided by however many index bits we need. So let's see, can we do some quick math? How many virtual bits do I have? 32, how many offset bits do I have? 12, how many index bits do I have? 10, so if I plug that all in there, it's 20 divided by 10 and then ceiling. Luckily, it's a nice even number, so we get two. So we need two levels of page tables in this case. And you can double check because if you write it out, it would look something like that. So we'd need two levels, we'd have an L one and an L zero and our offset. So thankfully, if we add 10 plus 10 plus 12, guess what, that's 32. So nothing terribly surprising. Yep, so this is like by virtual bits, it's like you can do translations for all of those bits. So you could of course make it smaller by making the virtual address smaller, but this says virtual address, we need to be able to address all 32 bits. Yeah, no, nothing that's unused. Yeah, so also in this case, if we increased our number of virtual bits to 33, while our calculation would change from 33 minus 12 divided by 10 and to ceiling. So it's like 21 divided by 10, which is 2.1, which we would have to round up. So if we add one more bit for the virtual address, guess what? We need another level of page tables to be able to support all 32 bits. Your L two page table in this case would only have two entries that are ever used because the index would only be zero or one and that would be a complete waste. So that is why the virtual address is supported by your system is going to be multiples of whatever the number of index bits is. So in this situation, where we have a 39 bit virtual address with three levels, if we want to add another level, well, we can now support 48 bits and it just goes up like that. So you may as well just, if you add another level, you may as well use the whole thing because otherwise you're just wasting space. So that is why that, oh, and yeah, question about the memory addresses we see in Visual Studio Debug, are they virtual or physical? So all the addresses you've seen in your processes at every point in your life, they're all virtual. So you have maybe in that computer architecture course you saw real physical addresses, but other than that, you have never seen a physical address in your life. They've all been virtual addresses. Isn't that fun to know reality? Yeah, why would the kernel ever show you a physical address? You can through the virtual address. Yeah, like there'd be no way for it to show you and then you'd be able to, you can already exploit stuff without information from the kernel. That's probably too much information to actually know or useless information because what are you gonna do about it if you know about it? You can't do anything about it anyways, right? Yeah, no, it doesn't increase the amount of memory you have but it makes it so that you can, every process can have its own independent view of memory, right, and have its own virtual memory so that it can't mess with another one. If you didn't have virtual memory, all your processes would be using physical memory and there's no way to protect processes from each other in that case. Yeah, so the kernel would aside, the kernel is the only thing that deals with physical memory so the kernel assigns physical memory to your processes through virtual memory. Otherwise, if your processes just use physical memory, you have no security, you're screwed. So like your banking app, if you just knew where it was in memory, you could just start reading it and do whatever you want. So for like very small embedded systems, they actually, some of them don't use virtual memory because if you're the only application on it, there's no point of using virtual memory, it's just slower. So if you're the only, yeah, so if you wanna do embedded, if you hate virtual memory, just go into embedded, make it so that on that piece of hardware, you're the only thing running, if you're the only thing running, you don't need to protect against anything else, you are the king. So in that case, you have to do kernel things like try and protect the memory and other stuff but someone's probably got a toaster or fridge or something that has like zero security and whatever and you just get hacked and no one cares. So don't be that person, but yeah. All right, so what we might have noticed too is that this is really slow. Like instead of accessing physical memory directly, if I have three levels of page tables, well, I have to access L2, then L1, then L0 and then the memory I actually wanted, it's really, really slow. So there's a few things, likely if you access some memory on a page, you're going to access the byte next to it or the byte four away if you're using integers or whatever and a process might only need a few mappings at a given time. So it only uses a few pages at a given time, it doesn't use that much memory all at once. If in the literature you might see the pages, a process is actually using is something called a working set. It's basically just pages it's actually using. So our solution to this is what we always do in computer science. It's slow, throw a cache on it, we're good to go. So the cache for your MMU is something called a TLB or a translation look-aside buffer. It just caches that translation that we did. So if you have a virtual address, first it will look it up in this TLB, which is just a cache. So it will just look up all 27 bits of that virtual page number. Doesn't have to be concerned about levels because it would have already been accounted for in here. So if it finds it in the cache, it would find the page table entry that you would have got if you walk through all the page tables. And if it's a hit, just uses that page table entry, gives you the translation directly. Otherwise, if there's a miss and it's not in a cache, well, then we have to do the thing in the red box. We have to translate it using the page tables. So we'd have to go L2, L1, L0, get the address. And then as part of it, it would add the address to the TLB and then do the translation and that part would be really slow. Yeah. So even if you only have one page table, that cache would have a benefit because if you have one page table, instead of one memory access, if you just had physical memory access, you have two. So one for the page table and then one for the original one. So here, no, the cache is separate. The cache is way faster than memory. So searching the cache is going to be way faster than memory. So we can see that. So here, because one of our goals was we wanted to have performance as close as possible to physical memory. So using the cache, we can calculate, whoops, the effective access time. So this is the calculation if there's only a single page table, which is what you suggested. If there are multiple page tables, it gets worse. So the time a memory access takes, if there is a hit in the TLB, is just the time it takes to search the TLB plus that original memory access. If there is a miss, well, you still have to search the TLB and figure out that it's not there. And then in this case, if there's only a single level of page table, there's two memory accesses. One for the page table and then one for the original memory access. So if we had three levels of page table, that number would not be a two. That would be a four because there would be one L2 and L1 and L0 and then our original memory access. Yeah, for right now, this one is just a single level. So one for the page table, one for the original memory access. If we had three levels, three memory accesses for the page tables, one for each level and then the original one. So the effective access time is just this alpha, which is the proportion of cache hisses, cache hits, multiplied by the time it takes to access the memory if there's a hit and then the opposite of that, the miss ratio times how long it takes to search if there's a miss. So in this case, if we had a hit ratio of like 80%, which would be 0.8 and our TLB search is 10 nanoseconds, which is 10 times faster than our memory where our memory takes 100 nanoseconds. The effective access time would be 0.8 times 110. So 10 nanoseconds for the search plus 100 for the memory access if there's a hit plus the 20% miss times 210 nanoseconds. So this is 10 nanoseconds for the search and then 200 nanoseconds for the memory operation. So our effective access time would be 130 instead of 100. So it's only 30% slower, which isn't too bad. Yeah. So the cache would be like a fully associative, blah, blah, blah architecture course. Plus all the accesses I have to do to translate the address if I had to like go through the page tables. No, cache is its own separate thing. So yeah, the cache is not connected to the memory. All right. So that is fun. So we have effective access time. All right. So now we can talk about what happens during a context switch. So every process has its own page tables. So whenever you context switch between processes, well, any of the entries in the TLB are only valid for the process that just got switched out and was using it. So you have to do something with the cache. You have to clear the cache or flush it. Don't ask me why we say flush, but it's the term. It's basically we clear the cache. So if I'm context switching to another process and starting to use their page tables, well, if I switch to their page tables, everything in the cache is now not valid. So I have to explicitly clear it. So on risk five, there's an instruction called S-Fence VMA. And that instruction will flush the TLB on x86 or like Intel systems or AMD systems. Whenever you switch that root page table register, it will automatically flush the TLB for you. On some more exotic architectures, the TLB might also have like a process ID so it knows which translations are valid for which processes, and then it wouldn't have to flush the entire cache. Yeah. So just whatever it would probably be like least recently used or something like that. So whatever is, yeah, whatever is used most recently. There's a bunch of different caching policies and stuff we'll see some once we get later in the course, but they're not that interesting. All right. So we can actually test the TLB. So there is this little sub project in our course, in our code called test TLB. And what it does is it allocates a big block of memory and then accesses the memory every so often number of bytes. And then it tells you how long it takes. And it was written by everybody's hopefully friends, Linus Torvalds that started the Linux kernel. So he wrote this code. If you wanna see code he wrote, it's just a little utility. So what this will do takes two arguments. The first argument is the amount of memory to allocate. So in this case, if I allocate 4096, I'm allocating a page. And then this number is the offset between memory accesses. So if it's four, it will access every four bytes on that page. So it will access byte zero, four, eight, 12, 16, and then record how long it takes. So if I run this, I can see that it takes 1.6 nanoseconds, which is quite fast. The worst case is if I do something like this. So yeah, also in this case, the 4096, how many memory accesses do I have there if I allocate a page and I'm accessing every four bytes? Yeah. Yeah, 1024. So it will calculate, see the whole time that took and then divide it by the number of memory accesses and that's how it knows how long each thing took. So for this, we have 512 memory accesses. For the first memory access, would that be a hit in the TLB or a miss? Yeah, a miss, right? Because it wouldn't have that translation available. It's the first time you're accessing this memory. So it would have to go through the page tables, figure out where it is, and then hopefully put in the TLB. And then for every other access, for the other 511, are they hits or misses? Hits, right? They're all in the same page. So if they're all in the same page, they would all be hits, so I would get one miss and then 511 hits. So that's why it's really fast. So in this case, if I allocate that gigantic number, so in this case, I'm going to allocate that giant number, which is 512 megabytes of memory. And I'm going to access every 4096 byte. So how many misses will I get for that? All misses, right? Every access is a different page. So I expect it to be slower. How much slower is this going to be? So this is like one now and a second. If it's mostly hits, let's see what happens if they're all misses. Yikes. So if they're all misses, that's real bad. It is much, much, much, much slower. How much slower? Oops. So it's about 24 and a half times slower if they're all misses as opposed to hit. So guess what? Things being contiguous mostly matters through the TLB. And that's why everything should be beside each other. Yep. So the much bigger one is always a miss because every single memory access, because I'm accessing every 4096 byte, every access is going to be on a different page. So if every access is on a different page, it's miss, miss, miss, miss. And I'm walking through the page table every single time and it's a disaster. Yeah, so if I allocate a bunch of memory, they're back byte pages, right? So then I have to translate it if I ever access that memory. So yeah, we could see a middle ground because so this allocates like 16 megabytes and then goes every 128 bytes. So I'll have a few misses and quite a few hits. In this case I can see that, hey, 11.5 is better than before. So you can play with this number and see what different miss ratios produce. So that's a fun, fun, fun, fun, fun one. Oh yeah, and a question, how does a kernel stop a child from accessing changing memory when the page tables are copied? So it's the kernel's job to manage the page tables which is also your job in lab three. So in order to make sure that the child processes can't monkey with the parent's addresses, you have to copy the page tables correctly and do a very good job of it. Otherwise you have security flaws. Everyone's favorite. So other things too. So way back in lecture, was it like two? We saw this SBRK instruction system call and that is actually your heap. So this will grow or shrink your heap and your stack has a set limit. It's like two megabytes or something by default. So whenever you need to, whenever the operating system gets a call to say increase the size of the heap, what it's gonna do is it could, if it needs to back it by real memory, it can grab some pages from a free list and just do all the mappings in the page table to make it point to now valid memory and then it fulfilled that request. So the kernel doesn't care. The kernel only allocates pages to your processes and pass that, it just throws its hands up in the air and says you deal with it. So things like malloc will only increase the size of the heap and then malloc manages the heap for you. So if you ask for any size byte allocation, it does it, it runs completely in user space. The kernel doesn't care. And also because of that, because the shrink or the heap doesn't really shrink, the kernel can't really free pages. So malloc generally never frees any pages, which is sometimes why you just see a bunch of wasted memory all the time. Some memory allocators can use Mmap. We'll see the Mmap system call a bit in the next lecture. That is a way for your processes to ask the kernel to modify its page tables. So you can just allocate large chunks of memory and virtual memory and then have the kernel take care with, have the kernel take care of backing it with actual physical memory. So a processes address space might look something like this. So this is from like, if you use that little xv6 kernel, this is how it lays out a processes address space. So what the kernel would do is take the first page, which would be addresses like zero all the way to fff, and it would map that to an invalid page. Why? Well, because the null pointer is zero. So if you try to write to memory or whatever, you're trying to be right to virtual address zero. So the kernel would just map that to an invalid page so that it knows that you're accessing illegal memory and then it would just blow up. So that will never be a valid address for your program to use. Otherwise, your null pointer is not gonna crash your program, which would be weird. Other than that, the kernel would be responsible for like allocating some pages for the instructions based off your file, some pages for data based off, all your global variables and everything. And then for the stack, well, it would allocate some pages, however many pages it needs to fill two megabytes. And then what it would do is it would put a guard page right under, assuming that our stack grows downwards. The reason for this is because your kernel, like we've all seen a stack overflow message, I assume. So how you get a stack overflow message because really you're just accessing invalid memory, the kernel will put a guard page there. So it's a known invalid page that is below your stack. So if you get a page fault on the guard page, the kernel knows that, hey, well, that was beside the stack. So you probably overflowed your stack if you got an error there. So that's how the kernel detects a stack overflow because it sees an illegal access to that specific page. So if you wanna get around the stack overflow, you could blow up your stack really bad. So if your stack jumps over the guard page, so it would be your stack overflow by more than a page, then you won't get that nice stack overflow error. You will get just some random memory error, or if you jump too far down into data, you'll read some valid memory and then bad things will happen. But usually your stack accesses do not go over a page and then here would be your heap that would grow downwards too. And then in this case for XV6, this gets into like very technical things. So the trampoline is where the kernels interrupt handler is. So whenever you do like a system call, that is an interrupt in the kernel and whenever the CPU or whenever the kernel starts handling that interrupt, it does not change the page tables as part of that. So whenever the kernel starts executing, it's using your processes page tables and there needs to be valid instructions to run. So what it does, this trampoline is the code for the kernel signal handler. So it will map it to a known address for every single process. So whenever a system call comes in, it starts executing it in your processes address space and it would set it up so that that code is readable and executable and you're not allowed to write to it. Otherwise, if it screwed it up, you could start modifying the code the kernel runs and then really bad things would happen. And then for the same reason, the trap frame would also be mapped into your address space just to make things simpler and that would save all of your registers. That's like the variable to save all your registers to and everything. So it's just like an implementation detail. We won't really need to know that. Real kernels can use virtual memory. There's like very finicky details you have to get right for that, which is lots of fun. So other fun things that the kernel can do because of virtual memory is it can avoid some system calls by providing some fixed virtual addresses. So for instance, there's a system call to get the current time on the system. And if that was a system call, that might be really, really, really slow. And really, if you only care about like nanoseconds and something like that, the system call would take longer than what you're trying to time. So what the kernel does to make it really fast is typically you're only reading information. So it will pick a known virtual address and map it as read only into every processes address space. And that way, if you know this magical address and where to read memory from, you can read the current time without having to do a system call. So that's how things like clock get time work and why it's so fast. Yep. Yeah, so the kernel would be writing to that page but every single process would not be able to write to that page. So then whenever the kernel changes it, every single process can read it, no problem. But if the kernel screwed up and made it writable, then if you could write to it and then change the time for every single process on the machine, probably do something fun with that. And yeah, so there's page faults, page faults, let the offering system handle virtual memory. It's basically just means that the translation failed, can't find the translation or there was an invalid bit or it can't read or write or something like that. And this allows the offering system to handle it. So what you could do is just when a process requests memory, you can just set up the page table to be like make it invalid. And then whenever it tries to actually use that memory, then you handle the page fault, allocate a page and back it by physical memory and then let the program resumed as if nothing happened. So you can only allocate pages when a program actually uses it, implement copy on write, which guess what, is lab three, you get to do that, that's a lot, it's a fun. Or later, you can just swap that information to disk. So what we saw, a page table is just responsible for translating virtual addresses to physical addresses. MMU is the hardware that uses the page tables. We could have a single large page table. It's a bit wasteful in terms of space because not all processes use memory, that much memory. Kernel only allocates pages, that's it. It does it using a free list. All of our systems will use a multi-level page table to save space and the TLB is just a cache to speed up memory accesses. So with that, just remember, phone for you, we're in this together.