 Okay, good afternoon everybody. Welcome to another wonderful day of operating systems. So today, talking about memory allocation at a fairly high level and we'll go over some things that you probably shouldn't do and some kind of fun new advancements and compilers to try and save you from yourself and we will work around their help and try and crash it anyways. So, you've been doing memory allocation, you theoretically learned it in first year. Some of you are better at it than others. So there's a few different ways to allocate memory and we went over a few of them. A few is like the simplest one is called static allocation which is basically just global variables. So global variables exist for the duration of your process, when your process ends, they end because everything is now destroyed. So this is all fixed size allocation. So if you have a global variable called char buffer and you give it the size of a page, well that variable called buffer is gonna last as long as your program lasts, as long as you put it in global scope. So that's part of what the kernel in your compiler does. It will make sure that as part of the program, whenever it loads it into memory before it starts executing anything, it will make sure to set aside some virtual memory for that buffer and then give you the address so you can actually use it. So again, with this, all the memory exists for as long as your process lasts, there's no need to free it because the free is exit. So everyone is good with this, everyone understands this. So good old global variables, thumbs up, no need to free so they are perfect. But often you actually need dynamic allocation because the problem with static is while you have to declare everything upfront, so you might not know how much memory your process even uses ahead of time. Sometimes they'll only use it in some certain conditions in which case, you know, if half the time your program uses next to no memory and in 1% of cases it uses two gigabytes while you're gonna have to set aside two gigabytes of memory which is of course going to be wasteful. So some other conditions that you might want to do static allocation or sorry, dynamic allocation is if you don't know the size of anything. So with static allocation, I have to declare the sizes up top or upfront. The compiler is gonna know some of them like how big an int is, how big a long is, how big a bool is or whatever, but for arrays, you're not gonna know the size of the array and it can't give you memory upfront if you don't actually know the exact size of that array. So if we do dynamic allocation, we have two options available and hopefully we know them so we can either do it on the stack or on the heap and the stack is where local variables exist and when you're outside of the scope of a function then they no longer exist. So like stack allocation, that's done for you in C so if I just have a normal variable in a function like int x, well what your compiler will actually do is set aside memory for you on the stack and there's actually, we've all used malloc before hopefully so there's actually a way to ask the compiler to ask for memory from the stack which you've probably never used before because it's done for you. So the actual function call to allocate memory on the stack is called alloc a and it's like malloc you say how many bytes you want and then it just gives you a pointer back. So C, most of the reason people use C is because C kind of does this automatically for you and saves you the headache. If you had to do this every time you declared a local variable you would probably go crazy in like not very long. So as bad as you think C is it's actually worse depending on what you use. So the rule for alloc a is there's no free with it and the rule is whenever that function that called alloc a returns it frees all the memory and all it does to do that is it just restores the pointer back to its original position so it can reuse that memory. So no matter how many bytes you allocate freeing all the memory is just done in one operation it just moves the pointer back up to the beginning you reuse the memory and we all good. And the thing about this is that you have to be careful not to shoot yourself in the foot because this won't work if you try to use the memory after returning. So let's see a little fun example here and see how we can break it. So here's our small little program I can make it bigger. So we have main it calls foo and foo gives us back a pointer to an int so we're just gonna print out whatever that pointer is and within foo we will create an x which should be allocated on the stack we will set its value to one we'll print out the address of x and we will return the address of x so at the point of foo being done x should no longer exist but since we return an address we still have its address outside of here which if foo returns it should be dead so this should be an error and this is actually a fun new advancement in the compiler because this is actually undefined behavior so you would expect here that whatever the address is here is the same here, right? Like address of x should be the same as p if I run it right? So this is a fun new advancement we'll compile it it actually gives us a warning if we were paying attention where foo return's address of a local variable and you're like oh warnings are fine compiled so let's go ahead and run it so if I run it I can see that my compiler is actually smarter than me so even though they should be both the same value because it's undefined behavior well c is allowed to do whatever the hell it wants to and in the last year they have recently decided that they will set it to zero so if you try and use it immediately and if I tried to like uncomment this line where I try and get whoops if I try and uncomment this line where I try and read its value well before it would just read some garbage value and now it's gonna seg fault because it's null if I dereference the null pointer won't work so that's kinda cool yep so this right now that I have uncommented prints the pointer so this will print the pointer and this also prints the pointer so I should be printing exactly what I got but the compiler is like hey you're doing something stupid so I will set it to null so if you try and use it you won't get weird behavior you will just seg fault which is probably a better option but unbeknownst to the compiler my background's in compilers so there's an easy workaround for this so it won't let you return the address of a local variable directly but I can do something like int pointer to x equals the address of x and then I'll just return the pointer of x so now hey I'm not returning the address of a local variable directly and my compiler's not that smart so I can actually just use it now and if I go ahead and run it hey they're the same now so you can actually outsmart your compiler by not doing very much work and it's still in defined behavior because you're returning the address of a local variable but if you do it in a roundabout way you can avoid all that overhead so now I can uncomment this since I actually have a pointer so here I set the value to one so you would think that hey if I read the value of it well should still be one well let's go ahead and see if that's true so who here thinks it's gonna be one still? We got hand wavy so it could be one could be one very definite one 43,000 and we can run this a few times 43,000 went up by one it seems to be kind of consistent now so the reason that it's just more or less random variable is because well we return some memory from the stack and then after foo returns it's freed so it can be used for anything else and that anything else well printf needs the stack because it would have some local variables and stuff so it would just set to some value it uses and then we're just printing out something that actually printf is using now because it should be freed so any questions about that fun stuff this can be a fun source of errors yeah yeah so we can see so let's see if nothing else uses the stack and we pray that it's still the value one so if we do this it should still be one so let's see so yeah so this is one so this is a case where you're technically using undefined behavior but after you return from foo we don't create anything else so we're good so we could copy the value and this works sometimes but it's not guaranteed to work yeah so in this case before I copy the value because it's just pointing to somewhere in a stack so the stack it would be freed after foo and that variable is probably not going to be touched whenever it just moves a pointer back and reuses the space so it's set aside to be used for something else but nothing else has used it yet so I can go ahead and use it so this is a fun source of bugs because if you have something like this in your program this will probably always work but it's technically undefined behavior and it will actually break because I mean we can just break it if we just go back so now because it's undefined behavior and this is like a new compiler thing in the last year well now if I do that seg faults so I don't get one anymore because it's smarter than me it returned null and I can't de-reference null so I can't try and use it or save it or do anything so this is probably yeah so this is a fun new compiler this is a fun new compiler thing that will probably cause some undergrad things to break eventually so that was just a little fun demonstration so any questions about that? That was more of a fun aside of the stack all right so let's get back into the actual content so for other dynamic memory where you can't use a stack so you might need to use other dynamic allocation and that's the malloc family of functions so that's like malloc, caloc, ralloc I think that's most of them and that's the most flexible way to use memory but it's the most difficult to get right because you have to handle the lifetime of the memory and make sure you free exactly once so as long as that memory I need lasts as long as a function lasts well then a stack is appropriate it's really fast to free but if I want, for example if I actually want to return a pointer to an int there and I want it to outlast the function well I suddenly can't use the stack anymore and I have to use something like malloc so the deal with malloc is you're allowed to get memory back however many bytes you want but the compiler is not going to know when you're done with it so you have to tell it you are done with memory and you have to explicitly call free and that gives us all our fun memory bugs we all know and love like you can't call free twice you shouldn't use memory after you free it which is more or less that same problem with the stack so that's kind of the same thing as using memory after we free it and also there is a new concern we have fragmentation so we saw fragmentation in the file system context before where we had wasted space so we didn't use all of our block well if you're doing a general memory allocator which we don't have to write a memory allocator in this course you'll probably write it in the next course but we can talk about it and like the big general overall algorithms they use so fragmentation is only a unique issue for dynamic allocation so you allocate memory they're differently sized contiguous blocks and some things we can't do while you could eliminate fragmentation if you asked for memory you freed something in the middle well if you want to make sure you don't have any wasted space you could move all the memory together to make sure everything is contiguous and that is basically what Java does so as part of Java running and it's whole heavy virtual machine it will try and do this to eliminate fragmentation but in C you don't have that luxury because C isn't allowed to change the address for you so if malloc returns you an address and it's not allowed to change or relocate the memory whatsoever so C's not going to be able to relocate anything because the decision is essentially permanent so a fragment is just essentially wasted space so it's a small contiguous block of memory that essentially can't handle any allocations you can think of it as just some wasted space in memory it's not useful anymore or you can think about it in the file system sense where I'm not using a whole block I'm only using a bit of it so I'm wasting a bunch of memory so in order for you to have fragmented oh sorry was that a question? Okay, what was it anyways? Yeah, yeah, yeah so easy case for fragmentation I could just allocate something allocate something else that I don't know it's a different size and allocate something else that's a different size and then free the second one so now there's a hole in between them and now if all my other allocations are like too big to fill that hole then that hole will never get filled and I can't do anything about it yeah well I said assuming all the allocations are too big for it, yeah but then there's a case where oh you might create other holes as you go on even if you do find one that can fill it and so on so there's three main requirements for fragmentation and they all have to be true as long as one isn't true you can avoid fragmentation so first one is all the allocations have to have different lifetimes so you can know this to be true so for stack-based allocations well they're all different sizes and you can't relocate anything but the stack won't have any fragmentation because everything only lasts as long as a function lasts so if you need four bytes and then you need eight bytes and then you need 12 bytes well it'll just allocate them all nicely in a row because they last as long as a function and then when the function's done it just deallocates all of them so it can space them right next to each other there's no way to stop using a global or sorry a local variable so I can't stop using one like halfway through the function the next one is all the allocations have to be different sizes in order to get holes so that you can think of as like our inode table so our inode table wouldn't have any fragmentation in it because the inode table everything is the same size so everything's 128 bytes so one block or one inode space for an inode is as good as any other one so you can always as soon as you free it you can always reuse the space so no issue with fragmentation there because everything's the same size so you don't have to worry about it and then the third one is you can't relocate previously allocations so if I could relocate something again I could just what Java does essentially is like pause your program, move all the memories to be right close together and that is why Java doesn't let you use pointers because it uses pointers that can change so it's like a level of indirection so you're not allowed to look at the pointers in Java because they might change under your feet when it actually moves the memory around and it takes a lot of smart people to go ahead and figure out oh if I move the address of something how do I fix up everything that refers to it you could imagine trying to change the address of something in C when like 10 things refer to it and you miss one and you are screwed so yeah something like Java will do that but something like C will not do that because all the decisions are essentially permanent okay so and there's also two types of fragmentation so we can finally get into this so there's external fragmentation that occurs when you allocate different size blocks so that's within the like memory allocator to actually control and there's no room to allocate between the blocks so that's kind of what we've been talking about so far and then internal fragmentation is if your allocator uses kind of blocks or some fixed size memory that doesn't exactly fit the allocation well then there's going to be wasted space in a block so for example for your file system that has all sorts of internal fragmentation because it only deals with in your case for lab six only deals with like a kilobyte block so if anything is smaller than a kilobyte any other space is wasted so that would be internal fragmentation because your file system only keeps track of blocks of a size of kilobyte it doesn't care about anything smaller so that's also why your file system doesn't have any external fragmentation because it again it only cares about things being one kilobyte and that's it it doesn't care about anything being different sizes if anything needs to be bigger it just breaks it up into fixed sizes of one kilobyte where it doesn't care they're all treated equally okay so if you're writing malloc or you're just using your program well you want to minimize fragmentation because that's just wasted space that you should prevent and there's lots of people that their full-time job is this so like imagine Chrome developers or something like this where they're wasting gigs and gigs of memory and a lot of that is probably fragmentation so they care so much about it that Chrome and most browsers will ship their own memory allocator to not use the same one you guys use because they know more specifically what they are doing and they don't want fragmentation whatsoever so if you're writing your own like general purpose allocator if you're writing your own malloc which we're not doing in this course you may do later you want to reduce essentially the number of holes in memory and if you have holes you want to keep them as large as possible and the idea behind that is that hey if I have really large holes I can actually use them for allocation and therefore it's not really fragmentation so your goal if you're a memory allocator is to keep allocating memory without wasting space so allocator implementations usually use a free list so we talked about a free list of pages well that wasn't that general because pages are all the same size and this is the more general one where we actually keep track of different size memory allocations so a general allocator will probably also have a free list and it keeps track of free blocks of memory by essentially chaining them together and this is just one big link list of free memory and you also have to keep track of how many bytes are free so if you're implementing malloc well you have to be able to handle a request of any size I can ask for four bytes, I can ask for eight bytes I can ask for 16,000 bytes I can ask for weird numbers I could ask for seven bytes and malloc has to handle that and then that's what you have to do if you're malloc and also well you have to implement free so what free would do is add the block back to the free list and if it's neighbor essentially if its neighbor is also free well it could merge it back into one bigger block so it can actually use that for a bigger allocation so using that kind of general, really general free list there's three general algorithms or allocation strategies you can do there is best fit so that's probably what everyone would think to do if they see blocks of memory with holes in them and an allocation comes in and they have to do something with it so best fit means it would choose the smallest block that can satisfy the request hopefully it's kind of our goal we were thinking about before of maintaining large holes so we can actually use them for allocations so this will use the smallest one possible and try and find an exact match so worst case it would have to search through everything in the free list but in best case if there's a exact match for the size of the allocation we need it would just use that one and it can end early all right the next strategy is probably sounds stupid to you and it is called worst fit and it basically just does the complete opposite so it just finds the largest block whatever has the most leftover space and uses that one so searches through the list finds the biggest free block and says yep that's a good home for me I will use that one and then the last one well is essentially first fit or essentially like it's kind of like the FIFRO idea where you just take the first thing that works so as soon as I find a free space big enough for the request I say okay I'm done I'll use that one so typically whenever you first do memory allocators you would use something like first fit because well you don't have to search through everything but let's take a look at some examples to see what we have so here's what I'm going to illustrate so at the top there that big bar is like how much memory the allocator is managing right now and anything in a color is used so I can't allocate in that space and then any of the blank areas there are free and they have a number associated with them and that's how many bytes are free so in this example my memory allocator at the certain point in the program there's an allocation in red that I'm not allowed to touch I have 100 bytes free and then between then next I have some blue allocation or teal or aqua marine I guess look at that color knowledge and then we have 60 bytes free after that so now if we have an allocation request so someone calls malloc 40 so we'll represent in green well where should I place this block so allocation for 40 which one should I use should I put it use the 100 space or use the 60 space yep 60 yeah 60 it's the smallest one I'd want to leave that nice big 100 byte hole so I'll go ahead and use the 60 space so I put my green allocation there at the front or the back whatever makes you feel good and then I would have 20 bytes free now because 60 minus 40 is 20 so I have 20 bytes left over my free list so this is a bit hard to see but now I have this purple block that is 60 so I should probably change the text color to white so this is yeah so this allocation is 60 so now where can I put this block well the 60 you can't see doesn't fit in the 20 so I have to use the 100 so I put the 60 in the 100 and that also doesn't show up that well on the screen so that's great so after this I placed the purple one and now I have another allocation of 60 bytes well my memory allocator in total I have 60 bytes free I have a 40 and a 20 but they're not contiguous they're not beside each other so it doesn't fit anywhere and now I would just say hey I'm out of memory sorry you actually have 60 bytes free but I can't do anything for you so that's not great so let's see what happens when we do worst fit so worst fit should be worse but let's see so if we're doing worst fit we're going to do the same allocations so we have the 40 byte in green that we can actually see so worst fit I'm not going to put in the 60 because my biggest block is 100 so I'll put it in the 100 and front or back doesn't matter so I'd split that 100 block into 40 that I use and then 60 is free so now my memory allocator has two 60 byte regions left and you can probably see where this is going so my purple allocation is 60 where do I allocate this? in this case it doesn't matter because they're both size 60 so let's put it at the first one and now I have my final allocation of size 60 and now it fits directly in my remaining space so I can actually use all the memory so in this case worst fit actually worked out better for us but nothing's ever really guaranteed the thing about this is best fit and worst fit are both slow and no one actually uses them to implement memory allocators because you have to search through the entire free list which is going to be really, really slow so what best fit does is it kind of tends to leave very large holes or very small holes and the small holes are probably gonna be completely useless so they're gonna be like a lot of really, really small fragmentation throughout the memory and then if you simulate these things well if you do worst fit simulation actually says that worst fit is actually the worst in terms of storage utilization so it's actually a fairly fitting name while the thing you actually implement if you simulate it is if you use first fit which again doesn't have to search through the list it just looks for the first free block that can fit well it actually tends to leave average size holes which seem to work because you can actually reuse them for allocation so it doesn't actually have that much practical fragmentation and actually works quite well so any questions about today? Sweet, yeah like I said wrapping up on the course so I'll probably just end early and then we can ask lab six questions if we wanna go over that. All right so what we'll be doing tomorrow too well the kernel actually has to implement its own memory allocations because if you are the kernel guess what the standard C library doesn't exist so you will not be able to allocate memory using malloc because if you are the kernel or the first thing booting well guess what the C standard library doesn't exist and the kernels not going to use those types of memory allocators we looked at today those are like really really standard allocators we'll see what the kernel actually uses tomorrow and the concept are pretty much the same for user space allocation so the memory allocators the kernel uses you could actually use yourself but what we'll see tomorrow is that they're not quite as general and have different trade-offs but what we saw today there are static and dynamic allocations so static global variables you have to declare them up front dynamic two types stack and heap heap ones are through malloc stack are local variables and for general dynamic allocations like using malloc and free well fragmentation is a really big concern people implement their own memory allocators and really go out of their way to reduce that and it's something that once you get into like high performance programming really really have to care about that stuff well it's going to be something unfortunately you have to care about because that's a computer so dynamic allocation it will return you blocks of memory the fragmentation between the blocks is called external fragmentation fragmentation within a block that the memory allocator is using is called internal fragmentation and we saw three general allocations for different sized allocations so worst fit best fit which again have to search the entire list and then first fit which finds the first free block so just remember full for you we're all in this