 All right, oh cool, the camera's frozen. Let's fix it. All right, so we'll get a great point of view here. Okay, oh welcome. So we're gonna talk about more memory management. If we have extra time, we can go over lab four stuff if you want. So let's get into more memory management and what the kernel actually uses in terms of algorithms because it does not do the worst fit, first fit, or best fit. But it's good to know those things, but those are like the general problems. But hey, if you're the kernel, you know all of the sizes you're going to allocate since you're the kernel. You're managing your own memory. So you get to be a bit more clever. So in the kernel, and this applies to, you can use this for user programs as well if you want. They do something called a buddy allocator, which is kind of a nice name, and it kind of lends itself to how this algorithm works. So it kind of restricts the problem so it doesn't take general allocations of any size. It only takes allocations of powers of two. So it only takes requests of powers of two. So like two, four, eight, 16, 32, you know maybe the size of a page, maybe more, maybe less. That's the general idea. So it restricts allocations to a power of two. And we'll see that that actually allows for a much more efficient implementation and a lot easier to kind of doing that merge step we were talking about before that we kind of hand-waved about. So basically how it works is it's kind of recursive. It splits blocks into two until you can handle the request. And because it's powers of two, if we split it in half, well, it creates two blocks of the same size and then we can keep recursively doing that. And then since everything's the power of two, it's really easy to merge stuff because you would merge essentially your buddy. So you got split off. So there's two of you. So now you have a buddy. So if both the buddies are free, you can merge back into one. And what we also wanna do too is enable really fast searching and merging. So we also don't wanna search, do linear searches at every single element to figure out where we actually place an allocation. So to implement a buddy allocator, one thing you could do is use multiple lists. So you restrict the request to be like two to some power of k where k could be anywhere from zero, which would just be one byte to some maximum number that you round up. So if you do that, your implementation would use n plus one free list, so one for every single power of two of each size. So you'd have a separate link list for each size. So you'd have a free list for everything of size four, everything of size eight, everything of 16, and you know they're all the same size. So you don't have to bother going through all the list of elements that are the same and you have like a logarithmic number of lists. So for some requests that comes in, that's two to the power of k. So if it's a smaller, if it doesn't quite fit in, you just round up to the next power of two that it would fit. And then you search the free lists until you find a block big enough. If, for example, if there's a request of like 32 bytes that comes in and we don't have any 32 byte free lists, you would look one level up until you find one. So maybe there's a 64 byte one free. Maybe there's a 128 byte one free. So as soon as you find the first free one, you would recursively divide it up until you get your required size. And then you would insert all the buddy blocks into the free list for whatever size you just split. And then for deallocations, well, every time you deallocate something, you will probably, you'll have a buddy either before or after you. So if your buddy is free, well then you can merge things back together. And then if you do a merge, you might kind of, you might do a recursive merge. So whatever sub-buddy you were before, you might have to merge that and then you just go up and up and up. But you'd only do it for one of every element on the list for deallocations, which makes your life a lot simpler and it's a lot faster. Okay, so here's our scenario. So generally the buddy allocator will have like a maximum size that it deals with. So in this case, let's just say our allocator is just dealing with 256 bytes of memory, mostly because I don't wanna write out like 20 levels or something like that. So this is what it would look like. So anything, so anything in the black background means that it is actually used. It's not part of a free list. It's not used memory or anything. It's just kind of an illustration to show you what the original block was and then what all of our splits are. So we have a 256 byte big block of memory to do somewhere in our allocations. We had to split that up into 228 byte blocks. And then we split each 128 byte block into 64 byte blocks. And one of our allocations in the blue here filled up one of those blocks of 64 size. And then in our other 64 byte block we split off into 232s. And the red allocation that used 32 bytes currently is used. So it's already allocated. And then it has a free 32 byte block there. So in this, so we'd have in our implementation there'd be a free list for every single level here. So there would be no free blocks of size 256, nothing free of 128. And for 64 byte blocks there's two elements in the free list. And for the 32 byte list there's one element in the free list. So if we get an allocation of size 28 where should it go? What should we do? Yeah, so we'd have to round 28 up to the next power of two because we were only dealing with powers of two. So if we try and give it 16 bytes that's not enough. So we have to round up. So we have to give it 32 bytes round up. And in this scenario, hey, there's already a 32 byte block that is free. So here same illustration except in red just means it was split before. Anything in black text means it was free. And anything in blue means it's currently occupied. So in this, our 28 byte allocation we would have to round up. And then there is something in our 32 byte free list. So we would do that allocation. So with that allocation of 28 bytes, how many bytes were essentially wasted? Four, was that, what? Four and a half. So there are four bytes wasted was that internal or external fragmentation? Internal because the memory allocator only deals with blocks of powers of two. So there's no fragmentation between the blocks because they're all powers of two. It's kind of nice. So there's no external fragmentation but there's gonna be internal fragmentation and that's our trade off. So that was our request. So four bytes, internal fragmentation. Yep, that's just something you deal with. Yeah, the question is what if you give a weird allocation of size 15 then you have to make a 16 block. Well, then there'd be a 16 block buddy along with it. And eventually if you free that one then you'd be able to coalesce the blocks and everything would be all good and you get that memory back. But if you don't do it, yeah, I guess you could call that external fragmentation if you would never ever use a buddy again. So here's what happens. So now what would happen? So we did our 28 byte allocation. We had four bytes of internal fragmentation. So now we have a request coming in that is 32 bytes and what should we do here? So is there any free 32 byte blocks? No, so what should I do? Yeah, I should look up, right? So I look up, I look if there's any free 64 byte blocks. There is. So what should I do? Yeah, I split it into two 32 byte ones. So I create a buddy. So I would just pick this first 64 byte one. I would split it into two. So now I create two 32 byte blocks out of it and I would use the first one or the second one doesn't really matter to actually do the allocation. So I split it all, I split it up again. So now in our free list, there is one 32 byte block for the 32 byte block free list and then free list for 64 byte blocks is one element because there's one free. And as a Sandy check, everything should equal 128 all the free blocks and all the used blocks. So what's gonna happen now if we free the size 64 byte block? Yeah, we're freeing it. So we're free, we're gonna free the blue one. Yeah, so I'd free the blue one and now it's but, it would check it's buddy, so it's buddies now free so they can merge back together. So I free this 64 byte one. So now there's two 64 byte ones free that are together because they were buddies and because they were buddies, well, I can just merge them back together and now I can have a free 128 byte block. Now, I can't have my free 256 byte block because I'm done at that point. Yep, yeah, so you can only merge your buddy. Yeah, so the question is, so I assume, we'll go for in this situation where we have, you know, two 64 byte blocks next to each other, they split into two 32s. So say we're using all 32s and then eventually, you know, we get somewhat unlucky. We free this one, so we can't merge them back together and then we free this one and we can't merge it back together. So in this case, there's a contiguous 64 bytes all in a row that you might wanna be able to merge but because we're using the buddy algorithm just to make things simple in terms of merging and not handling insanely complicated cases, you wouldn't merge those together because they're not buddies. So you'd only merge your buddy back together, which is why it's called the buddy algorithm. Yeah, yeah, so if you implement this, you might wanna do some heuristic where you fill up buddies before or something like that. But in general, you can make heuristics but they might not work. So just based off who uses it and their memory allocations, you might not be able to predict anything but if you're the kernel, you can do some pretty good predictions because you kind of know how your memory works. Okay, I was just checking that it wasn't me. Okay, so if we free this blue 64 byte block, then we'd just go ahead and merge them back together. Now we'd have a free 128 byte block. Okay, any questions about the buddy algorithm? Pretty cool. Yep. Yeah, so the question is, oh well, would you still call that kind of external fragmentation because you have enough memory to satisfy the request but they're not buddies so it's not a big block and yeah, you would call that external fragmentation. So that's kind of wasted space between them. So you don't get rid of all the external fragmentation but you get rid of some of it and you get more, you get internal fragmentation for your troubles. So this can be implemented in either because it's just a general memory allocation but in general, the kernel uses this so if you go read kernel code, they use this. So malloc generally doesn't malloc generally is more, I mean, there's lots of implementations but generally it just has a free list and it lets you, it like does allocation that actually matches what you request because this is only powers of two. So yeah, it depends. So malloc sometimes is slower so there's also lots of implementations of malloc. So if you like get into super critical performance once or I think the other systems programming course here, they make you implement a better malloc that's more performant. So like if you look at Chrome too, so Chrome doesn't use the standard malloc you guys use. Chrome uses something called J-E malloc at least last time I checked, which is much faster but you can swap in, do malloc, whatever you want. So that might be a fun lab in the future but not this quarter or quarter semester. I'm not in California anymore, thank God. Yeah, so yeah, there's always gonna be trade-offs so how fast it is versus how much fragmentation there is is generally the big trade-off. So some of them you might not care about fragmentation you want, I do lots of really fast allocations so for like slow ones that don't last that long so maybe I want a different malloc implementation. So generally they're tailored if you like get into super high performance code maybe you just request a big block of memory from the operating system and you say screw malloc I'm gonna manage my own memory so that's also one thing you can do but that's kind of like the ugly stuff you find when you actually start programming stuff that you actually care about the performance of. So we'll see one of the things but first buddy allocators are definitely used in Linux they're fast and simple compared to the general case where you have to fill allocations of any size. It avoids some but not all external fragmentation by keeping the kind of free physical pages contiguous and there's always gonna be some internal fragmentation if your requests aren't powers of two because you always round up to the next power of two so it's gonna be some internal fragmentation. Okay the next one is slab allocators that take advantage of knowing that you allocate a lot of things that are all the same size. So if everything's the same size well you can just have one big pool where everything's the same size and then you don't and in that case you definitely don't have any external fragmentation because you just keep track of the number of slots you have and what slots are free and if you free something you can just refill that slot. So all the things are kind of the same size. So for this implementation you don't even need a link list you could just have a one bit for every single slot and then that bit could be zero if it's empty one if it's used and then really easy to see what's used and what's free. So this would completely eliminate internal fragmentation because everything's the same size it would fit all in a slot. So when we get to the file system stuff so I don't think you don't have a lab on it because I got eliminated but in file systems there's this one structure that's used a billion times called an inode so you'll learn about an inode but the kernel does some special optimizations with the inode because it allocates millions of these things so every one of your files every your pipes and everything well every file and directory is represented by an inode so since the kernel creates like millions of these things potentially and they're all 128 bytes it just has a pool that's all that size so it just grabs them as it needs them and makes memory allocation a lot easier with that so slab allocators are generally much much better but you have to know everything's the same size. So it's just gonna be a cache of slots so each allocation just corresponds to a slab slot one slot holds one allocation it's yeah one slot holds one allocation so instead of a link list just use a bitmap so bit zero corresponds to slot one bit one corresponds sorry bit zero corresponds to slot zero bit one corresponds to slot one and then bit I don't know a thousand corresponds to slot 1000 so it's just a one-to-one relationship and it's quite space efficient because it's just a bit for every slot so for allocations you just sit go ahead search through that because it's a bitmap all close in memory searching every element is gonna be really really fast you can just check if there is any zeros in that byte or whatever to know that there is potentially a free slot there and then you can figure out what bit it is and just return that and then you just return the address to that slot because you know they're all the same size and then for the allocations you don't even have to delete the information or set it to zero if you don't want to just set the bit as zero and say that it is now free so also what you could do is you can implement a slab on top of a buddy allocator so for instance if you still want a general algorithm so you could have a buddy allocator and just say hey give me a giant block of memory and I will essentially implement a slab using that giant block of memory so this is kind of what it would look like so consider we have two object sizes we have A's and B's so you might have you know your buddy allocator you might just get a fairly big block of memory so you'd have a slab one A for all objects of size A and then say because it's a power of two you can only fit four in there because they're kind of a weird size so that block box would be internal fragmentation and you wouldn't yeah so it'd be some internal fragmentation and then you could have another slab right beside it that would be A2 that has the same issue holds four A's and has some yeah holds four A's and has some internal fragmentation and then maybe you have slabs for B's that actually nicely divide up the block so you have two on this block then you have two on two B so what you could also do is in this case you could reduce internal fragmentation if you didn't use different blocks for different slabs so if you just had one big slab for A's well maybe I could fit a ninth A there instead of only having eight and having some internal fragmentation okay so any questions about those types of allocators? yep yeah so in this case slab A1 there's two slabs for objects A one that lives on this big block given by some buddy allocator and then one that lives on this block so we have two separate slabs here for A's so you could make this better if we just got a bigger block of memory from the buddy allocator and just had a single slab for A's maybe you can fit in a ninth one oh yes yes yeah yeah yeah so in this picture each of these little slabs that block of memory would be given to you by some buddy allocator and then you say hey I'm going to use this as a slab yourself so you put it on top of it so so yeah so you just have a buddy allocator that does all the allocations and then you would just request a big block of memory and say I know I'm going to use a lot of object A's so I'll just use it as a slab so you might think hey I only need four and then you're like oh well I ran out of I used five so I need some more memory and then I create another slab or something like that yeah yeah man in this case slab B actually fits the blocks alright any questions about that because that's pretty much memory allocations yep yeah so slabs are going to be used if you know you're going to have a lot of the same sized objects so yeah so in the kernel there's a structure for you I don't know what labs you had before maybe you have a link list that has a bunch of nodes and you know the size of a node so maybe use a slab to just allocate nodes instead of using malloc or something like that so you make it a lot faster because you know you're going to have like thousands or billions of nodes yeah so you would have to do this explicitly in your code this is part of the memory management stuff you'd have to implement it yourself yeah so you'd have to implement it yourself or grab some library so like this is also a very common implementation thing if you want to speed up your code so like the Java implementation does stuff like this because object headers are all the same size and you know it's Java so there's like thousands of objects even if you just print hello world so it will use essentially a slab for any object allocations and things like that yeah every slab would have a bitmap which is just an array so we'll see how to use a bitmap when we get to file systems so stored just somewhere yeah it could be somewhere different, could be on the slab or before generally with most of the memory allocators in user space they just like add some accounting information to the beginning yeah so all that accounting information that's how why malloc you don't when you free you don't need to say the size of the thing you're freeing because malloc knows the size because it's keeping track of it okay cool so we saw even more memory allocations they'll take the rest of the class I guess to answer lab four questions if we have any so in the kernel it restricted the problem for better memory management so buddy allocator real life example of that we just restricted it to powers of two and then we saw a slab allocator that takes advantage of fixed size objects to reduce fragmentation so let us switch back to this so any lab four questions we wanna go over so sorry how to get the exit code from the child okay question when the child exits how are we supposed to get the exit code as the parent alright anyone wait wait wait wait in what in the w status so we went over it in the code examples and also I saw people if so as part of that way exit exit staff thing you have to make sure that that process if exited if it doesn't exit and that's not true if you have an assert there or something which asserts are good and that gets triggered you screwed up your file descriptors I guarantee you okay that's a good question so the question is why do I have to create all the child yeah why do I have to create all the children and then wait as opposed to just creating a child waiting creating child waiting creating child waiting answer was because they kind of need to all be running in parallel because they might be passing information back and forth between each other I assume that's what you were going to say too yeah yeah so this is like the most common thing that I've seen that you know I run cat cat or even the actual test was cat cat cat what the STD in thing is testing is if you type hey one okay it should be repeated back to you immediately everything's still running at this point right I'm running this so what would happen is I type one in standard input which is this cat so this and when I hit enter it essentially flushes it so this cat would read one and then right out one and then this cat is just sitting here doing read so eventually the curls going to schedule that at some point and it's going to call read and then it's going to read one because the other one just wrote to it and then it would write out one and then at some point the curls go schedule this cat and it's reading from zero then it would read that one and then output it to the terminal yeah okay yeah so so the question is if I do this instead if I just had fork wait fork wait fork wait what would happen if I had that and I typed in one and I hit enter but if I'm here and I press one is the first cat finished yeah so if the first cat's finished how could I do that yeah so in this case the first cat isn't finished so the first cat just read a one and then outputted a one and then later it's not done so I typed in a two it read a two so it's not it's essentially what's that stupid meme you're not this isn't done till I'm say it's done yeah so if if you do alright so if I do one two three here and I do fork wait fork wait fork wait and I type one enter two enter three enter anyone want to tell me what I'm gonna see but if I do fork wait fork wait fork wait what if I do the same input here I type one enter two enter three enter and if your implementation is fork wait what's gonna happen yeah so essentially if I type one two three one two three and hit enter well it's gonna stay there and just not do anything if I finally hit control D well then my first cat will finish and then it would have read one two and three and written it out but the next process didn't exist yet so it just kind of goes fills up a buffer and then whatever that cat's finally done then it would read one two three in a row then you'd wait for it to be done so it would pass it to the next one the next one read one two three and then output one two three all in a row but I don't want that because I want them all to be alive at the same time because that that's how it behaves right they're all alive they're all still have output and I want it to just flow like normal flow like normal cat yeah so so what's the question we have okay so so we have two processes P1 P2 and they're connected by a pipe oh that's unfortunate that's called the same thing let's just call the pipe B fun yeah so yeah so in this case so I wrote the pipe as being essentially a B because it's essentially like a buffer so what's gonna happen is well this is actually connected to our normal standard in so in this case if I wait I type a one and then hit enter and then right this is cat so as soon as you hit enter your terminal goes ahead and flushes it so it would read one then a new line and then it would write it out because it's cat that's what it does and it would fill up essentially this little you can think of it as just a pipe as being a buffer there's a file descriptor to read from the buffer file descriptor to write to the buffer so the first cat would just write to the buffer and if you did the waiting thing this process does not exist yet so this process doesn't exist there's nothing to call read on it so the buffer just that one just stays in the buffer and then if I hit two I do a new line then it just goes straight to the buffer three new line straight to the buffer and then eventually I'm going to close the file descriptor by hitting control D so that would essentially kill this process because now it's done and the new cat would just read whenever it does a read from that pipe it would read it actually might read them all in one big like you do a read it does a read of like a page size so would probably read all this in one read call instead of like three so it would read everything pipe disappears when it's empty there's nothing that could write to it and it's empty so the curl would delete it so the kernel only sends like the kind of end of file character if all the right file descriptors referring to that are closed so no more data can come in and then if you read all the data then it's done it can delete it and then if you call read on it again it would return zero bytes read meaning it's closed so if you delete your process wait whatever you exit a process all the file descriptors with that process get closed so you can't do that yeah first one that fails the first non first non zero so if it's like so the example is if you have okay so if you have like pipe uh... in valid and false again if someone's hard-coded these please don't if you do i might just do like in test cases so don't do that don't force me to do that um... so if you do this the first the first non zero one in order in the command line so invalid comes first and then false so if this one returns two and one i should return two in this case because it was first you have no idea what's going to run first right so you've no idea what's going to run first you could wait on them in order if you want that might be a smart thing to do one might finish before the other doesn't matter you could wait on them in the reverse order as long as you return the first one that dies that's fine but you can wait on them in whatever order you feel like i guess yeah yeah so you should wait on all of them but the yeah because you we want to be good parents right but so wait on all of them but the exit status of pipe is the first failing one yeah yes first in the list of arguments not in terms of time because unless you well you would know the time kind of if you call it just plain old weight yeah it's not consistent because it's whatever one finishes first but that's why i said in order of that not finishing time yep sorry oh crap uh sorry i didn't open it where are the questions on that oh sweet there's a bot uh i screwed up the chat what's the question in the chat sorry i screwed it up all right so can i explain why cat cat cat is always expecting input when the pipe is closed uh like this or wait like this so the pipe's not closed why would the pipe be closed you are yeah so what pipe is closed here there's no closed pipe so there's two pipes one this pipe it's right end is this cat it's read end oh god i can hear myself it's read right or read end is from this cat and then there's another pipe this pipe whose right end is from this cat and read end is going to this cat so those should be the only ones with pipe file descriptors and right now they're all open because whenever i type something it goes through the pipe so they're all open right now and then if i hit ctrl d then it causes a cascade of closing so if i hit ctrl d well then cat is going to get and the first cat's going to get into file and then because that reaches into file that process terminates and then when that process terminates right or sorry it closes all the file descriptors so now the right end of the pipe is closed so then now the kernel knows as long as you manage your file descriptors if that was the only right end of the pipe that was open then the read end of the pipe is going to show as closed so then the next cat can actually exit and then the same thing's going to happen for the next pipe yeah so you can actually you can actually create a process that will you can create a cat that is not even a zombie it's just like a waste so what you can do well i wonder if we could program it but so here so say i have a cat process and i created some type of pipe so say this is my pipe maybe i should name it something different here i'll just call it a buffer and say i have fd's one and fd's zero so if i do a system call on pipe then that would be my two file descriptors returned so this is the right end of the pipe and this is the read end of the pipe so for cat it'd be one of those is connected to it hopefully right but i could do it where in my cat process if i really screw it up say zero is just my terminal so whatever and then one actually no let's do the other way so let's say file descriptor zero is the read end of the pipe file descriptor one could be the terminal file descriptor two that's standard error so that could be the terminal two it doesn't really matter so ideally in this case if this is your cat process and then you close the right end of the pipe then everything's good the read end will appear close after you finish reading all the data but if you have screwed up when you do this and you haven't kept track of your file descriptors like i've been harping about well what about if you don't keep track of your file descriptors and this one has say you forgot to close i don't know some file descriptor six because some of you have some interesting code so say your file descriptor six open and it's actually the right end of the pipe well there's always going to be the at least one file descriptor open to the right end of the pipe so the read end is never going to look closed because that same process has the right end so this will never close and never exit and it won't be a zombie because it won't exit it'll just sit there forever and it can't do anything yeah standard sorry standard out to the right yeah okay yep no so when you before you exec clp the only file descriptors should be open are zero one and two in the child process and if they're connected to a pipe hopefully they're connected right because if they're connected right then eventually that read end is going to close if it's cat and then cause that process to exit and then if that process exits as soon as it exits it closes all the other file descriptors yeah yeah so that's why this works right it's a giant oops it's a giant cascade so as soon as i hit end of file i from my terminal there so if i do this and i hit control d then that starts a big cascade so the first process cat would get zero from reads so it's say it's closed so it would exit then as part of exiting that process it would close all of its file descriptors in this case one of the files its standard output is the right end of the pipe so it would close the right end of the pipe and because as long as you were careful that is the only file descriptor that points to the right end of the pipe so if that process is gone the right end of the pipe is gone and then nothing else could write to it because it's the only one left so then this next cat process would get whenever it tries to read from the read end it would say hey it's closed and then it would go ahead exit and then when it exits it would close the right end of the next pipe and then the next cat would go ahead read if there's any more data there and then see it get closed and then it would exit outputting whatever it read and then that process would be done and then all your processes have finished and they all finished nicely yeah one of the most annoying errors is if you really screwed up all your file descriptors some processes are going to get killed by a signal which is really really bad because none of the programs should get killed by a signal so the only way you're the test will fail with the signal is if you essentially gave it an invalid file descriptor so like you closed everything and like you closed file descriptor zero one and two so then as soon as it's tried to write from one or zero it doesn't represent anything so it dies right any other quick questions lab four yeah the the the biggest block you have pages or maybe you have multiple pages in a row all right well hopefully that answers lab four questions so just remember