 Hi, everybody. Sorry for the delay. I need to set up the camera. So, at this point are you finishing? Apparently, there's some sensor here. I cannot close the door. At this point, is everybody finishing up a sum of zero? How many of you have finished a sum of zero? Good. That's more than I expected. Yeah, so basically this year is how we have one bulk deadline for both a sum of zero and one, which will be next Friday. So, at this point, you should be already familiar with the environments, how to compile the kernel, how to run the simulator, and also you should have a pretty good idea of a sum of zero and ready to wrap up and move on to a sum of one. Because a sum of zero is kind of really just warming you up to give you some idea of what the environment looks like and how to play with the kernel code. And a sum of one is the first assignment that you should really you're supposed to write some code. So, as Jeff posted on the Piazza today, I'm going to, I will not talk about the environment setup for sum of zero, which I assume you already know how to do that. And today we mostly covered the basics for a sum of one, which is the synchronization primitives. So, where are you guys on the lecture? Are you talking about the processes? Are you talking about the synchronization primitives? So, do you know what locks and semaphones are? Yes, that's good. Okay. So, today we'll touch some basics for sum of one and show you how the OS 161 already provides some code for you to work on. My name is Jinghao and I will teach recitations this semester. So, first some logistics. The rest of it is today at four, this room, and Friday at 8 a.m. So, you just, you can attend easily when that's your schedule. You don't have to attend the one you registered on the hub. So, it's also open to graduate student. So, but the priority is given to undergrad who have already registered the course. And today we'll talk about the sum of one and concepts and all that. I already talked about that. So, the deadline for a sum of zero and one in next Friday. So, at this point, if you are still doing the sum of zero stuff, you really want to wrap up and move on to a sum of one. Because the initial time allocation for you guys is one week for sum of zero and two weeks for sum of one. This is already the second week. So, in a sum of one, you will be, you will need to understand various kind of synchronization primitives including lock, semaphore, which we will talk about today. And also condition variables and read-write lock, which we will talk about in next week's recitation. And also, you need to solve two synchronization problems, the intersection problem and well-made problem, which we will also talk about next week. So, to do a sum of one, the one basic concept you need to understand is what is a critical section? Basically, the critical section is one piece of code, several lines of code, that both serves these both conditions. One is that this piece of code access the shared resource. If there is no shared resource, then we don't need to do synchronization at all. A second condition is that this shared resource cannot be accessed at the same time. You have to enforce orders. Someone need to access the resource first and then someone access the later. So, one typical or classical example of this critical section, you can imagine that here is the shared printer case, where the shared resource here is a printer. And you have multiple users or computers who want to use this shared resource and do the printing job. So, in this example, our goal here is to design a self-print routine or method or function so that multiple users can safely share this printer. So, a strawman approach or a naive approach will be like this. I do nothing, I just in the self-print function, I just do the print. I don't care about the synchronization at all. So, of course, it doesn't work. Why? Yeah. Yeah, it doesn't do any checks. So, what if I was printing, I have 10 pages, and then some other guy can see, he also wants to print. I have to prevent him from printing, right? Otherwise, our print job will be mixed up. So, this is why we introduce the concept of lock. Basically, the lock is designed to enforce a mutual extruding concurrency control, meaning that once I acquire the lock, somebody else has to wait before I finish, right? So, the interface of the lock is pretty simple. Basically, two main interfaces. One is acquire the lock. Basically, I want to acquire the resource. And then, so, which is indicating I am about to enter the critic section. So, what will happen when I call lock acquire? What are the possibilities there? Well, in the most ideal case, nobody is using it, right? When I call lock acquire, I get the lock immediately, right? That's the most ideal case. What if somebody else has already hold the lock? Somebody else call lock acquire before you, right? Yeah, either you have to wait, otherwise, yeah, because there's only one lock available, right? Depending on how you are going to wait, there are two kinds of locks. Either you keep querying if lock is available, which is called busy waiting, where you just keep querying if lock is available. Another way is, so, you will call lock acquire. Inside lock acquire, you know the lock is not available. Then, instead of keep waiting there, you just sleep, right? And expecting the previous holder to wake you up when the previous holder is done with the lock, right? So, Jeff will talk about all these kind of differences in the lecture. So, all you need to do at this point, all you need to do is there are two kinds of locks. One is the spin lock, where you keep waiting on the lock. Another is called sleep lock or normal lock, where you sleep if lock is not available. And the second interface is pretty straightforward, lock release. You're done with the lock or you're done with the critic section. You just release the resource so somebody else can use the lock or use the resource. So, here's the second version of the self-print. Instead of just do the print inside the function, I first lock acquire the lock. So, if I wish line three, the third line here, I know that I'm the only person that are using this resource. In this case, it's a printer. And I also know that any further users will be blocked in line two, right? So, before I release the lock, I'm the only holder of this lock. Anyone who wants to use the printer has to wait, right? In line two case, wait in line two, the acquire function. So, once I'm done, I release the lock. I'm done with the print job. Somebody else can continue with the printer. Any questions so far? It's kind of pretty straightforward, right? So, the OS161 has already provided a spin lock for you, like I explained. This version lock doesn't do any smart things. It just keeps banging the lock until the lock is available. Then it continues, right? It's defining the header file, that's spinlock.edge, and implement it in the spinlock.c. This one has already implemented for you. You don't have to, you just need to understand how this spinlock works. And you don't have to write any code for this. So, let's take a look at the actual code. So, go to source code, directory, and go to kernel, include spinlock. Where is the header file again? It's not there. Oh, yeah, right. Thanks. Okay, here we go. So, actually, at this point, I mean, before Jeff explained the difference between the spinlock and the spinlock slip lock, all you need to do is understand the interface of the spinlock. Let's see, as we have explained, there are two major interfaces. One is spinlock acquire, which allows you to acquire the resource. And after you are done with the resource, there is a spinlock release function. So, these two are the most important one. At this point, you just need to understand the semantics of these two interfaces. You don't have to go to the details of how the spinlock works. Then, besides these two, there are some helper functions which initialize the spinlock for you here, and clean up the spinlock, and also check, there is a one function that allows you to check if you hold the spinlock. So, the spinlock part is quite simple, mostly because you don't have to understand how it works. You just need to understand the semantics. Now, so, we have already dealt with one printer example. What if you have multiple printers? In that case, how do you coordinate the access to the printers? Will this version two still work? How many no? Why? Why it doesn't work? Not necessary. I mean, it would work in the sense that we... More than a printer lock. Well, you have multiple printers. If you use one giant lock to protect all the printers, what will happen? One user acquires a lock, he acquires all the printers. In fact, he only needs one printer. But this will work in the sense that it will provide the correct output, meaning that no printing jobs will be messed up. But it's not the best in the sense that you are wasting the resources. We have multiple printers. Why only allow one user to use all the resources at one time? You should be able to allow different users. As long as there are printers left, you should be able to allow the users to keep using it until all the printers are occupied. So that's why we introduced the concept of semaphore, which is supposed to manage a set of resources instead of just one. And it allows up to a certain number of threads in critical section. In the printer case, suppose we have five printers, we will allow up to five users using the printers, and that's it. If you really think about it, actually the lock is a special case for semaphore, where the resource number is one. If the semaphore number is one, then there's no difference between lock and semaphore. They are both achieved the same effect. And the interface of semaphore is just like lock. It's quite simple. We have p and v. These names are maybe peculiar as the first glance. If you are curious, you can check Wikipedia to see why it's p and v, but let's assume it's no more introvert. p means you want to decrease, or you want to acquire a resource. So you would p the printer semaphore to acquire one resource, and then you do the printing job. So you know that not more than this number of users will be in the critical section. So once you finish the p or acquire, you'll know there will always be at least one printer available for you. And similarly to lock release, we have v, which is to release one resource. So p is basically acquire one resource, and v is to release one resource. And also similar to lock acquire, when you do p, there's a possibility that you need to wait before the previous users release the resource. So as spin lock, semaphore is also provided to you, and it's very important to understand how semaphore works for you to correctly implement another version of a lock, cv and the read and write lock. So let's go over the details of how semaphore works. Here, as I said, semaphore is defined in kernel, include, sync, and edge. And if you look at the interfaces, so here's the definition of semaphore. So everybody knows what a structure is in c, right? How many of you don't know the structure keyword? Okay, if you want to see that it's the best, struct is just a structure. If you don't know c, you can imagine structure as a class without methods. It's just a member fields in Java or any other object-oriented language. So here, we have a string, basically character pointer, called the name, which doesn't play any actual role, but just for humans to know the semaphore well. And we have a wet channel, basically a queue where the threads can slip on if there is no resource left. And we have, interestingly, we have a spin lock inside a semaphore. So as you can see that, spin lock is actually a building block for any other more complicated primitives, right? The semaphore applies when you try to implement the normal lock or the condition variables. So you should start noticing the building blocks for these synchronization primitives. One is the slipping channel or wet channel, where you can put a stress to wet for something, and another is the spin lock. And finally, we have a semaphore count, which represents how many resources are available that this semaphore manages, right? So this is the structure, and we have helper functions for you to create a semaphore or initialize it, and then also destroy it. So here we have two interfaces, P, well, this is a word, initial for a word, and the decrement of the count and basically acquire the resource and V. So any questions before? Yeah, not a collection. We don't have one spin lock, right? We have a count, right? Suppose we have five resources to manage. How many spin locks do we have? One. So initially, what's the count value? That's a good question. Suppose there are five resources. What do we do in the semaphore create? Basically, when we initialize the semaphore, what's the initial value for count? Five, right? Initially, it should be five. When I do P, so the basic idea is when I do P, I decrease it, right? I keep decreasing until it reaches zero, at which point I will have to wait, right? When I do V, what do I do? I increase the count, right? Basically indicating that one more resource is available. So that's the basic idea of P and V. Sounds very simple, right? When I P, I minus, minus. When I do V, I plus, plus, right? But as you may find out, it's not that simple. There are some corner cases you need to consider. So let's go to the implementations of semaphore and see what the corner case is. So here, first, the create basically initializes a semaphore. What are arguments or parameters this function takes? What does this semaphore call? The name. And what's the initial count? Initial count is basically how many resources that this semaphore are supposed to manage, right? So we just assert initial count should be positive, or although initial count of zero doesn't mean anything, right? And what's that? Exactly, but that's more advanced topics for semaphore. Yeah, you can initialize as zero and later on you can V it, yeah. So here we allocate the space for semaphore and we set up the name, set up the web channel, initialize the spin log, initialize semaphore count, and we're done. So you need to get used to the style of in C, how do you acquire resources? How do you allocate the memory? We have a destroy first. So we just release every resources we just acquired. For each memory we can unlock, we need to carefree and all that. So these are not minor details. So this is the interesting part. In P, we need to basically we want to decrement the count, right? For now, just ignore this part. The first thing we do is actually acquire the spin log. Why is that? Access what? Yeah, that's more or less. That's right in the sense that so in this case, as far as the semaphore concerns, what's the shared resource? Does the semaphore care what resources do you use between P and V? It doesn't know, right? It doesn't care about that. So as far as the semaphore concerns, what's the shared resource? What's that? Semaphore what? The semaphore itself. It's self. Well, it's more general. I want specifics. What's the shared resource in the case? P function. No, that's not function. What? Yeah, well, what I want is a resource to the semaphore. So what's the resource? Function is not a resource. You can always call function. Now. What's that? I heard something. Yeah. Some count. Yeah, some count. Everybody tries to decrement the count. I need to protect that operation. Otherwise, how do I coordinate between the various users or threads? As far as the semaphore concerns, the shared resource is only one int, the count. As long as I protect the count correctly, I can always ensure the correct behavior. Here, another question is, what's the spin-off I supposed to protect? The count. Now you understand why we have a spin-off inside the semaphore, because the spin-off I supposed to protect the access to the count. Because we want to check the count, which is access or read the count, we need to protect the access using the lock. We acquire the spin-off. After this acquire, I know that I'm the only person now looking at the count. And others will be prevented by the spin-off acquire. Now I'm the only person inside this section, so I check the semaphore count. If it's zero, it's a trick to... This here, we do while it's zero, or you can do if it's zero. It doesn't matter. So, suppose I'm the first person coming to the critical section. What's the semaphore count? It should be positive. Like five, so let's say it's five, so we will not enter this while. So we reach here. So after this while loop, I know the semaphore count is positive. So it's set for me to decrement the count. This is the only operation I really want to do, but in order to do that, I need to acquire the spin-off and check it first. Then I release the spin-off. I'm done. I'm the lucky person. I'm the first one. I just grabbed the resource and I go. So I'm the first user. Now the second user comes. The same thing happens. The second user acquire the spin-off. The semaphore count is four now, and meaning that there are still four resources available. And I decrement the count and continue and so on. So suppose all the five resources, suppose five user has caught P without calling any V. Now what's the value of the count? After the fifth thread or user caught this P function, this should be one. There is only one resource left, and I decrement it. Now it's zero, but I don't care. I already grabbed one. So I continue. Now the sixth one comes. What happens? I can acquire the spin-off. No problem, because nobody is using the spin-off. But the semaphore count exists because it's zero, right? Which means there is no resource left. I need to wait. So now you get an idea of why we use a while instead of an if. Because you may need to check the resource multiple times. So in the most simple example, I'm the sixth user, and nobody else is trying to call P. I'm fine, and I wait here. So I lock the web channel, release a spin-off, because I don't need to care about a spin-off anymore. And here is the question. Why do I need to release a spin-off? What if I go to sleep without releasing the spin-off? What will happen? What's that? Well, I really don't care about the seventh one. As long as somebody before me releases a semaphore, or incrementing the count, I can continue. So why do I release the lock? Give up some lock? Yes, exactly. So in order for the previous guys to release the lock, they needed to acquire the... Release some resource by incrementing the count. They needed to acquire the spin-off. If I go to sleep while holding the lock, then how are those guys supposed to increment the count? So they will get stuck. So in order for them to increment the count, I have to give up the spin-off, and then I'll go to sleep. And when I wake up, the first thing I do is I re-acquire the spin-off. Why that's the case? Why do I need to...? I'm already here. Why do I need to acquire the spin-off? What's that? Yeah, so one rule of thumb is that whenever you access a shared resource, in this case, the semaphore count, you always need to protect it by the lock, either spin-off or later on in some other lock you implemented. Another way to interpret this is that, suppose I'm not the only one waiting for this lock, while I was sleeping, the seventh guy coming also goes through the process and also wet on this semaphore. And when the first guy down with the semaphore and incremented the count, we both get to wake up. Then we need to compete for the spin-off to ensure that only one of them can go back to the while loop and check the count. So suppose there are two guys, the sixth and the seventh guy are waiting for the semaphore, and then both get waked up here. But I'm lucky, so I acquire the spin-off. I go back, check the count, which should be one by now. I only got waked up because somebody else waked me up. And somebody else should have already incremented the count already. So semaphore count is one, so it's good. I jump out of the while loop and decrement the count, indicating here, I'm now holding a while resource now, and I'm done with this PE function. I just release and continue. And that unlucky guy will get stuck here. So after the first guy releases the spin-off, the second guy can acquire the spin-off now. But at this point, the semaphore count is zero already, because I already decrement the count. So unfortunately, he has to sleep again, waiting for some other guy to wake him up. So that's how the acquire process works. Let me just quickly go through the release process before we can discuss this, so it'll be much simpler. So the V is actually kind of simpler than P. You first acquire spin-off, one rule of thumb, whenever you access the shared resource, you always grab the lock. Then you wake up somebody, so your first incrementing the count indicating you are done with the resource, now there's one more resource available, and then you wake up some guy in the wedge handle. This is why we need the wedge handle. We need it somewhere where the further acquires have a place to wait, and the previous releases know where to wake up those guys. So we wake up somebody. I don't really care who to wake up, just wake them up, or wake up one or wake them all. It's the same, and then release the spin-off, and I'm done with this V. So now go back to look at the P and V again. Any questions about this? So everybody understand how P and V works? How you can use spin lock, and how you can use the wedge handle? You need a spin lock because you have some shared resource you want to protect. You use the wedge handle because when that resource is not available, you want to have a place where the stress can slip on. So this is how semaphore works, and it's very important for you to understand how it works before you can continue and implement other primitives. So any questions? What's that? Which we don't care. I don't know. I mean, as long as someone is holding the lock, I cannot, holding some resource, I cannot continue. I really don't know who is holding it. There's no way for me to find out. So now do you have any idea how to implement the lock? I mean the normal lock? So as I explained, in the spin lock case, when you acquire a spin lock, but the lock is already hold by somebody else, what do you do? You keep a wild loop on that spin lock, right? Basically the CPU is busy. Now the assignment one requires you to implement a different version of the lock where you find, where you find the lock is not available, instead of you keep buying the spin lock, you go to sleep on a web channel. How are you supposed to implement that? Do you have any ideas now? That's one way. That's cheating actually. Well, it will work, and I don't know if we can detect that. I think we can. Well, in some private conversations, I have recommended people to use it, but as of now, I will still recommend to implement the whole process. Instead of just using the sample form with initial kind of one, you go over the process and implement the logic yourself. It will help you understand how this sample form works. So in the case of lock, or there's one difference though. Lock has a holder. The sample form doesn't have a holder. There's one interface that lock has, but the sample form doesn't. What is that? Lock do I hold? If you just use the sample form with initial kind of one, you can implement the semantics of acquire and release, no problem. But how do you answer lock do I hold? That's one question to consider if you want to go over the easy parts. So let's see what's already there in locks. So basically nothing. We have a NAM which does nothing and add whatever you need. So by now, you should have a pretty good idea of what you need. What do you need? Spill lock. So you need to protect something, right? What else? What does the sample form has? Wedding channel. So where people would wait if the resource is not available. You said count. In this case, I don't really care about count because count will be always one, zero, one, zero or one. You can have a building. Yeah, that basically can implement what sample form can provide. A sample form with initial kind of one, right? But how do you answer lock do I hold? What else do you need to keep in the lock structure? Some kind of identifier, right? So whoever acquired the lock should put his identifier in the structure. So later on, when I call lock do I hold, I can compare my ID with the ID in the lock structure, right? If they're the same, then I know I'm the holder of the lock. Otherwise, I'm not the holder of the lock. Some kind of ID, right? You need to figure out what kind of ID. You said PID. Well, at this point, there is no concept of PID yet in this OS. In OS 161. You're about to implement it in OS 72. So what else can you use? Again, we don't have a thread ID. Some other things you can use to uniquely identify a thread. I will leave you to figure that out. This kind of the most difficult question in OS 71. After that, you'll be fine. So we need a spin lock. We need a web channel. We need a kind of a count, right? Or Boolean to indicate if the lock is held by somebody else or not. And we also need an ID, some sort of ID to indicate the holder of the lock. That's the information we need for lock. And let's go back to the C code. Okay. So you have this many stuff in the lock structure. Then you should initialize them in the lock create, right? You can initialize a spin lock. You will need to initialize the web channel, initialize the status of the lock as unheld or available, or the ID and the ID. So in lock acquire, what do we do? So go think about a semaphore. What does semaphore does at the first thing? Besides our kind of characteristics? What it does in the first step? What's that? Don't remember? Oh, quite a spin lock, right? Because so conceptually, what do you want to do in lock acquire? Check the status of the lock, right? If nobody is holding it, I'm going to grab it. If somebody has already grabbed the lock, I need to go to sleep. That's the two things you need to do in the lock acquire. So whichever you do, the first thing you want to do is acquire the spin lock, because you are accessing some shared resource. In the case of the lock, what's the shared resource? The status of the lock, either acquired or not. And then after acquire the lock, what do you do? Again, similar to semaphore, you can actually copy pass the most of the code of semaphore to lock. It's okay. Just you need to understand what it does. So you check the status. If it's available, just be optimistic and say it's available. I set it to unavailable. And I release the spin lock. I'm done. I'm the first person ever tried to acquire this lock. Now, what about the second person? Acquire the spin lock. Check the status. Oh, I miss one thing in the first person case. What do I miss? I acquire the lock. It's available. I set it to unavailable. I release. Is that everything? Do I miss something? Ownership, right? I need to indicate that I own the lock. Not just the lock is owned by somebody, but it's me. So I acquire the spin lock, check the status. It's available. I set it to unavailable. I set it owner to me. I release the spin lock. I'm done. That's the case for the first person. Now, in the second person case, acquire the spin lock, check the status. Not available. I sleep on the way channel. But before that, ownership. So we are trying to implement some functions for others to use. It's handy to have a lock do I hold? So suppose I just want to poke the system. I don't want to work there. So I can call lock do I hold to check. Oh, sorry. That's not the case. The case is sometimes you have nested locks, which in example would be in function A, you acquire a lock. Then you call function B. Inside function B, function B may be read by some other third party library. And he will also want to acquire the lock. So it's handy for function B to have a piece of code saying, if I already hold the lock, I don't need to acquire the lock again. So we are writing some primitives for others to use. And it's best for us to provide the full functionality to check the ownership of the lock. So if inside of the nested function B, if I already hold the lock, then I don't need to acquire the lock again. It will be a waste. In the sample form, how do you say who owns the sample form? Are you going to? In that case, will the sample form scale? I mean, if the resource initial kind is 1000, are you going to keep 1000 identifiers? Yeah, because the sample form doesn't have ownership concept, but the lock does. So go back to the second person who needed to do all this. Lock the web channel, release the spin lock, sleep on this web channel. And when we wake up, acquire the spin lock again. And then the rest are quite similar to the first. So any questions on that? Does everybody know how to implement the lock? So we have about 10 minutes left. I'm done with all the contents I need to, yeah. Yeah, yeah, yeah. Inside the spin lock acquire, you will keep waiting there. So when you call spin lock acquire, inside this, this is a function, right? The function is somewhere else. Inside that function, you will keep banning the lock to see if it's available. After this function, you just continue execution, right? Then you sleep outside the spin lock. As far as spin lock concerns, it doesn't really know what you do besides acquire and release, right? So you can choose to sleep in between them. So I guess that's what I need to talk about today. Okay, so this is what we talked about today. And we have 10 minutes left. I'll be here. If you have any questions you want me to check, I'll be happy to answer them. Thanks.