 Let's get started Wednesday morning. You guys have how many hours until assignment one is due? Thanks for coming to class. What's that? Somebody is counting very carefully. I think it's less, more than 36, actually, 39. So how many people are making good progress on assignment one? Raise your hand if you feel good. OK, good. That's good. If you didn't raise your hand, then come start making progress on assignment one. All right, so today we're going to, on Monday, we talked a little bit about some of the problems. So we're still kind of getting into our unit on memory management. We talked a little bit about the problems with doing things in kind of a dumb way. And I may have actually spent too much time belaboring the point that if we do things in a stupid way, then that causes a lot of problems, which maybe everybody didn't need as much convincing of as I offered. So today we're going to kind of move on and decide that we're going to try to be a little bit smarter. And what we're going to do today is we're going to talk about how the operating system extends the memory abstraction. So the idea of memory as something that has an interface and something that we can assign kind of more properties to in a way that's richer and allows us to do some really interesting things. So essentially, this comes down to taking the idea of memory as having an address and an interface and extending that in ways that make this into a very, very powerful abstraction that we can do a lot of neat things. So today, essentially, we're going to be talking about what are called virtual addresses. But what I really want to convince you is that there's a lot of different parts of the system that look like memory. There are kind of memory-like things that aren't actually memory, don't actually have to be memory. And the virtual addressing and this extra level of indirection that we're introducing is what allows to do this. And this is always the thing that I forget to do. Six things to get ready and that is the one I forget. So announcements, not too much. You guys know, assignment one is due tomorrow night at midnight. We are also, it's not up here, but we're very close to finishing the assignment zero grading. So those assignments, you'll have your scores back for that pretty soon. All the code reading questions are graded at this point. We just need to figure out how to grade some of the script output that you guys submitted. So once we're done with that, we'll put that up and we will release all of the correct answers. So my intention is to have it be, you can go back to assignments here and see sort of side by side your answer and the correct answer and then some feedback in terms of the answer you gave and how it was graded. And I pushed some additional stuff onto the assignment one page yesterday, specifically details about testing. So I haven't given you the scripts that we're going to run, but I've definitely given you enough information that you could reproduce those scripts yourself. And particularly when it comes to the synchronization primitives and problems, one of the things that we're going to be doing is writing code that tries to tickle corner cases. So you should think about your code, especially the problems, but also the primitives, you know, reader-writer locks, for example. What will we be looking for? Well, one thing we'll be looking for is that you don't starve readers or writers. Can you guys design a test that allows you to determine whether or not you're doing that? I think you can. And we will. So you need to make sure that you are kind of writing these tests that push your code in the same way that we are. And if you have questions about that, ask. But I think there's a lot of information on the website now. And to some degree, what we're trying to do is to get you to think a little bit more defensively about how to write these tests yourself, because this is a really important part of good programming practice. Any questions on assignment one or the assignment? All right. So again, on Monday we introduced this idea of address spaces as an abstraction. We looked at what would happen if we just allocated physical memory directly. And we thought that that caused a lot of problems. And so we said, OK, what if we could give each process this really nice uniform view of its own memory? And then we kind of decided that that would be great and also hard. So today we're going to talk about the hard part. But does anyone have any questions about the address space, abstraction, problems with physical allocation, any of the stuff that we talked about on Monday? Questions? Doubts. Confusion. General irritation that might be laboring of things that might have seemed obvious. Wow, OK, you guys are really forgiving. OK, so let's review. So let's talk about our requirements. We had four requirements in order to do memory multiplexing. Remember the operating system? Responsible for multiplexing. That's why we have kernel privilege. And there were four things we had to do with memory. So we had to be able to grant memory. What does this mean? Let's start in the back today. What does it mean to be able to grant memory? You're looking around like I'm not calling any of you, but I am. Grant memory. What does that mean? What's that? Don't remember. Let's slide down the row. Can you help them out? What does it sound like? Grant memory. Give it out. I need to be able to distribute it. When an application needs it, when a process needs it, I want to be able to hand it out. So that's important. And there's two cases. We'll talk about those in a little bit more detail today. One case is when an application starts up, when a process starts up, there's some memory that it needs right away to hold code and some of its static variables. And then over time, I also might need to allocate more memory as the process continues and things happen. So two different ways I want to allocate memory, statically and dynamically. So that's one of my goals. OK, let's go this direction. What about enforce? What does it mean to be able to enforce multiplex? So it's ownership. So when I give memory out to a process in general, I'm giving it to that process. So enforcement means what? Yeah, and more importantly, make sure that the wrong person cannot access it. So remember, one of the colonel's jobs is protecting processes from each other. When I give memory to a process, that memory is private. And I want to be able to enforce these memory allocations. And also, I want to be able to do so efficiently. The speed at which I can do this really becomes an issue. Because as we talked about, I could check every memory reference that a process makes. But that would make your machine so slow that you would feel like you were living in 1965. We don't want that. All right, reclaim. This was a little bit different. We hadn't talked about reclamation before. So let me go back to the back here, John. Reclamation. What does it mean to be able to reclaim memory? So it means that when I'm finished with memory, I want to be able to destroy it. That's one thing. That's kind of revocation. But also, when memory is not in use, I should be able to take the memory away from a process seamlessly allocated to another process without destroying the contents. And a lot of what we're going to talk about for next week is essentially mechanisms for doing that. Last thing, revocation, all the way in the back. Yes. Oh, man, I gave you the answer. I haven't got a twitchy finger. Stop a process of using memory. So if I'm taking away a memory from a process, if it's stopped using it, I should be able to make sure that it doesn't touch that memory again. We talked a little bit about fragmentation. So what's our definition of fragmentation? What's a good working definition of fragmentation? What about over here? Definition of fragmentation. Anybody remember? Right. When a request for resource, and in particular memory, because we're talking about memory, when a request for memory fails, despite the fact that there's enough unused memory for me to satisfy that request. Now remember, there are two places that unused memory could be hiding. And this distinguishes between different types of fragmentation. So what is one? So we call this internal and external. So in internal fragmentation, where is the unused memory hiding? Garen? Internal fragmentation? Yeah. Between. OK, so between is external. Internal. Want to help them out? Oh, yeah. Right. So in internal fragmentation, the unused memory is inside of the allocations that I've already handed out. So a process asked for a big chunk of memory, and it turns out it's only using half of it. So this is internal fragmentation. Now, Garen, we'll come back. External fragmentation. So what is external fragmentation? You answered this. Between two allocations. Let's be more careful here. Between existing allocations. So especially with memory, when I need contiguous blocks potentially, I may have to allocate a contiguous 1k block. And I have a bunch of little 32-byte pieces sprinkled all over the place. And they may add up to like 10k. But I can't satisfy the allocation. All right, so we came up with this idea of an address space. And what is an address space intended to do? Provides every process with a what view of memory? Uniform, what else? Identical, right? I want every process to be able to look at memory the exact same way. What else? And what do I want to make it appear? I think someone said uniform. That's one good thing. What else? Plentiful, I want it to look like there's a lot of memory, maybe even more memory than the system actually has. What else? Contiguous, right? I want to make it look, we talked about, if I just handed out physical memory, I would have all these sprinkling of discontinuous allocations that would make it very difficult for processes to use that memory. So I'd like it to look contiguous, despite the fact it might not be contiguous. What else? I think made it uniform. We got that. Private, right? I want it to, unless a process explicitly chooses to share memory with another process, that memory is by default not shared. And it shouldn't be shared, right? By accident, okay? Let's see. One last little question, because I always think this is cool. This is one of those things, when I learned this in this class, I was like, cool, I learned something, right? Why don't I load the code for a process into the very, very, very bottom, i.e. 0x0 virtual address? Why don't I use that virtual address, Ben? To catch no pointer exceptions, right? This is such a clever thing, right? I mean, I can help programmers out by leaving a big chunk of memory at the bottom of the address space empty, right? And there's no penalty to that, as we'll see, in any way. But it allows, it makes sure that any dereferencing of null will land in an area of virtual memory that the process is not allowed to use, right? And that'll cause a fault, Isaac. What's that? Okay, fair enough, right? But in general, that's what people end up doing, right? So, okay, so, okay, this is actually, this is, okay, this is a really good point, right? It, and we'll come back to this because it's interesting, right? So there's two, there's two dereferencing problems people usually have, right? It's rare that a programmer sets something to null and then dereferences, right? What usually happens, right? Well, I usually forget to initialize the pointer at all, right? So I allocate a pointer to something and then I forget to initialize it. Now, we'll talk about this next week. What happens is when the operating system gives a process a new page, that page is zeroed, right? So the page is essentially full of zeros. The memory contents are quote unquote blank. And so that's what causes this, right? Because if you try to dereference an uninitialized pointer, it should point it to the bottom here, right? There's no requirement that to be null, you could fill it with random data or with a pattern, right? Like I think our kernel at times for testing uses dead beef, right? Because dead beef is something that you can spell with hexadecimal characters, right? Not for any other really good reason, okay? All right, so any other questions about address spaces before we forget into a little bit about virtual addresses? Any other questions, doubts? Okay, you guys did well on this, so I think we're ready to go on. Okay, so we have this nice abstraction, right? And now the challenge is how do we make it work, okay? And clearly, as we saw, right? Direct access to memory is in physical addresses is off the table, right? So what I've done is implementing address spaces requires me to break this direct connection between an address and where it lands, right? Okay? And we call, in operating systems, we frequently refer to this as introducing a level of indirection, right? So a level of indirection means that rather than letting you access something directly, I set up a pointer to something else that maps through that to get to the thing that you want, right? Now, we've already seen this one other place in this class earlier this semester. Does anybody remember where I set up a level of indirection so that I could do something that I wanted to do? Anybody over on this side of the room? File? What's that? Files? Files, right? It's specifically what? File handles. File handles, right? So exactly, we talked before about how I don't give processes direct access to files, I give them these things called file handles, right? Which, or I give them pointers to file handles, actually references to file handles, which then themselves point to files, right? And the reason I did this was partly because I wanted to be able to share these objects between processes after fork to enable IPC, right? But this is another example of this kind of design pattern, right, where I add another level of indirection here, right? And doing this is a really powerful idea, right? Because forcing the kernel to get involved, or allowing the kernel to get involved when processes access objects gives us a lot of control, right, and allows us to do some really, really clever things. So I mean, I've always really just loved this, you know, the virtual address idea, and I don't really know the history of it, so I made up my own pretend history, right? And in my pretend history, right, it's like, I don't know, somewhere I'm in the 1950s or something, and there's like a, you know, a software engineer and a hardware engineer out at a bar together, you know, and they've had a round of drinks, and they're kind of chatting, and they're like, yeah, man, they're talking about all these problems with memory accesses, and this is also terrible. And the software guy, because the software guy doesn't really know much about hardware, right? He's just like a scissor and tacker, of course it's not true, because at that time you would have known everything about hardware if you were a computer programmer. But anyway, let's pretend. And so he's like, you know, it would be great if we could just introduce another level of indirection, right? It would be great if we could just translate all the addresses, right? And he's thinking this is crazy, right? And the hardware guy kind of like starts to giggle, right? Like the way that you would giggle if somebody said something nuts to you, right? But also the way that you would giggle maybe if you realized that this was actually possible, right? And then once you do this, there's just so much cool stuff you can do, right? Once you make this one little change, right? Once you have this one level of indirection, there's so much power here, right? And just the tricks that you can play with memory start to open up, right? So again, I mean, one simple thing I can do that I wanted to do, right? We talked about it as a requirement for multiplexing was that I can revoke access to an object, right? And I do that simply by refusing to continue to translate that reference, right? You had a reference to the object one moment and then the kernel decided that you couldn't use that object anymore and it just, it said, no, I'm not gonna allow you to use that object anymore. So when you try to get to it, I'm just gonna stop that, right? I'm gonna fail or I'm gonna kill you if it's something that you weren't supposed to be doing, right? So that's one thing I can do, okay? I can also allow reference objects to be shared. This is what we did with file handles, right? I allowed file handles to be shared between processes for the purposes of doing IPC, right? And object sharing through references can happen either within the same process or within multiple processes, right? So there's actually functionality on most systems as I described for explicitly sharing memory between processes, right? And this virtual address abstraction we're gonna talk about today allows me to do that, right? That's what allows me to do that, okay? The other thing I can do is I can move stuff, right? I can move things around, right? And we'll talk about why I might want to do this, right? But I can take this object and the data in this object and I can, you know, I mean, if it's in memory, I can put it anywhere. I can move it here, I can move it there, you know? I can do that. I have that power now, right? Because your reference doesn't change but where the data is might change, right? And that's really cool, okay? And then I can alter the object too. I might change the object in some interesting way while preserving the interface that you're using to access it, right? You might not know that that object is different. The data is different. The data might be stored somewhere else in particular, which is what we're gonna talk a little bit about today, right? So this, I just wanna, you know, give you guys some sense of the beauty of this technique because this is a really, really powerful tool that the kernel is gonna use. All right, so let's talk a little bit about what it means for memory to even have an interface, right? So most of the time, as programmers, we don't think of memory having an interface, right? You know, it doesn't have objects or, you know, it's not implemented in software or something like this, but it has an interface, right? I mean, anything on a computer has an interface. And interfaces are, you know, the commands that you use to use that particular, you know, piece of the system, right? So what is the memory interface? It's very simple, right? Ben? Ooh, no, no, no, you've gone to the software level. I'm talking about even lower than that, even lower than that, it's a hardware level here. Right, I can load, load, right? Load and store, right? And if you guys look, I meant to get this into the slides and I didn't have a chance today, but if you go look at the MIPS architecture description, there's a bunch of instructions in the MIPS instruction set that do this, right? All these kind of like slightly different versions of load and store, right? Load into a register, load 32 bits into a register, load 60 bits into a register, blah, blah, blah, load into the upper part of register, lower part of register, whatever. And this is all variations on this theme, right? Load and store, okay? And what we're gonna talk about today is how I can, because I start to break this direct connection to physical memory, I can start using this interface to point to things that aren't memory at all, right? And figure out how to do this, right? But this is the memory interface, and it's interesting to think about having an interface in this one, right? Any questions about stuff that we've covered so far? All right, so again, we talked about the address-based abstraction requiring that I break this connection, right? And in order to make sure that we understand how this connection gets severed, we're gonna start using more precise terminology, okay? So rather than talking about a memory address, we're gonna talk about virtual addresses and physical addresses, okay? And those are the terms we'll use in this class so that we can know what we're talking about, right? A physical address is easy, right? When we talk about physical addresses, we're talking about memory, actual memory, you know, like, you know, whatever it is, you know, DDR3 or whatever, like a chip of memory inside your computer somewhere, right? That's what a physical address is, right? A virtual address, as I just pointed out, points to something that acts like memory, right? Meaning that loads and stores to that address that are executed by the process, you know, act as I would expect, right? A load, load some data and a store stores data, right? Now, virtual addresses starts to introduce more semantics about exactly how load and stores behave, right, so that's one of the things we can do. We took an interface that was very, very nicely specified and now we're gonna start introducing some new properties to it, which we'll talk about, right? And these are the semantics of virtual addresses, right? So now, virtual addresses can encapsulate location, right? We'll talk about virtual addresses that point to memory. Other virtual addresses might point directly to a file, right? So now, depending on the virtual address I'm using, that data might end up somewhere else, right? I could even have a virtual address that points to memory on another machine, right? So now, rather than just mapping to physical addresses, I can point those virtual addresses to a wider number of places, right? Permanence, this is another thing, right? How permanent do I expect the data that I write to that virtual address to be, right? Physical memory, we expect to be transient. When the machine shuts down or the process ends, that contents any data that's stored in physical memory is lost or things that look like physical memory, right? Virtual memory that looks like physical memory. But virtual memory that points to other places may have different properties. And finally, protection, right? So virtual memory also allows us to assign protection to addresses, right? None of, again, none of this stuff we can do with just physical addresses, not possible, right? All of this is made possible by this level of indirection and having the kernel be involved, right? So let's talk a little bit about location. So when I access a virtual address, right? I do a loader store. So that address could be in memory, right? This is the simplest case and this is what we'll talk a lot about, right? Translating virtual addresses to physical addresses that are in memory. But because the kernel is involved in this translation, what can I do? Does anybody know? What can I do with the data that the process thinks is in memory? What if I run out of memory and the process hasn't used some of its memory for a while? Where would I want to move it? I can move it to disk, right? So the data that was in this memory might have been moved behind the processes back to disk, right? So this is, again, something that is possible because of this level of indirection, right? Now the data referred to by virtual address might also be on disk, right? And again, what I'm talking about here are the processes' expectations about where the memory is, right? And what it acts like, right? So I can have virtual addresses that the process has set up to point to a file on disk. But what if I want to improve the performance of those virtual addresses? What might I do? I might be caching it in memory, right? So the process might do a right to something that it's set up to point to a disk and I actually might only do that right into a memory location, right? The problem is I need to make sure that that data makes it to the disk eventually, right? Because when the process used that virtual address, it expected the data to get to that file, right? And then finally, again, I mean, I can, you know, I can extend these semantics as much as I want, right? I could have virtual addresses that point to memory on some other machine, right? And doing loads in stores means figuring out, you know, how to access that machine, how to do the right, et cetera, et cetera. I might do caching the same way I do on disk, et cetera, right? So again, I just have these much, much, much more powerful semantics now that I've introduced this one level of indirection, right? Last thing that is worth talking about briefly is that memory can also be used to simplify access to hardware devices. So frequently what the system will do is it will set up hardware devices so they look like memory. What does that mean? It means the ports on the hardware device that the operating system uses to communicate with it are memory addresses. So, you know, to send the value of five to the port on the disk that tells it which block I want to read, I write the value five to a particular memory address. And then the memory management unit, which we'll talk about a little bit later, routes that properly and makes sure that that byte ends up at that port, right? I also can set up hardware so that hardware can write directly to memory, right? So, you know, I'll always hardware to do that means that rather than having to read data out of it byte by byte, I can give it a big chunk of memory that it can write data to. And then after I ask it to do a read, that's where the data ends up, right? So I expect it to be at that part of memory once the data, once the read completes, right? So this is just another example of the flexibility that I get out of this, right? So let's talk about about the permanence sort of properties that get, end up getting mixed up with this, right? Because this is another thing about how virtual addresses are created and the permanence properties I expect, right? So again, when I'm writing to physical memory, I expect this to be transient, right? It should last as long as the process lasts, right? Might be until the machine shuts down or crashes, it might be until the process exits, right? At that point, memory is clear. That's the implicit agreement that the operating system makes with the process. That we're gonna preserve the contents of your memory until you call exit, right? What about if I write stuff to disk? What do I expect there? Permanence, right? I expect it to store values permanently and the operating system better make sure that happens even if it's cached in memory, temporarily. So what about device ports? This is an interesting question. What sort of permanence do I expect with a device? What's that? If it's a removable device, I'll use transient. Well, let's say I'm communicating with the device via port and I write some data to that port, right? Do I expect that data to necessarily be there if I try to read from that port? No, because the device can change its own values, right? I mean, all these other things are storage, right? There's some kind of storage, transient storage of physical memory, disk storage. Devices aren't storage, right? Devices have a mind of their own, right? So I might write the value five and the next time I read it, I might contend, right? Because the device has changed the value on its own, right? So it's just a little bit different, right? Okay, so let's talk about, again, the last thing I can do and another really important thing is that I can do, yeah, yeah. No, no, no, I just mean, when I write a byte of data, what are the expectations about how long that data lives, right? And again, when I write the disk in general on a system like this, I expect it to live until I do something explicit to get rid of it, right? Or till somebody else overwrites the file, right? Whereas with memory, I don't expect the contents of memory to last after I call X, right? So again, another thing I can do with these addresses, I can imbue them with protection, right? So I can say, for example, there are some addresses on the machine that can only be accessed by the kernel or more specifically, will only be allowed to be used while in kernel mode, right? And this is a fairly simple way for the kernel to use a part of memory for itself, right? And to make sure that no process can ever access, right? Another way of doing that is just to make sure that none of the virtual addresses I give out to processes ever translate to a physical address that the kernel is using private, right? But on some systems that they do have this idea of protection, right? And I can also assign any of these protection bits or protection capabilities to an address, right? So for example, read write means a process can load and store from that address, right? And I might take away one or, well, I wouldn't take away both because then it would be difficult to use, right? But I might take away write permissions, right? And have something be read only or in certain cases I might take away repermissions and have something be write them, right? And then execute, right? So execute usually means that a process is allowed to load and execute instructions from this area of memory, from this virtual address. Specifically, on the program counter of my program is allowed to fetch instructions from that area, right? Why would I want to protect or have special permissions for executable parts of the code? Why might I wanna do that? Uh-oh. So I certainly, yeah, I certainly might want, I certainly might want, I'm not sure why that happened. I definitely might want to mark the executable section as read only, right? Now, there used to be this, people used to write what's called self-modifying code. Has anyone ever written any self-modifying code like for a program? Okay, Carl, yeah, see? So I think that this is kind of not in vogue anymore, I think people have decided it's hard enough to write code that doesn't modify itself. But there are ways that you can write these weird things with self-modifying code that actually overwrites itself as it's running, right? But typically we don't, a lot of machines don't support that anymore because they do mark the executable section read only. But what about other parts of the code or data in the process? Why might I not want them to have execute permissions? Yeah? We made good choices. Right, so I mean, there's a portion of the address space that I expect to execute instructions out of, right? And then maybe if I'm trying to launch some sort of buffer overflow attack, I might ask the system to be executing instructions out of an area that was actually set up as stack, right? And the system might want to say, no, you know, like, that's not okay, right? So I might want to explicitly say it's okay to execute out of this area and not out of this area because if somebody tries to execute code out of this area, something is happening that I didn't expect and I don't understand and I don't want to have, right? So that's a good example, right? All right, so now some of this, you know, we've talked about in the past. So this is gonna be a little bit of review about how we set up virtual addresses in the first place. I'm gonna move through this a little bit more quickly because you guys have seen some of this before and we talked about process life cycle, right? So you remember that exec is in charge of establishing a new address space for a process that is sort of reinventing itself, right? That is changing into some other process, right? It does that by looking in the CLF file, the executable and linkable format and that file contains among other things a blueprint for how to set up the address space, right? It says this portion of the file contains code that is supposed to be loaded at virtual address here, right? Essentially, when you guys do assignment two, you're not gonna have to write this, the code is already there, but you will receive calls from the ELF loader that essentially says here's a pointer to this box in a file, here's the length and here's where to put it, right? It's like you're setting up a house and the movers are telling you this box goes in the kitchen, right? The ELF format has very, very specific instructions saying these 10K are loaded at virtual address OX 10,000 or whatever, right? And your job is to set up the address space correctly so that that happens, right? And mainly what exec does is it sets up virtual addresses that point to other physical addresses, right? Virtual addresses that point to memory, right? Most of the things we're gonna talk about in this class, we talk about virtual addresses, our virtual address is the point to memory, right? But I think it's important to recognize that that's not required and that because I do translation, I can do other things, right? All right, and again, so I usually have a code section that's marked read-only and probably it's execute permissions and then data that I would mark as read-write but not executable, right? This is what comes out of the ELF file, right? This is what's there when the program starts. And so the heap is also established at runtime, right? And the heap starts normally from the lowest address that isn't used by code and data, right? That's where the heap starts and then as the process runs, the heap is allowed to grow through a system call that we're gonna talk about in about two slots, right? Meaning that the process is allowed to allocate more virtual memory that points to physical memory, right? And again, stack space for the first slide is set up. So this is what ELF kind of gets you going at runtime. This is what gets started off the ground, right? And let's go back. So this is our PMAP. This was PMAP output. We used this a couple of weeks ago to look at the address space of a process. And what you can see here is, you know, again, I mean, these are virtual addresses. This is a portion that Bash is using for its code, right? And you can see that these get loaded into, you know, pretty low in the address space, not super low, but pretty low. You know what, OX, you know, 80 million, 8 million, right? This is, this area right here, you'll see it's read write. This is going, and this is read execute. So this is code, right? This is read only. This is probably static variables. And then this stuff is read write, right? So this is probably global variables that Bash uses while it's running, right? That gets set up where space is allocated at runtime, right? All right, and then we saw we had some loadable libraries that got loaded much higher and then the stack is way up here, right? So this is on Linux. So the other question is, how large is the virtual address space on Linux? Can you guys tell by looking at this? So it probably starts from OX0, right? Despite the fact that the code is loaded pretty high, right? And the stack, again, we talked about last time is being way up at the top and that's at BF something, right? So where do you think this at, where do you think the virtual address space for process on Linux on this particular system ends? What address? What's that? No, no, B, B, remember hex. So it's probably C, OXC0s, right? So there's the, and I think that's what? That's, oh man, my hex is so bad. That's like a half a gig left at the top, right? That that's actually not part of the virtual address space of the process, right? So I've got a 32-bit address I can access at most four gigs, but there's half a gig that's chopped off here that the process is not potentially not allowed to use, right? As the stack starts at BFFFF, right? And we'll talk more about how different towards the end of class, we're gonna go through the MIPS memory model. And so you'll see a picture of how a particular architecture establishes guidelines about how these addresses get mapped, right? All right, so then we remember fork, right? What does fork do? Fork copies the address space, right? When a process forks, it's supposed to look identical to its parent. And fork accomplishes this by copying the address space of the calling process, right? Now the contents start out the same, but do they stay the same? No, right? Fork is supposed to establish an address space that looks identical, but where those virtual addresses point to new and distinct physical locations, right? So here, for example, if I ran this code, I don't know if this would actually print this, I don't know where this would be, right? But here I have a little program. It has a variable here that's either a global variable or maybe local to the function. It calls fork, right? If my return code is not equal to zero and the parent, right? And the parent is going to print the memory address, all right? So this is printing the address of i, right? Where i is located. And that's gonna print some value, right? Some virtual address that was set up in my address space to hold this particular int. And then if I print the int itself, it prints four, right? So what happens down here at the bottom in the child, right? What will this line print? Anybody? The address of i and what's the address of i? It's a virtual address. What's the virtual address of i after I call fork? Four? The same as what? And what is this parent? Zero x 20, yeah, hex is terrible, right? O x two zero zero one zero, right? So that should print the same virtual address, right? Again, the address spaces are identical, okay? Right after I call fork. But if I do a store to that address and then print the value, what's it gonna print? Well, it should print three, right? So the idea here is the virtual addresses are the same, but they point to memory that is supposed to look private. It is private, it has to be private, right? So when he stores to those addresses, now diverge, right? The addresses themselves, the virtual addresses are the same, right? But again, we've broken this connection with physical memory and so the kernel is required to make sure that those addresses map down to different pieces of actual physical memory, right? All right, we talked also about fork and the fact that copying all of the, because remember, if I really wanted to set up a private address space, I would have to take all the physical memory that was in use by the parent and copy it somewhere else and create new mappings for the child so that it pointed to those new locations, right? And that could potentially be really terrible. And next week when we talk a little bit about some clever memory, I'm out of water. Clever memory management tricks. We'll come back to this because there's a clever way to get around having to do this every time, right? A couple other ways that I set up virtual addresses, right? One of them is dynamically, right? So everything we've talked about up till now is static, right? It's exact, it's fork, right? It's stuff that happens essentially when a process is being created. But at runtime, I may need more memory, right? Now, Ben, back to your earlier answer. What, as a C programmer, what's the interface to memory? Malik and free, right? And what Malik and free do internally, right? Now, Malik is what's called a memory allocator, right? And it's good, Malik is designed to be good at allocating different size pieces of memory, right? But eventually, Malik, no matter how much memory it starts with, may run out of virtual addresses to allocate virtual address space. And at that point, what it will do is it will ask the kernel for more memory, right? And the system call that does this is called S-break, right? And essentially what S-break does is it says, dear kernel, I would like to be able to use a larger heap. So I would like you to move the break point, right? Or actually, another spug in the slide. It's where the process heap ends, right? So could I please have this much more virtual addresses that point to memory that I can use, right? That's what S-break does. And you guys have an implementation of Malik in your OS 161 tree in the user libraries, and you can look at it and low and behold, it calls S-break, right? So behind the scenes, when Malik runs out of memory and can't satisfy an allocation internally, it calls the kernel for help, right? It gets another page and then sets up that page to do smaller allocations, right? All right. The last thing I want to mention just briefly, because I talked about addresses that point it to files, is that this is the unique system call that sets up addresses that point to files. I'm not going to talk much more about it, but essentially what it does is it says, dear kernel, I would like this portion of this file to be mapped into my address space. And then any reads or writes to memory to the virtual addresses that I've established that point to this file will end up at that location in the file, right? And this could be a very clever way of setting up data structures and things in files, right? Rather than having to read, read, write stuff explicitly, I just take a file, I M-map all of it or a big portion of it, and then I could just use it like memory, right? Then if I establish a big array in there and write a bunch of values to it, when the process ends, the file will have that data. And if I load it again, the next process, it'll just be there, right? So this is kind of a way of making memory that looks like memory but has the permanence of a file, which can be a nice thing, right? All right, so let's look at, so every architecture establishes different guidelines for how its own addresses are laid out, right? And part of this is because, as I'm going to mention in a second, I probably should mention it beforehand, figuring out how to do this translation ends up involving both the operating system and hardware, right? So I need hardware support. I need hardware help. And because I need hardware help, unfortunately, hardware gets to set some of the rules, right? Like, the operating system can't just make up all the ground rules, right? Because the hardware has to help me, and hardware is inflexible and stubborn, right? I have to kind of take what hardware is going to give me. So every piece of hardware may define a little bit of a different way of doing this, right? So let's talk about MIPS, OK? And it turns out that this is an architecture you guys may care about because for assignment 3, this is the one that you're going to have to use, right? So the MIPS architecture that you guys are working on defines four address regions, right? So it breaks up 32-bit addresses into four distinct regions, right? So from 0x0 to 0x7, right? That is, these are addresses that are used by processes as process virtual addresses, right? So these addresses are translated by the kernel. And this means that a process can potentially address up to two gigabytes of memory. That's it, right? Only half of these addresses or of the whole 32-bit wide address can be used and mapped to memory by a process, right? So you'll notice when you start doing assignment 2 that the process stacks always start right up near 0x7f, I don't know, right? Now, remember, this architecture was developed at a time where machines had less memory, right? So now, if you took a machine with four gigabytes and you said a process could only address two of them, it would seem like a limitation. If you take a machine with 512 megabytes and say a process can only address two gigabytes of its 32-bit virtual address space, that doesn't seem like such a problem, right? It's four times larger than the amount of memory that the machine is going to have, right? So the next region, I have a slide breaking these down, right, but I just want to talk about the properties first. The next region are what are called kernel direct mapped addresses. And by direct mapped, I mean that the MMU translates these addresses, but it does so in a very, very, very simple way, right? All it does to translate this address is it subtracts 0x80 million, right? So 0x80 million maps to physical address what? Zero, right? And 0x9 accesses the last 512 megabyte bit, right? Now, having memory set up like this is actually really helpful in certain places because the hardware will always be able to translate these addresses, right? The kernel doesn't have to tell the hardware how to translate these addresses, right? They'll always work. And there's certain cases where this is helpful, right? Because I may be trying to set up a translation and I don't want to fail because there's some other translation that's not loaded, right? So this can be a nice feature. And some architectures had this idea. However, this only allows me to access the first 512 megabytes of physical memory, right? After that, the direct mapped addresses don't work, right? But this allows me to see directly the first 512 megabytes of physical memory, right? If there's less physical memory than that, then a portion of this is unused, right? And attempts to translate it will fail, right? The next region are what are called direct map. These are also direct mapped addresses, but these are uncached, right? So this region is cached and this region is uncached. And it turns out that the reason this is uncached, can anybody guess? Remember what we talked about before in terms of what different memory addresses could point to? And we said it might be a good idea, for example, to point to what certain thing to make it easier to use. What certain thing that the kernel might use? Anybody remember? Hardware devices. So this is where hardware devices live, right? This is how I, if I'm gonna use a hardware device by mapping the memory, I'm mapping it to this area. And the reason I don't cache values is because the hardware can change them, right? So here, the system can cache values because I know that the only way they can change is by the system reading and writing into those memory addresses. But here I don't do that because the hardware may change the value behind my back, right? I don't wanna fool myself. All right, finally, the very, very bottom here, these are kernel virtual addresses. So the top gigabyte of the address space are virtual addresses. They're translated the same, through the same mechanisms we're gonna talk about for translating user virtual addresses. The only difference is they are not translatable when you are running in user mode, right? So the other thing is all this part here, all the addresses that have the top bit set cannot be accessed when you are not running in kernel mode. That's just the semantics of the MIPS architecture, right? So two gigabytes of the virtual address space is set aside for the kernel's use only, right? Now for this class, these are really the only things you are going to care about, right? There have been people in the past who have decided, and you guys have a month for assignment three, so maybe some of you will try it, right? And I would certainly encourage you after you get everything else to work, right? But using the bottom section here for kernel virtual addresses has never been required for this class, right? It's a little bit tricky and it introduces some issues. So you guys will only really be playing in these areas, right? You will, assignment three is essentially figure out how to get this process of translating virtual addresses to addresses to point to memory to work, right? And in order to do that, you frequently need direct access to the memory on the machine, and in order to do that, use addresses in this range, right? So those addresses you know exactly where they're gonna map, right? Okay, and then here is a picture, right? These pictures are very helpful, right? So this is how this address space gets broken up. First half, user of virtual addresses, right? Next part, direct mapped cached, right? And I should have a picture of that just pointing right into the first 512 megabytes of physical memory, right? Then I have the kernel virtual addresses, which you may or may not care about, and then this, which you will definitely probably very, very not care about, right? All right, any questions about the MIPS virtual memory layout? This is an example, right? Different machines do this differently, right? As we saw before, Linux clearly has a larger process virtual address space, right? Because the stack's up there at BF whatever, right? On MIPS, that address, you know, that BF address is like in here somewhere, right? So basically, if you tried to run a process that was set up that way on MIPS, it would fail, right? Because we're trying to use these kernel virtual addresses to point to hardware, right? As a stack, not a good idea. All right, so the last thing I wanna point out today is that we're gonna start talking on Friday about how to do translation, right? How do I actually translate a virtual address that points to physical memory, right? What is the, you know, I've showed you all these great things I can do now that I have the ability to translate addresses, but I actually have to get it to work, right? And the thing here is that in a more, I think in a more fleshed out way than when we talked about threads. Although you remember when we talked about threads in the CPU, there were CPU features that were built specifically to give the operating system the ability to do certain things, right? I mean, if you look on the MIPS, for example, when an exception happens, it saves the cause of the exception in a special register so that the kernel can figure out what happened when it starts to run, right? That's an example of sort of hardware, software, co-evolution, right? In this area, this is even more pronounced, right? And the main reason for that is because the kernel cannot, software cannot translate addresses fast enough to get a system that would perform in a non-terrible way, okay? And so, what I do is I use hard, we're gonna talk about specific pieces of hardware that I use, but the memory management unit, you can really think about in our sort of policy and mechanism split, this is mechanism, right? This is how this actually happens, address after address after address, right? And the operating system's job is essentially to tell the MMU how, right? How do I translate addresses, right? The MMU takes care of most of the translation once the operating system has told it what to do, right? So this is another nice example of policy and mechanism split, and in this case, the mechanism and the performance of the mechanism is so important, right? Then what we've done is we've essentially baked it into specialized hardware, right? And we'll talk more about some of that hardware on Friday. So on Friday, and again, the goal here is essentially to allow the operating system to set the policy, but to involve the operating system as little as possible, right? So when the operating system gets involved, what you want it to be doing is to be making policy decisions, right? And telling the hardware something new about policies on the system, right? And as soon as the hardware knows, the hardware shouldn't ask again, right? And this is our goal here. This is the only way to get this to work, okay? So on Friday, we're gonna talk about address translation, where I talk about specific mechanisms. For again, now I've got this virtual address, and I need to figure out what physical address does it point to, right? And there's a lot of different ways to do this. So sort of historically, we'll talk about some of the simple approaches, and then we'll talk about some of the more modern, more realistic approaches, and then we'll talk specifically about more hardware features that allow this to happen really rapidly, okay? I'll see you on Friday. Good luck with assignment one.