 All righty, welcome back to operating systems. Yeah, our second last Friday, which means the exams in two weeks, yikes. So yeah, exams already submitted everything, so it's too late to change anything now. So yeah, next three lecture, or next two lectures or content, then one on virtual machines and then two reviews. So next week, this lecture will be a review, yay. So we get to talk about memory allocation today, so may or may not be fairly quick. I highly doubt we will fill the whole time. So for memory allocation, static allocation is the simplest thing to do or have big global variables. So you probably did this in one of your labs, so you just created a big old, I don't know, like you could do a char buffer of a fixed size and how that works is whenever that program loads, the kernel sets aside that memory for you, make sure that it's valid, may or may not be lazy about it and not allocate pages until you actually use it, but if you initialize that, well, it would have to set aside that memory and make sure all the values are copied over to it. And with global variables, it exists as long as your process exists so there's no need to free it. So whenever you get rid of your process, freeze the memory then. So this is good if you have memory that you know will last the entire lifetime of your program and you know exactly how big it will be. But most of the time, we don't have that and we have to do dynamic allocation, which is everyone's friend, malloc and all those, but there's two different ways. So why don't we wanna do that? Well, you might only conditionally require memory, so static allocations sometimes are very wasteful. The kernel will set aside some memory that you've never actually used, so why did you bother doing it? And if you don't know the size of the allocation ahead of time, well, static allocations, they're all a fixed size. The kernel has to know how much memory to set aside whenever it loads your program in, so you need to account for the maximum size, which sometimes may just be too big. Well, then if you have to do dynamic allocations, you have to ask, well, where do you allocate this memory? And then we can either allocate it on the stack or the heap. So how do we allocate memory on the stack, which you've hopefully been, well, you have been doing it if you've been using C. Yeah, you just declare the variable and it's used for you on your stack. So it's mostly done for you in C, but you can do it explicitly if you really want to. So if you have a normal variable in X, that actually lives on the stack, so the compiler would go ahead and grab, set aside four bytes on the stack for you so you can use it for that local variable. So internally, you can also just do your own stack allocations if you really want. There is a standard C library function called allocA and that allocates space for you on the stack and you can request however many bytes you want. So if you want to allocate the page on the stack or some weird number like five bytes, you can use allocA and it will allocate space for you on the stack and give you back a pointer exactly like malloc does. So it would return a void pointer. In this case, I would have to cast it to an imp pointer and then your compiler would know the address of X so it's somewhere on the stack and the nice thing about C, which is pretty much the only reason we don't use assembly, we use C is because it takes care of all the stack management for you where everything's located. I don't have to do allocA or anything like that. And the rule behind that is there's no associated free with allocA either because the rule is whenever a function returns, that function just free's all that stack memory and it can do that really, really quick. So all it does is it resets the stack pointer back to whatever it was before that function started. So it doesn't matter how much memory you allocate, you can free it all in just one step, you just reset the stack pointer back to the state. It was at before you actually called that function. So this also won't work if you try to use the memory after returning. So what does that mean? So that leads us to a fun example. So say I had something like this where I wrote a function called foo that returned a pointer for an int. Quiet you, oh, oops, I have to reload this file. Okay, so I have a function called foo that returns a pointer to an int. Inside this function, I just declare int x equals one, something we've always done. I can print the address of x, so that's a totally valid thing to do. Here I just cast it to avoid star because I have to make C happy. And then I return the address of x. So now in main, I just copy the value that foo returns into a variable called p and then I print the value of p. So if I go ahead and run this, p and the address of x should be the exact same thing, right? Everyone would agree with that. So if I go ahead and run that and I ignore what my compiler's trying to say and I run it, I get that. Does that make sense to you? So this is actually a new thing that has happened in like the last year or two, which is pretty new for compilers. So if I go ahead and recompile it and actually read the error message or the warning that my compiler told me, it said function returns the address of a local variable, which is a bad thing to do because well, this local variable lives on the stack. So if I take the address of it, it's pointing, it's an address of somewhere on the stack and that local variable does not exist after foo is done. So after foo returns, that should no longer be valid memory. I essentially called free on it and now if I have it in another function and I try and use it, it's like a use after free, right? Bad, I should not have anything. In fact, c calls it undefined behavior and because it calls it undefined behavior, well, they took advantage of it and that's why you get this completely weird thing. So the compiler identified that you took, you returned the address of a local variable. So it just put a zero in there just to make your life hell because technically it's undefined behavior. So it just made it null so that if you try and use it later, you just get a segfault and you have to try and debug it instead of just getting some random value and maybe really weird things happen. So they did that to try and save you. But compilers are very, very, very smart pieces of software but sometimes they look exceedingly dumb. So how do I get around that stupid compiler warning where it's complaining at me that I'm returning the address of a local variable? Yeah. Declare X as a global variable? No, so I still want an address of the stack. So if I declare it as a global variable, I'll just be somewhere in virtual memory, right? And it's valid as long as the process is alive. I specifically want to see P equal to this, like what we expect to happen. Any ideas with something dumb here? A double pointer? Yeah, but I could do something even sillier. So the problem it's complaining about is I'm returning the address of X. So instead of returning the address of X, I'll just create a pointer called Y, give it the address of X and then return Y. So now if I recompile it, I don't have that stupid error method or that warning anymore. And now if I run it, it's probably what we expect. They're both the same value now. So now I thwarted the compiler's attempts to try and blow myself up. So now what would happen if say I uncommented this? So would that say fault two or what might we get? Undefined, yep, it is technically undefined behavior. Is there a likely behavior? No, so no, we'd be, so P is actually a value now. So this would be dereferencing P. So reading whatever value P is pointing to. So P is like address of something on the stack, right? It was X, but here it is no longer X or it's just the same address that X was stored at. Yeah, it might just be one. All right, if we got lucky, hey, we got lucky. So that works because well, here I didn't call another function, I didn't create any more local variables. I didn't mess with the stack at all. So even though it's no longer technically valid, right, the operating system's lazy, it's not going to zero it or do anything like that. It just resets the stack pointer back. And if you were to reuse it again, it would overwrite that value. But in this case, since I don't overwrite the value, I get, I just read it, it's the same value. Am I likely to get the same value if I just move that line down? So is it still gonna print one? Think so? Any other votes? One? Well, all right, let's try. Why did that happen? Well, because we called printf, it probably had some local variables in it too, and it overwrote the value. And it just happened to overwrite it with whatever this number is. Because every time I run it, it seems to do the exact same thing. But technically, that is all undefined behavior and you shouldn't do it. So that's just a word of warning because x only exists as long as the function foo exists. If you take the address of it, it's essentially you're taking the result of an alloc a, and if it outlives foo, it's technically already free as soon as foo is done, so you are not allowed to use it again. Otherwise, you will get very, very random behavior. So, word of caution, do not do that. All right, everyone agree with me, do not do that, because it doesn't make any sense? All right, so that is your word of warning of the day. So, back on the things we have used, we've used dynamic allocation before, there's the malloc family of functions, so like realloc, calloc, free, all that fun stuff. It is the most flexible way to use memory, but the hardest to actually get right because you have to handle your memory lifetimes. What does that mean? Well, that means you have to free your memory whenever you are sure you are done with it and you know that it will never be accessed again. So, sometimes if your program is very complicated, you might not know when it should be free and or even getting that pointer is actually challenging and you also have to be sure that I only call free on that memory once, otherwise I have a double free and very bad things happen. So, there is a new concern we have to if you were to implement dynamic memory allocation that's fragmentation, so we kind of saw that when we talked about file systems and we talked about internal fragmentation, so wasting space within a block and now we will be wasting space also between blocks. So, more of an issue for dynamic allocation because we allocate different sized memory and contiguous blocks for some of the allocations your kernel does is only in pages, they're all the same size, so it's a lot easier if you have the general version of that problem where all the sizes are different, then it becomes much more challenging and especially fragmentation will be an issue if I can't move memory around. So, for malloc, if you get a pointer back from malloc, it's essentially a permanent decision until you call free. So, whatever address I got back from malloc, it has to be valid until I call free. The library is just not allowed to just randomly move memory around on you, otherwise you would seg fault and it would actually be from no fault of your own. So, a fragment is basically just a small contiguous block of memory that cannot handle an allocation. You can think of it as a whole, it's wasting space, whatever you wanna think of it as. And in order for fragmentation to happen with memory allocation, you have to have three requirements and they must all be true. So, the first is different allocation lifetimes. So, an example of having all the same lifetime where fragmentation is not an issue is your stack. So, for your stack, all your local variables last as long as the function exists. Within the stack, there is no fragmentation, so you just grow it however many bytes you need and because all the local variables last as long as the function, while you just get rid of them all at once, there's none of that weird staggered thing where you might only freeze some of them and not other ones. So, your stack would not have any fragmentation issues. The next is different allocation sizes. So, if everything is the same size, so like the kernel only dealing with pages, well then you don't have any fragmentation issues because they're all the same size. So, you at least don't have external fragmentation because, well, one block is as good as any other block. And then the third part is you have, you cannot relocate previous allocations. So, that means I can't just move memory around to make it all contiguous, I can't change addresses or anything like that and essentially all my decisions are permanent. If my decisions were not permanent and let's say I just had a bunch of holes of memory, well I could just stop the program and move them all together, just shift them all over, fill in all those holes until I have no gaps and then I don't have any fragmentation anymore. Yep. So, the difference between internal external fragmentation is whether you consider between blocks or within a block. So, like for file systems, like if we have a fixed IO block of 4,000 whatever, we only use 10 bytes, the rest of that block is internal fragmentation. So, it's wasting memory within a block and then external fragmentation, well we'll see here because we didn't see it before because for, if all our IO blocks are the same size, we don't have that problem because external fragmentation is just wasted space between blocks. So, it's like a block isn't big enough to do any useful allocation. So, yeah, we'll see an example of that too. So, yeah, here's the diagram. So, there's internal and external fragmentation. So, external is kind of new here. So, if those are all different blocks, say I have a red block that represents an allocation and then some free space and then another red block that represents some allocation and then a bigger free block. Well, if my allocation is slightly bigger than this free block, then essentially this block is now useless. I can't use it for any allocations unless it's small enough to actually fit. So, that would be an example of external fragmentation and internal fragmentation is, yeah, within a block I am wasting some space. So, here would be an allocated block. Red would be the memory you actually use and then the filled in block is just wasted space. I'm not using it yet, which we saw in file systems before. All right, so our goal is if we implement memory allocation, we want to minimize fragmentation. So, it's just wasted space, we want to prevent it. And the goal here is we want to reduce the number of holes between blocks of memories and if we do have holes, one idea we might have is to keep them as large as possible. The idea behind that is if the free blocks are large as possible, hopefully I don't have any external fragmentation because I can actually use those for allocation and then they're actually useful. So, our goal is to keep allocating memory without wasting space and this is essentially what malloc does. If you take 454, you implement these strategies. So, we're just gonna talk about them at a high level but if you take the next course on this, you actually implement them. So, it's kind of what we talked about earlier. Your allocator would implement a free list but instead of a free list of pages which are all the same size, your free list would also keep track of how big each element are. So, they keep track of free blocks of memory, keeping track of the size of each block and you kind of chain them together. It would just be a linked list of memory blocks with whatever size. So, you need to be able to handle a request of any size. So, for allocation, the general thing you would do is you choose a block big enough for the request, remove it from the free list, maybe you split the block off into two parts, you put the free part back on the free list and then for deallocation, you just add the block back to the free list and if you have two free blocks that are contiguous with each other or they're right next to each other in memory, you just create a bigger free block out of them. So, if there's a block of 40 bytes free next to another block of 40 bytes that are free, you just call that 80 bytes free. So, in general, there are three heap allocation strategies and while you would implement this in four, five, four. So, the first one is best fit. So, you choose the smallest free block that can satisfy the request because while you're implementing malloc, so they request a certain amount of size and for that, you need to search through the entire free list. So, you search through the entire free list unless there's an exact match for the best fit strategy and you try and get the smallest allocation that will actually fit this request. In worst fit, while you choose the largest block and the idea behind that is, if I choose the largest free block that is the most left over space, hopefully it will definitely fit and hopefully it will make another big free block and I can actually reuse that. But both of those strategies have to search through the entire list every single time. Again, unless in best fit, there is an exact match. So, the other strategy, which is like the FIFO of memory allocation is called first fit. So, it's just choose the first free block that can actually satisfy the request. It's like the stop early condition. So, as soon as I find something that's big enough, I don't care if it's the biggest, I don't care if it's the smallest, I just care that it works. So, in this, we have a bunch of memory allocations. So, here is all of the memory your malloc is actually managing and anything that is a color means it is being used, so that's use memory and anything with the blank background means that is a member of the free list and the number is how many bytes are free. So, in this case, I would have a free block of 100 bytes and a free block of 60 bytes. So, if I'm doing the best fit algorithm where I try and find the smallest block that will fit this allocation, if I have a malloc for 40 bytes that I'll represent in the screen box, where do I put it? So, do I put it in the 100 block or the 60 block if I'm doing best fit? 60, right? So, I put in the 60 block and well, if I put 40 in the 60 block, there's 20 left over. So, my free list would look like 100 and then 20. So now, if I have a purple allocation that is 60 bytes, which one do I put it in? Probably the 100, right? So, put in the 100 because well, can't fit in 20, it's too big. So, I put in the 100 and it would have 40 bytes left over. So now, if I have a purple allocation of size 60, well, I can't fit it anywhere. So, I have 60 bytes free. You're asking for 60 bytes and I can't do anything with it because I have external fragmentation in this because I've wasted space between the blocks. So, this 40 bytes and this 20 bytes are essentially wasted. It's, from the point of view of an allocation of 60, I'm full, I can't satisfy that request. So, I'm out of memory at this case. All right, let's do the same thing, but we'll do worst fit. So, where do I allocate this block of 40 if my free list is 100 and 60? Yeah, in 100, right? So, worst fit is I picked the biggest one. So, scan the entire list, pick the biggest one. So, if I put in the 100, I have 60 left over. Now, if I have an allocation for purple of 60, well, I have 260 free elements, so I don't care which one. So, I can put it in either one and now, if I have the last allocation of size 60, hey, guess what, now it fits. So, now I have everything fits. So, in this case, worst fit was actually the best algorithm, which it's fun. But, turns out they're actually both slow. So, if you actually like simulate, like keep track of what normal programs do in terms of malloc and free calls, and you try and simulate what algorithm would work best for that particular program, and then you run over a bunch of programs. If you simulate it with best fit, if you simulate, typically that tends to leave like very large holes, which can still be useful and a lot of very small holes and there's nothing really in between, and the small holes, while those are all external fragmentation, so you might waste a lot of space. If you simulate worst fit, simulation actually says, yeah, it is the worst in terms of storage utilization, so it has the most fragmentation. If you implement first fit, which while you can implement faster than either of these, because you don't have to scan the entire list every time, it actually works out fairly well and tends to leave average size holes, which typically are not fragmentation. So, all right, any questions? Wow, that was a speed run into the lecture. All right, any other questions, comments, concerns? Because yeah, your exam is written, so yeah, it is going to happen in two weeks. Sounds bad. And yeah, rest of the plan is we have one more lecture on memory allocation, what the kernel actually does, and we talk about virtual machines, and we have two lectures for general review. All right, so next lecture, we will talk about what the kernel does to do its own memory allocations, because while if you went into your lab zero code and you tried to write malloc in your kernel module, you would quickly discover that malloc does not exist in the kernel, so the kernel has to manage its own memory and we'll see what it actually does to manage its own memory in the next lecture. So, what we saw today, there's static dynamic allocations, so static allocations are just global variables, they last as long as your program lasts, and dynamic allocations, so that can either be stack or heap. For dynamic allocations in the general case, fragmentation is your big concern, and for dynamic allocation there are, it just returns you a pointer to a block of memory that you are not allowed to change, and fragmentation between the blocks is called external, fragmentation within a block is called internal fragmentation, and if you were to implement three general allocation strategies for malloc, for different sized allocations, we saw best fit, worst fit, and first fit, so just remember, we're on this together.