 Okay, robot check, sounds terrible. All right, robot check, robot check. Good, ish, okay, good enough. All right, so let's get onto our second review and things that will hopefully help. So lots of questions, if we can answer them, then we're pretty good for the final cause we've pretty much done all the hard content in the course, threads, concurrency, nothing gets harder than that, so we're all good. So hopefully it's easy sailing from here. So if you can answer these, you can probably take a breath, hopefully. So back from what we covered from the first review to this one, we learned about page tables, going to translate virtual to physical addresses. MMU is a hardware that actually does that. And we saw different ways to do this, a single big large page table, which is gonna be really wasteful because typically no program uses all the bits of memory and we don't want our page tables to be like a gigabyte large per process because that's terrible. So we saw basically how the kernel allocates pages, everything's the same size, so it's really, really easy. It does grab some pages from a link list. We all know a link list now. And if you want to save some space, you use a multi-level page table. And then if you have this, translating addresses can be slow because we have to have a memory access for each level. So we have a TLB or translation look aside buffer, which is basically just a cache for the MMU in order to speed up memory accesses. Then we saw threads. We all love threads, don't we? So threads, they enabled concurrency within a process. You saw even with your lab where you implemented user threads. Well, that was fun, probably a bit confusing. And it's probably the hardest thing you'll ever have to do, so don't worry about it. So we compared them to what we already know processes. Processes are nice because everything is independent. They have their own virtual memory, so they have their own address space. But for threads, everything shares the same address space. So if you have multiple threads accessing the same bit of memory, well, then you have a thing called data races. But yeah, we saw that there's different types of threads you can have. So if you have processes that enables parallelization because the kernel can go ahead and execute everything on all your cores, and same if you have kernel threads, it can go ahead and schedule them on all the cores. And each process can have multiple kernel threads that we know of if you actually use something like pthreads or they can just have one kernel thread, which is like the default, and then you can implement user threads on top of them. So most implementations will either be one to one, which is pthreads, and that or just one to many, which is one kernel thread to a bunch of user threads, which is what you did in Lab 3. So we didn't get too much practice with it, but we'll get a little bit more practice maybe as it goes on. So because we have multiple threads within a process and we learned how to clone processes and do things to processes and send signals to them, well, now the operating system has to do something if you have multiple threads and you do one of those system calls. And we also had the synchronization issues, stemming from all those fun data races. So we wanted critical sections to protect against data races. That was our like first thing we reached for. So we figured out data races, multiple threads accessing the same memory location, and at least one of the operations is the right. And it can happen concurrently, even with a single thread, you can have these concurrency issues. So typically, if you want to eliminate a data race, you make sure only one thread can execute that bit of code at a time. And that's what mutexes and spin locks did. Essentially, people just called them locks as a shorthand. Yep. Yeah. Yeah, spin locks are one that doesn't go to sleep. So it's only meant for like really, really small sections. They don't suck, they're great. So what about if the thing you're locking is really, really fast and in the time you did a system call, you could do it like a million times, then spin locks are pretty good. If you just try and try, you get it instantly. So you get like, it's that whole trade-off with scheduling, like response time versus waiting time and all that stuff. So spin locks, really, really good response time, but they're gonna waste some time. So if you use them in the wrong spot, they'll suck, and they'll really suck, but if you use them in the right place, they'll be really good. So it's like everything. This is computer engineering, not computer science. So everything has its own place. So you just have to figure out where it goes. Yeah, so that was spin locks for either one of those. So mutexes are basically just, instead of spinning, we have a queue. So if we don't think we can get the lock, we just go to sleep. But in either case, we needed hardware support to implement locks. You could do it without it, but the easiest thing to do and all hardware has that special compare and swap function, which is usually an actual instruction on your CPU and everyone just uses that. And we saw how you can implement a lock with that. So if you have more complex locks or centerfors or something like that, you need some kernel support for waking up. So putting a thread to sleep and then waking up a specific thread. So we didn't actually do that in lab three, but you could imagine adding to lab three the ability to put a thread in a block state and then specifically wake up a thread to continue executing. So we didn't do that, but that's something else an operating system would do. And then we saw it like also with having the right tool for the job, there's a read write lock. So we don't have data races if we have a bunch of readers. The only issue is whenever we have a writer. So if you typically have a lot of readers and only rare writes, you can do a read write lock which allows multiple readers all at once, all in parallel, everything is good. And then that's basically for protecting against data races and in general, whatever you use a center for, it's whenever you need to ensure a proper order between things like this has to happen before that or I need to wait for you to finish these things before I go. So centerfors at their most basic are just an integer and the initial value is whatever you choose. And it just has two simple operations. So post increments it, always increments it, does it atomically. So there's no data races involving that int. And then there's a decrement operator and typically it's called wait. So it will wait until it can actually decrement that number without it going negative. So if it is zero, it will sit wait for that number to get incremented to one or higher by something else. And of course we've been talking about data races so far and all that fun stuff. So we did that example yesterday, which was nice and fun. Then we saw some more advanced locking that typically you don't have to use, but if you ever need to use it, it is very, very useful. And we saw condition variables which were basically a way to manage your own queue. So the easiest way to think of a condition variable is this, they represent a queue and then you can atomically put yourself to sleep and then another thread can go ahead and wake you up. So we went into more details about locking, especially with that example where we were running a bank and showing our locking granularity matter. So sometimes when you try and speed something out by making it run in parallel, well you can actually slow it down if you lock everything. So essentially if you turn the whole program into a serial program, it will actually be slower than if you just had one CPU running on it because there's overheads with actually doing all this synchronization and all that fun stuff. So whenever you do this, you'll probably introduce multiple locks and then you have to worry about deadlocks. And we saw two main ways to get rid of them, either always acquire your locks in a specific order or if you hold the lock and you can't get another one, well you should let go of the first one and then yield or do something like that. All right, so now we can go into the review questions. Yep, so when I say serial running being faster, that's like no locks, no anything, just your normal program that doesn't try and use threads or anything. Yeah, yeah. Okay, so let's look at this question. Do you want me to do this question or does everyone probably get this question? It's just a bunch of forks over and over and over again. So I'll give you a bit to look at and see if we wanna do that or if we wanna move it to the end and do something probably more harder. More harder, good word. Okay, first question. How many processes are there in total? Yeah, eight? Everyone agree with eight? Thumbs up. All right, eight. What likely will they print out? Yeah, so are the relationships between the processes going change or just the numbers? So what do you mean by saying if it's child-centric or parent-centric? You think the child's just gonna print zeros? That, okay. Ish, yeah? Yeah, yeah. First would be equal to three, right? Fork returns the process of the new one, so yeah. Yeah, so his argument is we'll probably have something like, so we have this. So after our first fork call, well, one parent is going to have PID equal to the newly created one, so here I'll draw them separate. So here's our first process ID two. That's just chilling, running main, and it calls fork, so it creates a new process. It would be a child and it would get process ID three or something like that, right? So if it gets process ID three, well, in the parent, so in two, first, actually I'll draw them here, so wow, I can spell. So in process two, first would equal to three and then in process three, first equals zero. So everyone good with that? Everyone in agreement? So he's basically saying that, hey, whatever it finally gets down to the if statements, half of them are gonna start with first equals three and the other half are gonna start with first equals zero. So all in agreement with that? Okay, so at this point, we don't know what's gonna go next, but all the relationships are gonna be the same, so it could be like the child centric where I start executing three next and then three calls fork and da, da, da, da, da. But it doesn't really matter, the only difference if you go child centric or parent centric is just gonna be the process IDs, the relationships always gonna be the same though, right? Yeah, okay, so let's just say three, well, if it starts executing, it would execute this line, so it would fork again and three would probably make a process called four and in process three, or sorry, in process four, well, at the time of the fork, process three that made it had first and it was set to zero, so it's going to get a copy of that. So first equals zero just because it is the child of process three and then they would differ in whatever they get returned to by that fork. So in process three, second would be equal to four and in process four, second is equal to zero. Yep, good. All right, so if we go back here, so then four is going to execute. So let's say four executes, so then it would create process five and we're getting a bit tight there and then in process five, well, it would be a copy, so it would have first equal zero, second equals zero and then the only difference between four and five is going to be the return value of fork, which is third, so third is going to be five and then the other one, third is going to be zero, right? So then these two would do their prints, so they are done and done, so they would just print that essentially. So we can go back up to process three, which is just done the second, so it would fork again. So three would create probably process six and process six would be a copy of it at that time. So first would be equal to zero, second equals four and now the only difference is third. So third in this would be six and then third here would be zero and then they are now done, right? So people are falling asleep, does anyone need me to go over this more? No, okay, so is there any specific order that these processes will print in? So will whoever made is process five always going to print before six in this case? No, right, any process could print in any order, it's just how lucky you get. Nothing's waiting on another process, so you're just at the mercy of the scheduler. So you only know that you have to be created before another process that creates you. So like four can't exist unless three exists, but there's no weights, there's no nothing to actually stop and wait for a process to finish or do anything. As soon as it gets created, it can just execute to the end, depends on the scheduler. Okay, now for the super fun one. So I will leave you with this for a bit, this will probably give you nightmares of lab three and I guess that's its intent, it was like used on a final in like 2016 or something like that. So let's give that a read for a bit and see what this does. So the question is, so we have two thread functions here, thread A and thread B and then two contexts that are set up to start executing that. So U A wants to start executing thread A, U B wants to start executing thread B, we have a global variable called I, which is set to zero. And then the main kernel thread, whatever just calls set context U A and the question is what gets printed out or if there is a specific order of things that get printed out. Yeah. Okay, here I'll bring that up here. So what we got? We got A. Oh, sorry, A one, yeah, uh-huh. So what, so we'll get A three, so A three dead. I see some very concerned faces. Yeah, okay. All right, well, let's try it. Oh, yeah, yeah, I guess. So we're back to the infant loop case. So A two, okay, well, let's all think out loud together. So when we start executing, where is our U A context want to start executing? So U A probably wants to be here, right? U A and then U B is here. So, whoops, B. So if we ever set context to them, that's where we start executing. And these are threads, they're context, so you imagine they have their own stack. So A has its own stack, B has its own stack, they don't touch each other. So U A is going to execute first, it wants to execute thread A. So it would come here, create a int called B or D, sorry, and where is D located? Yeah, so one thing is the heap. So the heap, in the first course, they generally, they probably just tell you the heap comes from malloc. So did we malloc that? Is that a global? So it's a local variable and local variables are just allocated on the stack. They're like independent, so the threads local variables aren't ever touched by another one. If you want to think of it that way, or if you want to think about it, any local variables will be on the stack, so no one can monkey with it. Whatever you want to think of it. If they haven't told you what a stack is, which they should be arrested for, then think of it as local variable, private to a thread. So this local variable, that's A's D. So whatever it's in thread, or sorry, this thread A function, it's its local variable, so it won't get monkeyed around with. So create, allocate some space on its stack or its local storage, if you want to think of it that way. For a D, it sets it to zero. So then it goes into this while loop. Reads I, which is the global variable. What's the value of I right now? Zero, so is zero greater than three? No, because the alligator wants to eat the three, right? Anyone learn the alligator stuff? Yeah, I apparently have a PhD, I still remember the alligator, so yeah, don't worry about. So then it increments I. So it would change the global variable because it's just incrementing I, right? So I is now not zero, it is now one. Everyone okay with that? Global, so it is available to all threads. So we change it and then we print it off. So we can agree A, it would print off A one. So we're good so far. Now it sets D to zero. It was already zero, so nothing changes, it's still zero. And then it does a get context, which is just going to move this UA, whoa, that is not good. Can I not move that? Oh no, I just have to erase it I guess. So it will just, oh no, okay good enough. So it moves the UA to here. So if we ever set context to UA, it will start executing as if it just returned from that get context. So now it's going to check if D is equal to zero, is D equal to zero? Yes, and then it will change D equals to one. So if you want to write it out, that's easier to think about. Well, this D is now not a zero, it's a one. And we're never going to leave this function whenever we're executing A, so that's going to stay the same. So we set D to one, and then we set context over to B. So part of set context, swap the whole stack, we swap the whole stack, then we start executing thread B, which starts executing this function. So it creates its own D, so this is B's D. Jesus, that's bad, yeah, bad terminology. Then it checks I, so it reads I, I is currently what? One, and alligator, what does it eat? It eats the three, so alligator doesn't eat the one, so it goes into there, we increment I, so I changes from one to a two, and then it would print off B is two. And then it sets D equal to one, so it changes from a one to a one, doesn't do anything. Then we get context UB, so if we ever set context back to UB, this is the last spot, we set it. So it would look as if though it returned from get context. So now we check the value of D, it is equal to one because we just set it, and then we set D equals to zero, and that would be this one here, because it is a local variable to B, so we can think of it as just setting that. Then it does a set context to A, so it swaps back everything. So now we are executing that thread A function with A stack, which guess what? This still exists, and it is still one. It does not change to some other magical value or whatever it's the last thing it was set to. It is its own private local variable, and if you go back to it, it's the same value. It's as if, yeah, it doesn't like rewind history and save every, your whole memory of your stack, it just saves your stack pointer, and it was already allocated by the time we hit get context. So now it would resume, so now we are back to executing, oh Jesus, please work. Okay, so now we're back to executing thread A. So now we're back to executing thread A. Its value is, D's value is what? One, so is it gonna go into that if? Nope, so it goes, it doesn't go into this if, goes back up to here. What's the value of I? The value of I is two? So does the alligator eat the two? No. So that is still true, so we go to the next line, then we increment I from two to three, and then we would print A is three. And then next we would set D back to be zero. So that is A's local D, so now it's back to zero, and now we do another get context, which essentially we didn't even need to do because it's already set to start resuming from there anyways. So we check the if again, so in this case D is what? Or A's D is what? Zero, so is zero equal to zero in this world hopefully? So it would set D equal to one, and then set context back to be, then it swaps back everything, so now we're using B stack, and then we resume from wherever B left off, yep. Yep, yeah, after I print A is equal to three, I set D back to be zero, and then I get context again, but it doesn't really do that much because I'm gonna resume at the same spot anyways. And then D I just set to be zero, so I would go into the if, set it back to one, and then set context again. Okay, so now we're back in B. It's D is currently zero, it would go into this while loop, is three greater than three? No, three is not greater than three, so it doesn't do any of the code in here, it goes here, and it finishes thread B. At this point, we don't actually know what it's gonna do, so if it had a UC link, it would go to whatever that context was, but this doesn't say anything about that, so the default action is if it ends, it just terminates the whole process, yep. Thread A called this set context here. So, wait, I screwed up. So git context doesn't care, it just saves like your stack pointer, and yeah, it doesn't care what's on your stack. Yeah, if you, so before I called the git context, I created that local variable, so there was space for it, it existed somewhere. Internally, it would have moved a stack pointer up or whatever, and then when I git context, it just saves that stack pointer, but its address never changes, it never went out of scope or done anything like that, so it still exists. Yeah, like if I did this or something like that. So if I did something like this, yeah, so in my git context, I would resume that, and then whenever it executed this line, it would create some space for that X, and then set context, and then it would go back, and it would go back to whatever the stack pointer was there, so when you go back to it, there's now that X just doesn't exist, and then if it went back into that, if it would create a new X, yep. Reasonable way to say it's save context, or git context saves all the registers, and just the registers. It does not care about memory whatsoever. Yeah, because we've set D equals zero right before anyways. All right, so yeah, simplest terms, the git context set context, all it does is save register values, that's it, so there's a good point about making a local after it, it just wouldn't exist because whenever you go back, the stack point just resets to wherever it was when you git context, which would make it valid at that point if it resumed, but anything new doesn't exist, but anything that was created before will still exist, all that space, whatever it's set to. So any other questions about that? Okay, so that's probably the hardest one. Um, we can go back. So we can do some other questions, we can pretty much do the final that I have, well, we can do some questions from the final. So here's a few ones that I made doable because we haven't learned about sockets yet. So this main, all this main does is has a loop that just goes four times, and in the loop, we create a thread and then we detach it, and then in the run function, we take an argument, for now we don't really care, but it's a file descriptor. The thread sleeps for five seconds, then forks, so each individual thread calls fork, and then it checks if PID equal to zero, a.k.a. if I'm the new child process, well, then it would do one, copy that file descriptor to one, so it would change standard out, and then it execs and then it returns. So for this, how many threads do I create? Confused look, I saw confused four. This isn't, yeah, this isn't a trick question, it's just four, the loop like doesn't do anything crazy, it just starts executing main. So we create four threads, they're detached, they're not joinable, anyone remember the difference between joinable and detached threads? No, joinable I can wait for it, yep. Yeah, what happens if I don't join it? Yeah, the memory just kind of chills out, I waste some space, yeah. So I don't have to clean it up at all. So the clean up happens, whenever it exits, it just gets cleaned up at that point. So nothing needs to join it for it to get cleaned up, it just gets cleaned up, yeah. So you don't have to bother calling join on it. So in this case, detached. All right, so it creates four threads, and then it does this run function, it sleeps for five seconds, and then each thread calls fork. So what happens when a process with, in this case it would have five threads in total, right? So I created four threads and then I had my kernel provided thread that was executing main. So I have five threads in total in my process. So anyone remember what happens when you call fork, when you have multiple threads, I'll get copied. So I'll have, so each thread calls fork, so it will do something like what? So say I have process, I don't know. P2, it's got five threads in it, and then whenever one of them, so say this thread creates a new process, it will also have five threads. Yeah, yeah, so that would get really hairy quick because then it's like, oh, well, if some hasn't, if one of the threads hasn't called fork yet or some reason, you can't even like, you can't even really even have a hope of explaining what the hell they're gonna do because so much stuff is just gonna be in a random state. So whenever you fork with multiple threads, it creates a new process with a single thread and that thread is a copy of whoever called fork. So if I go here and I create, I don't know, so I'm gonna create process like three, four, five, six, and they're all going to be copies of whoever called the fork. So yeah, so this could be thread one, this could be thread two, this could be thread three, this could be thread four. So in that new process, there's only going to be that thread that called it. So at the end of this, I'm going to have four new processes, four P five, six, and each one of those is going to have a single thread executing within it. And then each of those new processes are going to just dupe a file descriptor and then exec, and then that process, which would be either P three to P six just becomes LS and starts executing LS things. So what would happen if I did an oopsie? Oops, oh wait, double oops. Don't have a child, yeah, yeah. So now I just have this one process P two. I have four threads executed that want to execute the run. I essentially have a really bad data race here, kind of a thread race. So they'll all try and execute that exec LP. And if you call exec LP with the process, it just transforms that process and starts executing a new one. So the first thread that makes it there executes exec LP. This current process with five threads turns into LS and the kernel creates a new thread for it and it starts executing LS and boom, everything's gone. Yeah, yeah, it pretty much just scraps the whole process, loads a new program in and just lets it start executing that one instead. The only thing that stays the same is like the process ID. Aside from that, everything pretty much gets scrapped. Most things, it might do some tricks with virtual memory so it doesn't have to like reload libc or something like that, but for the most part it'll get scrapped. So in this case, like my first, I would expect like four LSes to be called normally, but now I'll only see one and then that's it. Okay, I think that's pretty much all the questions for this. Oh yeah, what do we forget to do with newly created processes? So let's go back. So what do we forget to do? Sorry, we only changed what? Close, you were on a good track. So when we had a joinable thread, right? You said we had to join it. For a process, if I create one, do I have to do something to it? No, so the file descriptors, they'll just be copied or everything. I don't really have to close them because I, well, I should probably close FD. You're right about that, but I'm not a neglectful parent. So yeah, wait, right? I never waited. So if there's anything you learn in this course, whenever you create new processes, you should be a good parent. Don't create zombies in this. So yeah, so it would be a duplicate at the time of the fork, but all the children in this case just start doing LS. And then there's still my children. So if I last longer than them, I should wait on them so I don't have any zombies. So the full context of this code is it's actually like a web server and this loop actually goes on forever. And the idea behind this is this process never stops. So the LS process, all the children are gonna terminate really, really quick and whatever's calling main is going to exist forever. Like if it's a web server, it's pretty much a process that never ends. So if I don't call wait, I have zombie processes. What's bad about having a bunch of zombies? So the answer is a knit has to deal with them and they take up resources. So yes, they have to take up resources, but can a knit even deal with them if this process exists? Yeah, so a knit only takes over if they're orphans. So in this case, a knit can't even help us because if we're still alive, we're technically responsible, but we're like the, yeah, we're the parent that never dies. Yep, yep, yep. So each time it creates a thread and then forks, it creates a new process ID. So this fork at the very minimum, the first time through it would create like process 100, go through, then next time creates 101, then 102, then 103. And it's like the thread lab. It'll never reuse any IDs because I never weighed on them so it can't clean them up. So eventually I'll be at like process ID, you know, 10 billion and then eventually you're gonna run out of memory even if you're just storing an integer. You can only store so many of those. So you'll eventually run out of memory. Okay, then similar question. Of not waiting, what about if I do that? Yeah, now I'm essentially creating zombie threads, right? Every time I create a thread, now by default it's joinable. So it'll have some resources that are wasted. Every thread I create and never join. So it's essentially the same problem, but now it's threads. But with threads, hey, I could just detach them and then not have to worry about it. So you can see why detached threads would be nice for a nice infinite loop here. That just creates a bunch of threads and then doesn't really care about them. So I don't have to bother doing anything to clean up the resources. Okay, and then quick. So this is a center four one that says we get four threads and we want them to all go in some type of order. So I don't know if we can actually see that all. Okay, so let's just take it a little bit at a time. So each thread function here runs in a single thread and that's all it runs. So if we have thread one and it executes F one and then F two, which we can't reorder and we want thread two to execute function F three only after F one complete. So how would I do this using a center four and you may have this with, if you start doing multi-threaded programming and all that stuff, you may have this situation where you need to process some data or it needs to write something, blah, blah, blah and you need to wait for a consistent state and you only want to continue after a certain point and then start executing trying to go in parallel after that. So how would I make sure that F three only runs after F one completes? Yep. So let's create a center four, let's call it some one. What value do you want to give it? Okay, so zero. So we will just do, you want post some one and then you said after you want wait, I guess. Anyone else? So what's going to happen there? So we want F three to run only after F one computes. So these are all threads. They can all be scheduled at any given time. So what happens if the scheduler decides to run F or thread two while then it would run F three immediately and we're screwed. So typically with center fours, what the easiest thing to do is to place your weight first. So we're a little bit over time, but we'll go really quick. So it's easier to place your weight first and reason about that. So we would probably want to wait on semma one first so that if thread two happens to get scheduled and its initial value is zero. So if that goes first before anything, it's gonna sit there and wait for something to increment it and not pass. So if I leave the post above F one, while what might happen is thread one executes, increments it from a zero to a one and then thread two could go now and start executing and now it could decrement that from a one to a zero and then call F three before F one happens. So we're not quite there yet. So what would happen if I just put the post here? So now I see some nods. Now if I put the post here, well, if thread two goes first, the initial value is zero. So it's gonna sit there. It can't do anything. Has to wait for that value to get incremented at least once. And then thread one would go and it's only going to increment it if it calls F one. So it would call F one. And then only after it's done, it would post. And now thread two can actually pass the wait and then this would happen after F one completes. Chakaruni. All right, so we're a bit over time. So we can do the rest of them, finish it up on Discord. It's the final from like the same fall semester. So just remember, pulling for you, we're all in this together.