 Is it week three? So we're back to continue. So today what we're going to try to do is wrap up the discussion of the system call interface. We'll finish talking about exec and wait, which is what we didn't finish on Wednesday. And then we're going to start the first major unit of the course where we focus on the processor. We're going to talk about the challenges of multiplexing and abstracting this particular system component, which is fairly important. I mean, that's kind of one of the more interesting things about your computer. OK, so just a couple of announcements. Simon wanted to do Friday at 5. I hope that you guys have started that, yeah. How do we submit? That's a great question. Bullet point two. So Scott has been working furiously on a fantastic new testing tool that will also allow you to submit. It's awesome. You think we guys will like it? So this tool has two modes of operation. In the first mode, it tests things locally. So you can run this on your own machine, and it should provide a reasonably accurate estimation of the score that you're going to get on the assignment. For this assignment, that's modulo. Whoa, hello. I see why you're open. If I close you really gently, you'll stay closed. Nope. Try closing that. Can you guys close that door for me? Just sort of cold out there. Maybe. Here, I have a solution. Put this outside. Go out the other door. Put this outside. Just keep it closed. You have to stay out there now. Just hold the door open. Just like this. This all works? Nope. Just use it to prop this door closed from the outside. Put it up against this. Let's see if there are engineers as well as computer scientists. OK. So we should have the tool released to you guys today or tomorrow for local use. So that means that you can run it locally, and it will allow you to test your assignment in almost the same way that we were going to test it when you submitted to our server. The difference, of course, for this assignment is the reader-writer lock tests, because we are not giving you those tests. We have our own versions that we're going to run on the server. You guys are expected to write your own versions that run locally, so there may be a little bit of difference in terms of the points that you earn if your reader-writer locks don't work and your test thinks they do work. So that's the only caveat. But for locks and CVs and for the synchronization problems, we've given you all the test code that we're going to use, and so your scores should be pretty much identical. So we'll have that out later today. Scott's going to put together a little tutorial so you guys can install and use it. But that is how you will submit. And it's going to be awesome. Very cool. OK, so next week, I'm going to be in Florida, sorry, on Monday and Wednesday. So what we're going to do is I will, on Friday, we may end a little bit early because I'm going to try to sync us up with at some point last year. And then on Monday and Wednesday, I'll assign video lectures for you guys to watch in lieu of me being here. I will have Jerry and Ali come to class and put those on here during the 50 minutes. And you're welcome to come here and bring popcorn and things to throw at the screen or whatever. So you can do that, or you can watch them on your own time. Doesn't matter. But that's what we're going to do next year. And I'll add them to the course video playlist so you guys know what's happening. OK, any questions about minister via type stuff? OK, so last time we got to the point where we were talking about weight and exit. So we talked about the process lifecycle. We talked about how processors are created using fork. We were talking about how they changed into other types of processes by using exec. And we got to the point where we were talking about weight and exit. And weight and exit in some ways are kind of ideal to think about together. Because you remember, when I exit, I get to pass a small amount of information to the parent. And that information and the child's process ID are preserved until the parent calls weight. So together, weight and exit, regardless of what order they're called in. So that's another interesting thing to think about. The parent can call weight before the child calls exit. The parent can call weight after the child calls exit. In either case, the result has to be the same. And this is a little synchronization problem that you'll get to solve for assignment 2. All right, so we talked about why the process ID is retained. Anyone remember why I have to keep the pit around until the parent calls weight? Yeah, up in the top. Yeah, so well, I need to keep the return code and the process ID. And I need the process ID because that's how the parent identifies the return code it wants. So a parent may have 10 different children. And it needs to be able to identify to the kernel which child it wants to wait on. If the kernel recycles the pits too quickly, then it may have a case where there's a process that exited that had a certain pit. And there's another process that's running. Or there might be multiple processes that exited that have the same pit. Now when I call weight, I don't know which one you want. So I keep the pit around and the exit code. But after a child exits, that's all that I need to keep until the parent calls weight. So there's a name for processes that have exited but have not yet had their return code picked up. And they're called zombies. Has anyone ever seen zombie processes on their system? Anyone ever run top or PS? Or has anyone logged into a machine? And it's part of, I think, a standard login message now in some machines where they'll tell you that there are zombies. Normally, this is kind of a bad thing. And the reason for this is re-parenting, which we're going to talk about in a minute. So on a well-functioning system, there should be somebody to call weight for every process on the system pretty quickly. If I have a process that's been sitting around for a long time and no one's picked up the exit code, it usually means that something went wrong. OK. Yeah, I already said that. OK. So now, like I said, the ordering of weight and exit with respect to the parent and child. So if the parent exits first or the child exits, or if the parent calls weight first or the child exits first, the same thing needs to happen. Now, you might ask what happens if a parent exits? I'm trying to say exit rather than die. I think it's a little less sad. What happens if the parent exits before the child? So has anyone ever sort of seen the effects of this? You guys might have seen it. Maybe you're running in a bunch of machine and you've opened up like a GUI application, like a text editor or maybe a PDF reader. And then you close the terminal and that thing goes away. Has anyone ever had this happen to them? It's kind of annoying, right? And I'll talk about why that happens in a minute. So when the parent exits, the operating system does not kill the children. But there is this interesting question of now, who should pick up the child's exit code? So remember, the parent was responsible for calling weight on its children. If the parent is now gone, who is responsible for doing this? And what normally happens is that if a child is orphaned, its parent exits before it does, it is reassigned to the in-it process. And one of the responsibilities of in-it is to just make sure that it's repeatedly calling weight on all of its children to pick up their exit code so they don't stay around in that zombie status. I just told you that when the parent exits, its children are not automatically forced to exit. So why does this happen? This happens because when the parent exits, the operating system sends a signal to the children. The signal is called sigchild. And this is the signal that indicates that your parent is exiting. A process can choose to respond to the signal by exiting itself. And this is what causes the behavior you guys have experienced. So it's not that those processes are being killed by the system. It's that they receive a sigchild message and for whatever reason they've decided that when their parent exits, they are going to voluntarily exit as well. And if you're using bash, there's a fun command that you can run. So if you want to prevent this from happening, one thing you can do on bash is called disown. And this will make sure that the children that you've spawned using bash will continue to run even if bash exits. Any questions about wait-exit mechanics? Yeah, sorry, front row. So even if there is a grand parent process, we're saying when the parent process disappears, it will again fall to that grand parent and it will always get back up all the way to the net. Yeah. Yeah, and I assume the reason for that is that init is prepared for this behavior. So what does init do? I mean init is responsible on your system for starting up a bunch of things. Remember before when we looked at the process tree, there were all these processes daemons like SSHD that were started by init. But then what does init do? Well, it sits there reclaiming pits. It's one of the things that it's programmed to do. So by assigning it to init, you're assigning it to a process that's ready for this type of behavior. If I assign it to the grandparent, the grandparent may have no idea what's going on. Where did this child come from? init is sort of ready for this. It's a good question. Any other questions on this? OK. So one of the things you may have noticed is that when you use a shell like bash, you can create background tasks and then show their status. So you can have bash show you all of its children. The way that this is happening is that there's two forms of the wait call. One of them says, I want to wait until my child is exited and then this command should return. That's called a blocking wait. There's also a non-blocking wait. So by passing a flag to wait, I can tell the kernel, if my child is exited, return its exit code. Otherwise, return immediately so that I can keep doing other things. Has anyone ever started something from bash or from your shell and then it exited? When do you find out about it? You start something, it runs for a few seconds. Let's say it crashes or exits. When do you find out about it? When you enter another command. So if you start something from bash and then in the background and it exits after a couple of seconds and you're just sitting there at the shell, bash won't tell you. So now if you hit return, it turns out you don't have to enter another command. All you have to do is hit return. So what do you think bash is doing every time you hit return or every time it goes through the cycle of executing a new command? Along with executing the command, it's also doing what? Why is that the moment that I find out about the fact that one of my children, one of bash's children has exited? Yeah? It's peeking. Yeah, it's using this call, a non-blocking wait to peek, at the exit status of all of its children that you started in the background. So if you start something in the background, every time you hit return or every time bash goes through its main loop, it'll check all the background processes it knows about and are they dead yet? If they are, it prints off their exit code and the fact that they've exited. If not, it just shows you the prompt again and off you go. Does that make sense? Can pretty much explain the whole Linux system called interface by talking about bash, at least the process-related part. All right, and this is actually, I just want to point out, this is a pretty common. This is something that we're talking about specifically with wait right now. But a feature of modern operating systems is that frequently they support both blocking and non-blocking operations for a variety of different types of calls. So for example, we've talked a little bit about the file system interface, and you get to implement that for assignment two. Modern operating systems support both blocking and non-blocking file operations. So I can tell the operating system, I want you to read 512 bytes from this file and don't let me continue to run until that data is ready. Or I can tell the operating system, I want you to schedule a read of 512 bytes into this area of memory, but I want to return immediately. And then later on, I need some way to find out that that read is completed. But non-blocking IO and non-blocking operations are a really important part of user-level multithreading in general, just improving performance on these types of systems. So it's kind of a design pattern. Okay, so, and more or less, I told you we were gonna kind of build a simple shell before we got to the point where we could exec the line that was fed in. This is not too far off from what a real shell is doing. I mean, real shells are thousands of lines of code. This is five, six. But it's not that far off, right? So what is this code doing? I read a line of input, I fork. I need a new copy of me, so I don't erase the shell. If I'm, so who does this? The child, right? If the return code of fork is zero on the child, then I try to exec the input. I try to run the command that you gave me. What does the parent do? Call it's way down the child. Waits for the child to finish. So when bash is in blocking mode, when you're not backgrounding operations, this is more or less what it's doing. Try to run the command. Parent waits for the child to finish. Can I reverse these things here? Can I have, would it be possible for the parent to call exec and the child to call wait? Yes, no? Who wants to argue no? Yeah. Well, I mean, there's one real simple problem here. Remember fork, right? Fork does, so here's my child. Does the child know what its parent's process idea is? No, there's no idea, right? Now there's probably a way to find out, but from this, just using this code, if you swap those around, the child doesn't know what to call wait on, and remember that the semantics that we've discussed of wait are that the only process that's guaranteed to receive the exit code is a processes parent. So let's say I run this code. Remember, you have to start training your brain to think about re-entrant multi-threaded code. So once I run fork, I've got two copies of this code running. So let's say that the child runs first. Let's say it comes through here, and let's say the exec's been true and exits. What will happen to the parent? Yeah, but I mean it's gonna call this wait, and the wait should return immediately, right? The child has already exited. Let's say that the child runs while one, a C program that runs while one. Then what will happen to the parent? It'll hit this wait, and that wait should block forever, right? Until you hit control C, and then get tired of nothing happening. Okay, question's about this before we go on. So there's one, before we move on and start talking about the CPU, there's one little bit of confusion, a potential confusion I wanna clear up, which is that when you guys look at the code that we've given you for OS 161, the user level code, if you look at other C programs or other C++ programs, you may have seen, how many of you have seen fork, or you've seen exec VEC or whatever, or you've seen read, right? You've seen these functions being used, right? Functions that look like the system calls that we're talking about, okay? So it turns out that the functions that you guys are familiar with are in general not the actual system calls. What those functions are, they're provided by the C library, and they're very, very thin wrappers around those system calls, but they are not the actual system calls. So those functions usually take their arguments, figure out how to make the system call, and do a little bit with the return values of the system call. One thing they do, so has anyone ever used Erno? There's this whole thing with the C library where if you perform like a, if you call read in the C library, you're supposed to do the read. If the read returns negative one, it failed, and then how do you figure out what went wrong? Does anyone know? Yeah. Yeah, so essentially what happens is that there's this magic global variable called Erno that gets set with what went wrong. So you make the read call, you check for negative one. If you see a negative one, I mean something went wrong, and then where you find out what went wrong is in this magic value Erno. Now Erno is not something that the kernel knows anything about. This is entirely something that's set by the C library, and this is a nice way of sort of illustrating the differences between read that you use in the C library and the actual read system call, because the actual read system call probably returned a value of Erno directly. What the C library does is it said, this is a return value that's bad. I can tell that. So I'm gonna change the return value to negative one, and I'm gonna use this return value that the kernel passed me to set this global variable Erno and continue. To be honest, I really have no idea why it's done this way, but this is how it's done. So in general, when you use read, write, like when you see those functions in the code that we've given you, those are functions that are provided by the C library. And in general, again, they are very, very thin wrappers around the underlying system calls, and if you wanna look in your code, you can find out where those functions map onto the actual system calls themselves, right? Okay, so any questions about processes? This is kind of, we're kind of done with your high level introduction to the operating system. So this is the top down version. This is trying to get you up to speed with what the operating system does and what some of its core responsibilities are. Any questions about this before we go on? Okay, so this is cool because I like what's about to come a lot better than what we've been doing, so now that you have some idea what the operating system is, now we can actually talk about how it works. And so for the next couple weeks, what we're gonna do is we're gonna talk about the processor. We're gonna talk, you know, we talked about the fact that the operating system's primary responsibility is multiplexing and abstracting the lower level hardware. So we're gonna talk about how we do that for potentially the most interesting and important part of the system, which is the processor core or cores. So the first thing, you know, and this parkens back to the very beginning of class, we talked about some of the things that the operating system was trying to do. So we'll talk about limitations of the processor that the operating system is trying to hide or mitigate. We'll talk about some of the mechanisms that are used by the operating system in order to provide these abstractions in order to do these things. We've already talked about concurrency, so we're not gonna cover this in very much detail because we already kind of moved that forward so you guys could do assignment one. And then finally, once we talk about the mechanism, remember we had this mechanism policy split, we're gonna talk about the policies that get applied. So the mechanisms here are designed to make sure that the operating system can maintain control of the CPU and be able to divide it between the multiple things that are going on at once. The policies here are very interesting because they determine how effectively that, how performant the machine is, how fast it feels, how interactive it feels, how smoothly things happen, right? Okay, so you remember, the operating system is just a computer program. We've been talking about, what we've been talking about for the last couple of weeks is the interface that that program provides to the other programs that are running on the system. However, and I don't know if I wanna say this categorically, but the operating system is not just another program. It has special powers. It has special privileges that it needs because the operating system is in charge of the machine. So the other process is, remember, I mean, the operating system is in charge of safely dividing resources between multiple processes. And that requires it to have a couple of special features that make it a little different from other processes. In a lot of ways, again, it's just like another process, but there's just a couple of things that are different about the operating system that are important. Now, one of the things that's interesting is that all the interface that we've been talking about so far, creating processes, allowing processes to change, most of that doesn't really require this kernel privilege. A lot of this can be done in libraries. So for example, has anyone ever used Pthreads? So Pthreads is a user space library that provides a lot of really similar functionality. So you can use Pthreads and you can do things that feel like fork. Now you're not creating a new process, you're creating a new thread within this process you've already created, but a lot of what we've talked about can be done without these special powers. And one of the reasons for that is that a lot of this has to do with providing abstractions. When we start to talk about multiplexing, the idea that I'm gonna divide up a resource and make it efficiently and safely available to all the processes on the machine, that's kind of where this idea of privilege and power comes into play. And that makes sense, right? If you're gonna put someone in charge of dividing things up, they need to be able to enforce those divisions. Otherwise one of the processes could say, you know what, I don't like this operating system kernel at all. It keeps making bad decisions and I'm just gonna, I know that I'm important and I think I should just be able to take up all the CPU time myself, right? So we don't want that to happen. And I just said this, wow, sorry, get ahead of myself a little bit sometimes. So I need these special privileges in order to multiplex resources. And there's two things that, the processes that run on a machine have to trust the operating system to do two things well. One is to divide up resources correctly. And this is a hard task, right? I mean everything on the system has different resource needs and the ways that those needs are expressed to the operating system may be limited. And to some degree on a lot of systems, OS also doesn't have a lot of feedback about how things are going from your perspective. So this is a tricky thing. The other thing that the processes have to trust the OS to do is to enforce those. Because again, if a process could find ways to arbitrarily grab more system resources, then I'd have a big problem because I'd have, I'd sort of be encouraging operating systems to work around, sorry, encouraging processes to work around the operating system and gain an unfair advantage over other processes. Okay. Now, so you can imagine like, so you may be thinking, well, this all sounds very authoritative, right? Your computer is essentially kind of a little dictatorship. There is one process that's in charge and it's a benevolent dictatorship because you can remove the dictator if you don't like them, right? You can say, I've had enough of Windows 7, it's a piece of crap. I'm gonna put something else on here. But you might argue, hey, why can't I do this in a more cooperative way? What would happen if I just tried to create a system and I trusted all of the processes on the system to collaborate with each other and share resources effectively, right? And there's a couple of problems with that, right? So in general, when we start talking about the relationship now between the operating system and processes, and this is something that's very important to keep in mind for assignment two in particular, the operating system does not trust processes at all. It thinks, in general, they're buggy. It thinks that they could potentially be malicious. They may be trying to get me to crash. They may not be trying to get me to crash, but they just may do something that's so dumb that I crash anyway, right? So in general, I can't trust anything about the process. And when you guys start doing your system calls for assignment two, please keep this in mind. If the process says, here's some memory for you to write into, you gotta say, no, no, no. Hold on, I don't even believe that that memory exists. I don't think you have the patience to access that memory. And probably if I try to write into that memory, the machine's gonna blow up, okay? So starting with those assumptions, let's see if we can figure out how to get what you want to happen to actually happen safe. But in general, you know, again, the OS has this view of processes which is very defensive. It's in charge of maintaining the machine. It's in charge of making sure that it keeps control over what's happening. And the processes are not to be trusted and there's potentially all sorts of things that could be wrong with them. So at the root, so how does, what is the special power that the OS has and how does it get it? So that's what we'll talk about next. At the root of this, and this goes back to the relationship, the sort of nice relationship between the operating system and hardware. At the root of this are hardware features. So you might, you know, what does it mean for the operating system to be a privileged program or a privileged process? It means that there are instructions, processor instructions that it can execute that a normal process cannot. That's essentially what it boils down to. There are some, if you look at the entire instruction set provided by the processor, there's a group of them that either don't behave in the same way or will cause a fatal error if they're executed by a process that does not have what's called kernel privilege. So if I'm not the kernel, if I'm not the OS, I can't execute those instructions. Either they won't work or, in better cases, they will actually cause the operating system to start running. Has anyone ever tried to run sudo on a machine that they weren't supposed to before? What does it do? Yeah, but what does it say? It's gonna tell us to send. Yeah, it says, you are not in the sudoers file. This violation will be reported. You're like, oh my gosh, I feel like Ken Smith's gonna be in your office like two minutes later. So that's kind of like privileged instructions, right? If you try to execute one of those, immediately it's like, I am gonna run the kernel right now and I'm gonna tell the kernel exactly what you did and then the kernel can do with you what it wants, which is normally kill you because you did something weird. So yeah, so this is again, I mean, there's nothing mysterious at all about operating systems or computer science in general. And this is the difference. There are special instructions, whatever they are, that the operating system can execute that other processes cannot. The operating system also has, this also allows the operating system to have a different view of memory. So this is another feature that the operating system is different. Sometimes that different view of memory is because I can execute those special instructions. Sometimes it's just baked into the system. So when I'm running in privileged mode as the operating system, I just have a different way to access memory than a process does. Okay, so let's talk about the special instructions. So these instructions, and usually it's a small portion of the instruction set, not a large portion, and these are usually involved in multiplexing the machine. There are instructions that control how resources are allocated or set up cases in which the operating system can take control. These, and these are the ones that are typically protected, right? And I just talked about this. If I'm not in privileged mode and I try to execute one of these instructions, the processor itself enforces this. So this is not something that's done in software. The hardware says you are not running in privileged mode and you try to execute this instruction. Therefore, I will immediately start executing the kernel. There's no question about it. Okay. So the goal and the goal in making the system safe is to ensure that only the kernel code has access to these privileged instructions and runs in privileged mode. And untrusted user code always runs without these special privileges. Now, one thing I want to point out is even the kernel doesn't always need to run in privileged mode. And there are cases where I actually might want to have parts of the kernel that don't have access to privileged mode because it allows me to catch problems with them. They're not supposed to do certain things. And so that way, if they do them, the kernel will run and it'll see that it's caused some sort of problem, right? But in general, only the kernel should have access to these special modes. So you might ask, how do I get back and forth? How do I get into kernel mode? How do I get out of kernel mode? I mean, there is some mode, there's some bit you can imagine on the processor that determines can these privileged instructions be executed? So how do I turn that bit on and off? So we'll talk about that for the rest of today and probably a little bit of Wednesday in terms of how I get back and forth. We refer to that as how do I cross this protection boundary? And then we'll talk about how does this whole process get bootstrapped? Now, something that we'll come back to when we talk about virtualization in particular is that modern processors actually, there's actually a lot more subtlety here. So there's more, let's put it this way. There's more than two modes. So on x86 processors, which have kind of taken over in a lot of places, there are actually four modes from most privilege to least privilege. They refer to these as protection rings. So you can imagine that there's, if I'm in ring zero, which is the most privileged ring, I can do anything. And then as I move out to ring one, ring two, ring three, there are certain things that are off limits and they're sort of cumulative. So I lose access to certain things in ring one and then I lose access to more things in ring two and then by the time I get to ring three, I've lost access to everything. Now, this is one of those things that, I don't know the story here, I wish I knew the story better, but x86 protection rings were introduced pretty early in the x86 series of processors and they sat around for at least several decades doing nothing. So for decades, all code did was use two rings. Which two rings would you use if you were only gonna use two? Zero and three. User code ran in ring three, the kernel code ran in ring zero. And this just went on and on and on and then mid 90s, this new technology came along called virtualization. And lo and behold, the x86 have these other rings which was really weird because at some point you gotta imagine you're in some design meeting at Intel and someone is like, do we really need all four rings? Like no one uses those other rings and some guys are like, no, we need those rings. I think we should keep those rings plus of course, we're Intel, we've never abandoned a feature ever. If it was supported on the 186, it has to be supported on the core I5. So, and for years and years it was like, what are these rings for? And then suddenly virtualization and now you actually have a lot of code that runs in ring one because this is a core way that you implement para virtualization which is something we'll come back to at the very end of the class. So this is kind of, I just feel like this is a fun story. Usually when features hang around for a while they're not useful. This one turned out to be. I guess someone was very prescient. I'm sure there's somebody in Intel who is like, I told you so. And hopefully that person is rich because they were right. Okay, yeah, okay, I just said this. Okay, but for the purposes of our discussion you can think about the OS is running in the privileged mode and user code running in unprivileged mode and there being two modes. The complexities are not really that important. Okay, and this is really what we mean when we talk about the kernel versus an application. Application is code that runs in user mode, unprivileged code and the kernel is what runs with kernel privilege. And going back to how we started this discussion this is by definition what makes the kernel special. It's the one application on the system because it's a process like any other process. But it's the one process that's allowed to use this special mode of execution. Okay, so let me ask you a question. Why is the operating system allowed to do this? You guys have computers, those computers run operating systems. Why? Why is it like the Windows kernel that gets to be in charge on your computer or the Mac, you know, Mock kernel that gets to be in charge of someone else? Yeah. Okay, but who made this decision? Who decided this? Yeah. Yeah, not quite. The bootloader just did what you told it to do. Yeah. The BIOS. The BIOS just did what you told it to do. I'm giving you the answer to the question. Who decided this? What's that? It's not fun. Nope. You did, right? I mean, you bought a Windows machine, right? Yeah, when I said like, yeah, anyway, you got it, you got it, someone got it. Yeah, I mean, you installed the operating system or if you didn't, you let it sit there and continue to be in charge, right? So either way, you're guilty, right? Either commission or omission. So yeah, I mean, and now particularly, if you wanna take that machine and turn it into an Ubuntu machine, you can do that. It's your choice. Or into even a Mac if you have several weeks of your life that you're never gonna get back. You might be able to turn it into a Mac, maybe. But in general, yeah, as someone else pointed out, it's also the first thing that runs. And it's not exactly the first thing that runs because it's at the BIOS and again, we could do a whole week of lectures on startup. But it's not that interesting, I don't think. Except it's pretty cool. But essentially, things can be cool and not interesting. That's possible. So when the machine starts executing, when it powers on, there's all the stuff that happens and some of that is baked in to the machine. The BIOS or partially baked in. You might have a BIOS that you can reflash or whatever. But at some point, you get to a point in execution where the computer starts to execute software. And that's usually built into the BIOS in some way where when you install an operating system and you configure the machine in a certain way, the BIOS is trained. It says at some point, once I'm done, everything else that I sort of do that I'm hard of coded to do, I jumped to a particular point in memory and I start executing instructions. And it's really that simple. What is loaded at that particular point in memory? What is the first piece of software that starts to run? That's the kernel. That's your kernel. If it's Windows, it's Windows. If it's Linux, it's Linux. If it's Mac, it's Mac. Those instructions that get executed, that piece of software that gets run first, that defines the, that is on a machine, that is your kernel. Now, so here's how this works. Again, you might wonder, I can get back and forth between privilege mode and user mode, but how did the operating system get to be in charge? When the machine boots up and you start executing those first instructions, the BIOS has set up the machine so that the processor is in privilege mode already. I think in a lot of cases, when you turn on a processor, it starts running in privilege mode. So, I sort of hand over the system to the operating system in this privilege mode already. If I didn't do that, I'd be stuck because there'd be really no way for the operating system to kind of get bootstrap things and get things turned back on. But instead, I do, I say, here's the system, it's all set up in privilege mode, you're in charge, and then before the operating system starts to run user code, it's responsible for lowering the privilege level appropriate. Does this make sense? So, I start off running in privilege mode, I start off running whatever you've installed as the kernel, that operating system is in charge of adjusting the privilege level appropriately before it starts to run user code. And we'll talk about how this happens later today or Wednesday. Okay. So, another piece of terminology I wanna introduce that we'll use throughout this discussion is something that's called a trap. We frequently talk about trapping into the kernel. And trapping is, trap is the word that refers to something that a user program did that elevates the privilege level and causes the kernel to begin to run. There are kind of two categories of traps that we're gonna be concerned about. Well, we'll talk about that in a sec. One are traps that are intentional because traps are the way that a process gets the kernel's attention. It needs help to do something. It needs the kernel to run because it needs the kernel's privilege to help it with something, like read a file, for example. There are other traps that are caused by the user program doing something that it should not have done. Right, I tried to execute a privileged instruction, I read a bad piece of memory or something like that. And those traps also have the same effect, which is in both cases, the kernel begins to run. And it begins to run in privilege mode. Now, so on some level, what happens, you might ask yourself, what happens to the thread in my program that makes a read call? I think it's kind of a natural thing to think about what happens is that that thread traps into the kernel and that thread is now running in the kernel. So that thread traps in, it starts executing the kernel's read code when it's finished, it traps back out of the kernel and that read call completes. Now, that's only partially true because one of the things that the kernel does as soon as a user thread traps into the kernel, remember, I don't trust anything about user processes. So what does that include? What's one thing, if I just kept, let's say that I just kept running the same thread, didn't make any changes, what would I have to trust about that thread in order to keep doing that? The instructions that I'm executing, I trust because that's my code, right? That's my read system call that I installed and we'll talk about how things get from one place to another in a minute. But what big piece of untrusted, you know, what big thing when I not trust if I wanted to try to run a user thread in the kernel? Where does a thread keep its local state? Like when you make a, so when you make a normal function call, right? You know, you change, you know, essentially you start running somewhere else, some things get pushed out to your stack, when you're done those things get popped back off and I continue to run. If the kernel did things this way, what would I have to trust? I'd have to, well, I'd have to trust the user's stack and the user's stack is a chunk of user memory and so I don't trust that. So one of the first things the kernel does when you start to run in the kernel and you can see this in your own code in OS 161 is that it replaces the user's stack. It says I am not gonna use your stack, I don't trust that stack one bit. I have my own stack here in the kernel that's my memory that you don't have access to and I'm gonna use that. So to some degree it's an accurate way to think about it that a thread enters the kernel, executes some instructions and then returns back to user space but in reality that thread that's executing in the kernel bears only a very mild resemblance to the thread that performed the system call in the first place. So and you know, as you start, particularly as you start to look at the assignment to code that we've given you can decide how you wanna think about this. I think it's reasonable, particularly that we've given you synchronous calls to write to think about it as a thread trapping in the kernel, executing the code at least. That's just how I like to think about it. Okay, so now when the, we talked about how do we get back and forth between privileged modes, between unprivileged and privileged execution. We start in unprivileged, we start in privileged mode and then normally when I'm executing in unprivileged mode there's a couple of things that will cause the privileged level to change and those things can be grouped into three categories. One is that there's some piece of hardware that requires some help from the kernel, some attention. The second one is there's a piece of software that requires attention that is normally a process and the third is that there's some sort of software exception, some software did something bad and I need to handle. So, in the last two right here, software requests attention. This is also known as a software interrupt. The third one, software needs attention. So the difference between those two is that in the second case, there's no way for the software to proceed without the kernel doing something. Now, this last category, software exceptions, I'm not sure I've given you guys an accurate impression of what these are actually like. 99% of these are completely benign and will not result in the software being killed. But there's a small percentage that are bad and we'll talk about the good ones. The difference is in the case where a software requests attention, your process did something that it knew it needed the kernel to do. So the system calls that we've been talking about, if I want to fork, I know I have to ask the kernel to do that for me. On the other hand, if I try to read a piece of memory that it turns out is actually not resident, that the kernel has to do some work to get, which we'll talk about when we talk about virtual memory, I didn't know that. So in the third case, when that needs attention, it means that the software actually didn't know that it was doing something that was gonna cause the kernel to become involved, but it cannot proceed without the kernel's help. Okay, and we'll have more examples of these to make this more concrete. Okay, so hardware interrupts is not something I'm gonna talk an enormous amount about, but hardware interrupts are something that can cause the operating system to run and cause the privilege level to be raised. And hardware interrupts are generated by things that happen to the hardware that the kernel is supervising that require the kernel to do something. So the network card might use a hardware interrupt to tell the kernel, hey, I've got a packet of data here. Tell me what to do with it. Read it out of my memory so that it can be forwarded up to a process. A timer might tell the kernel, yeah, just a sec. You told me to interrupt you every 10 milliseconds and it's been 10 milliseconds, right? So here's an interrupt. Yeah, Ron. So if we're writing a bench-driven code with a sub-link of it, for example, and you have a software interrupt and an interrupt handler that picks that up, is the code in that interrupt handler being run at a certain state of the code? If you're writing code for a machine and you control the interrupt handlers, then you control the machine, right? So one of the ways that the kernel maintains control of the machine is by maintaining control of interrupt handlers. If I let a software start to handle one of these interrupts in general, right? I'm sure there's cases where you can get this to work. But there are certain interrupts where if the kernel doesn't handle them, then essentially it's given up control of the machine, right? So when you're writing an embedded code, frequently there's not a notion of an operating system, right? I mean, I'm writing a single piece of software that runs the whole device, right? So that's sort of back to those Apple two days where I put in a disk and it ran the whole machine, right? But yeah, in general, when I control the exception handlers, when I control the interrupt handlers, when I control the code that gets run when these things happen, I pretty much control the machine, right? I get to figure out what happens, right? And we'll talk about how the kernel uses these things to control the machine. Does that answer your question? So the way these are implemented, if you're kind of a hardware geek, you may or may not find this interesting, you can imagine that there's literally like some electrical circuit going into the processor. This is hard wiring into the chip. So it's like a binary signal. So when the network card says I've got a packet, it kind of raises its hand and that causes the processor, right? So this is actually implemented by the processor. The processor says, when I see a particular signal on this interrupt line, I jump to a particular point and start executing code. And that particular point is code that's inside the kernel. That's what's called an interrupt handler, Ron just mentioned, okay? So this provides a way for hardware to communicate to the kernel. And this is important because the kernel tells, the kernel may say to the disk, for example, read this sector of data from the disk. And this is a way for the disk to tell the kernel, hey, I'm done, right? So this is a way for hardware to sort of talk back to the operating. So let me do this and then we'll be done for the day because this is sort of the pattern that we're gonna use for everything else that happens, okay? Software interrupts, hardware interrupts, exceptions. Here's a couple of things that happen. And these things pretty much all happen, you can think of them as all happening at the same time. This is implemented in the hardware itself. So when a hardware interrupt is triggered, I enter privileged mode. This is one way that I get into privileged mode. I record some information about the state of the system that the kernel is going to need to figure out what to do. And then I jump to a predetermined memory location and I start executing code. This is kind of like making a function call in a weird way. It's not quite the same because this is hardwired into the chip. So there's one location associated with a particular interrupt handler. And anytime that interrupt fires, I do these three things. So this is a little mantra for the next sort of roughly day or two of class. Enter privileged mode, record some state about the system that's required to help the kernel figure out what to do, jump to a particular point in memory and start executing instructions there, okay? So on Wednesday, we'll continue talking about interrupt service routines.