 Welcome back to CS162. So today we are going to finish up the discussion that we were having of the reader's writers problem last time. And I have a little bit of a simulation through the code so we can kind of see how things proceed. So if you remember last time, we covered a lot actually. We talked about, among other things, the whole idea of atomic instructions or read, modify, write instructions. The primary one being test and set, which everybody knows about typically. But we also talked about swap, compare and swap and load link store conditional. The key thing to interpret with these was that everything that's in between the braces here happens all at once in one cycle atomically in a way that cannot be interrupted by any other thread. And so when one thread executes a test and set, all three of these things have to happen. And just to give you the way to interpret this, what we said is you give it an address and then you simultaneously grab the value that was there and store a one. Grab the value that was there and store a one and you do that atomically. And that's enough to basically build all sorts of interesting synchronization primitives. And we talked a lot about that in terms of how to make locks, for instance. Swap is similar, but that's where you grab a value and store something else. And so you grab a value, store something else atomically. Compare and swap basically says you take what's in memory and if it matches one thing, then you store something else there. And swap and compare and swap are available on architectures such as the X86. And there's a version of compare and swap that actually returns the old value rather than success or failure. And so these get the ability to do something very interesting, complicated. So, but for instance, Tested said it's powerful enough to build any sort of lock primitives you might want. And so this was one that we looked at where for instance we had a guard value and the lock itself was in shared memory. And so unlike trying to disable or enable interrupts, we actually can build locks that work across a whole core with multiple or across a whole processor with multiple cores or across a multiprocessor with many chips and many cores in each chip. And the reason for that is that things like guard and my lock are actually in physical memory and shared memory, okay? And notice that guard is actually shared across all of the implementations of locks, whereas my lock, you can have many different versions of this blue lock and they're all different locks. And we gave an interface of acquire and release where you give the address of my lock, acquire, basically use Tested and said in the spin loop here until it found that the guard was zero. And remember, because Tested and said as atomic, we grabbed the value of the guard store one there. And what we showed was if we built a lock this way where we had a kind of this red things like a lock over the lock implementation, then we could very quickly grab the guard lock, check and see whether the blue lock was busy or not. If it isn't busy, we set it to busy and return from acquire. So that means that the thread actually acquired the lock. If it is busy, then we put the thread on a weight cue and go to sleep. And then release is kind of the opposite of that where we sort of wait to grab the guard. If anybody's on the weight cue, we just go ahead and give it to them, wake them up basically. Otherwise we free the lock and we release the guard. And the reason this is not busy waiting is because what's happening here in red is very quick because the critical section of the lock implementation is fast. All right, were there any questions on that very quickly? So you need the address of the guard. And so yeah, I guess technically this is Test and Set and Amper Guard or right, that's a good catch on there I guess. Anybody else? Okay, good. In the if case, you're talking about on release, the reason we're not freeing the lock in the if case on release is because we're giving it in some sense over to the thread that we just woke up. And so the lock itself always stays locked in that instance. All right, hopefully that helps. Okay, so then the next thing we talk, so what's interesting about this is this is kind of a skeleton implementation because we really didn't tell you how to deal with this, putting something to sleep. Okay, what happens if the thread is suspended? Well, the guard is equal to one. Well, that's exactly what you see over here in a choir, right? Because guard is one, we put the thread to sleep and we have to set guard equal to zero somehow atomically. So that was some sort of dive into the kernel set guard equal to zero. I didn't really specify how that was, but the idea is that's very similar to what we did with interrupt disable where we put something to sleep and the interrupt got re-enabled when the next one started. Okay, so you need to always analyze a situation to decide whether busy waiting is gonna be an issue or not. This we would call not busy waiting because it's extremely fast. You're not waiting for a long time. You're just waiting for the previous thread to finish its implementation of a choir or release. Okay, now what if the thread gets interrupted while the guard is one? That's very good. That's the one instance where things might take a little longer, we won't worry about that case for now. All right, but that's very good catch and that's good thinking there. But for now, let's just assume that we're talking about this being very fast, okay? Now, I also told you about few texts. So the problem with this implementation here with test and set was that we don't really tell you how to deal with that sleep case because clearly with sleep you gotta go into the kernel. The good thing about this when you don't sleep is you're just using test and set on and sets and freeze on memory and so you're not actually doing system calls but when you have to deal with sleeping and waking things up, there is a system call. And so I gave you this good example of an interface that Linux put together called Futex for fast user space, mutex. And the idea with Futex was you'd go ahead and take a system call into the kernel and it would put you to sleep, okay? And this is enough of a primitive to build things like what we just talked about with the test and set so that when you decided that you couldn't get the lock, you could do this system call to go to sleep. The tricky part about that is you gotta make sure when you implement something that you tell it, both what your lock address is and the value that you expect if things are sleeping so that if there's a change from the time you decided you need to go into the kernel to sleep to when you called the kernel with Futex, if something changed in there, if Futex doesn't put you to sleep, it actually just wakes you up right away so you can check it again, okay? So that's what's clever about this implementation, all right? And this is an interface, you can think of this to the kernel sleep functionality and it's not exposed typically in libc to users. This is what libraries inside of libc might use to make the pthread mutexes and pthread semaphores. And here was an example, I'm not gonna go over this again. And the sleeping here, the question is sleep until Futex wake somebody up one thread, okay? Or end threads, you can kind of say how many in the wake up, okay? Where's that, let's see, do I have, oh, sleep till Futex wake, yes, you're right, right there, that's the typo, thanks, good catch. So in this example I gave you rather than test and set, we actually use compare and swap and swap and you should work your way through this, but this is a pretty clever implementation where the lock has three values, it's either fully unlocked, it's locked and only one thread has a lock and nobody's sleeping in the kernel or contested as a situation where somebody might be sleeping in the kernel. And if we do this in a clever enough way, you can make sure that a choir and release are extremely fast assuming there's only one person grabbing and releasing the lock and no contention. And it's only when somebody else comes along when the lock is held that then we move into this contested state and potentially put somebody to sleep. And you should look on this on your own, I don't wanna go over it again, I talked about it last time, but the key thing here is that the compare and swap and the swap, these first two are the where we grab the lock atomically in a way that will make sure that we don't have more than one person actually holding the lock at it. Okay, so now back to where we were when we finished up, we were talking about monitors as a good alternative to semaphores and a monitor is basically a lock and zero or more condition variables, the zero condition variable is not very interesting because it's just a lock, but a monitor is a lock and condition variables from managing concurrent access to shared data and it's a programming paradigm. It's a way of thinking and condition variables are very special entities because there are cues of threads waiting for something inside the critical section. Okay, and the key idea here is to allow sleeping in a critical section in a way that the person writing the code can forget about it, okay? And we'll show you, we're gonna go through the reader's writer's example in some detail again so you can see how a more complex example works. But the idea here is that you always grab the lock before you touch the condition variables and if it turns out that conditions aren't right, you can go to sleep holding the lock, okay? And this is the only situation where you ever ought to go to sleep holding a lock, okay? And what this is is a condition variable is a version of what we've been talking about in our implementations before. For instance, going to sleep with interrupts still disabled, right? Well, that's kind of the way the code works out. It turns out under the covers, of course, we end up waking up somebody else who then turns interrupts on. So it doesn't actually freeze everything up. We also talked about the test and set example a little bit ago trying somehow to both put the thread to sleep while setting guard back to zero. This is similar, okay? So condition variables under the covers take care of the right thing with the lock. But from the standpoint of programming, you think of the condition variable as putting you to sleep with the lock. And with semaphores, you can't do that. If you try to use a semaphore and it goes to sleep and you hold the lock, you've just deadlocked your execution, okay? So there's some operations on condition variables which are useful here. So one is weight, where you have to give the lock. You know, why is that? Well, because the condition variable when you puts you to sleep has got to somehow make sure the lock can be released. Signal wakes up a waiter and broadcast wakes up all of the waiters. And the rule is always hold the lock when doing any condition variable operations, okay? Always hold the lock. So the problem we were talking about at the very end of the lecture was the reader's writer's problem. And essentially the reader's writer's problem was one in which there's a database and the database has access rules. And the access rules are that either you can have many readers all looking at the database at once or single writer, okay, but not both. Okay, and the reason for that is as soon as a writer touches the database, it's gonna potentially disturb any consistency of that database until it's entirely done with their right. And so we don't want readers to be anywhere near a writer. And similarly, we don't want two writers going on at the same time because that could screw up the consistency. And so this model is we wanna have some way to have a single writer or multiple readers and an arbitrary number of threads that might be trying to do each and we wanna control the chaos. And this is a great, I like this example because it shows you how powerful monitors are compared to anything else. And I challenge you to think about how to do what we're about to do here with locks or semaphores. It's just a mess, okay? Now, why isn't using a lock on the single database sufficient? Anybody wanna remind me why I don't wanna just use a single lock? Yep, we want multiple readers. So if we have a single lock, the problem is if we grab a lock before we read then nobody else can get in there to read. And we already said we wanna have more than one reader. Okay, so we're already need something different. Okay, we want many readers at the same time, only one writer. Okay, so to remind you again, here's the structure of a monitor program using monitors. And remember, this is a Mesa scheduled monitor program. If you go back to lecture last week, we basically also talked about horror monitors. Okay, and that's the scheduling. Mesa or horror monitors talks about what happens when you signal and wake somebody up. The Mesa is far more common and it's much better on resources for the kernel. And so Mesa comes from the Mesa operating system from Xerox PARC. The horror monitor example comes from a mathematician, but we're gonna do Mesa. And in Mesa, the typical pattern is the following. You grab the lock and then you go into a loop and you sort of say, well, as long as the conditions aren't right, I'm gonna go to sleep, which is take my condition variable, go to sleep, okay? And I'll just, whenever I wake up, I check again. That's where the Mesa part comes into play, okay? So will you ever be using a horror style monitor in 162? Only in, you know, maybe exercises occasionally. We will stick to Mesa, all right? Because that's what you're gonna run into with any monitors and condition variables that you have out there, okay? So notice how we're doing this looping construct. That's because of the Mesa aspect. So whenever we go to sleep, that's because conditions were wrong, but when we wake up, we gotta go check our condition again. But if you notice between the lock and the unlock, the way to think about this and the way I want you to think about this is we have the lock through this whole loop. Even when we're sleeping, I want you to think that way. Even though you're more clever than that and somewhere under the covers, you know that the lock gets released and reacquired. But when you're thinking about whether your program's correct, you want, I want you to think that between when I grab the lock and I unlock, I have the lock. And the reason that's so powerful is that means that I can look at all sorts of conditions. I can look at multiple variables at once to see how they compare with each other. I can do all sorts of stuff. And because I have the lock, nobody can go in there and mess things up while I'm looking at them. And when I go to sleep, because the conditions aren't right, yes, I'm letting somebody else fix the conditions, but when I wake up, I know once again I have the lock and I can check things again without worrying about somebody getting in there. So now can things change before weight and after weight? Well, if I find the conditions aren't right and I go to sleep with a weight, things better change because if they don't, then I'm never gonna get out of this situation. So when I go to sleep, the hope here is that somebody else will come along and change the circumstances. So when I wake up and check the while loop, I eventually get out of here, okay? Okay, well, except it's not, you could say it's not equivalent to holding the lock, but you could think of the lock releases being inside weight, but none of the code that you see on the screen here is ever executed without holding a lock, okay? I realize that this is a strange sense of fooling yourself, but you gotta think of it that way. So none of the code you see on the screen here runs without the lock held, but inside of weight, the lock gets released and reestablished. Okay, and we're gonna go through this in more detail in the reader's writer's example just to see. Okay, and once I unlock, now I can do all sorts of stuff, okay? I don't hold the monitor anymore. I'm doing something because I've already checked the entry conditions, and then when I'm ready to finish, I do the checkout, and here's a simple checkout where I grab the lock, I signal somebody to wake up and I unlock, okay? And that in addition to signaling, I might change some parameters of some sort that they might check and decide it's okay, okay? All right, is everybody willing to go ahead and fool yourself a little bit that nothing between lock and unlock releases the lock, okay? That's the way you need to think while you're programming. Well, you don't have to trick yourself once you get used to this, okay? This is really a way of thinking, okay? I like to think that with monitors, I'm teaching you a pattern, a paradigm for programming, okay? And it's a way of focusing your attention exactly as you say on the parts that matter. So let's look at the basic solution, and we rushed a little bit through this at the end, but it was justified because I wanted to make sure you had it to mull over over the weekend. But we have correctness constraints, which basically say that readers can access the database as long as there aren't any writers, and writers can access the database as long as there are no readers or other writers in the database. And only one thread can manipulate our state information about who's where, okay? The basic structure looks like this. The reader, excuse me, the reader says, well, wait until there's no writers, access the database, check out, wake up a waiting writer if there is one. A writer says, wait until there's no readers or writers, access the database, check out, maybe wake up a waiting reader or a writer if necessary, okay? Now, this particular solution that we're gonna show you has writers as a priority. That's good. Let's hold on to that thought because we can ask ourselves whether we have to do it that way, okay? But that will be what we've got here, okay? Now, this is where things got complicated, but it's not really, okay? So these state variables are four integers and two condition variables. Those four integers keep track of the number of active readers, that's a reader actually talking to the database, the number of waiting readers, those are readers that are just waiting, ready to go, but they can't go for some reason. The number of active writers is the ones that are actually modifying the database. And we know already what's the maximum that AW could ever be. What's the maximum number of active writers? One, yep. The number of waiting writers is the number that are waiting to get in the database and that doesn't have any limit. And then we have two condition variables for sleeping depending on whether we're a reader or a writer, okay? And you'll see how this comes out in a moment. And here was our reader code. And what we're gonna do, those of you that looked at the number of slides probably took a quick in-breath there, worried about how many there are, but there's really a simulation in here that makes things faster. Not as slow as it seems with all those slides. So what a reader does is a reader first checks himself into the monitor, which means you always acquire the lock and then we do a loop. And our condition we're checking is, as long as there's either an active writer or a waiting writer, okay, any number. So we sum them together, it's greater than zero. We're gonna go to sleep, okay? There's that priority for writers that was asked about earlier. And so basically we're gonna say, well, we can't run right now. So we're gonna increase the number of waiting writers that's WR++ and we're gonna go to sleep on the okay to read, okay? And we have to give it our lock as well so that we can release the lock under the covers. And then when we wake up, we're gonna decrement the number of waiting readers because why? Well, we're not waiting anymore, we're running something, okay? We're not active in the database yet so we're not doing anything with AR, but we will keep looping in this, checking our conditions, going to sleep, waking up, checking our conditions, going to sleep, waking up until AW++WW is zero, okay? And the reason we have to keep checking is because we have Mesa semantics, which means basically that even if somebody signals us, we could put on the ready queue, then we gotta require the lock and we wake up by the time we finally get to run and once we emerge from condition wait, it's quite possible that that conditions have changed again to make it unfavorable for us to run so we always have to check our entry conditions, right? But assuming the entry conditions succeed, then we're gonna increment the number of active readers and release the lock, okay? And now we're gonna perform the actual read-only access in the database, okay? And then when we're done, we require the lock because we're gonna alter the monitor, we decrement the number of active readers, okay? And then we check if well, if the number of active readers is zero and the number of waiting writers is greater than zero, then we're gonna go ahead and wake up a writer, okay? And otherwise we're gonna release the lock. Now, if you look carefully, we know for a fact that there aren't any active writers to look at because we were an active reader so they ought to be sleeping, okay? And we know for a fact that we know there aren't gonna be any waiting readers either because there was a reader we'd get to go through. Now the question here about can we put WR plus plus before and WR minus minus what? After the loop, is that what you're asking? I think. So no, because waiting writer plus plus means there's somebody sleeping on the sleep queue, okay? And so we only wanna say waiting, excuse me, waiting reader plus plus if we're actually going to sleep. So WR plus plus and WR minus minus, we're tracking the number of readers that are inside this sleep queue. So we can't go on the outside because that wouldn't help us there, okay? Now, why are we releasing the lock there before we go into the database? Why don't we, yes, to allow more readers, exactly, okay? So we have to release here so that other readers can come through this entry point, okay? All right, now what about the code for a writer? Well, we acquire the lock. We have a different entry condition while the number of active writers or readers is greater than zero, we go to sleep. And if we succeed, then we increment the number of active writers and release the lock, okay? Okay, so let's go back for a second. Why can't we con broadcast here, okay? Somebody wanna tell me why we don't broadcast all the waiting writers, okay? So we only want one writer running at a time. Now, I'm gonna show you later that we could broadcast, but for now, let's do what seems obvious. We don't wanna broadcast because we only wanna signal one at a time, okay? So that's our reasoning for the moment, okay? So here, similar to what we said before, conditions a little different. Active writer plus plus basically says we now are an active writer, release the lock, perform the database access and checking out now, we acquire the lock, decrement the number of active writers and say basically now that if there was a waiting writer, then we signal it to wake up. Otherwise, if there's a waiting reader, we broadcast to them all, okay, and release. Now, Alexander's comment there is correct, which is why broadcasting will work. It's not as efficient. We'll get to that in a second. Just hold that thought for a few more slides here, okay? So once again, why do we broadcast instead of signal? Okay, because we can have multiple readers. All right, the question about why we can't increment and decrement waiting writers and waiting readers on the outside of the loop. Actually, that would technically work because we have the lock. I prefer this. I think it's a lot clearer because it shows what the conditions are. And if you would never condition signal if nobody's waiting, but let's keep the code this way now because I think this is a lot clearer, okay? Let's not confuse things too much. All right, so why do we give priority to writers? So notice we first check and see if there's any waiting writers before we decide to do something with waiting readers. Okay, good. So the real answer there is that's what we've chosen. The second answer is in general, there are far few writers than there are readers, so we just wanna get them out of the way. The third answer is that writers typically update the database and the readers are always gonna want the most recent writers, okay? Now, there was an interesting, hold on a second here. Let me just see. Now the other question was what happens if we signal and there's nobody waiting, okay? That won't happen here because we sort of check it before we do it. But in general, the key thing with a monitor is that when you signal, if there's nobody waiting, nothing happens, okay? So that's important. In fact, that's a crucial part of monitors. So when you signal and nobody's waiting, nothing happens, okay? And we will talk about that a little bit later, but the simple thing to imagine is if you've got a queue and you wanna signal anybody who's waiting, you just signal it rather than having to do something too complicated, okay? It makes things a little simpler, all right? Now, all right, here we go. We're gonna see how this code works. You ready? So we're gonna use an example. We're gonna have the following sequence of operators. We're gonna have a, oops, sorry. We're gonna have a read one from thread one, a read two from thread two, a write from thread three, and then a read from thread four. And initially, we're gonna set all of the variables equal to zero. So AR, WR, AW, WW, okay, you ready? So here we go. So first of all, R1 comes along and notice that we have nothing, nobody in the system. So everything's zero. So first thing we do is acquire the lock. We enter the monitor and then we say is AW plus WW greater than zero? Answers no. So now all is well, we increment the number of readers, AR plus plus, that gives us a one. And we release the lock. Now, I wanna point something out. Normally, you have to be very careful whenever you do plus plus on a shared variable. AR and WR, for that matter, are great examples of variables shared across an arbitrary number of threads, okay? So why can we say WR plus plus or WR minus minus or AR plus plus without worrying about this? Because we have the lock, exactly. So notice we are in a critical section. We acquired the lock, we're releasing it down here. Everything in the middle here you think of as a critical section, okay? And so therefore we don't have to worry about the atomicity anything else, okay? And after we've released, why release the lock there again before we enter the database, right? To allow more readers, exactly. Okay, so the condition variable and the monitor, monitor is actually being used to control access to the database so that it meets our constraints. So any thread that gets into this database, we've already checked its access, okay? And once it's there, it's accessing properly and we're not violating the reader's writer's constraints, okay? Now, here comes the next reader. R2 comes along, acquires the lock. Notice it can acquire the lock because the lock is free, okay? So it's not a big deal. Now it's gonna check this condition. Is AW plus WW still equal to zero or not greater than zero? Yep, so we increment AR plus plus, okay? Now we have two, release the lock and now we've got two readers simultaneously accessing the database, okay? So far this is kind of boring but now the database could be accessed for a long time. So these readers are busy doing something complicated. There are no locks that are held and only AR is non-zero. So no locks are held and the only this integer variable AR is two and nothing else is holding the system up. So we're good, okay? Now along comes the first writer. Now things get a little interesting. So once again we grab the monitor lock, that's great and now we say is the number of active writers plus active readers greater than zero? Yep, okay? So now we know there are readers in the database and so therefore we increment WW, okay? Because there's a waiting writer, we go to sleep and that's it. So that guy is sleeping, okay? And he's sleeping where? He's sleeping on this okay to write queue, okay? Meanwhile, our three comes along and notice that the original two writers are still running, okay? Now this is gonna be a little different than the two writers or two readers at the beginning, right? So we had grabbed the monitor lock. Oh, by the way, for those of you that are purest and wanna think under the covers, as soon as we do conditional wait, notice we've done that with the lock, right? We're still in the critical section but when we do a conditional wait, we not only give it the conditional variable, we also give it the lock. So under the covers, the scheduler releases the lock at the same time it puts the thread to sleep. So the lock is free but you as a writer of code should think of the lock as acquired for everywhere in between acquire and release, okay? This is, I'm telling you to fool yourself because this is the way to think in this paradigm, okay? So that's why when the reader comes along, we can grab the lock cause it's free, okay? And now is aw plus ww greater than zero, yes. Okay, so at that point we're gonna increase the number of waiting readers and go to sleep. All right, why did we do that? Technically speaking, because there are readers going on here, we should be able to let the reader go through and start reading. But why don't we? Why do we choose to go to sleep, okay? We wanna let w1 go first, exactly, okay? So because there is a waiting writer, we're gonna go to sleep as a waiting reader, okay? So now you see the writer is getting priority. So in fact, what's gonna happen is AR is gonna go from two down to zero as those two original readers finish and then we're gonna let the writer go forward and then finally we're gonna let that reader come in, okay? Okay, our three can't start cause there is a waiting writer. So here's our status. R1 and R2 are still reading away. They're checking out the whole database. W1 and R3 are sleeping. W1 is sleeping on okay to write and W3 is sleeping on okay to read. All right, are there any questions on our current state of the system? We good? All right, gonna move on. So now what happens? R2 finishes, R1's still accessing. W1 and R3 are still waiting. R2 finishes, which means they exit the database and they acquire the monitor lock, which is free, right? They decrement the number of active readers, okay? So now we're down to one up there. And now they're gonna check the exit conditions. And if the number of active readers is zero and the number of waiting writers is greater than zero, then we're gonna signal somebody. Well, if you look, there's still an active reader in the database. So you could say that this guy exiting doesn't have to do anything cause he certainly isn't gonna wake anybody up. So he's just gonna exit releasing the lock and now we're done with him. Meanwhile, we wait maybe a long time, who knows? And now R1 finishes, acquires the lock, decrements the number of active readers. And so now we just hit a milestone. We just went back to zero on the number of active readers. At this point, is active reader zero and waiting writer is greater than zero? Yes. That point we're gonna signal on the okay to write condition variable that somebody can wake up. Okay, so basically all the readers are done. We're gonna signal writer W1, okay? And then we release the lock, okay? Now, let me go back here for a second. I didn't actually simulate the release the lock, but because we have Mesa scheduling, when we signal all that we're doing at that point is just putting the W1 on the ready queue, okay? And so there's nothing happens here when I signal to W1 other than to take it off the sleep queue and put on the ready queue. Now, if this were poor scheduling instead of Mesa scheduling, what would happen is this signal would cause the lock and the CPU to go immediately to W1 and then W1 would do some stuff and when it released the lock, we would go back here to finish up, okay? So that has some really nice mathematical properties as we kind of talked about last time, but it's really hard on things like system cache and it's slow. And instead what we did when we signal is we just take that waiting reader, excuse me, waiting writer, put it on the ready queue and then we're gonna keep going, okay? And keep using our cache date until our quanta comes up. All right, so later when W1 receives a signal from R1 that wakes it up, it was put on the ready queue we said earlier. It ran, there was an interesting thing in Piazza today, which I answered. So what actually happens here is W1 is gonna have been on the ready queue. It wakes up and under the implementation of conditional weight, what it's gonna do as soon as it wakes up is it's going to try to require the lock inside conditional weight. So it'll try to require the lock and if it turns out that the lock is taken because somebody else got in there before us, then it'll go to sleep again, but now this time it'll go to sleep on the lock, not on the condition variable, okay? Now, let me make sure I understand Brianna's question here. So signaling does not release any locks, okay? If you look back here, when we did conditional signal, what it did was it just put that writer on the ready queue, then we released the lock here. So we actually decided to release the lock, but we could do it whatever else we wanted and not release the lock right away, okay? It's, so how do we exit the while loop for the reader and writer, okay? So the condition variable okay to read or write is changed. Can you explain what you mean by how is it changed? Okay, well, you're thinking about that question. I'm gonna answer Carolyn's question here. How do we exit the while loops? Well, we exit the while loop here because something about these variables changed, okay? And so let me answer that question with respect to the writer. So when we signal the writer, notice what we've done at this point. We've decremented AR down to zero and we signal the writer, okay? And so now AR is zero. And so then we released the lock and that signal put the writer on the ready queue and up here, the writer was on the ready queue, okay? It woke up, it tried to reacquire the lock, let's assume that worked, it grabbed the lock and now it returns from conditional weight. At which point we decrement the number of waiting writers because there aren't any anymore, okay? Well, no, excuse me, there's one less of them because we just woke up. We come back to the while loop and now in answer to the question, AW plus AR is now, well, what is AW plus AR? So AW is zero, AR is zero. When I add them together, they're no longer greater than zero. So that's what just changed. And so as a result, we're actually gonna exit the while loop, increment the number of active writers, release the lock and now the database has a writer in it and notice that active writers is equal to one and waiting writers is equal to zero, okay? Questions, okay? And so the condition variables merely let us wait and when we wake up, we recheck our conditions and assuming that whoever signaled us changed the conditions that would have put us to sleep, then we'll exit the while loop. That's exactly what happened here, okay? So when we're waiting on the lock but not the condition variable, that would be a situation where we executed condition weight, we went to sleep on the condition variable, somebody signaled us, we went on to the ready queue, we can't emerge from condition weight without the lock because remember the way I'm telling you to think about this is you always have the lock in between acquire and release, this is a critical section. So the implementation of condition weight under the covers tries to reacquire the lock and when it finally does, then it returns and now I know when I emerge from condition weight, I know I have the lock again. So okay to write is just a condition variable so there could be many writers on there, okay? Condition signal, how does this signal back here decide which one to wake up, it's undetermined. Okay, think of it as a non-deterministic choice, randomly picks one, okay? Now in fact, that can matter sometimes, you may have to be careful not to assume that somehow writers are gonna be woken up in the same order they're put to sleep. If we ever have a piece of code where we want you to make that as an assumption, we'll make sure to tell you that assume that they come up in the same order that they went to sleep. But unless they were told that for some reason, assume it's non-deterministic, okay? The question is if okay to write a cue, isn't there an inherent order? Well, there may be some combination of put on the weight cue, put back to sleep, somebody else gets to run. Think of it as you're just not sure only one of them wakes up, okay? It may, there may be many different reasons why they don't wake up in order. All right, so here's a situation where the writer is in the database. And if you notice, we have a waiting reader, so he's still sleeping. So we're writing away, finally we finish, we acquire the lock, okay? We have the monitor, we decrement AW to zero. And now we say, are there any waiting writers? No. Is the number of waiting readers greater than zero? Yeah, look, there's a waiting reader. So what we do is we're gonna broadcast everybody. So now here it's basically if, it doesn't matter how many people are sleeping, we're gonna wake them all up, okay? And then of course, back here, we're gonna release the lock and go forward. Here, potentially, suppose there are 20 of them, doesn't matter, they all wake up, but only one of them gets to run at a time. So even if there's 20 of them that were broadcast, it's the first one that grabs the lock again that emerges from condition wait. And it's gonna say, oh, look, I'm gonna set waiting readers to zero, it's gonna check its condition, okay? It's gonna say that while loop is no longer satisfied, it's gonna set active reader plus plus, I sort of hurried this along a little bit, sets active reader to one and accesses the database. If there were 20 of them, the moment that this first one released the lock, then the second one would succeed in grabbing the lock, emerge from condition wait, go through the while loop, exit and go to the database, et cetera. So if there were 20 of them on that queue and we broadcast to them, they would one at a time grab the lock, decrement the waiting reader count, increment the active reader count and access the database, okay? And then finally, we're done, we acquire the lock, we decrement the number of active readers, we release the lock and we're all done. At that point, the database is idle and we have made our readers, writers requirements. Any questions? So the thing to think about here is notice how clean this was, right? With the monitor paradigm, a lock in multiple condition variables is very clean, okay? Now, when you say this middle section here, the access database is, I don't know that I would necessarily call this a critical section because we can have multiple readers in there at once, but it's the resource that we're doing some sophisticated control on where we're saying there can be multiple readers or one writer, but not both at the same time, okay? So why again, the while loop in the, here, you're asking why is there a while loop here, that's because we have Mesa scheduling. Because when we go to sleep, when somebody signals us and we wake up, it's quite possible that somebody else may have grabbed the lock before we did and changed the conditions. Like, suppose we're the last reader and we're about to wake up, but what happens instead is a writer comes along and beats us to the punch and increments the number of active writers we're gonna go to sleep against. You always have to be aware of what's going on with writers we're gonna go to sleep against. You always have to keep checking the condition in a loop and when you can check the condition and you have the lock, then you don't go to sleep and you know that you have the condition. That's Mesa scheduling. All right, so questions here. Can the readers starve? Well, what do you think? Can the readers never get to run? Yep, why? Well, because we always wake up, check our conditions again. If some writer keeps coming along, they may prevent us from going forward, okay? What if we erase the condition check in the reader exit? So this is interesting, right? So if we say AR minus minus, and then we say, well, if AR is equal to zero and there is a waiting writer, suppose we don't look at that. Now what? Well, the potential here is we could end up signaling a writer even when there are still readers in the database or we could signal the writer when there are no writers. Okay, so does this still work? Or did we just screw everything up? So the answer here is not quite where Mesa is, so we don't care, but it's the same idea. We always recheck our condition. So if we woke up a writer and there wasn't any reader and there were still readers in the database, the writer would go immediately to sleep saying, oh, there's readers in the database. So even though we've woke them up incorrectly, the entry conditions take care of making sure that we never violate our invariance. Yeah, it's kind of a self-checking thing. And it means that relative to the non-Mesa scheduled or the horse scheduled situation, this one you can be a lot lazier, okay, if you miss something. Now, of course, this is inefficient because we're gonna waste time with scheduling, but it sort of is much more likely to be correct. And there may be situations where you can't get the exact conditions for signaling. And as long as the waiter checks its own conditions, then you should be good to go, okay? And even if we turn the signal into a broadcast, okay? That's okay, because even if we wake up 1,000 writers, only one of them will get to go forward and the rest of them will go back to sleep. Now the question is, how much time do you spend checking in Mesa? Not a lot, typically you don't loop too many times, okay? And the benefit of Mesa is you get cash benefit, the schedulers are simpler, the code is much easier to verify. And so the advantages of Mesa scheduling far outweigh the disadvantages. You know, the disadvantage being you have to have a while loop and you might occasionally loop more than once, okay? And now the question is, suppose we were keeping writers and readers separate, but suppose we only have one condition variable, you know what then? Well, here's an example. So here's the reader and writer and notice that I only have one thing called okay to continue. And so if my reader entry condition is not good, I go to sleep on that. And if my writer entry condition is not good, I go to sleep on that. And then when I'm done, the simple thing would be, well, I just, I signal on okay to continue, okay? And this seems like it ought to work based on what we just said, but if you're carefully thinking through, you can see that this might not be quite right because R1 arrives, W2, R2 arrive, or R1's still reading. And you get a situation where R1's signal is delivered to R2 instead of W1, it doesn't quite work. Okay, and so in this situation, you're gonna have to actually broadcast it to wake people up. And that's really because we haven't distinguished readers to writers and so we just gotta wake them all up and let them sort themselves out, okay? So when we get lazy, sometimes we have to get really lazy, okay? To get correctness. Now this is gonna have some inefficiencies to it in that there might be a lot of things that wake up and then have to go back to sleep. So as we know, so this wouldn't be as easy for, well, this would actually have writers with priority because any writers that happen to be in the system would wake up and run. If there was a couple of readers that got to go first, they might get to slip in there. So it wouldn't be strictly priority based. And there's also a way, this is for you to think about offline, but you can also arrange so that things come in exactly the order they run such that readers and writers get to go in phases. And so you don't have readers having lower priority than writers, you can actually arrange for something more sophisticated. But that's for you guys to think about. So the exam is Thursday. It's getting close, okay? Video proctored, you've seen that information, okay? We want you to have your webcam and your phone. You got to figure out how to position it. That's all on Piazza. And you need to talk to the DAs if there's some issue with that. Topics are basically everything up to today's lecture. If you notice, we really haven't done anything new today. We're gonna talk a little bit more about implementation of threads in between, but these are things you already know something about from the labs. But scheduling's not part of the exam. So there's nothing on the lecture for, there's nothing from Wednesday's lecture. So part of the video proctoring is requires a camera on your face. So talk to the DAs. So homework and project work is fair game. So you should know what you've been doing on your projects. Okay, so midterm review, there is one tomorrow. There's a Zoom link that's gonna be mailed out. And it should, it may have gone out already. I know that it exists and I know that the head DAs have it. So they may have posted quite yet, okay? So any questions? So yeah, the point of Zoom proctoring is the camera on you while you're working. So you need to figure out how to arrange that. So that's a good question. Actually, that's a very good question. Yes, you can have a cheat sheet both sides, okay? And written. I guess we forgot to mention that to you guys. You're welcome to put together a cheat sheet. Okay, but consider this otherwise closed book, okay? We will give you any information that you need. Okay, if you need man pages or other things we'll give those to you. You should be familiar with the simple calling sequences. Okay, and it'll be more mostly pseudocode, although try to write as correct code as you can if we're asking you to write C, all right? So today's lecture potentially is, as I mentioned in scope, but that's because this is stuff that we already talked about last week. Okay, you should probably know the signature, but we'll make sure that we'll probably make sure you have complicated signatures, but things like open have a reasonably simple signature. And if you transpose something, we won't give you a hard time about that, okay? All right. Now, the Zoom Proctoring info is on Piazza, okay? I think we've posted it, it will make sure that we have, we'll make sure that it's posted if we haven't, I thought it was up there. So let's hold off on any further questions about the video proctoring, but we do want, this is part of making sure that we have a nice clean exam and so everybody can feel comfortable that everybody else is behaving themselves, so. Okay, and we're gonna, the record, the way that set up for the phones is gonna be in the cloud, I'm pretty sure that's the way we sell on it, so you don't need a lot of local space for make this work, okay? So can we construct, so moving on to the topics here, can we construct monitors from semaphores? Well, it's pretty easy to make a lock with a semaphore, that's just the mutex version. Can we implement condition variables this way? So there won't be anybody, for those of you that are worried about the video proctoring, only the TAs are gonna be looking at cameras, not about everybody else, so. Can we implement the condition variable this way? Wait basically says for the semaphore, that's the condition variable, we just do a semaphore P and signal does a semaphore V. Can anybody say why they, this might or might not work? Okay, so semaphores have a Q, right, they can go to sleep, so that's not, you know, this has a Q associated with semaphore P. What else is an issue here? Yeah, so the big deal here, I'll assume this is what you meant, is that you can't go to sleep with a lock, with a semaphore, right? If you grab the lock and then you call wait, you're gonna deadlock your system because you'll put this to sleep and you'll hold the lock and everything will be broken. So this can't work for a condition variable even though this seems like it oughta, okay? So that will deadlock. Does this look any better? So this says, well, the way we do wait is we release the lock, we do a semaphore P and then we reacquire the lock and signal just does a semaphore V. What do you think? Okay, so the worry here that wait isn't atomic? Well, the problem is not actually out of misogy here, the problem is history. So if you think about it, if you do a bunch of signals and then do a wait in this implementation, the signals increment the semaphore and so the next waits are gonna go straight through. However, wait in a monitor immediately puts you to sleep no matter what the history was, okay? So a signal to an empty condition variable does nothing and this implementation doesn't do that trick, all right? So this is, it may be subtle but this would not give you a semaphore, give you a condition variable portion of a monitor, okay? Everybody with me? When you go, whenever you do wait with a monitor, it would, you're always supposed to sleep. The problem with this is if you do a bunch of signals and then do a wait, the wait is not gonna wait. So I would think of it, if you have signals prior and then you wait, you don't go to sleep and that's actually not the monitor interface. Okay, what if the thread signals and no one is waiting? Okay, that's a no op in a monitor but if a thread later waits, the thread waits with a thread V and nobody's waiting, you increment and later the P just decrements and continues. Okay, so anytime you go to sleep, well, I wouldn't worry about system calls now because we're assuming that semaphores do whatever is required to put you to sleep, okay? And so probably inside the semaphore might be a few texts or whatever we talked in the beginning, but yes, anytime you go to sleep, that's a system call but that's not really our issue here because we're assuming the semaphores have that figured out, all right? So the problem with the previous try is that P and V are commutative whereas signal and wait are not, okay? And so that's an issue, okay? And here might fix the problem. What we do is we say wait does release semaphore P acquire and then signal says if the semaphore Q is not empty, execute semaphore V, is this okay? Good, this is not okay because semaphores technically don't let you check their Qs, okay? So that's the issue, okay? And there's a race condition here and that the signaler can slip in after the lock release and before the waiter executes semaphore P. Turns out you can do this and you can even do it for whore scheduling. There's one in one of the books, not your current one, but you could look that up and it's a much simpler Mesa scheduled solution which you could also figure out. And as a hint, it has something to do with the fact that when you're holding a lock, you might actually have other variables, integers that could keep track of stuff. So conclusion was, remember this, this is the Mesa monitor pattern, okay? The Mesa monitor pattern is grab the lock, loop until conditions are right, unlock, do something, and then you exit by locking, maybe changing some condition variables, signaling and unlocking, okay? Well, this one's a little subtle. So I will say by the way, synchronization's the hardest topic that we'll cover in this class and especially the first time you see these synchronization conditions, it takes a little while to figure out what to look for. So this is a par for the course. You've entered in to the greater knowledge of synchronization here as a result of the last couple of lectures, but it'll take away a little bit for it to settle in. All right. Now, I just wanted to quickly finish up because I want to move on to some other things here, but if you wanted to do semaphores in C, you gotta be really careful because here's a situation where, or not semaphores, if you want to do synchronization support in C, here's a situation where if you acquire the lock and then you run into some error, you need to release the lock and return because otherwise if you just return and the lock is held and things might be broken, there's something which you can look up, do a Google on set jump, long jump in C, which is even trickier because this is the stack and so we run A, runs B, it calls something called set jump, which really says that if we now call C, D, E, here E can call long jump and it'll basically pop back to B and it'll pop off all those chunks of the stack. That's support in C, but if you have that, you can end up jumping back to B and the lock is still held. So you gotta be very careful with exceptions to make sure you can release the lock. And this gets even worse if you have more than one lock going on. So if you have lock one and lock two, then you have to figure out how to release them all under errors and so C is not great when you're dealing with lock acquire and release, but you gotta be careful. C plus plus is both worse and better for this. The one thing that's worse is this, if you notice this pattern here where I have a function, I acquire the lock, I call some other function and then I release the lock. Well, that other function could get an exception. So C plus plus and Java and some of those others have exceptions. Well, the issue there is if you throw an exception, it's not necessarily gonna return to do foo. It's actually gonna jump out of the caller and you've left the lock held, okay? So you might say, well, what I really do is I try do foo and I catch errors and I do the release, okay? This is a pattern you might be familiar with. Better in C plus plus is guards. So this is a pretty cool idea. Here's a function where I grab a lock, but I do it as a special guard lock. And what happens is this gets, notice that this is in the local variable position. I know you don't necessarily know C plus plus a lot yet, but here's a local variable position. What that means is this lock variable was actually allocated on the stack, on entry to this procedure. And any exit of that procedure, no matter what will release the lock. And so you can have exits. Normally you can have exits because of exceptions and the lock will always be properly released. So if you ever find yourself programming in C plus plus, you wanna make, and using locks, you wanna make sure that you have something like this, a guardable lock, so that it'll be automatically released no matter what causes your procedure to exit. The other thing is Python has a with key keyword, which I'm sure you're familiar with, which is similar. Okay, and this is again, with lock. If there's any reason that this with gets exited, this with block gets exited, then the lock will be released. And by the way, with is good for all sorts of things, including opening files and having them automatically closed when you exit the block. Java. Yep, Rust has a mutex guards. There's all sorts of stuff, okay? Most languages that are more powerful and more modern than C certainly have nice clean ways of doing this. I did wanna point out Java, which you're all familiar with for various reasons, actually has synchronized key words. So every object actually has its own lock inside of it. And so this class account, every time you allocate a new account object, then if you have a public synchronized method, then when you run that method, what it does is it sets the lock on that object and runs a lock. And so back when we were talking about the bank case, if you make things synchronized like deposit, then this balance plus equal amount, this automatically becomes a critical section that's protected by the lock that's inside the Java object. So that's kind of cool. And then Java also has support for monitors. And so in addition to that one lock, there's a single condition variable and you can use wait and notify or notify all or the equivalent of signal and broadcast in Java. So monitors are well supported by modern languages as well. Okay, so last topic, we'll see how far we can get with this. I wanted to do a couple of things just because I've seen some queries on Piazza that suggested it might be helpful to have a couple of quick discussions here. So if you remember, we were talking about multiple threading models. This particular threading model is the standard one that you're dealing with with Python, for instance, or even Linux, every user thread has a kernel thread associated with it. And the way that happens is that for every thread, kernel maintains the threads, TCB of course, thread control block, but also a kernel stack for syscalls, interrupts and traps. And sometimes this kernel state or the stack is called a kernel thread. Okay, so don't let that throw you for a loop. It's state and why do we call it a thread? Well, it's something that can be suspended and put to sleep inside the kernel. Okay, so the thread is suspended but ready to go when the thread is running in user space. And as soon as the thread goes into the kernel, then the kernel thread takes over, okay? And there are actually threads that are only in the kernel. So they still have a TCB, they still have the kernel stack, but they're not part of any process and they're busy doing things for the kernel, okay? And so those don't necessarily even have to run at user level. So Pintos, which you're now familiar with, if you were to look at thread.c, what you'd see here, for instance, is that the kernel portion of a process or of a process is basically this, a four kilobyte page which includes both a TCB at the bottom and the stack at the top, okay? And so what does that mean? That means that the kernel stack is maximum 4K. In fact, it's a little bit less than that. That's however big the size of the TCB is, okay? And so why is there a magic number here? Well, that magic number is some random bits that if your stack happens to overflow, it's likely to screw up the magic number and you might have some idea that there's a problem. But the key thing here is that when you're in the kernel and you're running your Pintos kernel thread, you better not be doing Fibonacci or anything super recursive because there's only a little bit of stack there, okay? And then also there is a page directory which points to a page table that's kept track of in the thread control block as well. Linux similar 8K, so it's two pages, okay? So two pages to hold your stack with a thread control block and then something called a task struct down at the bottom that basically is associating the TCB with task state which could optionally be part of a process. We're not gonna go into that in detail right now. But normally what multi-threaded processes are which is not Pintos, I'm sure you're all aware now that every process has exactly one thread in Pintos. Traditionally multi-threaded processes have a process control block per process and then each PCB has many TCBs. And so these are the TCBs, okay? Thread control blocks and every process control block has many of them if there are many threads, okay? Linux has one of these task structs per thread instead and threads belonging to the same process share things like address space and so on. So Linux is a little bit less clear about is it a process or is it a thread in some other process? But for now, rather than worrying about this, this idea that there is a single process control block that points at one or more threads is the way you ought to think about this. And in Pintos it's easy because you can only have one thread per process. Now I'll leave that. So what does our kernel structure look like? Well, here's two threads. They each have their kernel thread, right? Which means it's a stack and a process control block piece to describe the process. But the kernel thread is this kernel stack, okay? And then the kernel also has code globals and heap for all the kernel code. Now there was an interesting discussion I saw on Piazza about well, kernel, if the kernel is holding a data structure like a pipe, where is it? Well, kernel has got lots of memory space. It's got a heap, okay? It's got globals. So the kernel has a bunch of data that's unique to the kernel that it can store over time. So the stack is not the only place for data to be stored in the kernel, okay? There's also heap and globals. Now if we go to a process that has multiple threads, then what do we see? Well, I'm sorry about the typo's here with global wrapping around here, but there's basically a code global heap that's shared. And then each thread has its own stack and it has a kernel stack. So in this process number one with two threads in it, there are two kernel stacks to match the two threads that are at user level in that process, okay? And the code globals and heap for the kernel. Here is a full picture where we even have some kernel threads that don't have a user piece to them, okay? So in that scenario, now we can have these kernel threads doing things for the kernel. We have the kernel portion of, or the kernel threads associated with the processes. Everything that's got a kernel thread is now schedulable. So the scheduler in the kernel chooses between different kernel stacks and therefore different threads. And so when we give CPU time out, and that's gonna be next lecture, I know you'll be studying, but you should definitely make sure to come and hear about scheduling. Scheduling itself starts talking about how do we schedule across these kernel threads, okay? And of course, because we have to enter the kernel to do scheduling, then if we were running some thread in user space, we first transition into its kernel stack and then we do scheduling among those threads, okay? So we gave you this example. Remember the thread S goes to T and T goes back to S. And the reason I brought this up again is that scheduling, just like I showed you, we have threads have their own kernel piece, okay? And that kernel thread portion of a user thread is the thing that gets switched when we go from scheduling one to another, okay? And that actually was here as well. So here was an example I gave you. This is from a couple of lectures ago. Time is to the right where we have a user thread that's running. It's got its program counter or CSEIP instruction pointer and its stack pointer at user space. And then when an interrupt happens or does a system call, the very first thing we do is we switch over to the kernel stack and the kernel code. So notice that these registers are now in red and they're actually pointing at kernel code and kernel stack. And the remaining and the ones from the user are saved on the kernel stack. So clearly if we wanna start this user thread over again in user space, we need to know where we were for the user stack and the kernel stack, excuse me, the user stack and the program counter. And so we save them on the kernel stack, okay? And we also might, and there's also a page table base pointer, we'll get into more of that later. And then we save out the extra registers and now here in the middle, we're running on the kernel stack. We've saved everything we need for the user portion of that and we're running away and we're doing a system call, maybe we're doing interrupt handling, maybe we're doing scheduling, okay? But notice that the registers, that's this box here has stack pointers and instruction pointers that are all pointing into kernel code. All of the user stuff is saved on the kernel stack so that when we wanna return, now we basically undo it, okay? So we first restore the registers that aren't the stack pointer and the instruction pointer, okay? And then when we do a return, it returns the instruction pointer and the stack pointer and we're good to go. Now the question is, how does the interrupt know which kernel thread is associated? Well, the answer is that if you look at the lecture where I first introduced this, the stack pointer for the kernel thread associated with the running thread is stored in the TSS structure. So at the moment you do an interrupt or a system call or any transition into the kernel, what happens on the x86 is it immediately grabs that new stack pointer and inserts it into the stack pointer portion of the registers, okay? So that's how that happens. And so when we change from one thread to another, which I'll show you in this next slide, then we have to swap out that register for TSS because we've got a new kernel thread. And if you notice here by the way that we started with thread A, we ended with thread A, we just ran in the kernel in the middle here. The alternative is, and you can look at switch.s, we start with thread A, we go into scheduling, we restore thread B and when we're done, it's now running the other thread. Another view of this is in fact, here's the Pintos for instance, one thread per process. Okay, why do we need a TCB for every thread? Well, because the TCB has all the information about its stack, its priority, it's got a list pointers that point it with the other thread. So there's a bunch of stuff about the TCB that's important for maintenance, okay? And if you notice here, this is for example, just a different view of what I just showed you here, every kernel or every, excuse me, user thread has its associated kernel thread with the stack kernel stack on top of it, okay? And the instruction pointer is called the PC. Here's another view of what we were just talking about when we're running in user mode, the instruction pointer is pointing at code in user mode, stack pointer is pointing at the user stack. And then this kernel stack points at the kernel pointer in the kernel thread associated with the running user thread, okay? And if you really wanna know what is KSP, well, this represents that special stack in the TSS, the thread state structure that holds that kernel thread for us, okay? And here's an example where we're running in a kernel thread which doesn't have a user portion. So notice we're running in kernel mode, the programming level is zero. Notice it was three back here as user mode, okay? And here we are in kernel mode and notice we're running kernel code and we're running on a kernel stack. So the question about FI base is manually, it's set basically as part of our scheduling, okay? If you notice here's an example where we were running the user code but now we've taken an exception or an interrupt or a system call and now at this point we're on the kernel stack associated with the user thread. Oops, did I just crash here? Oops, sorry. So I wanted to say a little bit, although we're running tiny bit low on time, guys give me a moment. If you notice when Pintos hits an interrupt, what happens is the hardware says, oh, an interrupt is something, okay? That interrupt for a timer for instance might be 0x20, okay? Because that's interrupt, you know, number 20 hex. What that means is it looks at a table and says, oh, this interrupt is 20 hex. Let's grab the instructions to run and it turns out in Pintos what happens is we push the number 20 on the stack and we jump to an interrupt entry which runs a generic handler but at that point notice we know which interrupt it was, it was 20 hex, okay? This is an interrupt vector table, yes, okay? And so this is basically how the kernel ties in all the interrupts to the code that should run. So in stubs.s, there's a generic handler if you take a look at your code. What happens there is we enter the interrupt, we save the registers, okay? So this is a situation where we go to user to kernel via the interrupt vector that's gonna take us to this situation here where we are gonna go into the interrupt vector table, it's gonna tell us where to start running and when we enter the kernel, we're gonna transfer so that we're gonna start running on code associated with the interrupt, okay? So the various numbers, you take a look at that table correspond to different interrupts, okay? Some of them are system calls, some of their interrupts, okay? So here's a situation where now we just switched to the kernel thread for the process and we might've been pointing at code that was associated with the interrupt handler in that instance, okay? But we're running on the stack associated with the kernel thread associated with the user thread that was running, okay? And so here, we now call the actual interrupt.c to handle the handler for timer interrupt, okay? And Pintos has a second table which is a mirror of the first one, okay? But that table is for Pintos handlers to handle the timer interrupt, for instance, okay? And if you look in timer.c, you'll see that. So that timer interrupt is Pintos' version of what to do with timers and it's gonna deal with ticks, okay? And the tick updates a bunch of counters for threads and if it says, well, this thread's gone too long, then it's gonna set a yield flag and we know at that point that we're gonna yield the current thread and do something else, okay? Thread yield basically is on the path to return from the interrupt. It's gonna set the current thread back on the ready queue and then schedule to schedule the next thread which is next lecture, which selects the thread to run and then starts running it, okay? It's gonna call switch threads, which is switch. Remember, we talked about that earlier. It's gonna set the status to running. If it's a user thread, it's gonna activate the process and so on and then it's gonna return back to the interrupt handler. I'm just giving you this very quickly so you can see this once, okay? So here's a situation where we were running this guy and the scheduler decided the second guy's gonna run and so switch, switched us from the kernel thread on the right to the kernel thread on the left so that now when we go to return, we're gonna return to user mode, okay? So each thread is gonna have its own unique thread ID, okay? And the kernel thread is associated very tightly with the thread that's running because this is a thread control block for that thread. Now, so notice that we called timer interrupt, we did tick, we decided we needed to yield, we decided we needed to switch. So when we switch threads like this, now notice what happens if we return from interrupt, we're gonna return voila to a new thread, okay? That's exactly how scheduling happens, okay? So we just undo all of this and we return and suddenly we're running the new thread, excuse me, instead of the old one and the old one is on a ready queue somewhere. So this is the magic, right? The magic is interrupts, timer interrupts happen, they decide whether it's time to schedule, they pick a new guy to schedule, they take the current kernel thread, put it to sleep, they load the new kernel thread and then when they return from the kernel, they're now running the new thread and we've just scheduled thread B instead of thread A. Okay. This is my favorite quote, I have to make sure everybody sees this. Dennis Ritchie, one of the designers of C and one of the original UNSCs basically put this comment into the code in the core that runs switch. It says, if the new process pause because it was swapped out, set the stack level the last call to save you, this means that the return, which is executed immediately after the call to ARETU actually returns from the last routine which did the save you. So he's talking about switch. Look what it says, you are not expected to understand this. That's my favorite comment in any piece of code ever. Okay. Now, the question here is the time between timer interrupts decided by the hardware, yes. Okay, but only because the operating system is programmed it that way. So the timer is programmable but once it's been programmed then it goes off on a regular basis because of the hardware. Now, if you remember what scheduling is about, deciding who's next. And I'm not gonna go into this now but I want you to know next time we dive into that decision-making. How do we decide which is the next thing to run? The other thing I wanted to briefly say something about here if you give me just a few, a couple more minutes here, I'm almost done. If you remember, every process goes through a translation to take virtual addresses to physical addresses and that translation goes through a page table and that lets us basically make sure that every process has a protected space to run in and the kernel has a protected space to run in, okay. And so the address space basically is the primary mechanism for handling that translation and don't worry, we're gonna go into address translation in great detail in a couple of weeks. But if you remember, the basic idea was this one of mapping. So the code for program one is mapped to a code segment and data is mapped to a data segment, et cetera, which is independent from program two and program two basically looks just like this particular view of memory that we've been dealing with. And what we're saying is that this address space that you're used to gets mapped through the translation to specific places. What does that really mean when we're talking about kernel space? Well, what it means is the virtual space that a process sees in Pintos, for instance, it has kernel space at the top, okay, and user space at the bottom. So all the things the user is using are in this bottom spot, which has page table entries that point to physical memory. The kernel space, while it's mapped, isn't available to the user, okay. So there are a bunch of page table entries that are in the virtual address space, but if the user code tried to use them, they would fault, okay, and it would get a page fault. And why do we do it that way? Well, if you look at the page table entry, by the way, this is gonna be described in great detail in a little while, a couple of weeks. The user supervisor bit basically says is a page table entry for the user or not? If the page table entry is only for the kernel and we're in user mode, then you get a page fault and you can look at pagedir.c, by the way, to see this. So what does that mean? That means that if we take an interrupt, notice how my programming level went to zero, then all of a sudden the parts of the kernel space that were unavailable are now available and these page table entries are ready. So now we can have the kernel fully protected, but all of that space is now available for heap. You know, there was questions in Piazza, where are the pipes stored? Well, they're stored in kernel space. How are they protected? Well, they're protected because the user is not allowed to access them. And of course, the page table base register points at a particular place in memory where this page table is. And so when we switch from one process to another, we just switch the base table, okay? All right. And so for instance, one kernel, many stacks, kind of looks like this, okay, that's the many threads and those stacks are only accessible when we're in kernel mode, otherwise the users can't touch them. Okay. Questions? I think we've run out of time. I was going to look at a little bit more, a little bit more detail about the storage levels and kind of how things like pipes and stuff worked. We'll save that for another, we'll save that for next lecture. This will not be on scope for the exam. All right. So what I wanna say here in conclusion, we've been, we talked a lot about monitors. I will hope that everybody kind of has a good idea now how the monitor works. So the monitor is a programming paradigm. It's a lock plus one or more condition variables. You always acquire the lock before accessing any shared data. And then in the critical section of that lock, you check parameters and potentially go to sleep, okay? And so you always go to sleep, but only when you hold the lock, okay? Monitors are the logic of the program. You wait if necessary, you signal when there's a change so that waiting threads wake up. And monitors are supported natively in a bunch of languages, we showed you that. We went over in great detail in the reader's writer's example. We talked about kernel threads, which are stack plus state for independent execution in the kernel. Every user thread paired one-to-one with the kernel thread in a typical pin toss, certainly. And also in typical Linux, which is not running threads at user level, okay? And the kernel thread is the thing that lets you go to sleep. So the good thing about every thread having a kernel thread is you can put it to sleep if you try to do IO and none of the other threads are affected. Okay, next time we'll talk about device drivers. All right, and so the page table base register, one last question on the chat here is switched from one to another when you change the PCB, not the TCB. So when you change which process you're in, then you gotta change the page table base register. If you're going from one thread to another, you don't have to change it. And actually just I had a little bit of a seek out. Anyway, I could show you that later, but if you were to go back and take a look at the slides where we were talking about switching from one thread to another, what you would see there is that I basically changed the page table base register to page table base register prime. So all right, I think we are good. So I wanna bid everybody adieu. I hope you have a good night and we will see you on Wednesday, I hope, and good luck studying. All right, have a good night, everybody.