 All right, everybody. Good morning. Finning out, again, is it midterm season? Is that why? Maybe I'm just trying to get an earlier start. OK, so today we're done with CPUs. So we finished kind of one of the big major units in the class. Today we're going to move on to memory management, which will consume us for a couple of weeks. And we're going to talk about trying to motivate the problem of memory management and give you some context for some of the ideas and abstractions that are going to emerge from the challenge of allocating and managing physical memory on the machine. So this is pretty good stuff. I like memory management a lot. I think it's one of my favorite parts of what operating systems do and has some really sort of elegant, beautiful abstraction center, so it's a neat topic. All right, so at this point, the videos for this year are all up in the playlist. You should be able to get to that from the website. And we're going to start backporting stuff from 2012. So some of the 2012 videos will be up there, too, in case you guys want to watch them for review purposes. And at some point when the 2012 videos are done, then I'll probably take them down from the other site. OK, so the midterm is a week from Friday. It will be here during class time. I'm going to post, actually, I think, three midterms from last year. The practice midterm that I gave, a sample midterm, and an alternate midterm that I gave for people who took it earlier. So the format of the midterm is going to be the same as last year. It's 10 multiple choice questions, six short answer, of which you choose four to answer, and two sort of longer answer questions, of which you will choose one. And again, you guys will get a feeling for what that's like. My feeling with the exams in the class is that the programming assignments in this course are hard, and I think the exams should be easier. So I hope the exams are fun. They're definitely not designed to be too taxing, but you guys will get a feeling from what they're like from the samples that were posted. And then finally, in order to help the TAs help me write some exam questions and also to help them grade next week. And I think probably the week after the midterm as well, we're going to just be tweaking office hours a little bit. So keep your eye on the calendar online. That will be correct. The poster on the door is not always up to date. So this week, Guru didn't have office hours yesterday, and I think I've canceled some of the Z-Hays for today and some of the issues. So just use the calendar before you come into campus to make sure that they're happening. We'll try to make the changes at least a day in advance. OK, so we've just wrapped up our last bit discussion of CPU and operating system CPU support. And that was talking about CPU allocation and multiplexing policies, specifically what we call CPU scheduling. So any questions about that material before we leave the world of CPUs behind for good, at least until the midterm? Oh, also, next Wednesday, we will do midterm review in class. So the midterm will cover material up through next Monday. And I think we'll get through a couple of lectures on memory management before the midterm. So there will be some memory management material. But yeah, so you'll be responsible for stuff up through Monday. On Wednesday, we'll go through one of the practices in class just to give you guys a feeling for how to do that. All right, any questions about scheduling? Scheduling algorithms. Good fodder for exam questions. Easy to ask about. Important to understand. All right, so what is scheduling? What is scheduling? Paul, it's the process of choosing the next thread or threads to run on my CPU or CPU user course, right? Why do I need to do this? Simon, well, that's one of my goals, right? But why specifically is the operating system in charge of doing this? Tim, yeah, so I have to do it, right? So that's one good point. But why does the operating system have to do it, Alyssa? Yeah, okay, what else? I'm going for something more specific. Sam, yeah, these are all good answers. But it's, yeah, so we have more threads in the course to run it on, that's part of it. And also, it's our job, right? The kernel is the program on the system, the application on the system that has these special privileges that are required to start and stop threads, right? And specifically, the kernel is the one who handles these timer interrupts that give it a chance to run the scheduler and restart threads, right? So that's really why the kernel gets involved here. The kernel is in charge because the kernel handles these lower level interrupts and the kernel has a privilege access to machine, right? All right, so I just gave away the answer to this question but see if anybody was listening. When does scheduling happen, Jen? Front row, Jen? Yeah, what interrupts specifically, Charog? Yeah, there we go. Shield, block, exit, or a timer interrupt. When the kernel decides the thread has run long enough and uses a timer interrupt to forcibly remove it from the CPU, good. We started to get into this before, but what are some of the goals of thread scheduling? And this is also sort of how we evaluate schedulers. Goals of scheduling. Thor, you wanna help him out? Bethany? Yeah, so I might wanna minimize latency and give threads a chance to run frequently so that interactivity is preserved. That's one kind of side of it. What would be another goal, Robert? Yeah, I wanna keep the hardware busy. So meeting deadlines, that's kind of what Bethany said, how often when a thread is ready to run, how quickly can I get it onto the CPU so it can do whatever it's trying to do, and then also how completely do I allocate CPU system resources? Assuming there's work to do, I wanna keep the parts of the system busy. That's why you bought them and paid for them, right? Now for them to sit their idle. And on human-facing systems we're typically more interested in interactivity or deadline meeting. That's one way of thinking about it. Why is that true? Rune. Welcome to class. Question is, on human-facing systems, I typically prefer meeting deadlines to complete optimization of hardware resources. Why? I can see bus loads of people coming in from South Campus. Sure on. Yeah, so your time is more valuable than your computers and also you don't notice when the system isn't fully utilized typically, right? You just don't know, right? And then finally another goal of scheduling performance is just that the scheduler perform well, right? We talked a little bit about the Linux scheduler, about old Linux schedulers that's scaled with a number of threads. That's not good, right? I don't want scheduling decisions to take that long and I certainly don't want them to take longer as the machine gets more overloaded, right? That's the worst thing that could happen, okay? Two schedulers that don't use any external information about threads at Grimm, what's one of them? No nothing schedulers. Ron Robin is one. And then random, okay. These are sort of classic examples of schedulers that use no external information, right? Now let's say that we knew things about the future of the threads. What would we like to know? Yeah. So I can accept some priorities, right? That's not really about the future of the thread, right? That I would know, right? Because there's someone to be who would tell me what's the priority, Jeremy. Whether it's about to block or sweep, right? How long is this thread going to run before it blocks, right? And will the block or yield? Is it gonna go to the waiting queue or is it gonna go to the ready queue, right? And then if it's on the waiting queue, how long will it wait? When is it going to be schedulable again? So, because we can't predict the future, what do we do? Use the past to predict the future, right? We can say it lots of different ways. Use the past to predict the future, right? Use the past to predict the future. That's what we do, right? An example of a scheduler that does something like this is what? We actually go through a couple of them. Why don't we? Yeah, our multi, you know, our old friend multi-level feedback queues. Yeah. Yeah, and the rotating staircase does this too, right? Because the rotating staircase, you know, will put threads in a way it does this. It's interesting, right? And you may, you know, one of the questions that's come naturally to my mind is I've started to think about good exam questions is differences between MLFQ and the rotating staircase, right? I'll probably send out a little bit more information about the rotating staircase before the exam. Who's Engel Monar? Daniel. Yeah, he's the Linux scheduling subsystem maintainer and developer, at least as of last year. I haven't checked this year. Who's Khan Kalevis? Swetha. Well, that's Engel. This is a listening to the answer that the student before you just gave. Question? Tam. Yeah, I don't know how to say that word either. He's the Australian anesthesist and Linux developer who wrote the rotating staircase and several other schedulers with fun profane names. All right, so let's go through, just do this quickly, I think this is helpful, right? So I assume I have a rotating staircase deadline scheduler, I have a five millisecond quantum with 10 levels, right? So let's say, and let's say the levels are highest priority zero and lowest priority nine, all right? You could flip them, it doesn't matter, right? So a thread that starts in the highest priority level, which levels can it potentially run in in one iteration of the scheduler? Zoo, I missed somebody's name again. Your name's Daniel. Which level's gonna run it? Zero through nine, it can run in all of them, right? It's a high priority thread, right? What about a thread that starts in priority level five? What levels can it run in? Kevin, five through nine, right? Now, say at the beginning of time, we have one runnable thread of priority level zero, three, seven and nine. What's the longest amount of time before the thread at level nine has a chance to run? This requires a little bit of math. It's probably difficult to do in your head, right? But I have to calculate the fact that zero, and I'm not even sure this is right, actually, ah, damn it. Anyway, forget that. So how does this correspond to the formula? What's that? Yeah, this is wrong. Ignore this part of the slide, but it's not. Okay. All right, any questions about scheduling? Now, I will send out some more information about the rotating staircase. I realize the presentation has been a little bit lacking. Any other questions about scheduling before we motor on? It's your last chance. CPU questions, scheduling questions. Going once, going twice. Okay, cool. So let's talk about something else. So up into this point in the class, you know, we've been talking about multiplexing, but we haven't really talked about different types of multiplexing, and now that we're moving on to talking about memory management, we need to start thinking about different ways to share resources, right? So what we thought about primarily, especially in worlds where we think about sort of one CPU, right? But even if we have multiple cores, is this idea of time multiplexing, right? So if you imagine different ways to share a resource, right? There are resources where the way to share them is by dividing up access to that resource in time, right? So I give you some access on that resource, I give somebody else. But the idea is that the resource isn't really partitionable in any sort of meaningful way, right? So when a thread is running on a core, that thread is using that core. It's not sharing that core with anybody else, right? To a certain degree, right? Space multiplexing, right? So this is, you know, an example of time multiplexing is CPU scheduling on a single core system, right? Or single core, you know, allocating CPUs to cores, right? I take a single core. I can't divide it any further. It's kind of indivisible. And so I divide it up in time, right? I give it one thread, another thread, and I do that over time. Multiple threads have access to the same core, right? Space multiplexing is different, right? So it's this idea of I can share a resource by dividing it into smaller pieces, right? So memory management, you're gonna find out we do space multiplexing and that can be done on a fairly high granularity, right? We can't necessarily divide memory down to the level of bytes, right? Where I say I'm gonna give, you know, you one byte and you one byte and you one byte. But I can divide it down into pretty small pieces, right? And we'll talk about how big those pieces are later and why those pieces are the size that they are, right? Another example of this, however, is just important to point out is CPU scheduling on a multi-core system, right? So here the idea is that the granularity is much lower on a four-core system. I have four cores, right? So I can space multiplex threads. I can share the core resources by dividing them up in space, but I can only do it into four pieces, right? Whereas on a system with a couple gigabytes of memory, I might have millions of different pieces that I can hand out to different processes, right? So let's talk about some kind of straw, you know, I don't want to dwell on this too much, right? But let's talk about some very basic, right? So, okay, let's kind of bop up to the top of the stack, right? I'm the kernel, right? I'm in charge of multiplexity resources, okay? Now we're starting with a completely new resource. So you guys don't know anything about the types of privilege that the kernel needs to do this or the abstractions that we're gonna build up. So we're kind of starting over from the beginning, but we're gonna talk about all that stuff, right? But let's just consider the problem, right? I've got a big chunk of physical memory here, right? It's the physical memory that's in your machine, right? This term becomes important later because we talk about a different type of memory. That's not physical, right? And one way of dividing memory between processes, potentially, would be to simply take that physical memory and give processes direct access to the physical memory in some way, right? And you might wonder, don't processes have direct access to physical memory? And the answer is no, but maybe you think they should, right? So why can't I just directly divide it up between processes? The process starts running, it requests some memory and I just give it a big chunk of memory, right? So let's see how that would go here, right? So I've got Firefox, very skinny Firefox for some reason, and Firefox decides that Firefox is gonna take this big chunk of memory and that's actually a pretty accurate description of how much memory Firefox seems to use, I think. So now I run my virtual box because of course, I'm not watching YouTube videos, I'm working on my CS41 assignments, so that consumes a pretty big chunk of memory, you would imagine, and now I start up terminal and that doesn't consume much memory, but now I'm gonna try to start up some sort of graphics program, right? And that graphics program needs this big chunk of memory and now what do I do? So now I'm out of memory, so what do I have to do at this point? Well, I could wait, right? Probably what would I do instead? Josh? Oh, big borrower steal, right? I like this, I probably, well, I don't have a mechanism for borrowing yet though, right? I just, I handed out memory, right? I'm the kernel, Firefox came along, Firefox said to me, hey man, I need two gigs. I said, okay, here you go, right? I don't have a, like Firefox ran off with that memory, I can't get it back, right? So what am I gonna have to do? I could close something, what else could I do? Simpler option, Andrew. Yeah, I just fail, right? You just tried to, hey, there's no law that says just because you clicked on something on your system it has to run, right? A little box by pop-up saying sorry, you can't run this program at this time, right? Try closing some other stuff, right? And some systems actually do this, right? So, you know, it's not completely foreign to us, right? So, okay, that's not so great. So now let's, but now let's think about some of these other ideas that people had. But let's point out some clear, maybe these aren't super exciting, right? But some clear problems with this idea of direct multiplexing, right? Just directly handing out memory. So the first thing is the amount of memory that's in use by processes is limited to the physical memory on the machine, okay? And this might not seem like a real big limitation, right? Because you might say, well, isn't that true anyway? I mean, that is the fundamental limit, okay? But we're gonna talk about some abstractions that allow processes to have actually a much more expansive view of memory than what's available on the machine, right? And part of our, you know, the big, the big key abstraction here that we use to make virtual memory, to make memory management possible is gonna give us all sorts of neat powers, right? And one of them is to make the machine look like it has a lot more memory than it actually has, right? So here's this other question, right? So Josh mentioned, well, why don't we ask for some of it back, right? And the fact is that processes request memory that they either don't use or they use for some period of time and then they stop using, right? So what's an example of memory that a process might no longer be using? What might be in that memory, munition? Well, memory leak, okay, so let's say that we don't have a programming bug, right? Let's say a legitimate case where an application use some memory and then wasn't using it anymore. Sirach, when it's idle, okay, sure, if it's not running, right? But let's say the application is still running, right? What gets stored in memory by applications? Hmm, temporary files are files, right? So they don't get stored in memory. AJ, it's state, I mean, and it's programmers, right? What, yeah, this is a good question, right? Maybe we should back up a little bit. What gets stored in memory, right? What do processes store in memory? Jeremy, what's, what is it? What's that? Yeah, so dynamically allocated objects in memory, right? When you call malloc, that gets stored in memory. What else gets stored in memory? Even more, even more basic. When will it? Okay, so we have global variables that gets stored in memory, but there's something else you guys are missing. Yeah, I'm right. The code, the code that the application is running, right? Where do you think that lives, right? That's how processes work, right? They fetch an instruction from memory, decode it, execute it, and repeat that loop, right? So the code has to be in memory, okay? So now, again, you know, give me an example of parts of memory that a process might not be using. Sir, all sorts of parts of its code base, right? I mean, you're using Microsoft Word, right? Microsoft Word has like 68 different ways to do anything, right? You know, you chose one of them and maybe you did it differently five minutes before, and as soon as you click on a window, it's executing some code path, and if you don't click on that window for another five minutes, that code is dead, right? That code is cold and that memory's not being used, right? So all, if you look at, you know, the, excuse me, if you look at the code that processes load or the code that's in the L file, large portions of it may not be used by any one particular user, right? There are features that you don't use, there are parts of the code that might never be executed, right? So the code is a particular area where there might be low usage, right? Bless me. All right, so now what, you know, so now what could I do, right? So here the idea is that I'm focused on these big contiguous allocations, right? I've said that in order to give a process memory, I have to allocate it contiguously, right? So, you know, the idea is that this guy, this guy gets this big chunk here and this guy gets this big chunk here and this guy gets this big chunk here. So what could I do, right? I could divide it into multiple pieces, right? So for example, if Firefox exits, then at this point if I try to launch Inkscape, which is what this is, it turns out, then I might be able to satisfy Inkscape's request by dividing it into two parts, right? Okay, and you might say, oh, well this is nice, right? Now I can make sure that all the memory in the system is in use at any given time, right? But then this causes all sorts of other problems, right? So if VirtualBox wants to grow the amount of memory it has, now it's gonna collide with Inkscape's allocation, right? So I don't know if I have to beat this dead horse that much, right? But potentially this contiguous allocations can be another really serious problem, right? It also makes it very difficult for processes to know where things are. And when you're writing a program, it's nice to know where the function is that you're trying to call, right? When you call a function you're gonna jump to where that function lives and start executing instructions. How do you know where the function is, right? When you're using a global variable, it's nice to know where it is that variable in memory, right? If I give processes this direct access to physical memory what happens is that these discontiguous allocations mean that every time the process runs, it's possible that things are in a different spot, right? If I ran Inkscape a few minutes ago and it was loaded here then code would be here and other things would be there, but now it's got these two pieces and things like that. So this makes things very, very ugly, okay? So again, I don't know if this is, I don't wanna beat this into the ground, right? But doing this also creates this problem with fragmentation, right? And again, just this idea of where are things, right? Where is, I have this nice variable data, right? This variable is 128 byte array, well it's not 128 bytes, it's 128 times the size of an int, lives in memory somewhere, right? In order to perform this instruction, I have to know where it is, right? If you look at the instructions that the compiler generates, there have to be memory addresses associated with this variable in order for me to actually access it and change it, right? And if every time I get loaded, things get put all over the place in different places, I'm in real trouble, right? So we're gonna talk a little bit as we talk about allocators of various types about fragmentation. I think many people have been exposed to this idea before, right? Fragmentation is the idea that a request for memory on the system can fail, even if there is enough memory to satisfy the request, right? The reason for this is that in frequently, you know, because of the requirements of compilers and the code that you guys write, you actually need contiguous pieces of memory, right? Any pieces of memory that are actually contiguous, meaning that the addresses are next to each other, you know, in some address space, right? The locations of the memory corresponding to that allocation are actually, you know, a buddy, right? I got to use that word, wow. And fragmentation means that I can fail a request even if the amount of memory that I need is available because that memory is scattered in different places, right? And we discussed two different types of fragmentation, right? So internal, how many people have heard about internal or external fragmentation before? Oh, okay, well, maybe any talk, go slow down a little bit. Okay, so internal fragmentation occurs when unused memory is inside existing allocations, okay? And external fragmentation occurs where unused memory is between existing allocations, right? So let's go, we can back up here and look at an example. So in this case, right, let's say I couldn't, let's say there was no way to divide this allocation, right? And let's say it doesn't fit here and it doesn't fit here. So what kind of, but these two pieces are big enough to satisfy the allocation. So what kind of fragmentation would this be? External or external? External, right? Because, you know, I don't know how much memory is in use by virtual box, right? It's possible that it's not using most of its allocation, right? But the idea is I have enough memory on the system but I cannot satisfy this allocation, right? Because the memory is split up, it's fragmented, right? Now let's say there's a case where a virtual box actually isn't using most of its allocation. I gave it this big piece of memory but there's all this unused memory inside that allocation that I can't reclaim, right? And what kind of fragmentation would that be? That would be internal fragmentation, right? It means the fragmentation is inside of allocations that I've already made, right? We'll come back to these ideas as we go through memory management. So it's not the last time you'll hear about it. Yeah, Jeremy? About trying to process the theme declared to in some of the heap and then you deleted the first one and then you tried to create a double of the heap, would it go after the... It's, yeah, so that's a great question, right? So how heap allocators work is something that we'll talk a little bit about. But in general, allocators are various kinds. So how many people have used malloc, right? Malloc is an allocator, right? Malloc allocators have algorithms that they use, right? You guys can look at the code in your kernel for K malloc, right? K malloc is what's called a pool-based allocator that uses powers of two pools, right? That allocator suffers from a certain amount of internal fragmentation, right? But it also does not suffer from a certain amount of external fragmentation. So every allocating algorithm has different pluses and minuses, right? But in general, right? I mean, in general, allocators always suffer from a bit of this, right? But I would encourage you guys to look at, and we'll talk about this more when we get into assignment three, look at K malloc and see how it works, right? It's pretty simple. Long time ago, when I took this class, we actually made students write K malloc and it was a total disaster. So we don't do that, right? Because if you don't get K malloc right, nothing else works, nothing. And it's very hard to debug. So, okay, so this is how we distinguish. So again, internal fragmentation means that there is unused memory on the system, but it's inside allocations, right? And external fragmentation means there's unused memory, but it's between allocations in either case. I have enough available memory to satisfy the allocation, but I cannot satisfy it because that memory is not contiguous, right? It's not available to me, okay? And again, I mean, part of the reason for this is that when I have, like when you allocate something like this in C, C does very simple pointer arithmetic internally or the compiler does to figure out. So when you access data, you know, 512, it takes the beginning address of data and it adds 512 ints to it and that's how it figures out where the next memory is, right? So if that thing is split into several pieces, you're gone, right? This will never work, right? So things like this have to be contiguous, right? In some kind of memory, we're gonna talk, you know, today and on Friday about the big, the one big trick that makes this all work in user space. All right, so again, direct multiplexing, clearly just not a winner, right? You know, I limited the amount of physical memory at these discontinuous allocations and then I have all these problems with fragmentation, right? So here's another question, right? You know, can I enforce the allocations that I hand out, right? You know, is there any way to stop Firefox from just using other parts of memory on the system? And it turns out that this is actually not possible without some kind of hardware support, right? If I allow direct access to physical memory, I need some way for the, I need to be able to tell the hardware basically that while that process is running, here's the range of memory accesses that it's allowed to access, right? Otherwise it can potentially write and read and use pieces of memory that are actually owned by other processes, right? And then, you know, reclamation is also a problem, right? If, even if I had a mechanism for do this, right? So when we talk about, you know, here, so these are basically our goals for doing better memory multiplexing, right? And there was fourth, right? The first one is that I should be able to hand out memory to processes, right? I have to be able to grant, to grant requests, right? And, you know, and this happens in two places. The first time it happens is when a process loads, right? The ELF loader needs a lot of memory to set up the parts of the process that have to get loaded at runtime, right? And then as the process runs, it's gonna make other types of allocations because it's going to, you know, wanna add some memory to the heap, it's gonna wanna grow the stack, right? So there are these allocations that happen at runtime and then over time processes have other ways of making allocations, right? The second requirement is I need to be able to enforce these allocations, right? These are gonna sound a lot like, you know, what we talked about, we talked about multiplexing resources more generally, right? I should be able to prevent processes from accessing each other's memory, okay? The second thing I'd like to be able to do is if the kernel can identify memory that's not being used, I would like to be able to transparently reclaim it from the process without destroying the contents, right? If I destroy the contents, then I've broken the expectations that processes have about memory, right? Processes don't expect that if I write some data into that memory and don't use it for a while, then that data just goes away, right? That would make it very difficult to program. So I should be able to repurpose the memory, however, without destroying the contents. And then finally, I should be able to remove, I need to be able to revoke allocations, right? There's a variety of times in which I want to do this, right? Certainly on exit, but in other cases, right? So at certain times I want to say, you know, I've allowed you to use that piece of memory for a certain period of time, that time is over, right? We can map these requirements onto, you know, the CPU multiplexing that we just talked about, right? So what's the equivalent of granting on the CPU? What does that translate to? I did you? Oh, it's Akshay, sorry, yeah? Yeah, but what does this mean? Like grant, I'm gonna grant the CPU what am I gonna do? No, no, this is for CPU, right? So what does it mean to grant a process to the CPU? What do I do? Andrew? Yeah, I'm gonna schedule a thread, right? This is the context with just the mechanism by which I grant access to the CPU, right? What about enforcement? How do I make sure that threads don't run longer than I want them to? Yeah, I use the hardware timer, right? What about reclaiming? Can I transparently reclaim CPU resources? Does this have a good mapping? Looked at. Yeah, kind of, right? So I think that this doesn't really have a good analog in the CPU world, right? We'll see the difference between, so the idea is with memory I can get rid of parts of, right again, remember with memory I'm doing space multiplexing, so I can reduce the process it's allocation without it knowing, right? With the CPU it's a binary thing, so I do have revoke, right? Revoke is de-scheduling the thread, right? It's yanking it off the CPU if you're done, right? So this is essentially a mental mapping you guys can use to bring forward some concepts from the CPU to memory. So as we did with the CPU, right? With the CPU we had this thread abstraction, okay? With memory we have a similar abstraction and it's called an address space, okay? And we're gonna talk about this abstraction first and then we'll talk about how we get this to happen, right? There's a lot of nice features of this, right? So address spaces allow every process to have an identical view of memory, right? Identical view of memory for every process on the system and an identical view of memory every time they run, right? So every time an identical process runs regardless of the state of the system it has an identical view of the memory on the system, right? So all of nice things that this abstraction allows me to provide, right? The first thing is it makes memory look very plentiful, right? Potentially as large as the addresses on the system can support, right? So a process might think, wow, you know, I have this four gigabyte address space, right? The process has no idea how much actual memory there is on the system, right? If a process, if this process runs on a system that has five, 12 megabytes of memory or two gigabytes of memory the address space size is always the same, okay? So it makes memory look potentially very big, okay? It also makes it look entirely contiguous. No matter when I run, what machine I run on, what else is running, my address space starts at zero and it goes up to the top of whatever, you know, addresses I'm using, right? In this case I'm using 32-bit addresses. So potentially the top address is 0x, f, f, f, f, right? It turns out that we do something a little bit different to give the kernel some room, but we'll talk about that later, right? But essentially the address space always looks contiguous, right? It starts at zero and I have access to any part of it I want to all the way up to, you know, the top of whatever address that the machine supports, right? It's uniform, again, every time I run it's the same, right? Every time, and this is great, right? Because it means that every time I run I can put my code in the same place, right? And I can put my variables over here, right? It's like every time I start up I can do the exact same things, right? This is wonderful, right? And it's private, right? And there are ways to share memory between processes, but the general assumption is kind of like what we talked about with threads. The address, the memory is private to each process, right? Without a dedicated mechanism to do IPC, that memory is mine, okay? So this, you know, these series of, you know, characteristics are really nice, right? And let me try to convince you that they're really nice, okay? So again, the fact that address spaces are uniform simplifies process layout. So when processes start to run, right? They have to make decisions about, again, where to put things, right? I've got this big blob of code, right? That I need to live in my address space somewhere. Where does it go, right? I've got these variables that are initialized when I start up, where do they go, right? Where do I put, where do I allow Malik to allocate memory from, right? Where do I put stacks for my threads, right? Where do these things go, right? The nice thing is these uniform address spaces allow us to have conventions about where these things go, right? So for example, if you look at your OS 161 binaries, they always load their code and static variable starting at OX 10,000. That's just where they put them, right? I start loading my code here and I load as far as I need to go this way, right? Okay? The heap, right? Which is where Malik makes its allocations from. That's where dynamically allocated variables live, right? The heap always starts at this address, right? These are the conventions that are used by your system, but they could be different, right? There's a reason why some of these things are true and we'll talk about in a second, right? So I use some variable for my heap and the heap, when Malik needs more memory, right? It requests addresses this way, right? So if I start with Malik and then I allocate a bunch of big things, Malik asks for more memory going this direction, okay? Stacks start typically at the very, very, very, very top of my address space, right? So in this case it's OXFFF and they grow downwards, right? So remember, the stack also can grow, right? What's a case where your stack might get to be quite large, right? What type of programming causes the stack potentially to get very big? Yeah, recursion, right? You guys may have, you know, and if you've programmed recursion in other classes and forgot to have like base cases, you may have blown your stack, right? Which means that you made a stack that was so big that at some point the operating system said, there's something wrong with you, right? It's not normal to have a stack that big, right? And I'm just gonna kill you because I don't know what you're doing, right? Like maybe you wrote a recursive case that has like 60,000 levels, right? But I doubt it, right? I think you're broken. So, right, so the stack starts at the top and grows down, okay? So, you guys remember ELF, right? How many of you remember ELF? It's like a month ago, right? The ELF format, and you guys can look in your kernel and see how this works. The ELF format specifies regions of the file to be loaded and where to put them in the address space, right? And again, the nice thing about this is, you know, ELF binaries can be moved from system to system. And because I have this really nice, uniform, contiguous view of the address space, they always work, right? I can always put my code at 0x10,000. It's fantastic, right? And I mean, this allows us to do some other nice things, right? So I think people know this, right? But, you know, if I go back here, remember, this is all arbitrary, okay? And someone might say, well, you know, why not load the code here? Why not load the code at 0x0 and let it grow upwards? Why would I start the code at this sort of high, high address? It's not that high, only 0x10,000, right? But it's a little high. I'm gonna ignore you, Jeremy, because I think you know the answer. So it turns out, I'll give you a hint, that typically I don't load anything down here. I never load anything into this part of the address space. Why not? Kernel code's not there. It's a good guess, right? What's a common programming mistake? No, again, this memory is typically never allocated, right? What's a very common programming mistake that many people have probably made? All right, Jeremy, I'll let you. Yeah, how many people have had a null pointer exception when they've written a piece of C code, right? Why do you get that exception? It turns out you get that exception because you tried to access memory here that's never valid, right? If I put the code down here, what would happen if you accessed, like let's say you just started writing to zero, what would happen? You'd be overwriting some of your code, right, with garbage, right? And then your system would do something weird later in crash, right? But it'd be much, much harder to figure out why, right? You guys will do this in your kernel, I'm sure, right? In your kernel, the thing that gets loaded at zero are the exception handlers, and there's no protection, right? So I'm sure that most people in this class, assuming you do the assignments, will do this at some point. You will overwrite your exception handlers and something weird will happen, right? And your kernel will die, and it'll be because you had a null pointer problem. But in user space, what we do is we just create this big area down here that's never used, right, by convention to catch null programming problems. Manish, did you have a question? No, no, no, we'll talk about this later. It turns out these addresses are never marked as valid, right? The system knows, so if you try to access memory here, the kernel will cause an exception, right? This is a fatal exception, okay? All right, so here's another question, right? So I start my heap here, I start my stack here, okay, the stack grows down, the heap grows up. Isn't there a possibility that they'll collide? Could they potentially collide? I mean, they could, right? But what would have to happen in order for them to meet? How much memory would I have had to allocate at that point? To this process, right? Not even across the entire system. Let's say the heap starts like in the middle, and the stack starts at the top. This is a four gigabyte address space. So how much memory do I have to allocate before these two could meet? Like two gigabytes, right? So in general, that would mean that one of them would have to get really big, right? And it's possible, right? Especially on modern systems, but especially on older systems, this was just not considered to be very likely, right? On your OS 161 kernel, for example, this will never happen. You have like 512 megabytes of memory, right? The system will probably never be able to allocate two gigabytes to one process, right? Okay. So the nice thing is that this makes, like I said, this makes things very simple, right? So it turns out that this allows the compiler and the linker, the loader, to put things where they're gonna find them, right? There's one caveat to this, which we're not gonna talk about, which is dynamically loaded libraries still get loaded at runtime, and those have to be put somewhere and the binder has to know where they are, and I just don't wanna talk about this, right? You could, we could do like three lectures on linking and loading, which would be interesting, but off topic, okay? So I hope today the idea is to convince you that this is a nice abstraction, right? This would be a nice thing to do, right? We don't know yet how to accomplish this, okay? And as, how many people have ever read those, how many people read reviews on New Egg ever for products? If you don't, you should. New Egg actually has really nice reviews, right? But a classic New Egg review is like a negative will be, like it didn't cook me breakfast, right? Like that's for a product that like a disk drive, right? That you wouldn't expect to cook breakfast, so it's typically considered to be a mark that this thing's pretty good, right? So the question is how are we going to implement this, right? Like this is now kind of our goal, right? We want this nice, you know, address space abstraction. It's, you know, it gives this really, really nice view of memory, right? And there's a lot of nice things we can do at this point, right? The question becomes how are we going to implement it, right? So AJ, you got a question. So, okay, so the other thing I wanted to point out, right, is that, you know, for a lot, like this, this is four gigabytes, right? Right, this is a 32-bit wide address, right? On your system, it turns out that user address space is only two gigabytes, but, you know, again, you guys will talk about that more and why that is. These sections here are never drawn to scale on these processes, right? Like this would be an obscene, like this might be Microsoft Word, right? But this would be an obscene amount of code for a process to have, right? This would be a lot, okay? So why, are you asking why is there a gap here? Yeah, so that's a good question. I mean, I think one of the reasons there's a gap there is, you know, the heap is put, the idea is to put the heap somewhere where it's far enough from the code that I can load a fair amount of code and other things in there. But I could, in fact, actually on some systems, I think the heap starts right at the top of the code, right? So when I run elf, right, I load the code in there and then I set the heap address as like the next valid address that's right at the top of the code, right? Because this part never changes, right? The code is just kind of loaded at runtime and I know how large that segment is and it's not gonna change during the lifetime of the process. So you're right, I could start the heap all the way down here, right? And that would give me more room for the heap and stack to grow, okay? Good question. So here are some implications of this model, right? That I just want to point out before we finish for the day, right? So because my view of memory is now uniform, it means that addresses have lost their unique meaning, right? So when we talk about memory addresses for the rest of class, especially when we talk about addresses that are used by user processes, there's the address OX1000 has no meaning. It's meaningless, right? You have to associate it with the process, right? Because obviously, you know, two different processes that are different that have different code are both gonna load the code into what they think is OX1000, but it's not, it can't be the same thing, right? So in the future, it makes no sense to talk about user addresses without specifying a process, right? So in order to uniquely specify a user address, you have to give the address and the process, right? So we need a way of enforcing protection, right? So the idea is that we're gonna make these address spaces private, right? But we don't exactly sure how to do that, right? So just because we've created these containers, I mean, clearly they're still gonna be sharing the physical memory on the system, right? We have to find a way for them to do that. We have to find a way to map these address spaces down to the available physical memory. We need to do that in a way that protects the privacy and the security of data that processes put into their address space, right? The other thing that happens here is to some degree, we're kind of encouraging processes to take this, it's nice, right? It's nice to be able to say the process, hey, you have four gigabytes of, you have a four gigabyte address space, right? And yeah, I moved you onto a machine with two gigabytes more memory, but you still have a four gigabyte address space. But on some level, we're kind of encouraging processes to spread out, right? So if you look here, right, as somebody pointed out, I've got this, I've got my heap and my stack and I'm starting them far apart, right? Depending on how I map address spaces down to physical memory, this could be a big problem, right? Because for example, imagine I need a contiguous piece of physical memory that's that big, not in the trouble, right? I need two gigabytes of contiguous physical memory on a machine that has 256 megabytes, right? So that's gonna be a big problem, right? So I need a way to handle that, the internal fragmentation that's inherent to address spaces, right? Address spaces are extremely sparse, right? So a process has a view of four gigabytes of memory, but at any given point in time, it's probably actively using like 0.1% of that, right? So again, if I don't do this carefully, right, it's possible that I can have a lot of internal fragmentation and that would be really bad, right? So we need to figure out a way to solve that problem, and we will, right? So on Friday, we'll talk about address translation, which is the hammer that solves, that pounds in all of these nails, right? Levels of interaction and we'll start distinguishing between physical and virtual addresses. So I will see you guys on Friday.