 All right. Good morning, everyone. Thanks for showing up, braving the snowy weather at 9 a.m. There should actually be bonus marks for that, because it kind of sucks, but Anyway, so today we will be talking about more memory allocation. So this will not take the whole lecture I'll spend like the last half of it answering any lab 4 questions and going over stuff So we can get through this and then go over lab 4 stuff and Or play with the API's and make sure you actually understand what's going on with lab 4 and some of the errors that have been going on But anyways, let's take a brief detour to more memory allocation and what the kernel actually does in terms of allocation strategies So one of the allocators the kernel actually uses is something called a buddy allocator and it Kind of restricts the problem to try and minimize any external fragmentation so typically It just deals with allocation requests of size power of two so two four eight Whatever, you know 32 up to page maybe even higher than that so what it does is it restricts allocations to be exactly a power of two and lets you have a more efficient implementation and Basically, what it does is it will split a bigger block into two if The request is too small for that and then when you split a block into two well, then those two Smaller blocks are now buddies. So that's why it's called the buddy allocator So now if you free one of them, you know that it makes kind of merging things easier because If you get freed and then your buddy's free you can merge the nodes back together And then you might be able to recursively do that and create a much larger block So this is a whole idea behind it is it essentially restricts the problem makes some buddies so that we can do fast searching And also fast merging we can do them kind of we can do them logarithmically So the most basic implementation of a buddy allocator just uses multiple lists So you restrict the request to be some fixed sizes So like they're all going to be powers of two. So you pick a big end there That is your largest request size you will be able to make and then whenever a request comes in you just round it up to the next power of two if it doesn't fit and Then you just check if you have a free block there and if you don't you'd split blocks recursively to get that size so If we support allocations up to say a page then you would need n plus one free list So you need a free list for every power of two size and then when you get a request come in It will be to the power of something So you would search the free list until you find a big enough block So you would search the K list so if a request comes in that's like 32 bytes You would check to see if there's a 32 byte block free Well, if there's not then I check if there's a 64 byte block free And then if it there was I would just pick that and then split it into two 32 byte ones and then fulfill the request If I don't have a 60 or 32 byte I might check 64 and then 128 and you would just go up and up until you actually find a free block So as soon as you find a free block you recursively divide it until there's a correct size And this would create buddies all the way down and then you insert all the buddy blocks you create into the free list So you would have you would essentially be creating New free blocks of smaller sizes and then when you deallocate something Well, you know because you made them buddies that you can coalesce them back together if both the buddies are free And then you could recursively coalesce the blocks if needed just going up and then trying to create the biggest block you can So what's that kind of look like? Well, so this is so this say we have a buddy allocator and it's managing 256 bytes so that's all the memory. It's managing at the top So originally it would have been one block, but we used it and in this case It currently has two allocations one in the red block one in the blue block So anything with the block background just means it was split. It's not actually usable So there would be zero free blocks of size 256 because it got split into two and then there are two 128 blocks here that both got split into two so we can't use them And then there is one 64 byte block there all the way in at the left That was split into two 32 byte blocks So one of them is used for the red allocation and then we have one 32 byte block free in our 32 byte free list Then for the 64 byte, there's two allocate. There's two free blocks and Then there is one use block in blue So now following this algorithm how if someone requests 28 bytes, what should I do? Yeah Yeah, so if I have a 28 byte Request while buddy allocator has to be a power of two. So the next power of two is 32 so how many bytes am I wasting essentially? For and is that internal or external fragmentation? So that yeah, that'd be internal because the allocator itself is only dealing with the block size of power of two So it's within a block that it's dealing with that you have fragmentation so in This case if that 28 byte allocation comes off Well, I would first look for any 32 byte blocks and in this case I have free blocks in block and then split blocks in red and then allocated blocks in blue So 20 bit or byte request came in. Well, I would check my 32 byte blocks I have a free one. So I would just use that to satisfy the request So now I use that 32 byte one and now I just have two 64 byte ones free Any questions about that? Fairly good Okay, so we just put it in in this case. I colored it green. So now it's green so Yeah, so now what happens if we have another request come in of size 32 Yeah Yeah, so I would just pick one of the 64 byte blocks in this case I'd probably pick the first one Split it into two and then hey, I'm at the correct size now. I have two free blocks So I just go ahead and use one of them. So I split one of the 64 block ones into two Now I have a purple. I do the purple allocation. Now I have a 32 byte block free block Okay, so trick your question. So what happens if I free that blue 64 byte block now? Yeah Yeah, so I free the blue one and now as part of coalescing and try and make bigger blocks I would say hey my buddy is now free so I would merge them back together and then have a hundred and twenty eight byte block free and That's it. I can't merge them because the left side the other the buddy of the hundred and twenty eight byte block is currently used So I can't do anything with that so any questions about this pretty pretty good and actually a Fairly good algorithm to use for memory allocation. So it just restricts it by powers of two and it's a lot easier to implement So it's actually used it's fast and simple compared to kind of general dynamic memory allocation it avoids some but not all external fragmentation by keeping the free pages all contiguous But this advantage is it's always going to be some internal fragmentation as long as you don't use a power of two because you always just You only handle powers of two So there is going to be some External fragmentation if you want to think of it that way. So you could have the scenario where So say I have a bunch of 32 byte allocations come on and I split this 64 byte block and into two and then I Do all these allocations so I have four 32 byte Blocks allocated well if I come along and then free them say I free this one and Then later on I free the one next to it. That's not its buddy Well in this case you can't merge them back together because your buddy isn't free You can't even though there's like a contiguous 64 block bite there They're not buddies of each other. So you can't merge them back together if you did the implementation would be like insanely Insanely tricky to get right So the this you could think of as being some external fragmentation because you have a 64 byte block free Say if that was all your memory there, you'd have a 64 byte block free or not block You'd have 64 bytes free, but you wouldn't be able to do a 64 byte request Okay so the other one that's used is called a slab allocator and Basically all it does is take advantage of fixed size allocation So every allocation is the exact same size and we also know that if we have every allocation the same size There's going to be no external fragmentation because you're just essentially dealing with slots of the same size so if one's free and Another request comes in you can just refill it. They're all the same size. So it doesn't matter so and This you could have multiple instances of these Throughout your program. So like for instance the kernel Uses a lot of things called an I node that we'll see when we do with file systems and they're all a hundred and twenty eight bytes So because it allocates like millions of these things So every one of your files and directories is has one of these and we'll see that It just creates essentially a giant pool of I node of memory that it uses for I nodes and they're all the same size So it doesn't have to do that much bookkeeping so Every object has its own pool. There's no internal fragmentation. There's no external fragmentation. Everything is good. Everything is the same size So how you actually implement this is each allocation size just has a corresponding slab of slots and One slot holds one allocation. So if all your objects or structures are a hundred and twenty-eight bytes Each slot would be a hundred and twenty-eight bytes so What you can do is instead of a linked list keeping track of things that are free You can just use a bitmap so there'd be a mapping between a bit and a slot and you just have zero means It's free one means it's occupied. So if Yeah, so every single every single slot would just have a Bit associated with it. So bit zero would be slot zero bit one would be slot one bit 1,000 would be slot 1,000 and then it's really easy to see It's really easy to see if it's occupied or free and the Search for a free element. You just can iterate through it and then find if there's a zero there if there's a zero you can just Return the memory associated with the slot and then set that slot to one and then for Deallocations you can just clear the bit to zero and then you could just be lazy and not Set the memory to zero if you don't want because writing the memory is slow Yeah, so the question is how's a bitmap actually implemented. So just be like So everything's bite addressable. So you just have a bunch of fights and then you'd be able to figure out Which slot corresponds to what bit but you'd have to do some like bit arithmetic But when we get to file systems, this is exactly what they use they use bitmaps to keep track of things So we'll actually see how it's implemented. You will probably Yeah, that lab got cut so you won't actually do it But we might show some of it because it's actually fairly interesting So also in addition Your slab can be implemented on top of a buddy allocator So if I'm the kernel I could use a buddy allocator to do all the kind of more generic Request and then I could just say hey, I know I'm going to use a lot of One certain type of object. So I'll just request a big block of memory from the buddy allocator And then implement a slab on top of it and use that memory for a slab So that would look something like this so in this example The buddy allocator would give us memory for each of these slabs So they're big bigger blocks in this case say they're like 64 bytes or something like that and Then within each of those blocks given by the buddy allocator I could use one for a slab for objects of size a which in this case are small and I can fit about For them on a power a block. That's the power of two So I would have two slab a's for example in this case. Maybe the Blocks I'm using for slab B actually fit and then I don't have any Internal fragmentation with those ones while in my a's they don't quite fit So I might actually have some internal fragmentation because I'm using a buddy allocator on top of it So in this case too, you might be able to be a bit smarter So if I'm using a buddy allocator instead of having two slabs of Op or instead of having two slabs for objects of a maybe I just request a bigger one And then I could fit maybe nine instead of eight on it and use space a little more efficiently But that comes at some trade-off All right, any questions about those and otherwise we'll dive in the lab for stuff All right, cool So we saw that kernel restricts the problem for better memory allocation buddy allocator is one You'll see forever if you look actually in the kernel and how it implements memory allocations It's the buddy allocator and you some user space ones could use the same idea as well And then there's a slab allocator that takes advantage of six fixed-size objects to reduce fragmentation and that's something if you get into like super high performance If you want your code to be really really performant and you know You allocate objects all of the same size you could use that technique in your own program So you could just request a giant block of memory and essentially implement a slab on top of it Okay, so let's go to some examples and all right any questions about lab four Yeah, no that shouldn't be a problem. It's your code if your build directory if it doesn't clean your build directory It's like some weird permission issues on those machines Yeah Yeah, it should be fine So if you look at the code all it does is remove the build directory create a new one Do that C make command and then make all test itself So we'll do the same thing But for some reason for some random students that it doesn't delete the whole directory Like the testing directory is still there for some reason. I Don't know That's weird. All right Any other lab four stuff or weird stuff about lab four we've seen So there is So the last test there's extra data in the pipe means that the pipe so it's like The last test so the last test does this So if I type test and hit enter I Should only see test come out of it With a new line and if there's extra data in the pipe it means it read more than that So that's all that means So, yeah, if you have read write commands in it, you shouldn't You can also mess up file descriptors or have things Yeah, if you connect two processes together, you might have something weird Basically, you messed up your file descriptors or you wrote something to them Yeah, and one thing that's like kind of under specified is So in this case If you have some assert so say in this case, let's see my code So in some example, I just made I Create a pipe and then I fork so both processes have all the file descriptors open then in The child I just run cat and then in the parent. I would just kill it So if I just kill it, well, it's gonna exit But if I have an assert if exited then bad things are going to happen So if I run that Then the asserts obviously going to trip because I just killed it with a signal. So in one of the tests It's slightly under specified where if you do things properly one of your processes will get killed with a signal and You may not be handling correctly. So how that would happen. Let's see Yeah, so let's see Duke to So say I have this so cat Would write out to the pipe and then hope and then in this case The parents only reader the pipe. So nothing would happen. So if I just run it, right, I still kill it So if I do this nothing's gonna happen It's just gonna sit here and wait forever because I don't it writes things to the pipe and nothing's there to read it and Nothing will cause cat to finish. So it's connected to standard output if I did that That input is actually being read by cat. It writes it to the pipe and nothing reads that pipe So it's kind of useless so what you might see as an error if The parent goes ahead and closes fd's so the parent So the child here is writing to a pipe and then the parent just closes all the file descriptors and All the file descriptors in the child are close to all the extra ones. So if I So for bonus points, what will happen if I type in high here and then I hit enter type filled with descriptor error So The kernel is actually quite smart so What is going on here is There are two ways to exit process. There's if exited so clearly if exited isn't true so We could do something if exited is true But the other case How else could a process end? Well, it could end with a signal So we could check if it got killed by a signal. So f print f So if I do that Signal got me good So my child got killed by a signal Hmm. That's weird. So if I look at If I look at the weight documentation I can see well There's if signal returns true if that process was terminated by a signal So it was terminated by a signal and then it says oh, I can use W term SIG returns the number of the signal that the child process to terminate This macro should only be employed employed if if signal is true. Well, it's true So let's just print out what it is. Oh signal 13 got me So was it man signal? Wonder if it says Okay Okay, I'll just let you know signal 13 is a pipe signal so the pipe signal means that The kernel is smart. So there's in this case There's one process that can write to a pipe and then all the read ends are closed in both processes So the kernel is smart enough to know that Whatever it writes to the pipe is never going to be read by anything because nothing right now has a file descriptor open to The read end of the pipe. So the kernel is just gonna be smart and be like, yeah Well, nothing can actually read any data you write to this So I'm not gonna let you write data anymore and just kills the process Because Otherwise, why would you write data to it that nothing can possibly read? So this is like that under specified part that I posted on the discord Because one of your test cases is something like Something like was it you name cat true So because of this if everything is working correctly. Well, I have a pipe between Cat and true. So there's the right end of this pipe connected to the read end of this pipe And that should be this should be the only process that has a file descriptor open to the right end of the pipe and This process should be the only process that has a file descriptor open to the Sorry read and the pipe and this went to the right end of the pipe So because you launch all these and you have no idea which one Exits first. Well true doesn't read anything from standard in it doesn't care It just exits with zero and then This could exit before cat and it would close the read end of the pipe and get and then this cat would get a signal That would get that Signal pipe Signal if that makes sense Any other lab for questions? Wow. We have a lot of time or other fun stuff so Another fun question I saw was here. Let's get rid of was about the first argument like of This exec Vee so it might be weird. Anyone know why this thing's duplicated why there's two cats Yeah Yeah, so this is the program to run and then this is all the command line arguments that would be arg Vee and this is just a convention because Anyone want to tell me what happened if I did this so that so I got it does not exist here So it's gonna wait so if I do that doesn't get it do not exist It kind of work kind of works. It should just be doing cat stuff So it's all fine so it's just a convention so This is the name of the program to execute. So if I did ha ha here Okay, what's gonna happen now? Actually, I don't so if I do this what's gonna happen now, huh? It's all over. So if I do this ha ha doesn't actually exist So the first one is definitely the name of a program and the other one doesn't seem to matter so the other one only matters because If I write out main main would usually look like Main usually looks like that. So this arg Vee thing is set from exec Vee So if for some reason I read arg Vee zero, that's essentially what this argument is and No one essentially reads from it except which makes this kind of funny so I could do cat Cat by default is kind of what it should be. So let's make cat do something So usually the only thing that reads are zero is a help message So if I do that That's fine and run this. Well, you'd get the help message, right? That's all usage cat option file everything like that Well, if I go ahead and change cat to ha ha the convention is that's what you type to actually run the command So it should probably matte match stuff So if I do that and I do the help message instead of cat it says ha ha Just to print you up and it's only convention to give you better print messages because the expectation is I type ha ha And that's how I got ran this command So all of it says ha ha even at the top usage ha ha so Yeah, it's essentially and all it does is set this arg Vee thing. So arg Vee No one's probably ever looked at arg Vee zero ever in their program, but it's set and Usually it's just used and help messages by convention Any other lab for stuff or anything at all? Yeah Yeah, but malloc doesn't generally use it. So malloc uses like Depending on the implementation we probably use like the first fit or best fit thing like the really general one Yeah, the kernel would do that because if you're the kernel the only one allocating memory is yourself So you wouldn't do anything that silly So the curl developers are actually pretty good. So they wouldn't do one byte over the limit and make it waste a bunch of memory But yeah, so that's why they use it because it's restricted But if you're the kernel you're allocating your own memory. So you can be careful about it But there's a bunch of if you really care about performance There's a bunch of you can replace malloc with a bunch of different ones. So for example chrome I Believe they use something called J e malloc, which is a different malloc implementation with different trade-offs Mostly that it's faster, but might waste some more space that chrome likes wasting space anyways So they'll take the like performance bump Yeah, yeah, you could do that also Another option you have is you can there's a system call You can just get virtual memory from the kernel and just say hey give me some pages and then you can deal with them however you want in General there's only one thing that manages the heap and that should be the malloc Call and but if you want to request different memory from the kernel, you're free to Yeah So a question can you change the page size of the kernel and the answer to that is it depends on What architecture you have and what kernel you have? so That's actually a good question. So Let's go here. So Who remembers so when we had SV 39 That had a 39 bit virtual address. It kind of looked like this. There was nine bits For my L2 nine bits for my L1 nine bits For my L zero and then there is a 12 bit offset So if I'm following this typically your hardware will still have a set page Size that's still going to be four kilobytes But if you wanted to the next natural size to use If you wanted to do a bigger page size is well, I Would just get I would just get rid of a level of page table So I'll just get rid of this level and then increase my page size Which would increase my offset bits so instead of 12 now I have 12 plus 9 So 12 plus 9 would be 21 and then this would be an L 1 and then an L 0 So if this is how many bits I use at my offset, what page size is this? People are good at like powers of two To the power 10 is 1 kilobyte to the power of 20 is a megabyte Yeah, yeah, two megabytes so at least on in The Linux kernel you can use two megabyte pages any Recent is ish architecture will support that and some programs actually really why use two megabyte pages because Your TLB coverage gets better too because there's less things to translate So instead of translating 27 bits I'm translating 19 bits instead So and because my page sizes are two megabytes well if I do one translation it covers two megabytes instead of four kilobytes So it will be faster on most of them. There's like some kind of push now That's like hey, we have so much memory now things may as well be two megabyte pages but that's Still not super frequently used but some programs actually like this and you can there's like If you get really interested in the stuff, there's like blog posts about that You can go read blog posts of programs that like two megabyte pages, but then as You can see here. Well, I could be a little bit smarter and be like, yes screw this Multi-level page table if I remove another level, and I just have one level of page table How bigger my page is gonna be yeah, so there'd be one gigabyte pages So those are what they call them. So I think the two megabyte ones are called huge pages I don't know what the gigabyte ones are called But you can use the gigabyte pages, but if you want And generally all your TLB all your TLB entries will be hits because Yeah, because you have a lot of TLB slots and they each cover a gigabyte Yeah, the number of bits in the offset is However many bytes you can reference in the page So it's essentially to the power of that is the page size any other questions so far are we Lab four everyone's done lab four ish everyone's done done lab four Okay, well in that case I'll hang around if you have any lab four questions. I can Read something since we have extra time It's game to that point of the course where we're running out of content and I think we're slightly ahead of the other ones So just remember pulling for you. We're all in this together