 All righty, welcome back to Operating Systems. So we get to talk about memory allocation today. So we will just touch on it because it's covered more later. So based off your labs, what you guys like to do is just static allocation or just global variables. That's the simplest strategy. So create a fixed size buffer in your program, you know, char buffer, size of a page, something like that. And how this works is, well, when the kernel loads your program into memory, it knows, well, the compiler just picked a random virtual address where that array starts. And the kernel would manage that virtual memory for you, set it aside, make it valid, such that it actually maps into actually valid memory. And the reason why we all like that is because we don't have to free it, right? Makes things nice and easy. So why don't we have to free it? Well, because when our process terminates, then the kernel gets rid of all the memory then. So it lives as long as your process lives. So as long as your structures are a fixed size and they should last the entire program, this is a perfectly valid reason to use a global variable despite what they told you in 105. So perfectly valid reason. So sometimes though, it is not a good thing. So sometimes we want dynamic allocation. So sometimes while we don't know how much memory we need ahead of time, which case we need to allocate it. And sometimes if you have a fixed size static allocation and say you created, I don't know, 10,000 thread structures or for some reason and the program only uses two, well, you've wasted the law of memory because the kernel essentially had to set that aside for you. Hopefully it's lazy, but can be wasteful sometimes. So if you don't know how much memory you need ahead of time, you have to do dynamic allocations. You always have to do dynamic allocations if you don't know the size ahead of time. So if you don't know it whenever you compile the program, then you just have to do malloc. So static allocations need to account for the maximum size. Other than that, we have to use dynamic allocations. So the question is where do you allocate memory? So you can either allocate it on the stack or the heap. Anyone want to tell me how you actually allocate memory on the stack? There should be one obvious answer, yeah. So that's how the kernel would do it. So how do you do it in your program if you want to use your stack, yep. Use functions? Specifically what in a function would be on the stack? Yeah, all the variables, right? So if you create a local variable in a function, that's allocated on the stack for you. And you don't have to do anything. Does anyone know how to explicitly allocate on the stack? Say if you just wanted five bytes or something like that instead of just an int. Yeah, there's actually an allocate function for stacks. And it's called, unsurprisingly, it's called allocA. So stack allocation, mostly in C, it does it for you. That's actually primarily why we use C instead of assembly because it just handles a stack for us and we'll do the push and the popping and all that fun stuff. So if I have a normal variable, say called intX, well internally if you get into the compiler course while there is an explicit allocation function to allocate memory on the stack and it is called allocA. So part of what your compiler will do is figure out, well, what's the size of this variable and it will figure out what the size is and allocate it on the stack. So it would essentially look like malloc but instead of going to the heap, it goes to the stack. So internally it would be represented like this. So it would have a pointer to x and that would point somewhere on the stack. And then you can go ahead and use it. So yeah, allocA would just be a normal C function. So the kernel, it wouldn't be a system called because your stack's already set up ahead of time, right? Whenever it loads your program, all that stack space is set aside and then that goes back to like, you might have guard pages and all that other fun stuff. So the stack space is set aside for you. It's like a fixed size. We saw that too using pthreads like it was eight megabytes or two megabytes or something like that, it's a typical stack size. So this, yeah, we'll just use it, won't do a system call. Yeah, so allocA will look like malloc and it just returns a void pointer, so you have to cast it. And this will all be done, like you've never done this for local variables, this, the compiler would handle this stuff for you. But if you take the compiler course, then you'll probably see something like that because guess what? You have to do these allocations yourself because nothing else will do it for you. You are the compiler in that course. Yep, yeah, so if I allocate it on the stack, right? Well, that's valid only for this function. If this function ends, it essentially just resets the stack pointer and all that memory is freed. Yeah, it's just in the scope of the function like we learned before, but really it's really, really quick to free memory and it kind of does it for you. So whenever you exit a function scope, it just resets the stack pointer back to whatever it was at before we called the function and it frees everything in one step. So you don't need it? Yeah, you've never called free for local variables, right? So same thing with this. So it would act the same, you can think of like it would do an allocA internally. So yeah, that last part, this won't work if you try to use the memory after returning, yeah, that sounds awkward. What do I mean by that? So we have a little program here. So, do I need to make that bigger? Pro, let's see. So we have a nice little program here. We have a function called foo that returns a pointer to an int and so I declare an int x that should declare it on the stack. Then I'll print off whatever the address is of x. So it should be some address that is on my stack. And then I return the address of x and then here in main I call foo and I have a pointer. So after foo is done, that memory is not valid anymore. So the address of x is just going to be somewhere on the stack, but it's not going to be and used by foo because foo's already done running. So it would reset back to essentially whatever main was at where it doesn't even have any local variables or anything like that. So just reset and that's what would happen. So here I print the pointer. So if I ran this right now, I would probably expect percent x and p to be the exact same, right? So thumbs up. All right, so I run this, they're not the exact same. One's null, the other is not. This is fun because this is actually a new thing that happened like the past year or two because technically we are in undefined behavior land. So let's see if I modify this and I compile it. I see I get this little fun warning that even warns me that I did something stupid. It says function returns address of local variable which is technically undefined behavior because it points at some stack location that is only valid in the scope of foo. So as soon as it returns, it's no longer valid anymore. In that case, because it's technically undefined behavior, C can do whatever the hell it wants and they decided to just return a null so you crash. Yeah, the contents or the address. So it should be the same address, but technically if you read the C standard, you're not allowed to take the address of a local variable and return it because it's essentially a bad idea because it's now invalid memory as soon as I return. So is there a way to outsmart the compiler and get what we wanted before where we actually see the address return that doesn't do the stupid undefined behavior thing on us? There is no keyword it is. So compilers are very, very, very smart pieces of software that sometimes look exceedingly dumb. So a workaround for this would be, well I could declare a pointer to Y say and give it the value of X or the address of X and then return Y. So programming 101, well I guess that's not programming 101, but I can compile it now. I don't get any problems because I'm technically not taking the address of a local variable. I'm copying it to a local variable and returning the value of that. Yeah, so the returning null is just because the compiler really detected it and gave you null in the hopes that you sag fault immediately and realize the error of your ways. But if you circumvent the compiler, you get around that and you get kind of what we expected before. So right now what would happen if I uncomment this line and I try to read the value that gets returned. And he guesses as to what that would be. Undefined, garbage value, one. All right, well let's see. One, why? Yeah, nothing overwrote it. I didn't create a local variable after that. I just reset the stack and it's kind of chilling. That memory is just chilling there. We're not using it, nothing else has used it. Technically something else could use it, but it didn't. So we can just read it, it's the same value, nothing changed it. So if I change this from one to two, I try again, same thing, two. Yep, if I created it where? Before the print like here. Yeah, we can try, let's see. Let's see if it super lines up. Whoops, I got in there. Oh, I already called something x, sure it did. Unused variable, hopefully this compiler's not too smart. Yeah, so it turns out no. So that didn't happen because well, technically that local variable on foo stack, there's some garbage before that, so it'll like save the return address and all that stuff we did before. So the local variable is probably above a return address, so we probably overwrote whatever stack space was used for its return address. If I do something fun, what about if I do this? Should still be the same, should still be p equals two? Okay, why the hell did that happen? Well, I called printf, printf probably has some local variables in it and it uses that stack space to write some value and just happened to write that value because every time I run this, guess what? It's the exact same thing, it's the same garbage value over and over again. Nope, it's just because I moved it and here the address is pointing somewhere in the stack which if I print it immediately, well, probably unchanged, but if I call printf and let it go to completion, I probably overwrote some local variables or something like that and I changed the memory, yeah. Yeah, I mean, probably do a mallet call that would be a local variable, it uses a stack, so on and so forth. Let's see, other fun stuff. We could probably do something, I wonder if it lines up perfectly. Let's, so if I do something really silly, so move it back, let's see if this works. Unused variable, why? Yeah, whatever, that's fine. Oh, p is still two, whatever, weird. In fact, well, this is why it's undefined behavior because, well, we don't know what the hell is going to happen. So have I adequately shown you that that is a very bad thing to do? It is very confusing, you do not want to return, like get the address of a local variable because it's on the stack and it's not valid anymore after that function ends. So any questions about that stuff? Yeah, no, why is fine because it gets copied by value? Oh, that's fine, just the address of it. So yeah, I did not, hopefully did not break your programming brain. Everything's copied by value and C, you just don't want to take the address of a location on the stack such that the address outlives whatever you're using. So if I took the address of Y here and then I called another function, that would be fine because the address of X lives as long as foo lives, right? So it's valid. So if I did some other function here where I just pass it the address of X, that's perfectly valid. That's a fine thing to do because it lasts as long as X does. But as soon as you return from the function, that's essentially the same thing as freeing all the local variables. Probably easier to think of it that way, in which case if you use a memory after it's freed, then it's undefined behavior, it blows up, bad things happen. All right, any questions about that fun detour? All right, that was more of a fun, if you can call that detour. All right, so you've used dynamic allocation before, you've used stack allocation. So hopefully you've used malloc, I hope. Everyone's used malloc. So that's the most flexible way to use memory, but the hardest thing to get right because well, if we malloc something, what do we have to do after we're done with it? Before that, free. Yeah, we gotta free it when we're done with it, but we should probably error check malloc just to make sure we didn't run out of memory if you really want to be defensive. So why using malloc and dynamic memory is hard is because you have to handle memory lifetimes. What does that mean? Well, I have to know how long it should exist in memory. So how long do I use it for? When should I actually free it? I have to free it exactly. I should free it when I'm done using it, but sometimes if your program's really complicated, it's actually hard to figure that out. So you have to figure that out as part of your program and free exactly once. Yep, no, no. There's no function that just frees all of the heap. If you want to do that, then you're into garbage collectors and stuff like that in Java, which aren't perfect either. So that's the best you can do. If you don't want to manage memory yourself, you're into garbage collectors in Java and go and stuff like that. Yeah, so for the same argument they made for C, like you can malloc, and since you're writing a library, you're not sure if that memory is going to be used again and you have to keep it around if it's possible to being used, right? If you're writing a library, you don't know what function calls are gonna happen. So if you can make that argument that, okay, they might use it, I have to keep it around, then you're free from freeing it, right? Might still exist, yep. Yeah, yeah, when the process ends, like the kernel knows all the memory your process is using and just, it doesn't care if it's stack, heap, whatever, it just frees all of it. Cause they're just pages, right? It just would mark pages as being unused now. I can use them for something else. Yep, yeah, so if you just don't do anything and you just add them back to the free page list, then you might be able to read another process's memory if you got lucky. So usually kernels that care about security at all will, if they reallocate them to a different process, they'll wipe them clean. So they'll zero them out or just fill them with actual garbage values or something like that. Most of the time it's like actual garbage, which is why you see garbage values all the time because you just got some random values back because you got a new page. Okay, so now if we have to implement memory allocators, we have our new concern fragmentation that we talked about briefly earlier, but now, well, we have to worry about both kinds. So more of a unique issue for dynamic allocation. So you allocate memory in different size contiguous blocks, right? Whenever you get something back from malloc, no matter how many bytes you request, they all have to be contiguous, they all have to be in ascending memory addresses. So if you are Java or something like that, you can compact things together, but in C, whenever you get a address back from malloc, you're guaranteed it will be valid until you actually free it. So every decision malloc makes is essentially permanent. It can't go back and say, no, no, I want to move this memory somewhere else, not possible. So fragmentation is essentially any wasted memory. 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 just wasted space, it's some useless space that I can't do anything with like pennies back in the day. They were deemed useless, they're just wasted space. So in order to have fragmentation, you have to have three requirements. So you have to have different allocation lifetimes, different allocation sizes, and the inability to relocate previous allocations. So if I have all the same allocation lifetime, that's like a stack. So all your local variables just last as long as a function lasts. So I don't have any fragmentation issues with that. I can just keep pushing values onto the stack over and over again. And since they exist as long as the function exists, whatever the function's done, I can just get rid of them in one big step. So I don't have any fragmentation there, I can just allocate things right next together. The case where I have different sized allocations, that leads to fragmentation because well, that's with malloc. So you can allocate however many bytes you want, free them at will, go back and forth, and you might have fragmentation. So if I make a request for one byte and then free it, and it ends up being between two different blocks. So there's only like a one byte gap and I never make an allocation for one byte. That one byte is now useless, and that might be for bigger bytes too. Third condition is inability to relocate previous allocations. So this is one of the advantages of Java. So Java, right, everything is a reference. Basically you don't get to see the location of anything. So it's all abstracted from you. And because of that, what the JVM can do or the garbage collectors can do is move memory around as long as it updates all of the addresses and make sure everything is consistent. It's allowed to move memory around. So I could just move all of my allocations together at some point in time and make everything right next to each other and not waste any space. So any questions about those three conditions? So they all three have to be present in order for fragmentation to happen. Okay, so two types of fragmentation we touched on before. So we'll make a nice little thing here. So there's internal and external fragmentation. So external fragmentation occurs between the blocks. So when I have different size blocks and one of the blocks is useless and I can't actually use it for allocation. So say I have a red block allocated and I have this red block allocated and I just have this blank space of free, I have this free space between them. Well, if all my allocations are bigger than that, I can never use that memory, which case it's essentially wasted. The other one is internal fragmentation that we had before once we talked about IO blocks and not consuming the entire block. So internal fragmentation is space wasted within a block. So in this case, I drew the block as just a big circle or a big box and the red part is actually used for allocation and the dark part is used for, is just wasted space. So the darker boxes here would be an example of internal fragmentation. So good, all right. So our goal is we want to minimize fragmentation. So it's just wasted space. We want to prevent it as much as possible. So we want to reduce the number of holes between blocks of memory. And if we have holes, maybe we want to keep them as large as possible. So that way we can actually use that space to actually fulfill allocations. So our goal, if you are implementing malloc, which we just briefly talked about here, you'll actually implement malloc if you take 454. So the goal here basically is to keep allocating memory without wasting space. So allocator implementations, usually they use something like a free list and a free list of different sized blocks. So the kernel, well, it didn't have to deal with any external fragmentation because all of our block sizes were the same. So the more general cases you keep track of free blocks of any size and kind of chain them together, you implement it with a link list. And if you need to handle a request of any size for allocation, you just choose a large enough free block for the request, move it from the free list. If you had to split it off, maybe you return the unused part back to the free list and you use your part. And then for deallocation, well, you just add that block back to the free list. And if it's adjacent to another one, then you merge them back together to create a bigger free block. So if they're contiguous anyways, you kind of coalesce them together and get a big block. Yep, yeah, you have to search. Yeah, so we'll see our strategies now. So in general, if you do this, there are three different heap allocation strategies. There's best fit and yes, and they all involve searching that list. So best fit is I search through the list. I choose the smallest block that can satisfy the request. So again, search through the whole list unless there's an exact match, in which case I don't have to search anymore because it fits perfectly. Another strategy is worst fit. So I choose the largest free block. So whatever has the most leftover space, again, I have to search through the list. And the final strategy, which is probably a bit faster, is first fit. So I just choose the first block that can satisfy the request. I don't actually care how big it is as long as I can fit it in. So it's like the early termination condition. So as soon as I can find something that fits, I throw it there and I keep going. Yeah, yep. So the deep fragment strategies for memory don't work in C because you just get a pointer and it has to be valid as soon as it gives it back to you. In Java where you don't directly use the pointer, then it can move it around for you. But it has to like, Java is gonna use pointers too, right? But it just has to fix up all the pointers, which is sometimes why it's slow. But we can do that, but in C our decision is essentially permanent unless you wanted to make your own crazy memory system where you access all your memory through a library or something like that. So you could do that, but for malloc it's permanent. All right, so if we allocate using best fit, so here I just draw a free list and any block with a blank background means it's free. So technically in part of the implementation, it would be a link list, but for this we're not really going into details with the implementation, just how it works. So here I have a red allocation, then I have free space of 100 bytes, then I have a blue allocation or teal or whatever color that is, and then a free 60 bytes. So best fit, remember I will choose the smallest block that can fit this allocation. So if I have an allocation coming in that's green, that takes 40 bytes, if I'm using the best threat strategy, where do I put it? Yeah, yeah, 60, right? Best fit is I want it to be as small as possible while still fitting the allocation. So 60 is smaller than 100. So I would throw it there and then I would have 20 bytes left over. So I used 40 of it, 40 of that 60, so I still have a smaller piece that is 20 bytes free. So now I have this purple allocation of size 60, where do I allocate this one? Yeah, probably the 100 block, right? It doesn't fit in the 20. So if I do this, I put it in the 100 and it has 40 left over. So now if I have this picky kind of block and it's of 60 bytes, well I can't actually fulfill this allocation. So I have 60 bytes free in total but they're not contiguous, so I can't make this allocation because, well, I have external fragmentation here. So this makes sense, I can't do this allocation if I do this strategy. So let's do the exact same thing again, but with worst fit. So now if I have the same allocation of size 40, where do I allocate this block? In the 100, right? The biggest block total. So I would put it in the 100 block and now I would have a free block of size 60, right? So now if I have an allocation come in of size 60, where does it go? Yeah, it doesn't really matter. Probably would go in whatever one exactly matches it. Well, actually it would have to search the entire list. So who knows, might go in either of them. So say we put there. So now if we have an allocation, that pink one again for 60, well guess what, now it fits. And now all my memory just fits all right beside each other. It fits exactly in the remaining space. So this is just to illustrate that, despite the name, the worst case sometimes gives you the best outcome. So that's the idea behind worst case. So when you actually implement these, they're both really slow, although I think in 453 you might implement them like this and try and make it go faster. But if you simulate a real program making real malloc allocations, best fit tends to leave like very large holes and very small holes. There's really no in between and the small holes might just be straight up external fragmentation and be useless. If you do worst fit simulation says it's actually worse in terms of storage utilization. So it does actually deserve its name, despite my previous example showed you, it's just, it all depends. And if you implement first fit, well one, you can implement it faster because you don't have to search the entire list every single time it's just the worst case. And if you simulate it tends to leave average size holes and turns out to be not too bad. All right, so any questions for today or in the future? Yeah, so that was the condition because if you could real, if I could move memory, right? Let's say I had this case where I couldn't fit anything. If I could move those blocks over, I just move the blue block over and the green block over and then suddenly I can fit the 60 size, right? But in C I can't move things because malloc gives you an address, has to be valid until you free it. Yeah, yeah, so realloc will resize that pointer and give you a new one and then guarantee to copy all the contents, so they're still valid. But you get a new pointer, it might be, yeah. So part of realloc is you get a new pointer so it can make a new decision. And then you're supposed to, you don't use the previous, if you look at the spec, you're not allowed to use the previous pointer you gave to realloc, you have to use a new one. Yeah, yeah, that'd be an example of that. Yeah, so if I malloc'd these and then freed it and then I moved it, yeah, you could, but you're not guaranteed that people call free on blocks you want them to call free on, right? You don't, if you're writing malloc, you just have to handle whatever happens. You have to write free in malloc. You don't get to choose when they free it, right? Think if I took your program and I just randomly freed memory of yours and moved it around, would you ever debug anything? Probably not, so it would make our programs even worse than they are. All right, any other questions? Comments, concerns, all right. This is, yeah, we're almost done. Is this our last Friday? No, second last Friday, all right. So let's wrap up today then. So the kernel has to implement its own memory allocations. We'll see what the kernel actually does tomorrow. Concepts are the same for user space memory allocation. The kernel eventually has to allocate its own memory too. So you might have noticed if you poked around in your kernel code in lab zero that you can't call malloc or anything like that because malloc is a C function. It doesn't exist in the kernel. So the kernel has to do its own memory allocations. So what we saw before this though is there's static and dynamic allocations. So static, basically just global variables. They last as long as your process lasts. And then dynamic, so I request memory and then I free it. So dynamic allocations will always return blocks of memory and fragmentation between blocks is external and fragmentation within a block is internal. And if I'm implementing malloc there's three different general strategies for different sized allocations. There's best fit, worst fit and first fit that I'll use link list and just keep track of a free list of different sized blocks. So just remember, pulling for you.