 Good morning. It's March. Welcome to March. The semester is going by fast. So today, we're going to keep talking about memory management. And in particular, today, we're going to figure out how to implement this nice idea that we came up with on Wednesday. So we looked at problems with multiplexing physical memory on the machine. And we came up with this great idea that we could provide every process with this really uniform, nice view of memory. But we don't know how to do that. So today, we're going to talk about one of the main methods that allows us to do this. This is kind of a neat trick, I think, and really a powerful idea. So it's March. And if you guys remember, vaguely, that was kind of our target for having you guys onto assignment two. So just to avoid scaring myself, I won't ask for a show of hands. How many people are finished with assignment one? But if you're not wrapping up assignment one, then you are behind. So if you're behind, then I would think about trying to catch up. Because again, we're moving on. And there's plenty to do on the last two assignments are hard. The last two assignments are the meat of the class. The first two assignments are maybe 20% of the overall programming work. The last two are the heavy hitters. So you guys need to wrap up assignment one, get the primitives done, do your best with the problems, and get on to assignment two. An approach to starting assignment two would be to start looking at the design component before you leave for spring break. So I'm sure you guys have exams and other things. But the design document is something that it's better to kind of sit down with your partner to read the assignment, think about it a bit, sleep on it, come back the next day, talk a little bit more. Writing a good design is not like programming where you just need to sit down and crank it out. You have to read it, think about it, figure out what you don't understand, come back, talk to the core staff, get a clear idea of what you need to do so you can write up a clear design. The more time you spend designing up to a certain point, the less time you'll spend debugging. I would say five, 10 minutes of good design effort can translate to several hours of reduced debugging. So it's a good trade off. It might not feel like one. You might want to be anxious to get onto the coding, but spend some time on the design and give it the effort it deserves. All right, so on Wednesday we talked about address spaces. Anybody have any questions about address spaces before we do some review? So address spaces were these abstractions that we were going to set up to simplify memory management. They allow us to set up processes the same way every time. They will give processes these nice expansive views of memory. And there were all these nice things about them except for the fact that we don't know how to implement them. We don't know how to actually make them work. But any questions about the abstraction before we do some review? OK. So remember, we had these requirements for how we were going to multiplex memory. There were four things that the system needed to be able to do. We talked about granting memory. What do we mean, Alyssa? She's got her laptop open. Cheater. Yes, imagine. Almost exactly that, right? So we need to be able to give processes memory. We're in charge of handing out memory. That's our responsibility. We're the kernel. We have the special powers. We have to use them. But one of our responsibilities is to actually allocate memory. And there's a couple of different times that processes will be able to request memory. We'll talk a little bit more about that today. What do we mean by enforcement? Yeah, so essentially, enforce memory allocations. And this can mean a couple of things. It can mean making sure processes don't seize more memory on the machine that's available to them. We don't want processes gone wild and just kind of like taking things that aren't theirs. We also don't want processes to use memory that belongs to another process. So we don't want processes to be able to grab memory that's regardless of whether it's belong to someone else or it's unused. There are ways that processes get memory. And the kernel, asking the kernel is, well, I shouldn't say they're ways. Asking the kernel is all of those ways. What about reclaiming memory? This was something that was a little different, especially when we talked about CPU, Robert. Yeah, so we wanted to be able to, in certain cases, without necessarily telling a process, but be able to sort of monitor a processes memory, the usage of the memory we've given it and potentially take memory back. And the problem with this is the processes don't necessarily have a good idea of when they're using memory or whether or not the memory they've allocated is in use. When I give a process a CPU, it's executing instructions. Maybe it's sitting there running no ops for some reason. But in general, it's an active use of the processor. But when I hand out memory, what I want to know is, is that memory an active use? Or did I give it to the process? The process did some stuff on it 10 minutes ago, but hasn't touched it since. And we'll never touch it again. So I want to be able to monitor allocations and potentially repurpose unused or lightly used memory when I need to. And then what about revoking memory allocations? Manish. I said, remember, I'm going to play four corners today. Yeah, I need to be able to take memory away from processes. So even if I've handed out memory to a process, you need to be able to say at a certain point, the process is no longer allowed to use this memory. We talked about fragmentation as being a challenge when I manage spatial allocations. Memory is something that I'm going to multiplex spatially. I'm going to divide it up into pieces and hand it out to different processes. So we talked about two types of, so first of all, what is fragmentation? What's the concept of fragmentation? Yeah, Tim? Great. Right, so very specifically, if I can't satisfy an allocation for memory, despite the fact that I have enough memory to satisfy the allocation, so why can't I satisfy this allocation, Sarah? Yeah, because the allocation is contiguous and I've got memory scattered over in little pieces and I don't have a good way to merge it all together. So I have enough memory, but that memory is somehow split up into pieces that are too small to satisfy the allocation request that I'm given. And we talked about two types of fragmentation, internal fragmentation and external fragmentation. What is your name? Yeah, internal fragmentation. Yeah, so can you be more specific? In internal fragmentation, where is the unused memory that I can't reclaim? Yeah, we don't even have to say, I mean, we could talk about fragmentation in a more platonic way. It doesn't even have to be to a process, it could just be inside an allocation. If handed out an allocation, there's a portion of that allocation that's unused, but I can't reclaim it. And then what would external fragmentation be then? AJ, stop saying processes. Allocations, right? Yeah, I know, we talk about process memory allocation. That's fair, but processes are just sitting there in memory. So unused memory is between existing allocations. So I can see there's these little slivers of memory that are available on the system, but I can't get up. So great, we had this really nice idea. Let's create an abstraction for memory management, and let's have that abstraction have as many nice properties as possible. So what do address spaces do? They give each process what kind of view of memory? We kill, OK? So it is individual, but it's also what? How? Welcome to class. Josh, you want to help them? Identical. Every process sees memory on the system in the same way. They all see the same, the memory looks the same to every process. What else do address spaces allow us to do? What else does it make this look like? Sean, give me one. There are four things. Well, no, no, so what properties of the address space? The address space abstraction makes memory look OK. So it is the same size. That's true. Sammy had something. Yeah, looks contiguous. That's important. So I can allocate things in this address space that look contiguous. It may not be contiguous if we'll talk about that today. That's one of the tricks. What else? Yeah, Paul. Yeah, it's private, or mainly private. Again, similar to our IPC mechanisms, the default is private, and if you want to share their mechanisms to do that, but the idea here is that processes memory is private. What else? Dan? Yeah, it's a lot. I make it look like every process is this huge gob of memory. Like I just gave you four gigabytes of address space. That's kind of nice. Last one. Jen, you took yours. I know it's hard to be the last. Maybe I'll just. Plentiful, contiguous, uniform, and private. I'm not even sure what I meant by uniform. It's up there on the slide, so let's sum up something. All right, who remembers this? Why don't we load stuff into the very bottom of the address space? We talked about this at the very end of class, humbly. Yeah, this allows us to catch no pointer problems. Somebody asked after class, well, why do I leave so much memory? I mean, I started the code at like 0x10,000. Well, first of all, remember that, or maybe you don't know this yet, but wasting address space doesn't mean wasting memory. And that's not a huge amount of your address space. That's a pretty small piece of it. And also, remember that if you have a large object and it's null, the offset into that object, if you try to index it, can be quite big. So leaving a lot of empty space means I can catch null pointer exceptions, even if they're on really big arrays. OK, any other questions about address spaces before we talk about how we're going to implement them? All right, so now this is our task. So we've come up with this nice abstraction. This abstraction has features that we like. This abstraction allows us to simplify a lot about how processes lay out memory, about how processes access memory. So our goal now is to figure out how to implement this abstraction. And to some degree, I mean, I think it's clear, even from a sort of inductive approach, that we have to break this direct physical address connection. We can't allow processes to have a direct access to physical memory. And we knew this because we talked about before that, too, it's no longer meaningful to talk about addresses that are inside an address space. It's no longer meaningful to talk about them without talking about the process, because two processes can have the same address that point to different places. The way we're going to do this is we're going to introduce a level of indirection. We're going to introduce a level of indirection, which requires a translation. And a lot of what we're going to talk about the next week or two is this level of indirection and translation and making naturalization fast and making it efficient and all sorts of fun tricks that we can play because we suddenly give ourselves this power. But where have we seen this before already? We've seen this once before where we introduced a level of indirection, an extra level of indirection that allowed us to add some desirable features to something. Yeah. Right hand. Yeah, remember the file handles? So we could have had file table entries point directly to files. But by introducing this extra mapping where we map file table entries to file handles to files, that allowed us to do these nice things after fork. So here's another example where I took a mapping. I added an extra layer of indirection and that gave me some new powers that I wanted. Introducing a level of indirection also gives the kernel control. And the kernel wants control. And we need control. Part of our job is to control how processes access resources. That's what we do in order to multiplex them. So if instead of giving a process, and if I gave a process a direct access to physical memory, then in many ways my hands are tied. It can use. And if the hardware is allowed to translate that, it just used that address, then I'm kind of stuck. It's very difficult to do some of the things that we wanted to do, to be able to reclaim memory, to be able to revoke memory, to be able to do some other nice things. Forcing processes to translate this reference gives us actually a fair bit of extra power. And we use this power in a variety of interesting ways. So here what I've done, what I'm going to do is I'm going to, instead of giving processes access to memory, I'm going to give them a reference to memory. And I'm going to translate that reference for. Turns out that reference looks a lot like an address, and it is an address. And we'll talk about how all this translation happens, and that's kind of the whole goal of memory management. But here are some of the things I can do. So for example, if I give a process of reference to memory, and I'm in control of that references translation, then if I want to revoke access to that memory, I can just stop translating the reference. So the process says, hey, I want to use this memory. And I'm like, I don't know anything about that memory. I told you to stop using it. So that's nice. References allow things to be shared in a kind of nice way. So I can give two different processes, potentially different references that actually point to the same memory. Again, we don't usually talk about this when we start talking about memory management, because we're focused on it being private. But this is a way that I can control sharing of memory if I want to. If I want to allow processes to share memory, this is one way to do it. If I have a reference to an object, I can move the object without altering the process code. I give you a reference to some memory, and I'm in charge of translating it, and I decide to move it somewhere else. I don't have to tell you. I just have to remember where I put it. So you get to keep using the same reference, but I get to play games with where the memory is, which is really, really cool. And also, I can alter reference objects. I don't know what I had in mind with that. But that is something you could do. It's not something maybe that we use in memory management, or if it is, I forgot what it is. So here's an interesting question. What is the interface to memory? So let's ask a different question. What's the interface to files that you guys are familiar with? We talked about this, and you're going to be implementing this fairly soon. What's the implements? Just parts of it, interface to files, Wembley. The interface, if you know it is an object that helps implement this interface, but what's the interface? Yeah, thanks. What's that? Yeah, but again, I'm going to keep asking this question, because I don't want to. No, interface. Yeah, open, close, read, write. That's an interface. An interface is a set of functions that you use to access. I have files, processes. You can open files. They can close files. They can read and write to files. You can alter the file handles through things like doob. They can move the file pointer, whatever. That's the file interface. That's how you use a file. What's the memory interface? Yeah. Yeah, that's how I get memory. But how do I actually use memory? Sirach. Well, that's how you do it as a programmer. But what is the actual interface of the machine level? Yeah, load and store. You guys don't think about this, because the compiler generates this code for you. But at the end of the day, load and store is the interface to memory. It's an interface provided by the processor itself. The kernel does not provide this interface. You don't make a kernel system call to write a byte of memory. That would be the slowest operating system ever. So this is an interface that's provided by hardware. It's partly because we want it to be really fast. Memory's supposed to be fast. Dists are slow, memory's fast. So forcing the kernel to get in the way is going to slow things down a lot. So hardware provides this interface, load and store. And if you look at the instruction set for the MIPS system that you guys are working with right now, and there's usually a variety of types of instructions of this sort. But they all really break down into load and store. Load some, a byte or several bytes sometimes into a register or a set of registers and then store from a set of registers into memory. That's typically the interface. And again, there's different little variants on it. But this is it. So the address-based abstraction, again, we talked about now we need to break this connection between the memory address and physical memory. So what we're going to do is instead of giving processes, user processes, direct access to physical memory through physical addresses, we're going to start forcing them to use these things called virtual addresses. Virtual addresses in many ways look exactly like addresses. They don't require, in fact, that we really change this interface at all. We're giving addresses. These addresses are references. They're references in the way that we talked about just a few slides ago. They're translated. And the kernel is in control of that translation. And we'll talk about what we do to make that translation efficient. But the kernel is in control of that translation. So when we talk about addresses in this class, we always have to be careful to make sure we know what we're talking about. So a physical address, a physical memory address, points to a byte of memory on the machine. A virtual address is really almost an abstraction in itself. And the idea is virtual addresses can be used in loads and stores. And they should point to something that acts like memory. So what does memory act like? What are my expectations about memory? This is not a trick question. It should be fairly simple to answer. Alyssa. Yeah, when I write something there, then I read it back later. It's the same thing. That's what I expect from memory. I store something, and then I load it later. So what's the other, just to distinguish memory from disk? What do I not expect memory to do? Frank, even more basic than that, Tim. Yeah, it's not persistent. We start talking about file systems after spring break. We'll talk about a lot of stuff that we do at the file system level to make stuff persistent. Memory doesn't do that. Memory, machine goes off, there goes your data. You shouldn't rely on things being in memory. But if the machine hasn't crashed, then we expect things in memory to be there when we get there. And the nice thing about introducing this new abstraction is that virtual addresses allow us to add a whole amount of semantics onto memory addresses that physical addresses don't allow. So virtual addresses allow us to move things around. Virtual addresses, actually, at times we can make virtual addresses, and the memory interface to virtual addresses, actually point to things that are permanent. So it's possible that we can make virtual addresses point to something that acts like memory, but is also persistent. Which is kind of cool. So I have a memory-like interface to something that's actually persistent. Kind of useful. And also it allows us to do a lot of protection, which is one of the things that we're really interested in. So when we start talking about virtual addresses, the data that's referenced by a virtual address. When I do a load or store to a virtual address, the data that's referenced can be in a variety of different places. And this memory interface is actually used in a variety of different ways. The first two are the most common. So the first place it could be is in memory. That's pretty common. This is kind of where we expect things to be. In this case, virtual addresses are mapped to physical addresses. So the kernel has to take the virtual address that a process is using and translate it into a physical address. Physical address points to a byte of memory that is loaded in the motherboard on your machine. The virtual address is in abstraction. The nice thing, again, about this is that we can move things around. So something that the process thought was in memory might not be in memory. And this is one of the things we do to try to make things perform well on modern systems. Stuff can also be on disk. In this case, a virtual address actually maps to some byte on disk. So the byte I'm trying to access through a virtual address is actually stored on disk somewhere. But even if I've mapped a virtual address to disk, which you can do through calls that allow you to take a portion of a file and map it into your address space so it looks like memory, the kernel might still be caching that in memory. So even something that you think should go to a file might be cached in memory on its way there. And if we wanted to, we can make virtual addresses do all sorts of other fun things. So for example, virtual addresses can point to memory somewhere else. I can extend the address space abstraction and allow loads and stores to memory address to actually land on some other machine. That could be kind of cool, maybe. The performance isn't terrible. The other very common use for virtual addresses is to allow the operating system to control devices. So we haven't talked about this and we're not going to talk about it too much. But one of the kernel's jobs is to interface with devices, to control devices. And frequently the way you control devices is you send them commands by essentially sending a byte or four bytes of data to them that gets routed to a particular port on the device that tells the device to do something. So if I want to read a particular block from the disk, I write that block into some port on the disk and then I tell the disk to do a read. Frequently the way this is done is by actually taking those ports on the device and making them look like memory addresses. So the way the kernel does is it writes to a particular memory address, a special memory address that it knows maps to the disk and then the disk receives that command. So when processes are using virtual address that point to memory, what do they expect about the permanence of that memory? We just talked about this a second ago. Andrew, if I've set up a virtual address to point to memory, what do I expect about the permanence? About the permanence? Yeah, what do I expect about the data that's written there? Well, I believe. Yeah, so memory stores values transiently, right? So if I write to memory, I expect that the value, if the machine reboots that those values are gone, right? What about if I write to, what about if I've asked the kernel to give me memory address, virtual address is the point to disk? What do I expect about those values that are written there? Yeah, permanently, right? So this is just something to keep in mind when we talk about virtual addresses. So the permanent semantics can also be different, right? So this is trying to just give you some introduction to sort of the richness of virtual addresses, right? Virtual addresses take this memory interface and they change almost everything about it, right? They change the permanence guarantees, they change the locations, right? So here's an interesting question, right? So let's say I have a port on a device, right? So let's say I have the register on a device that I have to write to in order to initiate a read on the device, right? And this can be true for the CPU too and other things, right? What about a virtual address that points to that device port? What do I expect about, you know, if... So let's say I have a device and I write a value into a port on the device and then I do a read. What do I expect to get back? What's that? That's it. Yeah, so this is entirely device dependent, right? Remember, devices aren't memory. So if I tell the... So some devices, it might say, when you write, you can write to this register, but it'll always return zero, right? So devices are changing that memory address themselves, right? And when we look in a few minutes at the virtual memory layout for your kernel, we'll see that that has some implications for caching in particular, right? So the final thing that we get with virtual addresses is this idea of permissions and protection. And there's all sorts of different things I can do here, right? Frequently what I do is I take some virtual addresses, right? That the kernel and the CPU know how to identify. And I say these addresses can only be used by the kernel when I'm running in privileged mode. So I can take a portion of virtual addresses and I can say that they can only be used or translated by the CPU when it's running in privileged mode, right? So this is kind of nice. This allows the kernel to essentially protect access to an arbitrary area of memory from all user processes, right? So I know that no user process can translate this part of memory, right? What's an example of something in memory that the kernel might want to protect from user processes? This is not supposed to be a hard question. Yeah, anything, right? I mean, almost anything, right? The parts of the kernel that the kernel is interested in users messing with are almost the null set, right? So, right, kernel code, no interrupt handlers, whatever, right? I mean, in general, there's large pieces of memory that the kernel wants to make sure that no user process will ever touch, right? Oops, sorry. And then I can also take virtual addresses and essentially assign them almost, you know, arbitrary permissions, right? I can take a bunch of virtual addresses and assign them to one process and say no other process can translate those addresses, right? No other process can use them. I can also assign, you know, read, write, and potentially execute permissions to virtual addresses similar to the way I did for files, right? So read and write, I think, make a little bit of sense, but what would it mean to mark a part of virtual memory address as executable or not executable? Let's say it's not executable. How would that mean? What would potentially cause a fault or a problem? Thor, he's giving me this miserable look. I wish everybody could see it, Tom. If I mark an address as not executable, then what will produce an exception? When would I enforce this property? Yeah, when the CPU tries to read and decode an instruction from that address, right? Remember, that's how CPUs work. They read an instruction from memory, decode it, execute it, and repeat, right? So when the CPU tries to read an instruction, right? So when my PC gets to that value and the CPU tries to read and execute it, the CPU is gonna say this virtual address is not marked as executable and will produce some kind of exception, right? So virtual addresses, a big chunk of virtual addresses, right, so yeah, and then we have to start talking about where do virtual addresses come from, right? So a big chunk of virtual addresses are usually created when the process starts running, right? And they're created by our old friend, exec, and they're created from this blueprint that we have in the ELF file, right? Remember, the ELF file contains this complete description of exactly how the process wants its address based to look, right? This is, you know, a diva dressing room, you know, no blue M&Ms, right? And the virtual addresses that exec creates mainly point to memory, right? Mainly, and there's a couple of different things that exec is gonna do, right? So exec will set up mappings for the code that's used by the process, right? So remember, the code that the process runs has to be in its address space somewhere, right? So, and this gets done typically by exec at boot, right? So, sorry, not at boot when the program starts running. So all that code is loaded in the file, right? If you decode like bin bash, you'll find all the instructions needed to run bash, right? And the ELF format will tell the operating system where bash wants that put, right? So bash will tell the system, I want my code here, right? At some point in the address space, you know? There is usually some data that also gets loaded. This is a little bit of review, because we looked at this when we looked at, looked at process system calls, right? Typically the data, the static data, right? So if you initialize a static variable in your C code, or you create a static global, or even just a global variable, right? The space for those gets allocated when the program begins running, right? They're not dynamically allocated, they're statically allocated. So they get allocated once, right? The heap is set up, you know? So ELF will kind of tell the system where the heap will start, right? But the heap is used for these dynamic allocations, right? That are done by malloc. And these are runtime allocations that are used for dynamic memory management, right? And then the other thing that exec will typically do is, or sometimes this is just a system convention, is set up stack space for the first thread, right? So these are kind of our four main areas of memory within each address space. I have code, right? Stuff I'm going to execute. A space set aside for statically initialized data, a heap that's used by malloc and stacks that are used by each running thread, right? For local variables. So we've looked at this before, but I mean, it's worth looking at again, right? So here's an example of memory mappings for a sample process, right? So this is bash. And this is a simple Linux utility that you can use on your virtual machines or in other places. So what is, so I think we've done this before, right? But as we'll do it again, what's this area right here? Anybody? The top one. Yeah, Spencer. Right, so this is bash code, right? Bash has 876K worth of code, right? That code is going to be, is loaded here, right? This is the virtual address where this code is loaded. What are, how is this code marked? What can bash do to this area? So yeah, it can read and execute. Usually in order to execute, you have to read, right? The processor has to read the instruction and then execute it, right? So typically things are about read and execute, right? What is this, it's not marked write, right? How many people have ever written self-modifying code? Yeah, you guys weren't born in the right decade, I guess. So heroic programmers in days past, right, would write programs that would actually modify their own code as they ran, right? So a program that would actually rewrite its own code as it ran, those programs would have a problem with this because they wouldn't be able to write to their code area, right? Typically we don't encourage that programming style anymore, right? But, you know, if you want to enter kind of an obfuscated code competition or something, then that's a good way to do it, right? All right, so now I have this second segment here, 4K, what's in here, look? Yeah, there's some data in there that also came from the bin bash executable. What is it, how is it marked? Yeah, so what would be in here? So it is data that's used by bash, it's not code, but if it's marked read only then, by definition, stuff in here are what? Yeah, it's not marked executable, right? This one's only marked read. Yeah, Sean? Static. Well, they're static, they're constants, right? Can't change them, right? So any constants that are used by bash are loaded here, right? Then what's this area right here, 20K, loaded right above the constants, what's in there? Yeah, Tom? Is that the data? Yeah, so this is read write data, right? This isn't any sort of static data segments that bash might have voted. And this was also initialized from the executable, right? So you have, if you've ever declared an array in C and initialized it statically, then this is where this would end up. And then there are actually two more pieces here that are also read write data, but these are marked as a non, right? And the difference between this segment and these segments is that these are initialized to zero. So if you ever have initialized a big array in C and you haven't put an initializer in for it, then the L file will say allocate space for this and fill it with zeros, right? So it doesn't come from the file, right? It just, because the system knows where to get zeros. And then, you know, here we have our dynamically loaded libraries, right? And then we have a stack for the bash byte, right? Any questions about this memory math in kind of its own? Why would a dynamically loaded library have right permissions or a segment with right permissions? Good question. Anybody want to take a guess? What do dynamically loaded libraries? I mean, what are dynamically loaded libraries? And what do they probably have? Yeah, and like, you're writing the C library, right? What might you have declared inside the C library? It's just more code, right? Just like the code that you wrote in bash, right? So what did bash have in this area right here, this read write area? This is remembering the answer to the question from two minutes ago. Sean? Yeah, constant static variables, just, you know, and any sort of statically initialized global variables that the C library needs. That's what's there, right? That's why it has its own data center, right? Because code needs data, right? You write code, you have a few global variables sprinkled around. Everyone knows it's on a good idea, but you do it anyway, right? Sometimes it is a good idea, right? Sometimes it's what you have to do, right? All right, so, so exec, so remember, so exec creates, you know, exec takes this blueprint and creates all of, and lays out the contents of the executable file within the address space, according to the blueprint in the L file, right? Fork just does this copy, right? So Fork just takes, you know, the parent process and copies the address space, right? And here what I've shown is the semantics of Fork where it's only gonna copy one thread, right? So we talked about Fork and it's kind of gnarly interaction with threads, but here's the case where the semantics of Fork are that it only copies one thread, right? So I discard stacks for threads that aren't copied into the new address space, right? But all the code from the parent process and including the heap and anything else, right? Any memory the parent has allocated just gets copied, right? So I take the address space and I just make a copy of it entire, right? So what ends up happening after Fork, right, is that the child has these same virtual addresses. This is a good example of how virtual to physical translation works, right? But they have to point to different physical memory locations, right? So for example, here's a little piece of code, right? So before I do the Fork, I initialize i to two. Who is this? The parent process, right? Return code is not equal to zero so the return code is a pit at the child, right? So this is my parent. It's going to print the address of this variable and let's say it prints like, you know, OX 20 million 10, right? Whatever, right? This is a, and actually in this case, this variable would be probably be on my stack and so the address would be different. But let's pretend this variable was globally declared, right? Now I set it to four, right? And now I better, after I set this to four, it better print to four, right? Because remember, I have all the same variables as the child, but me and the child should not be sharing memory, right? Fork makes a copy of the address space, but each address space is private, right? So what happens down here, right? So this is who is down here now? Child and what should the first print have to print? What the? What's that? Same virtual address, right? These are virtual addresses, right? They should, right? And what should, but what should this print? Great, three, right? I better have my own private memory, right? So here's a case where, what happened here? This guy ran, he had a virtual address of OX, you know, 20,000 10, he did a store, right? This C code translates to a store instruction, to that virtual address, and then this printed four, right? Then he did a read from it and printed the value, right? This guy did a store to the same virtual address, did a read from it and got a different value, right? So proof positive that if your VM works, these two things now point to different physical memory, right? So one thing you might know about fork, right, is that, we talked about this a little bit before, you're doing all of this copy, right? I'm creating this huge copy of the address space. If the parent has a lot of memory allocated, I potentially have to find a lot of memory to put the child's copy of the address space into. And this is potentially fairly expensive, right? And then frequently, again, the next thing I do after I call fork is what? I call exec. And then what does exec do? exec says, blow away the whole address space, right? So fork is like, I just copied all that stuff, man. I come in and do, so anyway, but we'll talk about a way to solve this next week or maybe after spring break, right? The other place that virtual addresses come from is from this system called called S break, right? And this is something guys will implement for assignment three. S break is quite a simple system call. All it does is it asks the kernel to move what's known as the break point, right? Or essentially the point at which the process heap ends, right? So here's my process. It's been running for a little while. K malloc, sorry, malloc has allocated some variables on the heap. Now malloc runs out of space. So malloc, the malloc library that's running in the user program needs to ask the kernel for more memory, right? It calls S break. It says, could you please move my break point? What does the heap look like after this? What happens to the heap? Looking for someone who hasn't answered a question recently. Yeah. Well, that would be, so what would happen if I suddenly decreased the stack? That would probably be bad, right? There would be some threads out there that wouldn't be too happy, right? But again, I have all this space, right? So what does the heap look like after this? Damn, the heap gets bigger. And the lines around the heap get thicker, right? And the heap text gets taller, right? Yeah, and the heap gets bigger, right? So this is S break, it's not hard, right? This will be by far the easiest part of assignment three. The rest of it's very hard, but this is very easy, right? And the final way, and I don't want to talk about this too much that we create virtual addresses, or a final way is through a system called called Mmap, right? And Mmap is a system called that creates virtual addresses that actually map to a file, right? So that's all I said about it, and we could talk about this later, but Mmap is used in certain cases where I actually want stuff to be permanent, I want to read and write data to a file, but I want to treat it like memory, right? And that can be nice, right? Because read and write sometimes is in the right interface, right? So for example, if I'm a database, I have this complex data structure, I want that data structure to be permanent, I want it to live in a file, I call Mmap, I get a big chunk of memory, that memory points a particular point in a file, now I can use that memory like it's memory, right? And the kernel is in charge of making sure that any changes I make to that memory region end up on disk, okay? So let me see how much time we have, because this stuff is going to be, all right, I'll go through this quickly. This stuff will be covered in recitation, right? This is now sort of OS 161 specific stuff, but this is some example of how some of this works, right? So here's an example, machine memory layout, right? Some of this starts to get very device specific, right? Different devices, different hardware devices implement different memory regions differently, right? And have different semantics about how these things work. The 32-bit MAPS architecture has the following semantics, right? So addresses are 32 bits wide, right? So the smallest address I can use is OX0, the biggest is this thing, right? OXFs. The MAPS architecture defines four address regions, okay? The first two gigabytes, so this is four gigabytes of address space. The first two gigabytes are reserved for process virtual addresses. These addresses can be translated by processes when running in unprivileged mode, right? And they can point anywhere, right? These are translated by the kernel, according to mappings that the kernel sets up, but these are accessible to user processes, right? So the user process has at most two gigabytes of address space it can use, right? Remember we said four initially, and now it's like we're changing the game, right? But it's two, right? Sorry, this is MIPS, right? Machines have like 120 megabytes of memory, right? So it wasn't like a huge restriction to be like, you're gonna have two gigabytes of virtual address space, okay? The next 512 megabytes, and I have a graphic for this, don't worry, is what's called kernel direct mapped addresses. These are only accessible when running in privileged mode, and these are translated through a very, very simple process, right? The way that the MIPS CPU translates addresses in this region is it takes the address and it subtracts OX80 million, right? It lops off the top bit. So where does this address translate to? If the kernel, if you do a write to this address, where does it go? Door. Zero, right? And this actually turns out to be where the exception handlers are located, right? So this can be a kind of a nice technique because this means that there's a portion of the address space that is translated directly by the CPU and the kernel doesn't have to load any mappings, right? So this mapping is done directly, right? The next 512 megabytes are similarly mapped by hardware, but they're not cached, right? And I'm gonna let Aditya talk about in recitation why the caching behavior of these two segments is different, right? The final top one gigabyte are kernel virtual addresses, right? These addresses are translated by the system like in the ways that we've talked about using all these flexible different tools, but they're only accessible when you're running in privileged mode, right? So what ends up, the memory map ends up looking like, right? Lowest virtual address, highest virtual address. User virtual addresses blow 0x80 million, right? This 512 megabyte direct map section that's gonna be quite useful to you, right? And right now all of your kernel addresses are in here and pretty much they always will be, right? You won't really use any of these other areas, right? Kernel virtual addresses, if you get super ambitious for assignment three, you can try to figure out how to use this part of the address space. I don't recommend it, but huge props if you can get it to work. It should work, right? It's just very hard to do properly. And then in this area, this actually turns out to be where devices are mapped, right? And I think, okay, cool. So on Monday, we're gonna talk about the mechanism and policy associated with actually translating these addresses, right? So I haven't told you how this happened. Both hardware and software are involved and we'll keep going and this'll be fine. So see you on Monday. And this will be the last material on Monday that will be on the exam.