 Let's have class. All right, it's Friday. Who's into Friday? All right, boo. OK, got some boo's. I don't know why people boo Friday. Friday seems pretty even in this divided era. Friday seems like it'd be pretty uncontroversial. All right, so today we're going to, the plan is to get this to work first, OK? The plan is to talk about deadlock a little bit and then finish up some other parts of the process system call interface. We, well, I think today's lecture should be about on point as far as time is concerned. If we end a little bit early, that might be intentional because I'm trying to synchronize with next week's slides. So next week Carl will be lecturing, which will be awesome. Possibly the only person on earth that might be a little bit more manic than me. So he'll be here Monday, Wednesday, Friday. That's the plan. He's going to use my slides, cover the same material, modulo, time, things like that. And he should be great. So I will be gone. OK, assignment one checkpoint. At this point, like, you're in trouble, right? If you're not done, you're probably not here. So that's, I'm preaching to the choir. How many people are finished? OK, sweet, good. All right. Assignment two checkpoint, the first one. So remember, assignment two, actually the first part of assignment two is do a week from today. We are trying this year to get you guys off and running. So here's what always happens with assignment two. People do assignment one and like right now the median score on assignment one is like a 50 or something like that. So people are like, OK, I got this, no problem, right? You know, people said this class was hard and it's not hard. It's just easy. I'm acing it. I'm killing it. You know, and then they get, and so like weeks go by and assignment two and they're like, oh, this is nice. Like it's spring. It's starting to get warmer. You know, everything's good. And then disaster strikes. So the goal this year is to not let that happen. And so in order to try to make sure that doesn't happen, we are splitting the assignment two into two parts. The first part is do a week from today. So again, keep in mind, when one assignment finishes, that doesn't mean you have a week vacation before you start assignment two point one. It means that you should be starting it now. I've had questions about assignment two in office hours. I can tell people are on their way. The first part of assignment two is to essentially do one system call. Eventually you're going to have to do like a dozen, but we asked you to just do one. The system call that you have to complete in order to get that to work is a bunch of other things you're going to have to do. But please, you know, again, don't take my word for it. Just get started and finish the first part of assignment two. And then maybe it really is easy. I mean, I do get people on the course evaluation that say this course is really easy. Not most of them, but maybe you're one of those people, but find out. So, you know, if you're one of those people, then you'll have assignment two done already or tonight or whatever. But I suspect for some of you guys the difficulty level is going to, you're going to start to feel the difficulty now. Another way to think about it is for assignment one, I think you ended up writing like maybe 100 lines of code total. Maybe more if you did some really nasty things with root or writer locks or the synchronization problems. For assignment two, it's like 2,000 lines. So there's more code to write. If the tests are a little harder to connect with the code that you're writing, it's hard. So just get going. Okay, any questions? No, yeah, so I think that was on the slide. We have new test 161 targets that we're going to push out like tonight or tomorrow. And we have a few more upstream changes that we're going to pull in. This time, I suspect there will be no merge conflicts unless you've been poking around with stuff inside the test 161 directory. But we do have a merge for you guys. The merge will include a target for 2.1. So there will be a, you'll be able to do test 161 run assignment 2.1. That'll just do the right thing. Any other questions, logistics stuff? Obviously, recitations next week we'll move to assignment 2. We're done with assignment 2. Okay. So the last thing I want to talk about, so as I posted on the forum, I've moved the producer-consumer example out of this lecture just to try to make sure that we can get through today. If you want to see it, it's online. In last year's lecture, we go through kind of a full example of how to do producer-consumer buffer using condition variables. It's probably useful. I mean, you guys have solved some of these problems now yourself. Just another example of a different synchronization problem. A little bit more real world than arranging whale mating or regulating access to a fake stop sign or something like that. But it's up there if you guys want to see it. Okay. So let's talk about a problem you can run into with locks. And this is a real world thing. This may be something that you guys encounter when you do assignments in this class. Now that you have the changes that have come in from upstream, you also notice that there's a way to turn on a deadlock detector that's part of your kernel now. And this is there to help you. So if I have locks, and the locks are designed to establish critical sections or help protect access to resources, there are times when I need two resources. In order to accomplish something on the system, I need to modify one thing and I need to make some corresponding modifications to some other resource as well. How do I do that? Frequently I might end up in a situation where those two resources are protected by different locks, which is good usually because in certain cases maybe there are changes to those resources that don't require both locks. And so threads that are modifying resource A or resource B can proceed safely without requiring both locks. But what about a thread that needs to modify both? Okay. Now let's consider the following situation. How many people hit a deadlock on their stoplight problem at some point? Okay, good. That was the point. That was the whole point of that problem. So you guys, I'm preaching to some people that understand this already. So I need multiple resources and let's say I've got two threads, thread A, thread one and thread two. Okay, thread A and thread B, and they both need access to resources one and two. And here's the canonical deadlock. Thread A runs, gets the lock for resource one, thread B runs, gets the lock for resource two. So now what happens? What's the next thing that happens? Well no, they're not waiting yet. Nobody's waiting. They've got locks. What needs to happen? What's that? Not yet though. Not yet. This hasn't happened yet. So we're good now, right? Yeah. Yeah, at this point it doesn't really matter what happens if the code, if I've already written the code. If the next thing that these threads are going to do is try to acquire the other lock, it doesn't really matter the order. Either thread A runs and tries to get the lock for resource two, in which it's going to sleep. And then thread B will try to get the lock for resource one, or if I swap these two around, it doesn't matter. Once I've acquired the locks in the wrong order, I'm done. And it doesn't really matter how things work out. But this is one way that this can work out. And now essentially what's going to happen is that these two threads will sleep forever. This makes sense to everybody. Thread A has the lock for resource one. It's waiting for the lock for resource two. Thread B has the lock for resource two. It's waiting for the lock for resource one. Neither one of them can make progress. Therefore, neither one of them can exit the critical section that they're inside. And I'm stuck. OK. So this is something that's called deadlock. This occurs, you know, there's a bunch of different ways this can happen. But it occurs when it typically can't happen with a single lock, although we will talk about self-deadlock in a minute. That's usually more of a programming problem rather than a lock problem. But it occurs when a set of threads are waiting for each other to finish. And because of the structure of how the threads are waiting, nobody can make progress. And so because nobody can make progress, none of the locks will ever get released. And because the locks will never get released, no one's ever going to come off a weight channel and everybody's going to be stuck there forever. OK. Self-deadlock. So you can deadlock on a single lock. How would I do that? Don't worry about dying inside a thread. Well, that's not going to, well, yeah. Yeah, that's true. So that's a good point. If a thread dies inside of a lock, that lock might remain held forever. And anybody else who queues up behind it is going to have that problem. What's a more common way? Well, remember, this is self-deadlock. I'm deadlocking myself. Yeah. Yeah, so let's say that I try to grab a lock for a resource I already hold. So I've already got a lock for resource one, and then I try to grab that lock again. What happens? Well, your code can check for this, and it probably should. I'm going to sleep forever, right? Because I'm waiting for myself and I can't make progress. This seems totally inane, though. Why would anybody ever write code like this? This happens. I'll just point that out. You may encounter this semester. I mean, you can solve this with a recursive lock, but why does this happen? Again, this just seems like a programming mistake. Why don't I just go up to 10 lines earlier in the function where it says acquire lock, test lock, and get rid of that call, or remove the second call to lock acquire? So that's the canonical case. I call some function. Maybe it's a function that your partner wrote. That function to be safe grabs a lock, but I already have the locks. I was already accessing the same structure, and so this is what happens. Usually you don't see this within the same block of code. Usually you see it when you're trying to use some other piece of code that itself accesses the resource and causes this. And there are solutions to this. There's something called a recursive lock. You can implement recursive locks if you want to. Don't change the locks that we gave you to be recursive locks. Implement a different recursive lock if you need one for some reason. They can be useful in certain situations. But normally if a thread tries to acquire the same lock it already holds, that should be something that should cause your kernel to panic right away. You want to notice this as quickly as possible. All right. Yes, I just said this. Okay. So there's a number of conditions that are required for deadlock to happen. And solving deadlock problems involves relaxing one of these conditions. So the conditions are as follows. First of all, I need shared resources. Sorry, I don't have locks, I can't have deadlock. The second is that there's no way to preempt, there's no mechanism for taking away resources from a thread once it's already acquired them. Does this make sense? So what would I have to do? How would this work? Let's say I wanted to be able to deallocate a lock from a thread. How would I have to be able to do that? This gets complicated, right? A thread cause lock acquire. Once that call completes it assumes it has the lock. It starts to do some stuff. If I want to take that lock away from that thread, I need some pretty complicated logic inside that thread that allows me to unwind what it's done. And this is something that you can do in certain types of systems. Certain types of systems have a way to say, okay, here's the set of changes that that thread made. I'm going to revert those changes, take the lock away from the thread and move it to some other. Obviously, this is something that would be almost impossible to implement out of your system, right? Once I start making changes to the system, I can't just stop in the middle of them, take the lock away. I've broken the critical section, right? The series of things that I wanted to look atomic is no longer atomic because I've stopped the thread in the middle of it. Multiple independent requests. So this means that a thread is allowed to hold one lock while it acquires other locks. And again, you can imagine trying to make changes to your system so that when a thread tries to acquire a lock, I track what other locks it's holding and if it holds another lock, I fail that request or panic or something like that. And you could do this and it would cause you to have to refactor some of the code that you might normally write, but it's actually pretty useful in a lot of cases and safe for threads to acquire multiple locks. And so if you decided to enforce this, you're going to find yourself with some fairly strange programming design patterns that emerge. If I require that a thread can only hold one lock at a time. Again, deadlock doesn't come about because we're stupid. It comes about because we're not careful about how we do things. Holding multiple locks at the same time is pretty normal and fairly useful in certain situations and required. This is, as far as ways to attack the deadlock problem, this is the one that we typically look at. This is our line of attack. This is the weakest part of the deadlock requirement, which is a circular dependency graph. So if I imagine all the threads that are involved in my deadlock situation, there could be dozens. I could have thread one waiting for thread two just waiting for thread three, which is waiting for thread four, which is waiting for thread five, et cetera, et cetera, et cetera. I have to loop back. There has to be a cycle. If I follow that graph, it has to close. So I have to come back. So thread one's waiting for thread two, is waiting for thread three, is waiting for thread four, is waiting for thread five, blah, blah, blah. At some point, someone at the end of that chain has to be waiting for thread one. Why? Why is that so important? Well, okay, so let's say I don't have a circular dependency graph. What does that imply? There's someone who can make progress, right? Everyone has to be waiting for something. So if that graph doesn't close, that means that there's some endpoint that's not waiting for anything, right? And so that thread makes progress and eventually that thread will move on and the thread that's waiting for it and I'll sort of unwind things from the other direction. So that's why there's this requirement of the circular dependency graph. That's what makes it deadline. So here's the classic synchronization problem. Again, I'm not going to go through this in too much detail because you guys have done something somewhat similar to this, although a little bit different, a little bit more like kind of, but you really can kind of see the stoplight problem as an extension of the dining philosophers problem. So the dining philosophers problem is this sort of stupid synchronization problem that's in a lot of textbooks where I have philosophers that are dining together and for some reason are eating food that requires chopsticks and so they are trying to begin eating and despite the fact that they're philosophers, they can't figure out how to properly set a table and so there's one fewer chopsticks than there are philosophers and so when they start trying to grab chopsticks, what happens is everyone grabs one chopstick, but of course one chopstick isn't very useful unless you plan on stabbing things with your chopsticks, which some people do, I understand that's like a valid way of using chopsticks if you're not very good at it, just stab the food. That's not like condoned, I think. I don't know if you go to a restaurant that has chopsticks, if you go to a restaurant that requires you to use chopsticks and then start stabbing things with them, I don't know, you just make it some funny looks, right? Okay, so at this point, everybody's got one chopstick and the problem is that acquiring this second chopstick is the issue. At this point, I'm stuck. So everybody is acquired, has one chopstick, but no one can make progress. No one can finish their meal because nobody has two chopsticks, but nobody can get a second chopstick because everybody already has one. Right? So remember to meet the conditions for deadlock, what did I need? I needed multiple independent requests. I need two chopsticks. If any one of these philosophers decides to be the poke eater, then we're good because everybody else can keep going. Right? If any one of these philosophers was actually able to finish their meal using only one chopstick, they would drop it, somebody else would get two, off we go. But they all need two and because of the order in which they've grabbed the chopstick, I had this circular dependency graph. And so here are the ways that we can approach this problem. By relaxing, so when you have deadlock, you need to relax one of the conditions that cause the deadlock. So the first thing is, if I don't wait for that second resource, so imagine that I had some way of determining whether or not I was going to be able to acquire access to the second chopstick. So I can say, okay, is there another chopstick available? If not, drop the chopstick I have. Right? So rather than waiting for another chopstick to become available, if I know that there aren't any available, I just put down the other chopstick, somebody else can make progress. So this is the waiting condition. Breaking cycles. So we'll come back to this. This is the canonical way. If I establish an order in which the chopsticks always have to be acquired. So I have chopstick 0 through 4, and I always have to grab the higher-ordered chopstick first. That will break this cycle. I can show you why in a sec. So another way to do this is to have some arbiter at the table that can notice this problem and then grab a chopstick from a philosopher. And again, figuring out how to handle this is tough. Because you may have already run some code that assumes that you have that chopstick and backing that out is hard. Or I can make sure that I always request both chopsticks at once. So this is not making multiple requests. So I could redesign the table in some way so that there was a way to request two chopsticks. And I could have like a part of the system that handed out chopsticks and only hands out pairs of chopsticks. We'll never give you just one. Okay. Does this make sense? Yeah. And again, the typical way to do this is the cycle breaking. So if I imagine I go back to my diagram here, here's what's going to happen. So I used to know who these philosophers were. Does anyone know who these were? Who these are? Okay. I could start making jokes now, but I don't want to offend anybody. All right. So this guy, here we're going to go... So what's going to happen? At some point, so let's say that the chopsticks are numbered zero, one, two, three, four, and the rule is... Well, sorry. Let's just say they're numbered one, two, three, four, zero. And the rule is I always have to grab the higher numbered chopstick first. So this guy grabs one. He has one in zero. This guy grabs two. He has two in one. This guy grabs three. He has three in two. This guy grabs four. He has four in three. Which chopstick is this philosopher going to try to acquire first? Four. Four, which is the one that this guy already has. So as soon as this happens, he's going to sleep without a chopstick, which will allow this philosopher to get a second chopstick. He'll eat. He'll drop the other one, and everyone will make progress. So this is how I avoid this cycle. If I put things in a well-defined order and I enforce that order, I can make sure that I can't have a cycle, and it's all on this philosopher to kind of help us by breaking out of the loop. Okay. Now, starvation is something you guys are probably also somewhat familiar with by now, given our mysterious hidden reader-writer lock tests that have been testing for this. Starvation is similar to deadlock, but it's not the same. So in starvation, what happens is there's some group of threads that never make progress, despite the fact that another group of threads is making progress. So in deadlock, all of the affected threads are sleeping. In a starvation condition, some of them are making progress. Some of them are still sleeping. And those ones that are sleeping, they may wake up periodically, but somehow they just never get a chance to go. They never get a chance to acquire the resource or do whatever there is to do. And starvation is one of these things that, you know, deadlock is permanent. Once I get into a deadlock situation, unless I have something monitoring the system that's going to panic, or going to break out of the deadlock in some nasty way, that's just going to be that way. Starvation is more temporary, because on some level, it's usually difficult to star a group of threads forever. I can have a group of threads that don't run for a long period of time because some other stuff kept happening. But once that stuff stops happening, then everything else sort of comes back to normal. But this is still a bad thing. So you can almost think of starvation as sort of like a denial of service attack on a particular type of thread. You know, as soon as that ends, things come back to normal and everybody starts making progress. But there's some condition under which a group of threads can't make progress. All right, so one of the things that can cause deadlock is, you know, a correct and sort of good attention to synchronization. So as you guys start dealing with synchronization problems in your kernel, one thing you might start doing is being like, oh, okay, well, I'm not sure that this data structure is safe to access. I'm going to add a new lock and then I'm, okay, maybe there's some sort of race condition here so I'm going to expand the scope of this critical section. It is a fairly well-established, if terrible and sad way to approach some of the parts of assignment three by just locking entire functions so that they only execute in serial. At some point, this can cause deadlock. So if you are over synchronized and you are acquiring locks, too many locks, you may find yourself acquiring locks in the wrong order and the system can end up in this sleep state. Now, of course, you're doing this because you're trying to avoid race conditions. Remember race conditions? These, you know, undeterministic results of a system that are caused by different ways that threads can interleave. I don't, when you're in the kernel, you don't want race conditions. And so throwing locks at the problem is a good idea. Even if that eventually causes a deadlock and makes you have to think about your locking conditions a little bit. Now, from the perspective of, you know, imagine that you work at a company and you ship software, right? Actual software that people use. Which one of these would you prefer to have happen in a deployed system? Deadlock or race condition? Why? Okay, so that's a good point. Yeah, deadlock doesn't necessarily lead to an inconsistency, so that's good, right? Like, imagine you're the bank. What would you rather have? Customers not be able to make deposits for a few hours while you debug a software problem, or all their account balances be wrong by an amount that you don't know. Right? Like, I would take the form. Typically having things shut down is easier to detect and it doesn't lead to these inconsistencies in the system. So, you know, you guys will be thankful when you hit deadlocks in this class, because it's like, oh, okay, good. The system has stopped. You know, I kind of understand what's happening, right? Whereas a race condition, also deadlocks tend to be more deterministic, right? Race conditions, it's like, oops, one out of 10,000 times you ran that and it returned the wrong result. Not good. Because somehow, when you guys end up running things on the Test 161 server, it's always that one time, right? I mean, the Test 161 server has been carefully designed to give you the lowest possible score, right, on any test. Not true, but I mean, that typically seems to be how things work, right? All right. Last, before we move on and finish up this union on synchronization, and again, I like the fact you guys have already had some experience with this, because I want to talk just a few notes about how to use the right tool, because you guys are going to, look, the toy synchronization problems you saw for this assignment are toys. That's why they're called toy synchronization problems. You'll have to solve real synchronization problems on other assignments. So, here's some idea of how to do things. In a lot of cases, you can solve the same problem using multiple primitives, even if you only have, at this point, you have semaphores, locks, CVs, and reader writer locks 4. In a lot of cases, you might have multiple choices. There's usually one primitive that is just a much better fit than other primitives. And I said this before, if you find yourself solving a problem and suddenly you're having to throw, like, more shared variables at it and, you know, it's hard, it's getting hard to reason about what's happening, you might want to take a step back and think about whether or not you can approach that problem using a different primitive and how it will work out. All right, and you already did this, so this is awesome. Okay. Another way to, and finally, just some last advice about approaching synchronization problems. So whenever you're trying to solve, you're trying to make sure that multiple threads can do something together safely, the first thing I want to do is identify the constraints. What properties of the shared state am I trying to enforce? So, for example, for the stoplight problem, I'm trying to make sure that there are never two cars in the same part of the intersection at the same time. That's one of the things that the test code checks for. That's a constraint that, again, these are silly, stupid problems, but that's the constraint that we gave you. Two cars can't be in the same part of the intersection. And, of course, cars have to finish. If a car enters the intersection, it needs to leave at some point. Identify shared state. So, for the deadlock problem, the stoplight problem in the shared state is the intersection. There isn't always shared state. Sometimes what you're talking about are coordination problems. So, how many people managed to solve the whale mating problem without any shared state, other than, like, some synchronization primitives that you might have had to create for? I think more people than you think think about it. You can do the whale mating problem without any shared state other than a couple of synchronization primitives that you have to create. You don't need, like, a global counter or a global state variable or Boolean. You can do it with just the primitives themselves. So, sometimes there isn't any shared state. Obviously, for the stoplight problem there was. Choose a primitive that you're going to use. Again, think about which primitive might work best. Try to think through a little bit about the design pattern that's going to emerge if you use different primitives. Pair, waking, and sleeping. So, this is really critical whenever you use any of these primitives. All the primitives that we've had you use have some sleeping behaviors and some waking behaviors. And if those aren't paired properly, even without deadlock, I can have thread sleeping forever. So, whenever you use a CV and someone calls CV weight, you want to be able to make sure that that call is going to complete at some point. So, if we're ever called a CV weight, there has to be maybe multiple calls to CV signal and calls to CV broadcast. Same thing with time release. Acquiring release are easy because they're typically being done by the same thread. When you get to CVs and semaphors, sometimes the operations are happening in different threads, and so it requires a little bit more thought to make sure that you have the pairing that you want. And then finally, look out for any cases where you're acquiring multiple locks or multiple resources and just be aware that it's going to lead to deadlock and just establish an order for your locks. So, there's actually a tree, but there's actually David has some nice comments in there in certain cases when he's acquiring particular locks, pointing out that there is a locking order that is implicit. So, he says it's safe to acquire this lock here because this other lock is always acquired before if they're acquired together. So, if one thread acquires lock one and lock two and another thread only acquires lock two, that's always safe. But he has in the code themselves whenever he's acquiring multiple locks. And then, yeah, and then, you know, before you actually start writing code or as you're writing code, you know, think about corner cases. What happens if this thread ones for a long time or what happens if this thread gets blocked for a long time and make sure that things don't work. Okay. So, we are done with synchronization. Any questions at this point? You guys feel comfortable with this? I guess. Whatever. It's new. If it's new, then, you know, trust me, you get good at this after a while. You get more practice. Yeah. We can talk about it. Yeah, I mean, the best one is the one that gets you full credit for you. I mean, we have preferences. I don't want to release the solution code because, you know, yeah, maybe. I mean, we can talk about it in office hours and stuff like that. I mean, I would argue for the two toy I think one of the cases is so obvious that we don't even need to talk about it. The other one, there's more of a choice. The other one, I still think that there's a better set of primitives. Better primitive. And then the reader-writer lock implementation is actually pretty interesting. So, I would think about that a little bit. We've had some good conversations at office hours about that problem. Particularly, you want to think about some of the trade-offs that you had to establish between concurrency and fairness, concurrency and starvation. Good question. All right. Okay. So, let's move on. So, at this point, what we're going to do is we're going to get back to talking about quickly the rest of the process-related system calls. And then next week, well, some of the stuff Carl's going to do, which I'm kind of annoyed about because I really like that material, is not annoyed at Carl. I'm just annoyed that I don't get to talk about it, but is talk about some of the process of getting in and out of the kernel. How does the kernel gain control? Why does the kernel have some of the special information about some of the relevant portions of the system call interface? So, you remember, so again, before we got interrupted by synchronization, we were talking about fork. Remember that fork returns twice to two identical processes, modulo, all the new options to clone and other things, but that's sort of the default behavior of fork. Create an identical copy of the parent, modulo threads, but after fork returns, there are two processes running this exact same code base. And the only way you can tell them apart is by looking at the return code of fork. All right, all the contents of memory. And remember, this is at this point. Just want to make sure this is very clear. These are two independent processes. Other than certain sharing that goes on in the file table that you guys will get a chance to review for assignment two, the two processes are independent at this point. If the child decides to create new variables, run new code, change the value of shared variables, all of those changes are isolated to the child. The parent and child start out with the same memory contents, but they are using different memory. Does that make sense? I made two copies. It's like I made two copies of the same file and then you added one and I added the other and at that point our edits diverge. Okay. All right, so we had some issues with fork. We didn't want to copy and there were some ways to sort of work around this. And this gets more just when we come back to memory management. We'll talk about this in a month. And then fork establishes this tree of processes on the system because every process has a parent. What's the first process that's set up on the system? On Linux type systems, it's a special process called init. And init is now responsible for starting up any other process on the system and running. So this is what the process tree looked like on a particular system. So you can run it and you can see various things that are hanging off here. At any point in time I can organize all the processes that are running on the system into an ace of the graph like this. All right. Any questions about fork before we continue? Just refresh your memory here. Okay. Good. So, talk about there's three other process-related system calls we want to talk about. Change, you know, death. How do the processes exit? And then I don't know. This doesn't fit into like a good life-based metaphor. Right? There's really no well, maybe this is like parents waiting on their kids to grow up so that they can like go back to partying and ripping the house apart. I don't know. Whatever. Yeah. So, exact is how processes become other types of processes. You might think if I just had fork, wouldn't I just have a bunch of copies of init line around? Right? So let's talk about fork. You know, if all I had was fork, then all I could do was make copies of the same process. This is not very interesting because I don't really I don't want a system that has like 68 copies of init because then it's not a very interesting program. I want to be able to change. The way I do this is with something called exec. And exec actually is a stand-in for a whole family of related system calls that have slightly different semantics and you know, it's not that the differences aren't that interesting to be honest. Essentially, what exec does is it tells the kernel, I want to become something new. I want to change it to a completely different kind of process. You know, maybe my parent was init, but I want to be a web server. You know, maybe my parent was a web server but I want to be you know, Python interpreter so that I can run some sort of web scripting code or whatever. Right? The way that this is done, you guys have run files, you guys have run commands from the shell, right? You guys have given the shell a path to a command or if you just type a command that's in your path, the shell will find it and run it for you. So this happens every time, almost every time. There are certain commands in the shell that are built into the shell, whatever. But almost every time you press return, this is what happens. The shell forks itself and then the child that was just created by the shell calls exec to run the command that you asked it to run. The way this works is pretty interesting and it's one of these topics that I wish I could talk about for a week but really doesn't really belong in this class or fit into the time we have available. If someday in the future, maybe, who knows, UB will actually have a computer systems class. This will be awesome to talk about because LinkedIn and loading is actually pretty cool. But anyway, you can imagine that how do I set up an entirely new process? What do I need to know about that process in order to set up an entirely new process from scratch? Probably one of the more important things I need is I need a blueprint for how the memory that the process is using is supposed to look. So I need to know what's in the process's address space. Remember, the address space contains all of the code for the process itself. It contains all of the global variables that are initialized in the process and their initial values and things like this. So at minimum, those executable files that you load contain all this information. There's a bunch of other things in there as well. They may contain requests to use external libraries, blah, blah, blah. And then, what's the entry point? So where, when I start up the process, you imagine that the kernel, you know, takes the call to exec. It creates a brand new process. It loads the initial contents of memory from that file that I passed in. But then it also needs to know when I launch this new process out into the world, what's the first instruction that I should execute? So the first thread has to come to life somewhere. Does anyone use, did anyone write tests and use thread fork? Is part of their OS 1621 kernel? Oh, that's sad. Come on. Write tests. Anyway, so if you look at thread fork inside the kernel, it basically does the same thing, not quite, because it doesn't create a new process, but it requires a function. It requires you to tell it where should you start executing. And it creates a new thread and that new thread begins life inside the function that's passed in. Okay. Now there's, you can imagine there's a specific format. The kernel has to have an agreement with you about the format of these files. So if I ask you to execute a program for me, the kernel has to understand what's in that file. And so the actual, has anyone ever tried to open up one of the binary files that you run on your system? Just maybe by accident, like with Vim, or maybe you thought it was a Python script and so, and it's just like, it's a bunch of binary data, right? So compiled code is in this format called Elf, the executable and linkable format, and Elf imposes structure on the underlying binary data in the file. So to you, that file looks like gobbledygook if you try to open into text editor. To the kernel, that file has a specific structure that allows the kernel to take it and extract the information it needs to set up a new process. All right. As you would expect, there are always utilities out there to help you investigate this. So there's something called readElf, and this is something I found in like 10 minutes online. I suspect you can install it in your own virtual machine fairly easily. ReadElf will read the content of the binary file in an Elf format and display information about it. So here I'm running that on bin true. It tells me it's an Elf file. It tells me its type is exact. It's an executable. What other type of Elf file might there be? Yeah, there's also libraries. There's like shared object libraries and static libraries and other types of things. But this is a program. This is an executable file. This program is designed to be run on its own. And then it gives me all sorts of other interesting information about the program. It tells me here is some of the code for the program. Again, these flags over here look similar to things you've seen before. This is the actual source code for what is this? Bin true. Pretty boring program, but there it is. It's not very much source code. It's only 100 hex in both the file and memory. And so the idea here is that when the kernel starts the program, it knows where to find the instructions for bin true in this file. And then it copies those instructions into the memory when the new program before the program gets started. This is how the instructions for programs get from the files that they're located in into the memory when the program runs. So this program is requesting something called libloadlinux.so. And this is essentially a little helper function that's used to load shared libraries. I never heard about this before when I did this. And so I had some fun. I tried to run this by itself. It has this fantastic message. Chances you did not intend to run this program. Chances are you did not intend to run this program. So yeah. And anyway, but this is used to help load shared libraries into the program. All right. So there's, to a large degree, exec loads almost all of the content for the program from the executable file. There's only some small, but there's some, there is some information that I want to be able to pass to a new program. So for example, all the programs that you guys run, if you use vim to open a file, if you use cat to see the contents of a file, if you use grep to look around your source tree, what else do I need to give to those programs so that they function correctly? I mean, it's not enough to just run the program, right? What else does it need? Usually they take arguments, right? I mean, maybe vim will load up and it makes you interact with it, but if I run grep without any arguments, it's just going to sit there looking at me like, okay. Then what? You want to grep for nothing. Okay, sweet. I'll just sit here then. Actually, I don't know what grep does if you run it by itself. Probably just exits, right? I mean, you didn't tell me anything to look for. Like, I need to give you some information about what to do, right? This is pretty common. And so, how this works is pretty interesting. So when you call an argument, so this is called argument passing, the parent collects the arguments, it passes them to exec, exec sets up the address space for the child and puts those arguments on the child stack before it starts running. So when the child starts running, sorry, when, sorry, sorry, I'm using the word child. When the new process, so when I call exec, I'm totally wiping out my current process. I'm asking the kernel, destroy me and rebuild me like this file. Right? Except for, I get to pass the small amount of information along. So I tell the kernel, I want to preserve these arguments. Those arguments get saved by the kernel, my address space is destroyed, I fire up a new process, load those arguments in, and then the process starts running. Is that a question? Okay, yeah. Yeah, sorry, I think I was talking about forking. So when I call exec, it replaces the currently running process. And there's essentially no trace of it left. I completely change myself from, you know, bash to been true. Question? Yep? Ah, great question. Okay, so what would happen if the shell just called exec? Doesn't matter. Let's say it calls the second it works. No more shell. I mean, I still want the shell. Yeah, okay, so fair enough, right? The fork plus exec paradigm is sort of like old Unix sort of nomenclature, right? I think, so there are two things that potentially might be true. There may be a new system call that does both. I don't know, right? And I'm pretty sure on windows because windows has 60,000 system calls, there probably is a way to do both at once, right? But you're right. I could just call one function and say, I want to create a new process and load this blueprint into it. Yeah, absolutely, right? Combining fork and exec is a historical feature of this, right? So exec is from a return code perspective. Oh, sorry, yeah, right? But remember, I mean any, so that's a great question. When I call exec, if that fails, I need to make sure that the process that called it is still there. So this is still true even when I call exec. And this is something you guys will have to think about when you implement exec for assignment 2. Exec should be able to fail in a bunch of different places but still return to the caller, right? So if the first thing the kernel is like, comes in and is like, get out of here, I'm going to destroy you. And then it's like, oops, like, I just murdered an innocent process. The only thing I did wrong was try to exec a file that didn't exist. So there's some careful bookkeeping I do so that even very late into exec, I can fail back into the caller, right? And if I had this new function, I would basically do the same thing, right? Good question. So as far as return codes go, exec is like the dual of fork, right? Remember fork returned twice? Because there's no process to return to when it succeeds. Sorry, I should be careful. On success, it never returns because there's no process to return to, right? I'm gone. The caller is gone. The caller has been replaced. So here's a way to think about exec. I need to copy arguments into the kernel, right? Because these are going to be passed to the new program. I use the file that was passed in address space. That address space replaces the caller, and then the arguments get pushed onto the stack. So, I might as well spend like at least one 1,000th of the time I spent on this ganky diagram explaining it to you. So let's go through it again. All right. I copy arguments. So remember, I want to give arguments to the new program. Those get copied into the kernel. Kernel takes the file that I passed, interprets it using the l-format. That generates the old address space. Now, this is the point of kind of no return, right? When I actually wipe out the old address space and write in the new one, usually what I'm going to do is keep the old one around for a while until I'm really sure that everything worked out okay. And then one of the last things I do is get rid of that memory that corresponded to the parent. Because if anything goes wrong, I can just swap that back in and I'm good. And then I write the arguments in. And then off I go. Let me think about it again. Okay, so exec, there's one other interesting thing here, by convention exec does not modify the file table of the new process. This is interesting. Remember we said that there's almost nothing left of the caller, but the caller's file table is left. Or there are ways to make sure that the caller's file table is preserved. Now, if the new process wants its own file table, it can just go through and close everything and reopen and there's no requirement that it used the file table of the parent. But the reason to do this is to preserve these pipes that I wanted to set up. So remember, I had this nice way of forking so that I set up communication between the parent and the child so I can establish a shell pipeline. If exec modifies that file table, I'm in trouble. So a lot of standard UNIX utilities. So you might wonder like, when I pipe data into grep or I pipe data into a command, what does that work? Because normally it's used to reading things from standard input, right? The way it works is that a lot of these programs, when they run, they just read input from file descriptor zero. They don't care where it points. If file descriptor zero is actually a pipe that comes from the previous process in the shell pipeline, that's cool. If file descriptor zero is you typing at the keyboard, that's cool. If file descriptor zero is a manual standard input pipe that you piped up, that's cool too, right? So one of the things that makes pipes work is that a lot of these utilities don't mess with the file table. They read from standard input. Sorry, I should be careful. They read from file descriptor zero. They write output to file descriptor one and they write errors to file descriptor two. Where those things are pointed, they don't bother about, right? And so that's why you can use a lot of these utilities both with this program, with input from a file and all these flexible ways, that's what makes these types of things really cool. Okay, so I will have Carl pick up here. I'll answer one question while you guys are packing up. Carl will be in the house. Very much in the house. I'm sure next week I'll be out. Good luck on Assignment 2.1. We have our first weekend remote office hours this weekend. I think there's four hours where we will have people manning the forums to answer questions as close to real time as possible.