 Today, the day that your life is going to change forever. Promise. I'm going to tell you something today that's going to shock you, amaze you, cause you to rethink some of your life choices, and to change the way you look at computer systems forever. So today, what we're going to talk about is address translation. Specifically, we're going to introduce a level of indirection into how user programs deal with memory, and we're going to talk about why we're going to do that. And then we'll start getting into discussing what's difficult about it. So please keep working on assignment two, of course. We have one new group that's joined the 8080 club. That would be Harish and Akhilesh, are those guys here? There they are. Nice work. Good job. So now that club now has three members in it. I don't know, maybe eventually I'll get generous next week, and it'll be the 79 plus club or something like that. But 8080 has a nice ring to it. What's that? You're here this time? All right, congratulations. These guys, yeah, they were not here last time. So you guys are celebrated on video. OK, so again, two more groups that you guys should be befriending. They should have some new friend requests on Facebook later. Maybe after class you might want to say, hey, why don't we grab coffee and talk about file handles? Because that's a fun thing to do with coffee. So last time we introduced the address space abstraction and discussed the goal. What are we going to try to accomplish? How do we want memory to look to user processes? Any questions about this before we do a little bit of review? So remember, we had four multiplexing requirements that we were going to try to achieve in order to perform spatial multiplexing of memory. You're going to grant, enforce, reclaim, and revoke memory. What do we mean by granting memory? This is pretty simple. Anybody? Grant? No, but what does it mean to grant memory? What am I doing? Should be able to give processes ability to use memory. And we're going to talk today about multiple times that that happens, particularly in cases where memory is generated by or allocated by system calls. What about enforcement? What does that reflect? Yeah. Yeah, I want to enforce my allocations and protect them from each other. So I have rules that I want these allocations to follow. I certainly don't want processes to be able to write or read from memory that they have not been granted the ability to do so. Reclaim, I want to be able to transparently take existing memory that I've already allocated, and I'm not revoking that allocation. I'm not taking it away from the process, but I want to be able to repurpose it temporarily. And today, we'll talk about the primary mechanism that allows me to do this safely. Finally, at some point, I may want to prevent a process from using memory that I've previously allocated. So at some point, I should be able to ensure that it cannot any longer use some memory that I've given permission to use before. So fragmentation. Fragmentation is a problem. It's a problem that occurs when I can't satisfy an allocation, despite what? So there is enough free memory to satisfy the allocation. The amount of free memory on the system is greater than the request, but I still can't satisfy. And this happens for two reasons. So in internal fragmentation, the unused memory that I can't allocate is where? I just called me. Yeah, so in this case, it's inside the allocations I've handed out. I gave out memory, and maybe some of that memory is not being used, or some of it was never used. In external fragmentation, where is the unused memory that I can't allocate? Yeah, it's between my allocations. So I have little chunks of memory, but my request is for a contiguous chunk, and so I have to fail. So we introduced the memory management abstraction, which is the address space. And an address space allows each process to have an identical view of memory every time that it runs. We make it look like the process has a lot of memory, potentially more memory than exists on the system. Always potentially way more than I've actually allocated to the process. That memory is contiguous, so this address space consists of a continuous range of addresses. The memory is uniform and private, private to me. And uniform means that things that look like they are next to each other behave like they're next to each other. And the private to me, meaning that these semantics of how memory is shared, most memory that I allocate to processes is private. There's ways to create shared memory regions. We can talk about those at some point, but by default, the memory that's allocated to a process is private to the process and the threads that run inside that process. So we talked about the fact that normally when I lay out my process, when I create a blueprint for normal processes, I don't put code or anything. Variables, the heap, the stack at address 0. Why not? Why don't I do this? Yeah. Yeah, because this gives me a chance to help processes debug null pointer problems. If 0 was a valid address, which it certainly can be, there's something special about it, then when I had a null pointer problem, I would end up either reading from some valid memory that's been allocated to the process. And instead of crashing, which is what I want to happen when I have a null pointer that's being used, the process would just get bogus data. So this is a convention that makes it easier to debug your faulty programs. But there is nothing required about this. This is something that the linker does to help you. But just keep in mind, there's nothing special about address 0. It's just another number between 0 and 2 to the 32. Any questions about address spaces before we start talking about how we're going to do this? So again, this seems like a fantastic idea. It seems like this would be a great way we talked about last time a bunch of ways that this simplifies process startup. My variables are always in the same place. I can put a heap in one place and allow it to grow in one direction, to put a stack in another place and allow it to grow in the other direction, because I have such a huge address space that those two things are never going to meet. So that's fantastic. The challenge here is going to be implementing this. So that is your mission of many address spaces. So what's the key thing that is implied by the address space abstraction? So there's a key difference that's sort of clear between how address spaces handle memory and physical memory. So remember what that is? Yeah. Yeah, so essentially, we'll get to virtual addresses, but essentially address translation. So it's clear once you have this address space idea that addresses somehow have lost some property they used to have. Used to be that if I had an address, I knew exactly what byte of memory that pointed to. And it was unique. So I would never have two addresses that were the same that pointed to different bytes of memory. Now clearly, this is the case, because after fork, remember, the child has all the same addresses in its address space that the parent does, but the child has to have its own private memory. So clearly something weird is happening now. This is sort of like when you think about postal addresses. If you were looking for 5 or 12 Main Street, that is probably not enough information to find Main Street. If you had a GPS coordinate, just kind of what we used to have, then you know. You know where you're going. But if someone just said 12 Main Street, you would say, I need some more information. Maybe the state, the city, zip code. It's just not sufficient to identify unique address. And we also need to make sure that these addresses are private, so we need to make sure that we're able to implement the protection guarantees that the address space is supposed to provide. And finally, now we've created this problem that we didn't have when we were trying to allocate physical memory, which is it's possible and very likely in certain cases that we're going to actually give. We're giving processes the illusion that they have this huge amount of memory. So we're kind of encouraging them to allocate memory, kind of encouraging them to spread out, put things all over their address space. And so now we're going to get into a case where we actually have more, we've given processes more memory than the system actually has. So we can do this. In a lot of cases, this is really useful. But it creates problems with determining how to actually use the memory that the system has. If I could never over allocate, sort of like overbooking memory. If I could never overbook memory, then I never had this problem. But now that I can overbook memory, I've got to figure out who actually gets to get on the plane and who's got to take the voucher. So here's the point at which I'm going to tell you something that you may find extremely disturbing, which is that your programming life up to this point has been based on a lie. You believe that you've been dealing with memory? You have not. You've not been dealing with real memory. You've been using memory. You've been allocated things. You've been thinking, this is all great. And it turns out that that's not true. And I'm going to tell you something today that's going to be, again, I hope that nobody is super disturbed by this. You probably, by this point, are aware of the fact that Santa Claus is not real. The memory addresses that you have been using are not real either. They're as real as Santa Claus. You've been using memory. You've been using memory addresses. You've had pointers to memory, and you've been thinking that those things are real. They're not. So what we're going to do is we're going to introduce the concept of virtual addresses. And virtual addresses use a technique that we've seen deployed before. We're going to introduce a level of indirection between your notion of what something is and that actual thing itself. And the place where we use this before, does anyone remember? Does anyone in the process of working on this now and having to deal with this? Yeah. File handles. So we did this with file handles. And the reason we're going to do it here is sort of a superset of the reasons that we did it with file handles. With file handles, there was a specific problem we're trying to solve. Here, this is going to give us a lot more power. So rather than handing out the actual object, if I give you a reference to that object, it gives me a lot of new capabilities, me being the operating system. So by giving you a reference, it allows me to revoke that reference. This is one of the things that I need to be able to accomplish in order to allocate and manage memory the way I wanted to. I can share that reference. So this allows me to implement memory sharing when appropriate. I can move things around behind your back, which is super useful, particularly when dealing with processes that aren't using some of the memory they allocated. And I can make changes to that reference without telling you. So that's another nice property. So imagine, and this is essentially what we're doing here. So to illustrate this, I have a reference to this object. So the kernel is now in charge of turning that reference into the object that you want. So if I want to stop doing that at any moment, I can. I'm now in charge of how that reference is translated. So if I don't want you to use that memory anymore, I can do that. I can make that go away. I can give two different programs references that can be different, that can look different to them, but can turn out to actually refer to the same memory. This is something that we'll feature that we'll utilize when we talk about how to optimize fork. And I can move things around. So that's another useful thing. I can take an object, and I can put it somewhere else. All I need to do is, and I don't have to tell you about that, because you're just using the same reference that you were using before. And then I can change it from gray to green, which is clearly something that I want to do frequently. So I can make changes to that object behind your back. So here's an interesting question that you guys probably haven't thought about before, which is, what's the interface to memory? Memory has an interface. Everything in computer science has an interface. What is the memory interface? Two high level. Memory interface. Two functions. You guys probably don't use this directly, but this is being used for you. What can I do with memory? Load. Load takes an address. Load returns the contents of that address. Imagine it returns one byte, or four bytes. Some amount that is stored at a particular address. Store. Address and value. I give you an address. I give you some data that I want to store there, and this function will store that, or will attempt to store. So now, with physical memory, this makes a lot of sense. But what virtual memory allows us to do is extend this interface and essentially apply it to things that might not even be memory. You can think of virtual memory as taking the memory interface and providing it to processes. But because I'm using virtual addresses rather than real physical addresses, and because those virtual addresses are translated by me, I can do all sorts of fun things now that I couldn't do before. So when we're talking about addresses, this is an important distinction to make. And this is terminology that I'll use in class. A physical address is something that refers to a unique byte of memory on your computer. We talk about physical addresses. We're talking about actual RAM. Some transistor deep inside your RAM chip. That is a physical address. And those addresses are unique. Virtual addresses point to something that acts like memory. It obeys the memory interface. I can load and store to and from it. It doesn't have to be memory. In some cases, it's not. In many cases, it's not. In some cases, it is. In some cases, it's not. The other key distinction here is that virtual addresses exist inside an address space. And therefore, they make no sense without the address space and so without a process. So virtual addresses, as we pointed out before, many, many different. If I have a process that calls four 10 times, every one of its children has the same virtual addresses. So if you say, what's that virtual address, 0x10000, I can't answer your question. You haven't given me enough addressing information. You need to tell me the process ID. So if you say, for process 10, what's located at 0x10000, now I can answer your question. Virtual addresses are only unique within an address space. So only unique to a particular process. This is an important distinction. And because of this single level of indirection that I've created, I can do much richer things with virtual addresses that I could just with physical addresses. Let's talk about some of these. So yes, so this is how things actually work. We talked about this at the very beginning of the class. So this is the real red pill of the course, I think. Real addresses. So where are virtual addresses located? Virtual memory can actually be a bunch of different places. So one of the places it can be is in memory. And in that case, what I'm going to do is I'm going to translate that virtual address to a physical hardware address. However, note that because I'm giving you a virtual address now and not direct physical access to memory, now I can reclaim memory and move it around. So now I can take memory behind your back and put it somewhere else. And all I need to do is make sure that the interface to memory that I've provided you continues to work. So as long as when you call load, you get the value back that you stored last time, I'm good. It doesn't matter where that data goes, particularly when the process isn't using it. It can be on disk. So there are times when the kernel allows you, there's a system call that we'll get to toward the end of class that allows you to request that the kernel take a part of your address space and map it into a file. And this can be very useful in certain cases, particularly when I'm implementing a data structure that I want stored in the file, but I want to manipulate it like it's in memory. So this you can do. In this case, the kernel might actually be caching those contents in real physical memory while you're using. And then writing the data out to disk as I go. But in this case, the process ID and virtual address maps to some byte of data that's located in a file. And this is, so again, I can have memory. I can have something that looks like memory that's in memory, that might get moved to disk. I can have something that looks like memory that is actually on disk that's being cached in memory. So things are starting to get wild. I could have this in memory on some other machine. Doesn't even have to be on your local machine. I could decide to allow you to use the memory interface to access data that's stored somewhere across the network. In that case, I would map the virtual address to an address for the machine, and then maybe either a virtual or physical address on that machine itself. And finally, one of the other common uses for virtual addresses is to make hardware devices look like memory. So a hardware device frequently has some ports that I can write or read values from or into. And by mapping that device into memory, I can simplify access to that device for drivers and other pieces of code that need to use. So in this case, I map a virtual address to a device and a port on that device that accepts a value. So frequently, if I'm communicating with the device, I might want to say disk, read data from this block. Telling the disk how to do that might require writing the block ID into a particular port on that disk device. And I can do that by mapping it into memory. Any questions about this? Hopefully, this gives you some idea of the richness of the memory interface that I've created by giving processes virtual addresses rather than physical ones. I can do a bunch of fun things. It really dramatically extended what memory is and where memory can be. So this is pretty exciting. So permanence. So memory has these permanence implications. If I write things to memory, how long do I expect them to stay there? Forever? RAM. What's the difference between RAM and the disk on your computer? You've probably noticed this as you've been using your computer, or maybe you have never noticed. How long does RAM store values until you shut the computer down, at which point RAM loses all its contents? Now, you guys want to talk about sleep mode or whatever, and there are other ways that OSes play games. But if I remove all the power from your machine, the RAM will lose its contents. It requires power to maintain its values. The disk, on the other hand, or Flash or other things that we think about, disks don't have that property. They store data permanently. So if I write something to the disk, store it in some magnetic spot somewhere on the disk, or in Flash, in a transistor somewhere, these values persist. So I can take the disk out of your computer, I can fully power it down, I can bury it in the earth, and a year later, I can dig it up, and all the contents will be there. I cannot do that with a stick of memory. So when I'm mapping virtual addresses to RAM or to disk, there are implications. So if a process says, I want a part of my address space to point to a file on disk, then the kernel is now responsible for making sure that those contents get to disk so that they're stored permanently. In most cases, when virtual addresses point to hardware addresses, the expectation is that that data will be lost when the program exits or when the machine shuts down. So when I map virtual addresses to point to device ports, this is sort of an interesting case because it's not really like memory or like a disk. The hardware can make changes to these addresses without my knowledge. For example, the hardware might have a register that it uses to signal that it's completed doing something, and it changes that register all by itself. So in this case, the semantics of typical load and store are a little different. So normally, if I load a value, I get the value that I stored previously. With hardware, it's possible that when I load a value, I get some other thing because the hardware changed the value on its own. So permissions and protection. Here's something else I can do because I have these references. I can divide virtual addresses into some that are used only by the kernel. So these can have all the properties that the user space virtual addresses have. The only difference is that the kernel can refuse to translate these addresses when a process is running in user mode. If a process is running in unprivileged mode tries to access a kernel virtual address, I can kill it. I can say, you're done. You did something wrong, and you need to exit now. And just like files, I can apply the same type of permissions that we're familiar with to virtual addresses as well. So what would it mean for a virtual address to have a read or a write permission? It's pretty intuitive. It means that I could read or write to that virtual address. What about execute? What does this mean? What would it mean to provide or not to provide an execute permission for a particular virtual address? What would that give the process permission to do? Yeah. Yeah, load data from that and interpret it as an instruction. And as the processor is running along, frequently processors allow you to tell it, hey, this particular memory region, it's OK to load instructions from this area. Remember, instructions are just values. They're just values that I'm going to happen to give to the processor and tell it to do this 32-bit thing. And so in certain cases, I want to make sure that I only execute code from particular areas of my address space. It could be important for a variety of reasons. So let's talk about where virtual addresses come from. We'll talk about four sources of virtual addresses that you guys need to be familiar with. So the first one is exec. We talked about exec a little bit last time. Exec takes the blueprint from the L file, and it determines how the address space should look when the process begins to execute. And typically, exec sets up several different parts of the address space. So one thing that exec does is it says, here's the content of the code region, and here's where the code should be. We'll say the same thing for variables that are initialized statically. And any variables that are going to start with zero, it also allocates some space in the address space for those. And just in this case, the values are not stored in the exec file because they don't need to be. Exec just tells the kernel, I want some area in my address space between this virtual address and this other virtual address, and oh, by the way, fill it with zeros. So that works, because those values are going to have zero. I also set up, when the process begins, I also set up a heap for it, which is where the first break point will be. And we'll get to S break in about 30 seconds. And as the process begins to run, I'm also going to set up a stack. So if you guys have been working on exec, you've probably encountered some of these things. You're not going to see the stuff that happens with the L file because, happily, we did not make you implement loadL, you would not have enjoyed that. But you do see that you need to manipulate the stack and things like this to make sure that the arguments are where the C programs expect them to be. Going back to the examples we used a month ago, here's an example of this. So this is what happens. You could imagine this happened right after exec. I have bash as code. Here's the code of bash. I have some read-only variables that bash uses. Here's some statically initialized variables that are read and written to. There's this anonymous section here, which is, I'm suspecting, a heap. And then I have a stack up here at the top. Loadable libraries we're not going to talk about, but there are usually regions that are defied for libraries that are loaded at one time. So the second place virtual addresses come from is fork. All this is a little bit of review. Fork makes a copy of the address space that requires allocating potentially a lot of memory. So remember, right after fork, the child should have the same virtual addresses as the parent, but those virtual addresses need to point to different memory. Or they need to act like they point to different memory. So for example, this is my code. So I called fork. If the return code is not equal to zero, what am I? Parent or child? The parent. So what I'm doing here is I'm going to say I'm going to print the address of i. So i is a variable on my stack. After fork, both the parent and the child will have an i. So what will the next printf show for the parent? I've set i equal to four. That should show what? Pretend this is a Google coding interview. What will the show? Four. Not that hard. So now in the child, so below the if, now we're in the child, what will the first printf print? What was that? It'll print the same memory address as the parent, because these are virtual addresses. What will the second printf print? Three. So here is demonstrable proof that your conception of memory has been wrong all along. Two addresses that are identical. And yet you can very easily run this code yourself and see that this will work. Two addresses that are simultaneously storing the same value. Sorry, different values. And the only reason for this is these two addresses are not the same at all, because there are two different processes, and so these addresses have to behave as if they point to different memory. Any questions about this? If you understand this example, then you understand most about what's happening with virtual memory. Let me point out something here as well. One of the ways that the operating system accomplishes this feat of implementing address spaces and doing it safely in particular is I never give a user program a real address, ever. Every memory address that a user program uses is translated by the current and by the system. Everyone, they are all fake addresses. None of them are real. So if you are using an address as a user program, that address is a virtual address, and that address has nothing to do with real hardware. So for example, this machine might not even have this much memory, so there's no way to interpret these addresses as physical addresses. And the nice thing about it is that this means that virtual addresses really expose nothing about the machine. By using my address space, I don't necessarily know how much memory this system has. I don't know who it's been allocated to. There's nothing about the underlying physical hardware in terms of memory that virtual address is exposed. Any address I give to a process is a virtual address that the kernel is in charge of transit. Oh, hello. Always one thing I forget to do. All right, so we talked earlier in the semester about how when fork executes, and if I actually had to make a copy of the entire address space, it would require taking any physical memory that I've allocated to the parent and making a copy of it, and that would essentially double the amount of memory that was used by both processes. And this is not a good idea, particularly given the fact that the next thing that the child does is frequently called exec, which is going to load a completely new address space anyway. And I just want to remind you of the fact that this is a problem, and in about a week, when we talk about clever memory management tricks that we can do, because we have this concept of virtual memory, we'll come back and we'll talk about how this is actually handled, or in one way to handle this. But I would encourage you to think now that we have this level of indirection. How would we use it to solve this problem? What's a clever way that I can use virtual memory to limit the overhead of calling fork? While still, again, I still need to be able to get this to work. This has to work every time. But I can do this without copying much memory at all. So the third way that I can create virtual addresses is through a special system call called sbreak. And what sbreak does is sbreak moves what's known as the break point. That's the point where the heap ends. So when I initialized the process, there was a point at which the heap started. And when I want the heap to go further, when I want more dynamically allocated memory from the kernel, I call sbreak. So if this was my address space before, let's say I have three threads, here are their stacks. I've got some code down here. After I call sbreak, here's what happens. And sbreak allows the process to tell the kernel, here's how much I would like the break point to increase. What calls sbreak? Because if you see before what call that you are probably familiar with, has anyone here ever called sbreak before? Probably not, unless you were just playing around with the system call interface. But what have you called the calls sbreak? Malik. So internally, when Malik runs out of memory, it will call sbreak. It will not call sbreak every time, but it will call sbreak when you're trying to allocate a chunk of memory. And the heap that it's already created and is managing can't satisfy the allocation. So this is how processes request more memory for their heap and for the use of dynamic allocation libraries like Malik. So here's a fun one. I told you before that virtual addresses could point to a file. Here is how I do that. So the mmap system call creates virtual addresses that map to a file. So this is kind of open, in a way. But instead of returning a file descriptor, I'm asking the kernel to point to take a piece of my address space and point it somewhere into a file. And so what will happen is, if I write any data into that memory region, those writes you can think of as being routed into the file itself. And this is a neat, neat thing to be able to do. OK, so now that we're talking about virtual addresses, let's talk about the specifics of the virtual memory layout and a specific machine. And I've chosen this one just randomly. So the mips are 3,000s, maybe a machine that you've heard about before, or the system 161 simulator. I don't know, maybe you guys haven't heard of that. So system 161 emulates a 32-bit MIPS architecture. So 32 bits means that the addresses themselves are 32 bits wide. So they start at 0x0, and they end at 0xf's, which is how I've decided I'm going to talk about that address from now on, because there's no other way to say that. So the MIPS architecture defines four address regions that determine how virtual addresses are handled by the processor. And so when you guys are working on assignment 3, you're going to have to conform to this, because this is baked in to the MIPS architecture. And so the simulator is faithful to this. There is a region that starts at 0x0 and runs up through 0x7s. And this is process virtual addresses. These are virtual addresses that are accessible to threads that are running in unprivileged mode. An attempt to access these addresses will be translated as instructed by the operating system. So this is 2 gigabytes. So half of the 4 gigabyte virtual address space, 2 gigabytes are available for processes. So this puts a limit on the total address space that a process can use. And this is something that, for a long time, nobody cared about, because keep in mind, for decades, the machine that you used, you might have been happy to get a machine that had 4 megabytes of memory. That was a lot. I know it's hard to imagine, but when I was younger, I can't remember, actually, what the first machine in college I had. Maybe it had 64 megabytes of RAM. That was a lot back then. A 2 gigabyte address space, when you have 64 megabytes of RAM, is totally fine. Now that we have machines that you can buy that have gigabytes and gigabytes, 128 gigabytes of RAM, this started to become a problem. Hence, the move towards 64-bit wide addresses. The next memory region is 512. The next 512 megabytes starts at 0x80 million and runs through 0x9 apps. These addresses are what are called direct mapped. So they are translated. They are not used as direct hardware addresses, but they are translated by the MIPS processor itself in a way that's extremely simple. It takes the address, and it removes the top bit. If you access 0x80 million, where does that address point? What physical hardware address is emitted? Zero. So this allows the kernel to access up to 512 megabytes of memory on the system without doing any special translation. If I want to access any address up to the first 512 megabytes of physical memory, I take the physical address, I add 0x80 million to it, and I use it. And this is a really nice feature. This is something that you guys will use frequently when you're doing assignment through. The next 512 megabytes are kernel direct mapped addresses. And there are two more regions to talk about. There's 512 megabytes of these kernel direct mapped addresses. These are uncached. The reason that these are here is for talking to devices. So they're also direct mapped in that they're translated directly by the processor, but they're used to communicate with hardware devices. And the reason they're not cached is because, as we pointed out before, the hardware may change those values without our knowledge. So if I cached the values of a hardware port and I read that port, I might get a stale value. You don't need to really concern yourself with this region, but there it is. The final 1 gigabytes are kernel virtual addresses. So you'll note here that there is a split. There's 2 gigabytes of addresses that are accessible to user programs that are running in unprivileged mode. And there are 2 gigabytes of addresses that are accessible only when the system is running in privileged mode. This final 1 gigabyte of virtual addresses are used by the kernel. And these are translated in the same way that user virtual addresses are. They use a different set of mappings. And these allow the kernel to use virtual addresses very much like a user program would. And there's reasons that the kernel might want to do this. So here's what this looks like. I've got half for user virtual addresses. This is the first 2 gigabytes of the 4 gigabyte address space. I've got this direct map region for the kernel to use to access the first 512 megabytes of physical memory. I have the virtual addresses at the top. And in between, I have this area that I use to communicate with devices. Any questions about how this works? Where are global variables that are shared between processes? Are you talking about kernel global variables? So if 2 processes want to share memory, there's also a command that allows a process to share memory with another process. That's one mechanism that allows processes to share memory. But in general, any object used by any process has to be located in some process address space. Does that make sense? So if 2 processes are sharing a variable, one of the processes has to store the variable. The other process can have access to it. Good question. Yeah. No, so your allocator allocates memory out of here. All the addresses that your kernel uses right now are all in this direct map region. kMalloc that you guys are using is actually, you can think of it as actually allocating physical memory. It's taking a portion of the physical memory that's available on the system and giving you an address to it. So for the purposes of this class, the only things you have to concern yourself with are this bottom region, which you're going to have to implement translation for for assignment three, and this region, which you're going to use to access the physical memory of the machine. You are welcome to try using kernel virtual addresses. Nobody, as far as I know, has ever successfully built anything that uses those. But there they are. That's a challenge for people that might be done with assignment two already, for example. You guys should go on and actually implement page kernel memory. There are some problems with doing it. There are some challenges that occur when you start to use kernel page memory. And I'm not going to talk about what they are, but I'm happy to, if people want to actually do that. Yeah, do you have a question? Nope. All right. So on Friday, we're going to start talking about how we translate addresses. But what I want to point out is that this is another one of those cases where both hardware and software are involved, and hardware and software work together to get this to happen. So it's probably clear to you by now that I have these virtual addresses that I'm giving to processes to use. And somehow, when those addresses are used, I need to figure out what to do. So a process issues a store to a virtual address. That store needs to go somewhere. It needs to go into memory. Maybe it's going to eventually go on to the disk. But I need to direct how that takes place. It's also possible that the process issued a store to a variable that it's not allowed to use, in which case I need to handle those cases as part of the enforcement test. But this is another nice split between policy and mechanism, and what we're going to be designing is a system that uses hardware mechanisms to do the work. So we want this to be fast. For example, if every memory access had the overhead of a system call, we'd be done for. Memory is supposed to be fast. And so what we're going to do is we're going to get a specific piece of hardware, which we call the Memory Management Unit, to perform the translations. Particularly to perform translations between virtual addresses and physical memory addresses. So once I have the content in memory somewhere, I'm going to tell the MMU where it is, which process is allowed to use it, and what addresses the process has that point to that physical memory. Once I've done that, the translation is extremely fast. It could be performed while the instruction is executed. So the MMU makes this fast. The operating system is in charge of the policies and instruct the Memory Management Unit in terms of what to do. So the Memory Management Unit translates virtual addresses to physical hardware addresses. The kernel sets the policy by telling the MMU what to do. And the goal here is to allow the operating system to express its policies and do as little work as possible. The fewer times that the MMU has to ask the OS what to do, the faster the whole system runs. And so as we start talking next time about different policies and different strategies for mapping virtual addresses to physical hardware addresses, we want to keep these goals in mind. We want it to be fast. And mainly I just want it to be fast. That's really my only goal. But part of making it fast is ensuring that the OS is part of the process as rarely as possible. So on Friday, we will talk about address translation. And we'll start talking about ways to do address translation and in particular how to do it quickly. I'll see you guys on Friday.