 All right, so let's get started. So welcome to week two of recitations, CSE 421. So first things first, so have all of you implemented assignment 0. So anybody who is not yet done with assignment 0, any problems until now? In case anybody really has problems with assignment 0, come to me after this recitation, and I can help you guys set things up. Because that is very, very, very important. Once you have the perfect setup, only then you can go about developing things. All right, fine. Now assignment 1. So have you guys read through the online stuff posted about assignment 1? You guys know a general idea of how to go about doing things. What's the gist of the entire thing? What are you guys supposed to do? Could anybody just say? It has to do with synchronization primitives. So as you have been attending the lecture classes, what you would have seen was that there are many synchronization primitives available. So let's name out a few. I'll use the board here, because I don't want to lift up the projector over and over again. So what are some of the synchronization primitives that we learned at class? Some are four. OK, fine. What else? Conditional variables. What else? Read, write, locks. So you guys have actually read through the assignment 1 description and also a little bit of the code. So that's why you guys are able to say all of these things right. So what is the underlying fundamental primitive on which all of these things are built upon? Like could anybody say out the answer? There's one thing which was discussed heavily in class from which all of these are actually built on. Yep, spin locks. So using spin locks in your assignment 1, you can go about developing locks and then go about developing conditional variables and go about developing read, write, locks. So last year, how it was was that some of course you guys had to implement. This year, we have relaxed those constraints and SEMF4 is already implemented for you in assignment 1. So when you're running through the tests of OS 161, you can find that SEMF4 tests passes because it's already implemented for you guys. Have all of you tested your simulators, SIS 161, and seen the output like how it feels like and how things actually happen there? So when you run this particular test, SEMF4, I think it's SP1. So when you run this test, you'll get a pass because it's already implemented for you. So what you'll actually have to do is, right now, implement locks. Not right now as in like not in this recitation, but your first step is implement locks. And after you implement locks, then focus on conditional variables and then read, write, locks because using locks, conditional variables can be implemented. And using conditional variables, your read, write, locks could be implemented. Made sense? Awesome. And how do you go about implementing these two is by using spin locks. So I think I'm clear until now. So let's refresh our theory just a bit. So what do you guys know about a critical section? What is a critical section? With all the rest station, with all the theory classes that you guys attended, right? So it's a very, very simple question. I'm just like trying to set the pace in motion. So yeah, critical section. OK, fine. So what is so much unique about a critical section that is different from the rest of the code? Shared resources. That was the word that I was looking for. So we have like multiple threads here, all right? And they're all like executing concurrently. And this is a critical section that you don't want to share with other pieces of code. So I'm just drawing time here, time along the negative x direction, all right? So if this piece of code is handling this critical section, it should make sure that no other threads actually try to access the same critical section in overlapping time periods. Made sense? So if a lock is held on this critical section, right? This thread that is requesting this critical section has to wait before this thing releases the lock. And only then this can execute after the critical section releases this lock here. OK, I'm just like refreshing whatever has been discussed in class. So your duty as of now in OS 161 is to enable locks. So had you guys gone through the code, like the lock specific piece of code? Not yet? Fine. So the place where you would actually have to look for is in sync.c. But before I do that, I'll check out a different branch. Get branch, get check out assignment 1. So it's actually present in Khan. I'm already inside the kernel. It's inside threats, subfolder, and sync.c. So what I have on my right is all the API calls that are available in this particular file, all right? So I scroll down here, your lock acquire. As you can see, your lock acquire, acquire, create, destroy, and lock, do I hold? And lock release. These are the things that you should concentrate on implementing it, implementing first. So I'll just write it down. So lock create, lock acquire, lock release, lock destroy, lock do I hold? All of these APIs have different meanings associated with them. So when you just go through the code like this, right? For example, I'm just looking at lock acquire code here. When you see something like write this, I mean the font color isn't all that perfect, but yeah, you can get a gist of it. So it's like, if it's just given like write this, don't get overwhelmed that it's something very complicated to write. So just four to five lines of code here would do the trick. And four to five lines of code at lock release would do the trick. And lock do I hold? It's basically two lines here. And yes, and lock create and destroy few primitives that are already available here. So what all data structures are actually required to create a lock? Could somebody tell me that? See, if I have a lock, what all parameters do I need along with a lock? Stack. Stack is more specific to a thread, not to a lock. Weight channel. Do you guys know what weight channel is in OS 161? So a weight channel is nothing, but it's OS 161's representation of putting a thread to sleep in a queue. So it provides you a data structure like this. So this is basically, I'm drawing a queue here. So queue is what? Like take from the front and insert it in the other direction. So OS 161 has something called weight channel. It's a structure. So when I say something like a data structure, it's something that's already present in some header file somewhere. So I'll just open that up and show it to you. So yes, struct weight channel. Yes, you see this thing? It's implemented in thread.c. Well, it's not actually in a header file, but it's there in thread.c. So just read through the description here. Like what it says is a weight channel is protected by an associated past in spin lock. And what is a weight channel is basically, it's a wrapper around a thread list of weight channel threads. The more and more you go about implementing OS 161, you know like what exactly these things actually mean. OK. So fine. So I was talking about locks, right? So let's look at locks here. So struct lock. I'll just split this thing into two. I'll go here. I'll open up the definition of struct lock. And this is present in include sync.h. So your struct lock, as of now, it has just got a name associated with it. It is a character pointer to lk underscore name. A question. So do you think just the name alone is sufficient to identify the lock? Yes or no? And why? I mean using a structure like this with just the name. Can I ensure protection of my critical sections? Why? Or let me phrase it this way. So what else do I require in this struct that ensures proper critical section execution? A weight channel. Why a weight channel? Like you guys are giving me answers. It's well and fine. But I would also like to know like why? Why do we actually want all of these? Just to make sure that you guys understand things. Why a weight channel is required in this struct lock? Fair enough. And you need to put weighting threads. Cool. So how about threads? Do I require some identification of the thread that this lock is associated with? So let's write that down. You said weight channel, right? And I'll create a pointer to it. Lkw channel. What else do I require here? Thread. Why a thread? So that we know like which thread is executing its critical section. OK, fair enough. So I'll use struct and thread and lk underscore, I'll call it holder, just to know like which thread is actually holding the lock. What else do I require? Well, the last thing that you'll require is not very obvious, but in the general frame of things, it makes more sense. So any ideas like what would be the last data structure that you would like to include here? As I said, to implement all of these, you would require something on top there. Sorry, I erased off that thing. You guys require a spin lock. So a spin lock is also required. And when you read through the documentation of spin locks in OS 161, there is some reference given to like allocating memory to spin locks. So what it basically means is that you don't create a pointer to struct spin locks. You just create a struct itself directly, because it's available publicly to all files. So the way in which you should go about editing that code, your struct lock is by adding all these functionalities here. So your struct lock should look like this. So once you get that thing done, then we can go about implementing your lock create, your lock acquire, and all of those things. Made sense? I'm going really slow so that this thing actually fits in everybody's minds. Any questions so far? No. Cool. So I'll anyway go about implementing the stuff, but I don't know whether I'd be able to complete it in this bestation. But let's see how it goes. So struct wchan, I'll call it lk underscore wchan, then struct thread lk underscore thread, struct spin lock lk underscore spin lock. And I'll delete off these comments, because I don't need these comments. Fine. So I modified my struct lock, and let's get back to my sync.h file. So what do we have here? We have our lock acquire. This is create, and destroy, and lock to i whole. So how do you guys think that we should go about writing these functions? Any ideas? Like, why should we go about doing whatever we're doing? What do you think is the most simplest function of all among the locks that are available? Read. What? What did you say? Create. What's that? Destroy. Destroy is where in which you make sure that you do not leak memory to the kernel. It's just like calls to k free. But the most simplest one which you would first go about implementing, at least me, that's my way of programming, is I look at this one, like lock do I hold? Then I know that it's something that it's a test against myself. So names like these would easily give away an answer of how exactly the code needs to be implemented in that function. So I'll just jump to that function now. And I basically know what I need to do here. So what does it say? It has a lock here, and it asks, like, lock do I hold? So I'll just have to, how do I, sorry. So I just have to make a test against myself. When you guys read through the code, you'll get to know that current thread points to the current thread you're actually running on. So let's go to thread.h struct thread. That's not what I need. I need a lock. Yes, struct lock. So I check here. I delete of this code because this is not what I need. And I just return, like, lock. What is the thread that is associated with the lock? It's LK thread, right? So LK thread is, I check this, whether it's equal to current thread. That's what I do. And I delete of this thing. One line solution, like, gave me an answer to whatever the function name asks me to do. Now I have my lock do I hold already implemented. Now your other complicated things would be in lock acquire, lock release, and other stuff. So I'll just close this thing. So let's see. Lock destroy is where in which we'll go about implementing next set of stuffs. So as you can see, so that our locks do not actually leak memory to the virtual machine, we are freeing things here. So because we added more stuffs to the struct thread, we need to clear values in lock destroy. And always you'll have to do the same strategy over and over again. If you add anything to an already existing struct, you will have to make sure that you clear things. Because if you do not clear things, your kernel just blots up. And after a while, it'll hang. And it'll hang for various other reasons which you wouldn't even anticipate. So what I'm saying right now would become more important when you're implementing assignment three, virtual memory, where you create a parallel VM that is parallel to the dumb VM that is already running. So always make sure that you clear things. So let's look into what other fields do we need to clear. Let's look into like struct lock, LOCK. So we free the lock weight channel. And then we free what else? There's a name and there's entire lock itself. And we do not free the thread. Because if we free the thread, what's going to happen is whatever thread is holding the lock, that thread itself, the memory of it just vanishes away. Then you're basically killing the thread when you're using the lock. If you release a lock, if you destroy a lock, for example, you're killing the entire thread. You do not actually want to free the thread. You just want to free the weight channel. So we do not do anything with the thread as of now. So we just add only one line of functionality here. And that should be fair enough. So our lock destroy is more or less implemented. So the basic challenge of implementing locks lies in lock acquire and lock create. Lock release also to a certain extent. So let's look at lock create. Could anybody tell me the difference between lock create and lock acquire? Like, why would I require two APIs rather than just one? I can, rather, you know what I can do. I can just write a functionality that says that, OK, whenever I create a lock, I inherently acquire it. Why do I require two APIs here? You don't always have to create a new lock, OK? A lock is basically meant so that it could be reused over and over again. So a similar argument could even arise for conditional variables and other synchronization primitives. But why exactly like all these primitives are designed so that lock create and lock acquire are in two separate APIs? Fine, I'll tell you an answer. It's because whenever you create a lock and when you acquire it, you're basically giving excessive privilege to a thread to acquire the lock when it makes a call to lock create. So have you heard of priorities? Like priority inversions, all of those things you'll learn along the theory of this course. So if you're just making both of them into one, whatever thread is actually creating the lock, you're giving it excessive privileges to get the first execution level. You do not want that thing to happen. So what the developers or usually proper developer would do is to split things up in very modular levels and have synchronization primitives, sorry, have spin locks between them so that they are executed atomically. Did it make sense? Whatever I said, kind of. The more you implement this, the more modular you make stuffs, the more smaller you make your implementations, the more better it is. Fine, anyway. So let's look at lock create. So we have a struct lock here, and we are allocating memory to the lock. We also are assigning the past in memory here to this lock. Fine. So what other things needs to go in here? Let's have a look. So I'll open up my struct lock here again. And my weight channel, and my thread, and my spin lock. So when you go about looking through the code of OS 161, you'll know that there is something called spin lock init, where it's defined in spinlock.c. So let's see what it requires. It takes in a spin lock. That's it. And what other stuffs do we have here? We have spinlock init, spinlock doi hold, spinlock release, acquire, and clean up. As of now, I just initialize my spinlock and create. This spinlock is already given to you in OS 161. Even here, you can notice things like init is different from acquire. So you can just see a reference implementation of what exactly these things do that is so very different from each other and not go more into that. So you can look into that when you're like browsing through the code. So let's get back to this code. So spinlock init. So in spinlock init, I require a spinlock here. So I'll open up my struct lock again. So here I have a lock that points to lk spinlock. I initialize my spinlock here. And what else do I need to do? I need to initialize the thread. So I have lock pointing to lk thread is equal to null. Any questions so far? Are you guys extremely confused? Because this code, see, whatever I'm doing right now would really make sense only if you would have gone through the code and seen the general structure of how OS 161 is. Because if I'm just adding stuff here, it'll be like, why am I adding this? Why am I doing this? And if I try explaining it, it will just not make so much sense if I don't know how OS 161 is actually implemented. So your basic objective is to be really familiar with OS 161 code. And especially whatever is existing in the kernel sub-directory, cool. So what I'm doing here is just initializing stuff. That's it. Nothing so fancy, nothing so great. Pointers, all of these things, the more and more you're comfortable with, see, it will come to you naturally. What other things are here? And my weight channel. There is something called weight channel create. And where is it defined? It's defined in thread.c. And what does it take? It just takes in a name. And let's go back. Weight channel thing. And how do I give a name for this? I want to use the name of the thread. So if I want to use the name of the thread, I should see how struck thread is. So I go here. And my struck thread already has something called character start name. I can use this, or I can use the second one, like TW channel name. So I go here. And w channel create. I make a pointer to lkwchan. And this w channel, sorry, like what am I doing? Make a pointer to the thread. And this thread has a constant character pointer to the w channel name. So I invoke that. So I finish initializing all my fields here in lock create. Now my lock destroy have actually cleared up my memory. And in lock, do I hold, I'm actually checking, like me being the current thread, do I actually hold my thread? And lock create is where in which I create my structure, where in which I give all the memory that is required for my lock. So just seeing if everything compiles properly, the way in which your locks work is in your implementation of acquire and lock release. This is where in which the core of your lock functionality lies, these two functions here. So if you write this thing properly, your locks test should pass. So let's see if everything is fine. I'll not go to this terminal and go to this terminal. So I'll jump to my DOM VM. It's in DOM VM, right? So I'll v make it here, error code. Expected struct spin lock start. But argument type is of struct spin lock. So I should pass in the address of the memory location. So what line number it is? spinlock.h. Now sync.c, line number 37. So I'll go to line number 37 here. Which line is this? It's 158. So I pass in the address of this location. All come here. Compile again. It generated me the kernel file. So I installed this thing. It created a symbolic link in the root directory. And in this particular window, I just have two panes here. Notice the middle dividing line. It's basically splitting up my gdb output and my sys161 simulation. Let's run this code. And as far as I know, it should probably fail. And it shouldn't execute. Because I haven't done anything properly. I've just added stuffs to my struct lock. Let's see. Oh, great. It didn't even compile properly. Not compile. It didn't even boot up properly. And to go about debugging this stuff, what I do is I add a w flag here. And in my undo this. And I'll quit my gdb. And once again, start my gdb. Once when I start this thing. You guys are familiar with what I'm doing, right? You guys, the more and more you're doing a os161 assignments, all of this would be second nature to you. gdb will automatically, you know, something don't work then automatically fire up gdb. Great. So I open up os161 gdb. And what is it? It's my kernel. And I know in the back of my head, like, what did I actually go about changing? I changed lock create. And I changed lock to I hold. And I changed certain stuffs. So let's place in a break point at lock create. And I'll also create a break point at lock do I hold. And also a break point at lock destroy. Because even that, I changed a bit. So I will continue with my execution. I'll see where it does this sys161 stop executing. The program is not run. So I should use this stuff. Target remote, it's in my unix subfolder. And it creates a socket to gdb. Now I'll continue with my execution. So as you can see, when I hit continue, right? The break point one has been reached in the code. So the boot process is like this. So initially, when it boots, it boots in a single thread. There's just a single thread. There is no other thread that are there. So your single thread, your process would be something like first your boot function call is called here. And then from your boot, you are like semfs. I don't know what semfs actually means. This thing gets called. And then your virtual file system, like you see that thing, vfs big lock, all of those things get called. And if you want to know the exact sequence of all of this, check main.cfile in your kernel sub directory. And only once, when you get all of this, your console gets displayed. And once your console is displayed, then you can see all your test options available. So what we identify here is that in the boot process in vfs, when it's trying to acquire a big lock, that's when it's actually failing. So we know that our lock implementation is actually having an issue. It's not implemented correctly. So the reason why I'm showing this all to you is because to know what's the flow of going about debugging programs. So if everything was done right and if my program was perfect and if I say this is the code that you need to implement, then this learning process of going about debugging stuff, it's pretty hard to grasp. So I'm just trying to make up errors and trying to debug them. Great. So breakpoint one, in lock create, this is where I get an error. So let's see if this is actually causing an error. I'll continue this. Lock, do I hold? It goes to the next function. So I know that lock create the first time it passed. So it comes here. Lock, do I hold? I go next. Now what I see here is my lock, do I hold? That has some issues because when I continued from there, it said that remote connection closed. Maybe my return statement is not correct. So what I'll do is I'll go back to my code here. I'll change things here. Lock, do I hold? I just return true. And I recompile my code. Oops. Oh, yeah. I need to make this thing void. You see the error that I got here, right? I tried to be make here. It says that unused parameter lock. It's because this flag is enabled in your GCC compiler. So if there's some unused parameter, it treats itself. It treats my make file. It treats that execution to be an error. So that's why I just made it void so that just doesn't make any sense. So it's just basically a void type. Fine. So I'll, oops, be make. Now it didn't give me any error. Be make install, I come here. And I rerun my steps again. And once again, I do target remote. Great. So I continue. So lock create. I continue again. It comes here. What now? Once again, it checks. Lock do I hold? So we know that that was the place that was giving a problem. So you see the way in which I was able to identify a problem using GDB. So I come here. And what I see here is the second time my lock was created. That's when I get an error. So what we think is there is something more that we need to go about debugging. So I'll rerun my experiments again. This is 161. Target. I'll continue it for the first time. I'll continue it for the second time. And this. And I step inside this particular function. And after I step, I just execute the next instruction. And then here you can see VFS big locks release. I'll just list the code around this particular function. So as you can see, my code is actually present in line number 133. So let's just step into this function again. So it was in line number 136. Before now, it's in line number 137. So I'll click Next. It checks if VFS big lock depth is equal to 0. And then you can see the call to lock release. I think that's where my problem lies. I'll step into this function again. And lock release. I'll step into that function. So in GDB, what next does is it will go to your next line of code. And what step function does, it steps inside the code. So it steps inside the function call. I stepped into lock release. So just list the code around it. So my list is there. And lock release. As you can see, the default stuff is displaying. Like write release and void lock. Let's see what it does. So I'll just click Next. VFS big lock release. I don't know what it means, but I'll click Next. VFS sync. Which code is this thing actually running? VFS big lock release. Give a device name. I don't even know what it is. Next. Next. I just can't handle this. I think I'll just die now. What actually caused this error is I'll type in back trace. So panic has been called. A panic has been called by lock create. So you can see when it's initially started, like the MIPS console started start.s. And then main.c. And then boot. And VFS bootstrap. And then sync.c whatever. And in my lock create, with the VFS big lock parameter, that's where I get a common exception, a trap that has been generated. And it gives me a panic warning like this. So when I just step over and over again, it'll mean bus power off and lean bus power off. Everything just powers off. And things just don't work properly. And no wonder my code just quits. So I know that there's a problem in my lock create. So let's go to my lock create. Any questions? I'm just developing things. So it's nothing very fancy. But it seems intricate because I'm doing a lot of stuff together. So what's the problem here? Maybe the code that I added is all wrong. So I'll just comment it off just to see if my lock create actually passes. So I'll substitute the beginning of the line with nothing. I'll recompile my code again. Be make install. Try running my kernel without the debugger. As you can see, it just booted because I just commented off my code. This time I didn't have to use a GDB. I was just like, you know, raw gut. That's what you call it. You get to know things just didn't work. So let's try commenting off little by little. Let's, sorry, uncommenting. Let's uncomment off this stuff. Let's see if my build passes. Be make install. And quit here. And once again. It did pass. Now what could be an issue? Maybe the weight channel. I once again be make install. And quit. Run it again. My weight channel is the one that is giving me an issue because as you can see, it panicked. And it just turned off. So just to be sure, if just the weight channel is the one that is causing an issue. Let's just uncomment off this. Sorry. Let's see if spin lock in it would also give me the same stuff. So be make install. And 6.161. And it just booted fine. So I get to know that my doubly channel create is the one that is causing an issue. So fine. I just uncomment off this code as of now. And maybe we can look into these things later. So my doubly channel create, destroy, and my acquire and release. These are the things that we need to write. The reason why I went about doing the first stuff is because just to show how to go about debugging things. So let's see. Quick overview. What my acquire should do? How should I acquire a lock? Any ideas? I'll show you the struck lock alongside. Struck lock. Where is lock? Spin lock. So a quick overview. What do you think should go inside this lock? What should go inside lock acquire? What should go inside lock release? You have your struck lock, which is here, right? What should I check before I acquire a lock? Yes. If the current thread holds the lock, then I don't have to acquire it. You can implement it both ways. If the current thread actually acquires a lock, you don't have to acquire the lock again. Or if other threads are actually holding the lock, then sleep the thing. So I'll just write it down. So check the weight channel. Then check current thread status. And then acquire the lock. This is considered this to be your general algorithm here. And then in void release, here you check if you have the lock, if yes, then release the lock. Fair enough? Cool. So how do we go about implementing this? I'll give you a hint. Hint, check implementation of p. And for this, check implementation of v. So I hope whatever has been discussed today is clear. So that's the general algorithm you need to follow. And once you get those things set up, your lock should work fine, and your SP2 should execute perfectly. And read through the code again, become really, really very familiar with OS 161 code base, especially the kernels of directory. So anyway, this is the end of rest station. So yep, good luck with implementations. Yeah, for those of you who are still