 All righty, welcome back to Fun Oil Offering System. So today, we are going to do what you need to learn for Lab 4. So this will be a more or less completely free form lecture where we go. We do an example, I show you the things you need for Lab 4 and then you should start on it. So read the feedback. So the Lab 4 tests I describe in detail, a simple test and then a harder test. So you can know how the testing infrastructure works and what this is supposed to do. So you are implementing cooperative user threads in Lab 4 and just so you have a picture. So I spent probably too much time on this. This is what a process looks like now if it was to have four threads in it. So each of the threads are independent of each other. They have their own registers and they have their own stack. Their stack is still in virtual memory, which is why it's overlapping with the box and what else is in virtual memory while the heap, global variables, and every thread would share that because they're in the same virtual memory. If one happened to get a pointer to another stack space while they could modify another stack, but if you do that, you are going to have many an issue. So they should be completely independent. And then aside from memory and threads, the only thing that else is in your process is open file descriptors. And that's what we know so far. So any questions about that? That's what it looks like, lots of fun. All right, so remember you have access to these examples. Look at the materials repository. So that's clickable in the slides if you want to download the slides. And today we're going to be in the lectures 18 context and cues directory. So here's our first example, how to use something called you context. So you context will do all the heavy lifting of saving all the registers and also doing context switching for you. So that is a nice thing to do because that's most of the work of implementing your own threads. So let us go and look at a fun little example here. So at the top, these are all the same includes that you will have in, that I think are the default in lab four. And I put in comments everything I am using. Yep, maybe. So here, yeah, so here the Valgrind thing is just at least for now. If you happen to use Valgrind, while Valgrind gets very upset with you if you start context switching around and context switch to a different stack and you don't tell it about it. So this include is just to make Valgrind happy. So I have my own new stack function and it registers the stack with Valgrind. So Valgrind doesn't yell at you relentlessly for using it because it gets very upset if you do not do that. So as of right now, that is just for you. So you can use Valgrind in lab four and you do not pull your hair out. So that is everything I use. You should not use anything else from Valgrind. You should just use my function. So I have a nice helper function for today's little example just called die because I woke up today and chose violence, but it does our normal thing. So saves error no, prints a message that describes the error and then just exit with that. But in lab four, you shouldn't need this aside from your own development and debugging because nothing should cause the program to cause the process to execute unless yeah, nothing should cause the process to execute with any errors. So here I have a new stack function. You do not have to understand this but we can actually understand it now. It basically just allocates some space for you of size, the default stack size which is like 16, whatever, 16 kilobytes. Don't have to care about that but it just makes it executable, read, write so you can do whatever you want with your stack and then it will die if it fails, registers it with Valgrind. You just have to use this new stack function and then you should also use the delete stack function whenever you are done with the stack in order to clean it up. So aside from that, the new thing is going to be these you context underscores T's. So that represents the virtual registers if you want for an individual thread. So that's like where everything gets stored. You don't have to know what is in that structure. All of the functions will go over today just modifies that. So you create a you context, the idea behind that is they're supposed to represent a thread so they save register values in them. So I will create three contexts because I'm going to have, I guess, three threads in this example. So here I declare some pointers for threads one, thread one stack and thread two stack. I do not declare stack for thread zero because I'm assuming thread zero is going to be the main thread that starts executing in the process. So I don't have to declare a stack for that one because it's already using a stack. Otherwise, I couldn't use any local variables or anything like that. So declaring two stacks. Inside here, I have two functions. I'm going to run in my threads but let's first go into main. So the first call we have is something called get context and what that will do, it just takes a pointer to a you context and it will just save whatever the registers are in the current running thread to that context. So if initially that's the first thing I do that T zero you context will be initialized to all the registers at the time of that get context call. So this will be the default main thread. It's initialized as that get context and it saves all the registers including the program counter and everything like that. So in here, the other one, if there's a get context, probably a set context. So if you do a set context, what that will do is it will be, it will just resume from whatever that you context is whatever it last called get context. So in this case, if I immediately set context back to thread zero you context, well the last time that thread zero you context got saved was right here. So if I do a set context, it goes back as if this function returned and then it would go here set context back to it go up here and that's probably an infant loop, right? So it just goes down, sets context back to where it came from wherever the last get context was execute set context again goes back do, do, do, do, do and we can go ahead and see. Oh no, did I write? So get context essentially just saves the current thread like all the information about the current thread into this structure including the program counter and if you go back to it, it'll be like this get context call returned. So this structure just gets initialized as part of the get context like it gets values written to it so the program counter would be saved as if it's about to return from get context and then set context just takes the current running thread and changes all of its registers to whatever that structure says. So it essentially just makes it go back in time as if it returned from get context again. That kind of makes sense. So in this here, let's run it because if I run it I can see oh no over and over and over again. So because I do a get context here that means whenever I go back to T zero U context it looks like it resumes from here because that's where I saved it. Like the get context just saves all the values to that structure. So if I set context boom goes up, boom goes up, boom goes up. Just keeps on going over and over again. Any questions about that? Fun infant loop. Okay, yep. So if I was to manipulate a variable. So if I just do like this plus plus X and then like that. All right, so if I do this, so this is why it's important to know what is on the stack. So this will create a variable called X that is an integer on the stack of whatever this thread is. So the main thread and then it initializes it to zero. And then as part of this get context it would save the complete state of the thread at the time of this call. So whenever it called that it would save the stack pointer. So it would have an X allocated on it and that's it just holds a stack pointer. That's the only thing that has to do with the register. It would be some virtual memory location where that actually lives, but for the purpose of that the only register that's involved is the stack pointer, right? So here it would increment X. So it would like essentially load that from that memory location. Go ahead at increment it and then write it back out. And then here if we do a set context back it goes back here but we didn't allocate anything new. So the stack pointer wouldn't have changed, right? It didn't change at any point during this. So when we go back it's still the same thing. And because it's just on the stack it doesn't rewind anything that's not a register. So it would go just go back to it X already exists. It's on the stack and its value is now one. It would increment it again. And if I do this I should see it go up super fast. So yeah, this is also why it's important to know what is on the stack and what is actually stored in the register. So it's a good thing we took 243, yeah. Yeah, so all set context does is use this struct and essentially go back to wherever it last called set context in the exact same state as whenever it called set context. And the only state it keeps track of is all the registers. So set context goes back to a get context. Whatever the last get context is. So that's a good question. Anyone guess what happens if I do something like this? Should just be one, right? One over and over again. So why is that? Well, at the time of the get context calls X didn't exist. So it will save the stack pointer there. And then right after get context it would have allocated some more space on the stack and then incremented it to one, then printed it out. And then we essentially set context. So we go back to the last get context. And at this time that was not allocated on the stack. The stack had some empty value. So when it goes back, it's just gonna do the same thing. Again, it's gonna get allocate X on the stack overwriting the old value. So we've reset it from zero or from one back to zero because it is going to do the same thing, incremented it and then X is gonna be one again. And it's just gonna do this in an infinite loop. So any other questions about that? So it's not really the same thing as go to because it's like literally every single register goes back in time. So go to just lets you kind of change the program counter. This is everything, like every single register. Cause yeah, if you go to then it would still be allocated on your stack, your stack pointer wouldn't change and it would get way worse than this. At least this is consistent unlike go to. All right, any other questions about that? Okay, cool. So let's see how we do some other things. So let's create a new context to represent a new thread. So we will create thread one stack by using this new stack function. So now we get a pointer back that represents a stack. We would do a get context. So even if you just try and make a new context that executes another function, you still have to call get context to initialize that you context. So here we have to call get context on the T1U context. And then there are some fields that we can modify in order to manipulate it. So one thing we can do, so you'll only have to use this one is use the U context. There's a variable called UC stack and you have to set these two variables. So this SS underscore SP is the initial stack pointer. So it should be equal to wherever thread one stack is. And then the next argument is whatever that stack size is and we just use this define as a good default value. So after that, well, we can do a make context. So what make context will do is set up that stack you set in that, what's it called, set up the stack that you set in UC stack. And it also lets you reset the program counter to execute a different function. So in this case, you have to give it the context you want it to modify. Then the function you want it to set the program counter to. So that would, whenever I do a set context on this, it would be as if it starts executing t1 underscore run. And then here you can give how many arguments you're going to pass to the function. You can only pass integers. So if you don't want to deal with this, so this is like some weird casting thing. People really hate this because of the casting. You can get rid of this cast if you just don't pass any arguments to it if you just want a function that takes no arguments and returns nothing, then set argc to one and then, or sorry, set argc to zero and then you don't have to pass anything. In this case, I'm just showing you how to pass arguments. So I will pass one argument and I will make it an integer 42. Any arguments you pass have to be integers. So I set that up and let's go ahead and see. So if I set that up and then I call set context, t1 u content, oops, address of. So if I set that up and then I call set context on it, it's going to be as if it starts executing this t1 red. And what does t1 run do? Well, it just prints off hooray. I'm executing now and then whatever arg zero is. So if I go ahead and execute that, I should see hooray. It got arg 42. So that set context switches the current running kernel thread over to execute whatever that t1 context was, which we initialized to run t1 run and we set up its own independent stack and everything so we're all good. So any questions about that? Yeah, so set context just essentially goes to wherever you did the last get context for that stretcher or whatever make context was, the last one between them. Yeah, so yeah, that's a good question. So what happens after t1 run finishes, right? So after t1 run finishes, the default thing to do is just to end the process. So if you don't set anything special by default, it will end the process, which if you have multiple threads still existing wanna switch between them probably isn't good. So you do not want to do that in lab four. You want to keep the process running as long as there are threads available. So one thing I can do is I can use this UC link field. So what this UC link field does is something that's fairly special and it will make it change that behavior. So if that thread finishes whatever run function you initialized it to, this UC link will automatically do a set context to that you context whenever it's finished. So in this case, if I do a UC link to thread zero, it will immediately come back. And in this case, it would come back to the last time it did get context, which is here. So that probably wouldn't be a good idea. So let's run it anyways. So it's just initializing it over and over again, jumping back to it. I also infant looped myself, which was probably not a good thing. So what did I comment? So any questions about that? That just changes the behavior of what happens whenever you return from the function that the thread is trying to execute. That's what it was, T2. Good with that. So you can see how easy it is to give yourself an infant loop here, right? A lot. So if you thought infant loops were fun before, whew, oh boy, they're fun now. Is there an equivalent break statement? Nope. Yeah, there's no break or anything. You just have to set context whenever you want. So what else do I do in the program? So let's rewind. So we just make a context. We make a new thread that wants to execute thread one. And let's comment it out for now. So here I'm going to make a new context and do it even shorter. All you really need for a new thread is a new stack. You initialize the you context by just calling get you context. If you eventually call make context, it will update anything else it needs to. You just always have to initialize it with get context. If I forget this here, and I just do a make context, like really bad things are going to happen. It is undefined by the C standard, so never do that. You have to always initialize it with get context, even if you do a make context on it right after. So we do a get context for thread twos you context. We set this UC stack, the stack pointer and the size, and then we make a context. So we make another you context and all it wants to do is that thread two will just execute thread two run. And in thread two run, well, it says thread two should be done. Switch back to thread zero and it deletes the stack of thread one and then does a set context back to thread zero, which was the original one. So at this point, this isn't super great because let's see here. If I just set those up and then do a set context to thread one, again, we will have the situation where something fairly bad happens. So in this case, if I just set context to thread one, well, what's going to happen is thread one executes, prints it and then it exits and the process terminates because I didn't set UC link for it. So let's modify to get and hopefully do some more fun things. So let's make it that whenever thread one finishes, then it will, it's UC link, it's set to thread two. So it should start executing thread two whenever thread one finishes. So what should happen in that case? So in that case, thread two would start executing, it would print, oh, thread two should be done, switch back to thread zero. So it would delete thread one stack, which should be okay because thread one doesn't exist anymore, it returned from its function. And then I do a set context back to thread zero. So is this a good idea? Yeah, yeah. So if I follow the, if I follow execution for main, I called get context on thread zero here and then throughout all this, I never changed it. So if I go ahead and compile and run this, it should run thread one, then run thread two, and then at the end of thread two, it UContext, or set context back to thread zero, which is going to be here and it should just do the same thing. We should get an even larger infinite loop. Yay, we're just, all it does is finish thread one, thread two, then recreate some again. The whole thing goes over and over again. So how many times have we infinite looped ourselves three times so far? Yeah, yeah. So here my UC link, where is it? Yeah, my UC link for thread one is to execute thread two. Yeah, do that. So all this takes is a pointer to UContext. So all it does is essentially make it set or call set context whenever it's done using that. And that will just be whatever its current value is. Right, it's just a pointer. So it's pointing at something, so it'll use whatever the most up-to-date one is. Whenever you last called make context or get context. All right, so that's fun. Let's see. What do we want to do next? All right, so if instead of doing set context, really what I want is for thread zero, like I just want to swap over from thread zero to thread one and I want to essentially resume thread one from wherever it last left off most of the time, right? So in that case, there is an exception to it coming back to get context or set context. There's this helpful function called swap context. And what that will do is it'll essentially call get context on the first argument. So it'll call get context on this and it would resume as if it just returned from swap context and it will call set context on the second argument. So what this will do is it will start executing thread one and whenever you do a set context back to thread zero, it would continue from here. So it would continue as if the swap context has just returned. So that's probably more what we want if we're switching between them, right? We don't want to go back to thread zero at the very top and restart everything. We only want to execute statements once. So if I do this and everything works properly, I should get everything running. Yeah, sorry. Yeah, for swap context, the UC link is irrelevant. UC link only matters to describe what happens whenever that thread function returns, whenever that thread function finishes. By default, it'll end the process. If you set UC link, it will just do a set context to whatever that context was. That's the only difference. It doesn't affect swap context at all. So if we do a swap context instead, well, let's go back to the beginning and make sure we read everything again. So initially we do a get context and initialize thread zero context. Then we create a thread one. We do a get context. We set up its stack and we'll set up its UC link so that whenever it finishes, it automatically does a set context to thread two. And then here we do a make context and we just show you that you can pass an argument. We don't do anything with the argument. So we'll keep that commented out. So after doing that, we create thread two. It looks a bit nicer because we don't screw with the arguments or anything. So we just declare a new stack, initialize the you context by get context, set up the UC stack, and then just do a make context with it. So whenever we do a set context to thread two, you context, it will start executing thread two run. And here I didn't set a UC link. So if it happens to return, my process is over. So after that, I do a swap context. So what this will do is do a get context on T zero, so that if I do a set context back to T zero, it resumes from here, resumes here. And then it will do a set context on this. So right now this you context, it is set up from our make context, so it's going to execute T one run. So that should print. And because we set UC link, whenever this function finishes, it does a set context to thread two, because that's what we set the UC link to. In thread two, we say T thread two should be done, switch back to T zero, delete thread one stack, and we do a set context to thread zero. So thread zero should resume right here. And then it'll print, main is back in town, then we can delete thread two stack, because it's now done, and then we return zero from main, exits the process, we're all good. So that's why we got all those three lines there, and they were all from different threads. So any questions about that fun stuff? Yeah, so I can't delete the T zero stack, because I'm just using the main thread stack. I never set up a stack for thread zero, because I'm just reusing whatever one I had. So that's why here in the get context, I don't do a make context or anything, because the stack pointer will, I'm using the main thread stack. All right, any other questions about this? That was in T one context? Yeah, so in this case, T one got cleaned up by T two. But of course I could forget to delete the stack and then memory leak, yeah. Yeah, so that's a good question, let's see. So I deleted set context to you, or to T zero context at the end of T two run. And then here, let's see, it's just called you context, UC link equals so like that. So if I do that, that should do the exact same thing, right? Because it's returning from T two main, and like I said, UC link just does a set context on it. So let's go ahead and make sure that we are not insane. Boom, no problem, works fine. So one of the tasks you'll have to do is like implicitly call the thread exit. So you'll see someone there might not write one. So you essentially have two ways of implicitly calling exit, and I will leave that up to you. You can use UC link, or you can use an argument. So I showed you how to use both. You have to do a little bit of engineering work to do the implicit exit, but it's not too bad. All right, any other questions before I switch to a queue really quick? Okay, if we have time we can come back if anyone thinks of anything. So the other thing I told you is you don't have to write link lists. You can, we only need one ready queue for our program. So we can just use one of these sysq.h, they lots of link lists. They're a bit weird to use. So I'll show you how to use them. So typically you need to declare a struct that will hold any of the data you actually care about in this case. If it's your ready queue you should only need a single integer. You don't need to like have a link list of thread control blocks or anything like that. In fact, well, I would recommend against doing that. So for this, all you really need is an integer ID and then this macro will essentially define the previous and next pointers for you in this structure. So this will just declare the previous and next pointer for you and we just call it pointers. And it needs the name of the struct as its argument. Why? Because there's C macros and you can like if you wanna look at this mess you can but just know that it's supposed to be the name of the struct and that's it. So that will define the previous and next pointers for you in your structure. And then this tail queue head, so it's called a tail queue because you can add things to the beginning or the end. So it's a doubly linked list. You just have to tell it the name of what you wanna call the list. Typically you just call it list head but it will also contain like the first and the last pointer. Yeah. Yeah, so it's a special node that represents the list. Yeah, so in this case we call it list head and we say what the element is. So it's a link list of list entries. And then here that will set up that structure for us. So we have to declare a list head. So it will be struct list head, list head. So this will represent our list. So this will be the node that just has like the front and the back pointer and that's it. So with that messy of the way that's about as messy as it gets. Then after that to print a list while you have a pointer to your list entry and then you can use this tail queue for each if you wanna iterate over everything in the loop. You don't have to write your own for loop where you like get the next pointer or do any of that garbage. You just use tail queue for each. Tell it the name of your pointer variable then give it your, so the address of the list head is essentially just your list and then you also have to tell it what your pointers are called and this name should match exactly whatever you use for the tail queue entry. So you just have to tell it what it is, whatever you called it. And then in that, that entry will be valid every time through the loop so I can go ahead and print off the ID. So this will print off the ID of everything in the loop in order. So if I want to print just the last entry of the list well there's a special macro called tail queue last and it will get me the last element of the list. So all of them will just take the address to the list head which represents list and also the name of the structure because it's kind of annoying and then you can print off whatever the last element is in your list if you really want to. So here's what main looks like. We have to initialize the list so that will set all the default variables for you. You give it the address of the list head and then we could assert that, hey, this list should be empty if I just created it. So there's tail queue empty that will check if that queue is empty. You could imagine where this might be if this represents your ready queue there's nothing in the ready queue that means and a thread is about to end that probably means you can't switch to anything. Probably means you're the last thread able to run and you should probably kill the process. So here I just demonstrate how to add things to the list. So this struct list entry is on the stack because it's a local variable. Obviously that will not work for you. You should probably malloc it or it should be somewhere in dynamic memory. But for the purposes of this I'm just gonna throw it on the stack. So I'm going to create one list entry with ID one and then if I wanted to insert it into the back there's tail queue insert tail. So that needs the address of your list. Whatever the address is of the entry you want to insert and then the name of the pointers that you said at the very beginning. So that will insert it at the end of the list. There's also an insert head if you want to use that but if you want to use that for this lab you're just using a first in first out queue so that's probably bad. But at this point I just print that I inserted it and then call print list then create another entry called e2 set its ID to two. I added at the end of the list. So now if I look at this well my list should contain a one and then a two in that order. And just to make sure I will print the last element of the list using that function. Then if I want to take something away from the list I can do tail queue remove. So list head what the pointer is and then the name of the structure again. So after this if I remove one the only thing left in it should be element two. So this should be fairly boring. If I run it I see that hey it works like a list. Inserted one one element in the list inserted two list has one then two and the last element is two. Then if I remove one well the list just has one element in it. Should be fairly boring right? But saves you from writing your own link list. I suggest highly suggest you use this one. There's also like let's see. No there are no bonus marks for building your own link list and you will probably get negative marks because you will do it wrong. Okay. Yes so other things you can do the man pages for this too. So other things you might want like there's tail queue first to get whatever the first element is in the list which you might want if you want to take something from the front. There's all these type of functions they're just named fairly generically. So just look at the documentation for them. They it's not terribly, yeah it's not terribly surprising. So any questions about the fun link list? Again implement your own if you really want. Again highly suggest not to. But I mean this also should be the first lab that you have to use link list but that's not true for a lot of you. So all right any other questions like list fairly boring? Okay any questions any other questions about the U context? Another fun stuff we can do with that. Otherwise I will ask you a question and make sure you know it. Okay here's a fun question that was on an exam. So there are some global variables here. There is an X initialized to zero. Then we have two U context, a U context A and a U context B. You can assume that U context A is initialized to start executing thread A and then U context B is initialized to start executing thread B and one kernel thread starts off by calling a set context which shouldn't have an underscore there for UA. So the question is what happens? So what gets printed out? So there are some print statements. Let us take a look, ponder it for one minute or two and then we will come back to it and start answering it. All right, do we have any initial educated guesses here? Yeah, infinite loop. Everyone else think infinite loop? A lot of head noddings. All right, so let's see what happens. So we have UA initialized to start executing here. Actually let's make the arrow go the other way. So UA wants to start executing here. UB wants to start executing here. So it says we start off by calling set context of UA. So it would start executing, oops. Oh no, I have to do this, okay. So UA declares a variable called D and it does it in A stack. So it declares a stack variable called D and it is initialized to zero. So here D equals zero. Next UA is going to, thread A is going to continue your execution was the value of I, hopefully zero. So it's zero less than three, hopefully. So it will increment I, so I plus plus. So this is going to change from a zero to a one. It's going to print F, A. A one and then it is going to just set D to zero again. So it's not going to change. Then it does a get context for UA. So UA will stop trying to exit, whenever we do a set context back to it, it will come back here. So next thing it does is it checks if D is equal to zero which currently it does and then it sets D equal to one. So that is its variable D that is on the stack. So it changes its D to one and then does a set context to UB. So UB comes in and it declares a D on its stack. It is equal to one. It will check I is I less than three or what is I? One, that's less than three. So it will print off B two because it would have incremented that global variable from A one to A two. After that, it just reinitializes that D variable to one so it doesn't change. Then it does a get context of UB. So UB will continue if we do a set context now back to here. It checks its value of D which by my math is equal to one sets it equal to zero and then does a set context to A. So does a set context to A so it means thread A will resume from here. So if thread A resumes from here what is the current value of its D one? Is it equal to zero? No, so it's going to come in here go back to the top of the while loop and is I, what's the value of I? Two, is it less than three? Nope, or well it is less than three. Whoops. All right, so it would increment I so I now goes to three. Then it does a get context call for UA which doesn't really change where it's going to go. So, oh it would also reset D to be zero, sorry. So after printing, okay, one line at a time it would print A equals to three after that and then it resets its D variable back to zero. Then it would do a get context which wouldn't really change that much. Then it would check if D is equal to zero is it equal to zero? Sure is. So D is currently equal to zero so it would set its D back to one and then do a set context to thread B. So now we go resume thread B which would resume right here. So is thread B's D, what is the value of thread B's D? Zero, so is that equal to one? Nope, so it's going to not go in the if statement and go back to the beginning of the loop. Then it will be okay is three less than three. Hopefully not in this world. So it would just go here and it would exit from thread B. It doesn't say if it has a UC link or anything that thread function is now done so we just assume the process terminates. So this would not be an infinite loop. It would print A1, B2, A3 and then process ends. And why is that? Well, because it's on its stack, doesn't change. That was a lot of, no, do the same thing. We just swap the values. It just looks exact, like we just made it look exactly like that one, right? Just looks exactly like thread A. So yeah, that would also work if we just swap the values like that. But if I didn't do this and didn't do this then that's probably an infinite loop. So knowing that also, actually it shouldn't really, if this helps in lab four, you might want to rethink things. Yeah, probably shouldn't. All right, so with that, just remember, phone for you, we're on this together and good luck, start lab four.