 All right, welcome back. So today we are getting into the nuts and bolts of the operating system and talking about things that actually matter and are actually testable. So today we're talking about processes, which is a term you may not have heard before, but you've seen it's basically just whatever you run a process. Yeah. Okay. I guess the microphone does not work here. So I will try and speak louder. So we're going to talk about processes and the process is just a running instance of your program. Let's see. Hello. Hello. Hello. Nope, definitely does not work. Well, oh, thank you. Okay, that works a bit better. There we go. So we are going to talk about processes. So at its core, a process is just an instance of a running program. So whenever you run a program, it is a process, and it exists until it eventually exits from main as far as you know. But we saw before that a process actually ends whenever it makes a system call called exit group. But the big difference between them is a program or an application. If you want to call it an app, whatever, it's just a static definition. So it's that ELF file. That's just a static definition. And within that file contains all of the instructions, all of the global variables you use, any memory allocations that you might want to make, and any symbols it uses. And then what a process is, is the operating system's job. It executes a program and the kernel manages processes. So it can run many copies of your program if it wants. Each one of those would be its own unique process and then keep on executing until it eventually is done. And then you as a kernel need to manage all the processes from all the users and multiplex your hardware. You might only have four CPU cores and you're running hundreds of programs. So you have to make an illusion that everything's running at the same time, even though physically maybe only four things can run at the same time. So another way you can think of a process is like a combination of all the virtual resources when it's running. So if we consider like a virtual CPU, which remember virtualization is like the concept of fooling something into thinking it has all the resources and it's the sole arbiter of that resource. So a virtual CPU, each process would just assume it gets its own CPU and that's it and it gets to ignore all the other processes. And the fact that there might only be one physical CPU, the kernel has to handle that. So if you are the kernel or operating system, so I use the terms kind of interchangeably. So the real terms are the kernel is definitely part of the operating system, but the operating system is more than just a kernel. But the kernel is definitely part of the operating system. And that's just the thing that runs in kernel mode that can access those special kernel mode only instructions that interact directly with the hardware. So the process contains a whole bunch of other information like all the resources it can access to, all the memory ranges that it's valid, that it can accesses, maybe certain files it can open, things it can do, things like that. So every execution runs the same code, so all of our processes would be running the same code, there's just those static instructions. But each execution or each process runs some specific code at part of the time. And like the program counter or something like that would be considered part of the process because that's the actual process program actually running. So there's a question, if a program is a stack definition, what about dynamic memory allocation? So when your program executes, the dynamic part comes because it allocates memory while it's running. So it executes some instructions, makes a function call that says, hey, I need some memory, and then that would just make a call to the kernel probably if it needs to get more memory, and then it would carry on. So that would just be instructions that are executed. So like you could consider like your malloc call. So your malloc call would be part of the standard definition of that program. And when it actually runs and does the malloc call, then the operating system manages what memory that process can run or can actually access. So because we have this, so hopefully that answers that. So because we have this process contains both the program and information specific to its execution. So going on that again, any dynamic memory allocation that'd be specific to its execution that's different between processes. So one time it runs, once running, it would get malloc get some memory region. The other one would run malloc, probably get a different portion of physical memory. So this allows multiple executions of the same program, which is good, especially if you want to make things run faster. That's one technique you can use. And also it even allows a process to run multiple copies of itself. So you could even split the work into like four chunks and have each process run a unique chunk. And that's one way you can actually make things run a lot faster. So we'll get into more of that later in the course. So the unique part of a process is the process control block. And that is the structure that the kernel manages that actually keeps all that execution information. So specifically in Linux, if you really want, you don't have to do this as part of the course, but the great thing about Linux is it's open source. So you can read the source code if you want. You probably can't understand it right now, but you can kind of guess what some things are. So there's a task struct. You can browse that link actually goes to the proper one and you can see it's just a struct and it has just lots and lots and lots of fields. So some of the fields contain something like process state, saves the registers, some scheduling information. So that's like the information we'll see much later in the course. There'll be dedicated topics towards that where you actually decide which process to run at a given time. Then memory management information, which would be the result of any of those dynamic allocations. Then IO status, like if it's reading from a file, accessing the web, whatever. And then lots and lots of different types of accounting information. And we're going to explore some of that. You're gonna get familiar with it from Lab 1. So the way to run multiple processes at once, there is an old approach called unit programming. That's like old DOS operating systems and stuff that you probably don't know what the heck that is. So back in the olden days when there wasn't multiple cores and computers filled up this entire lecture hall, the model at the time was it just runs one process at a single time. So you don't even need that abstraction. They just have the CPU until they're done. So you can't do two processes in parallel or concurrently, no matter what. And don't worry if you don't know exactly what those two terms mean, because they sound like the same thing. We'll get into what those mean later in the course as well. So everything nowadays follows the multi-programming model that allows multiple processes that can run in parallel or concurrently. And the idea behind that is the kernel or your operating system would still maintain control and be able to decide what runs when. So modern operating systems, if you have four cores, its job is to kind of get out of the way and run all of your applications as fast as possible on all the cores as possible. Yep. So the question is if there's something called workers and then should that be related to the number of cores you have or something like that? Yeah. Yeah, so we'll get to this later, but if you have multiple threads or multiple processes because you're limited by your physical hardware, like typically the most you want is to match the number of cores on your machine, because otherwise if I have four cores and I have like eight processes all working at the same thing, well physically only four can run at a time and there's some overhead associated with switching between them. So you'd want to aim to match the number if you want to go as fast as possible, but typically it doesn't really matter. You want to have at least that though. Anything less than you don't have things running. Okay, so each process is in a type of state. This state diagram is very important. So this is like the lifetime of a process. So initially you create a process. So whenever I try to execute anything like that Hello World program, that Hello World little one I wrote and the one in JavaScript that took way too long, then whenever you first execute something it goes in this created state and that's when the kernel actually loads that L file or that program into memory. And then after it's done loading into memory and everything set up so it could execute, it goes into this waiting state where it doesn't execute a single line yet. It's just able to execute. So it would go into this waiting state here and in this waiting state it's runnable. So you might just see this being called runnable. You might see it be called ready. Basically the process could run. The kernel just hasn't decided to put it on an actual physical core yet. So at some point your kernel is going to make a decision to run your process. As part of that it would have to go from it's currently executing kernel mode. It would have to while in kernel mode decide to run your process, set up everything, make sure the program counter is all good, everything like that and then switch to user mode and then it just lets you have the CPU and your process actually starts executing in user mode. And then eventually once you're in this running state the offering system through some mechanisms you'll see in a bit will say hey your turn is done and then kick you off into the waiting queue again until it decides you get to run again. Now sometimes you might see this other arrow from running so if you just call something like exit group you'd be in this terminated state where your process is done it can't execute anymore so the offering system can go ahead and hopefully clean it up. After another option is while it's running it might make some system call that interacts with something that's much, much slower. So for example your hard drive is much slower so if it's trying to read from a file through that read system call it might go into this block state where the kernel cannot schedule you to run because you're waiting for something. So this would be like I'm waiting for some input from a file. Then eventually that will all happen in the background while something else gets to execute and then it would go from this block state back to ready or waiting whenever that operation is done whenever it's read from the file and able to execute again. And this is just gonna go on and on and on until eventually it calls exit and then it goes to that terminated state. So this is kind of the preview from lab one. So on Linux you can read the process state of the running system by using what's called the proc file system. So there's a standard slash proc directory that you may have seen before or you may not and that represents the kernel state of the process. So they aren't real files that exist on your hard drive. It's just a fun illusion but it really looks like that and you can access them like a file so you can read them, you can write to them, all normal file stuff but it just uses that same interface but they're not real files. The kernel is just faking them. So within that every directory that has a number so within proc there would be a bunch of number directories and a bunch of other directories and anything that is a number, that's the process ID that represents a running process, they're all unique and so if you have like proc slash one, that's process ID one and all the information in that folder is process information about process one. So in lab one there's going to be a file called status there and you are going to implement kind of like a rudimentary top or PS where you actually get to see all of the processes running on your machine. So you're essentially going to make like well, when a really kind of bad version of Windows task manager or activity monitor or something like that but you could extend it, you could make it a lot better. We're just going, a lab one is going to be like a basic version of that where you're poking around in this proc directory. So after that, so that whole transition between waiting and running, well, what decides that is something called the scheduler and that decides when to switch things and that will be a whole topic in like two-ish weeks time. But for now, all we need to know is that to create a process, the operating system at least has to load into memory when it's waiting, that scheduler just decides when it's running and we'll get into algorithms of how to decide what's running, what's fair, what's good to do. First, we're just going to focus on the mechanics of actually switching processes and do some more background on seed just to get used to like this whole system call thing. So this is a core scheduling loop just so you have it and we'll kind of fill in details as we go. So what you're doing is if you need to change between processes, well, the kernel would interrupt whatever is currently running. So it would be running in user mode, it would have a process counter in some state, some registers, and then the kernel somehow gets control. So it starts using the CPU and it's in kernel mode so it can do whatever it wants. So one of the things it needs to do is pause whatever is currently running. So it would need to save the registers of whatever is currently running, save it state so it could restart it again later. So for example, it would have to save that thing's program counter and then it would get the next process to run from the scheduler. So it would just ask it, hey, what should I run next? And then all it has to do is restore that information that it previously saved and then it would just start executing that and let it run. And then eventually this whole thing is gonna go again and this is like the core part of your kernel. This is your scheduling loop. This decides what runs and what doesn't. So this is like, at the most basic fundamental level, this is what your operating system does. It just decides what to run, lets it run and then ideally it just stays out of your way and lets you run instead of it run. So there's some scheduling decisions so you can either let processes schedule themselves which sounds like a bad idea or you can have the operating system pause and maintain control. So the first term is cooperative multitasking and that means each process that mechanism of pausing the currently running process that means that the kernel does not control that. The process has to opt into it. So cooperative means all the processes so all the user processes have to opt into being paused. So of course this kind of sounds like a bad idea because if a process wanted to it could just hog the CPU it could just never relinquish it and never say, hey, I'm done. You can pause me for a little bit. It could just be like, yeah, I'm not gonna bother doing that. I'm really important. I'll just execute until I'm done. So that's not really something that's used anymore but it's a simpler thing to actually implement. You're essentially just kicking the can down the road. But the real thing that we'll see is like true multitasking where the operating system of the kernel is the thing that retains control and actually pauses the processes and actually switches them in and out without any intervention from the processes. So no one right now has ever written some code where they give up CPU time but yet you can still browse the internet, watch a video on the side, whatever when you execute your program so you can thank true multitasking for that. So typically what happens is the operating system of kernel will give each process like a set time slice is the simplest way to think about it. And it could be really, really small, like 10 milliseconds, something you wouldn't necessarily notice. Give them all time slice and then you could just cycle between them is like the simplest thing you could do. And then they would maintain control by periodically like setting an interrupt on itself. So the kernel would handle that interrupt and start executing again and then it can go ahead and switch the currently running process. So the mechanism of like swapping processes is called contact switches. So we said kind of like at minimum, we need to save all the current registers. We have to save all the values and we're also using the same CPU to save the registers as we want to save them from, which is kind of weird. So there's some hardware support for saving state. However, that might be slow because you might not even need to save anything and any extra work you do here, you might be doing this like millions of times a second. So any extra work you do here is just kind of wasted time because it's just switching between processes instead of doing any useful work. So there's some hardware support for that but it's completely pure overhead. It's not doing any useful work. You want it to be as fast as possible and to be as fast as possible, you do as little as you possibly can. So usually there's going to be some combination of hardware support to help you save really important registers and like a single instruction without modifying anything. And then you can use some software to figure out if I need to save anything else that I might not necessarily always need to save. So this course will be again, straddling that line of like kernel mode and user mode, staying mostly in the user mode but using system calls a lot. But we've never used a system call and system calls in C are like really rare. There's pretty much always and in this course there's like a C wrapper around the function call where it might not necessarily directly call that at minimum it would have to like translate the C ABI that we talked about yesterday to the system call ABI and then actually make that system call. But typically they're kind of ugly and C wraps them a bit to make them nicer and sometimes it will combine two system calls into one. So you get more functionality without having to actually implement the two system calls and we'll see some examples of that today. But most system calls have like a corresponding C call and the additional things that they do is set a essentially a global variable called error no. So most of them will return negative one if there's an error and then set that global variable to some number to indicate what happened. And there's some standard functions that will actually interpret what that number means and print you off a nicer error message. They might also do something like buffer reads and writes to reduce the number of system calls like for example, you might be able to just completely write a large file which would take multiple read system calls or multiple write system calls. So it would bundle those up and do them all for you in a single function call. Sometimes they'll simplify interfaces which we'll see later and then sometimes they also add new features which we'll get into very shortly. Okay now, so C the exit function is like kind of related to exit group but what we saw before was that process just ends but in C exit has some like nice additional features. So our exit or exit group that system call terminates the process, it's done, it can't start anymore. So at C but the C version of the exit call there's a feature where you can register functions to happen whenever you exit and they happen before the process dies. So this is an example of something you can do. So I can in main, so execution still starts at main so don't worry about it. I can register, I can use this at exit function so this will register a function to execute whenever that program tries to exit. So it's going to register this fini function here and all it does is going to essentially print out that it's there. So it's gonna print out that it's there and then in main it's going to print do main and then return zero and then this return zero we already know returning from main doesn't really end anything. So that actually translates into an exit like C call so you could either exit zero or return from main and all returning from main does is we'll call exit with whatever that number is for you to make things a bit nicer. So this, so if we execute this, so let's go here. So here's that code again. Do we have any questions about the code and what should this print if that function actually runs? Yeah, yeah so if I execute this and it does what we think it should do I should see print do main first so because all I did was register something to happen when it exits and I haven't exited yet so next thing I would do print do main and then return zero which is like an exit so then as part of the C library it's going to run all the functions that you registered and in this case we've registered one so it would run that dofini right before it exits. So if we run that, so if we run that that's exactly what we see. Yep, sorry? Yep, yeah so I can go ahead and do something like if I didn't want to return from main and then say I do something like so another way I could do something like this so if I do something like this what should happen is I register that function and then I call do main and then I exit so then it should run that thing I registered so it should print dofini and then actually exit and then the process should be done so I'll never see that test printed, yep. If I wanted to skip it then I wouldn't have registered it in the first place. There is, we'll see it in a second, yep. If I register main, oh geez. Well, good thing this is a virtual machine so that's a good question so let's just complain because it's the wrong type. Can I force it? Okay I'm not gonna force the issue now but you might be able to do that but that essentially it's recursion and you've probably just infant loop. Yeah you just infant loop. Okay so let's just go ahead and run this because I don't think I ran it yet. So if I do that I get do main dofini because I called exit and that ends the process. If I wanted to actually see test, well I could do something like this so I should see do main test and then I exit so then I should see the dofini and then it should actually exit. So if I do that then that's what I see. All right, any other questions about that before I do like hard exit? Yeah, so that's a good question. So why, so the question is what about if I just do it at the end instead of registering it and the answer to that is your program is probably going to be much more complicated and it's a cleaner way of doing it. So say I'm doing something like, say I have like a function like foo and then I discovered there's an error in it where I wanna exit my program. Well, instead of trying to remember anything I could just exit like oh there's an error, exit there and it still runs that foo one instead of me having to go through here and like call it and then make sure that I called in every place that there's an exit and then later I might decide hey I wanna register more things then it's like well do I go back and I have to add it to every place I have an exit then so it's just a much cleaner way of doing it. It's like a good interface if you actually want something to run so you don't have to remember anything and you might have exit somewhere else. Okay so if we were like, this is like a very unusual thing you can do but if you really really really really wanna use a system call in C well you do something like you include this. So this will give you all the magic numbers in a somewhat more readable format so you can actually do a system call and then in this header file which you don't need to know cause thankfully we're actually not going to use raw system calls but if you did you need to include that and then you get a system call function which is just a really really light wrapper all it does is essentially change that C A B I to the sys call A B I and that's it. So if I was super super serious I could do sys call it's called sys exit group so this would do like the exact same thing I had when we had that minimal example and do a system call exit group right away so anyone guess what would happen if I compile and run this should I see dofini or not? I still see it, okay. So we got a vote that I'll still see it I don't see it anymore, why? Well because I'm using a raw system call exit means the process is dead no questions asked do not pass code do not collect $200 nothing like that it's not the C wrapper so C might do some interesting things that don't necessarily correspond to a system call and this would be an example of that yeah so the question is is the at exit a system call or is it multiple system calls or something like that so the answer is no it's just part of this like the C standard library so they define the exit function for you so all exit would do is read all the functions you registered call all the functions you register and then do a system call exit and that's just that's just the implementation of the C standard library so now you know how exits implemented in C more or less so yep so here so if we just have our normal example like this so if we just have our normal example like this we just execute this once there's only one thing running main starts from the top like we've always known first call is going to register this function to run and then next it's going to print do main so we'll see that immediately and then it'll hit return zero return zero is the exact same thing as just exit zero so when you call exit zero because it's a C wrapper around stuff as part of the code of exit it's going to run all the functions you registered and then finally do that system call exit so because we call exit while this will do two things first it will call finy and then we'll see that do finy printed and then the next thing it's going to do is sis call exit yep and we can also see that we also have our magical tool yep yep yep yeah so if I have a system call here it does the system call and since it's an exit system call that program is now done running it's gone yep yeah so it's not good it's not going to execute return zero or anything after that point because now it's gone yep yeah so C's typed it has to return nothing because no one's there to read the return value the exit implementers don't even know what your return value means yep so yeah so if you read exit so it's just void void and it's just runs at the end you can't pass it anything because they never know when it's going to call they don't know what arguments to give it it just runs a function and yeah this is just a C thing you could implement this too if you really wanted to or do something so you could do your own call like wrapper around system calls if you really wanted to as well if you wanted to do arguments or whatever but there's like you won't need to no add exit for anything in this course but it might be useful later so there's like a defined order they'll run in the order that you register them or sorry reverse order that you register them so there's actually a defined order as well okay so that was oh wait before that so just to see that hey that's not like a system call or anything well we have S trace so if there's an at exit system call we would see it so we can S trace this and see that there's not so this all this crap up to BRK that's like the C standard library loading so that was the same we saw in the first lecture so and then this is what our program did all it did was write domain and then exit grouped in this case so if you're ever confused about what your program's actually doing the answer is S trace and they can't hide from you because they have to do system calls to do anything useful alright so the next one is going to be a bit more mind-herty so it's called execve and this is actually how you load another program so that was the first line that we saw with our hello world that we didn't call so we'll go into what it actually does and what it does is replace the current running process with another program then starts executing that and whatever program made that system call it now no longer exists it gets replaced by another one so it has the following API it takes three arguments so a path name so that's like the full path starting at slash to the program to load which would typically on Linux be that L file and then there is argv so that's your whole command line argument so that's just an array of string pointers and it's terminated by a null pointer and it represents all the arguments to the process so when you're in bash and you run a command well eventually it's going to call execve and bash is going to actually split all of your different arguments into different pointers and call that and we'll see we'll see the format of that and then there is an additional argument an environment and that represents the environment of a process which is like your path if you type emv all that stuff gets passed to it as well and then if it returns it would return an error and if it's successful it doesn't return because your process is now something else so it would load whatever that executable is and then start executing it and your process is now gone so let's see an example of that I'll just show it in this so here's my execve example I need to turn that so I have main again so in main I'm going to print f I'm going to become another process and then I'm going to set up the argv so it wants an array of character pointers which are just strings terminated by a null one so in my array of character pointers I'll give it ls and then null to say there's no more in the environment I'll just set it to an array with just one element which is null to say I'm not going to use anything with that and then I'd call execve and then I'd give it the path to ls so if you actually like do which ls to see where something is it will tell you that that's where that file actually lives so it's in user bin ls and I pass it the arguments and the empty environment so anyone has to guess what happens when I execute this program so and at the bottom here I have this printf that says hey if execve works this will never print so am I a liar or not so that all fits so if I'm not lying and things work how they should it should print this I'm going to become another process and then if execve works it should just become ls and then do whatever ls is done and when ls is done that process is now done because that would be the thing that calls exit and this would never print because this process is actually ls now and if there was an error then this should be the return should be negative one and it sets error null and then what you can do is if you don't want to figure out which number corresponds to what error you use this pError function and it will print off a readable version of that error and you can have a custom message that has a little bit more context so if I go ahead and run that oops if I go ahead and run that build so if I run that it says I'm going to become another process and then nothing else prints from that and I get the output as if I just ran ls because that process is now transformed into ls and it would load it and then start executing it wherever it starts so does that kind of make sense to everyone? yep it just replaces it so like this so this execve example when I execute this it's a running process so it would have some process ID process ID 100 so it would go print off that I'm going to become another process and then call execve and then process ID 100 it would get replaced so it would load in binls and then process ID 100 would start executing ls yeah so the question is if it's currently running and it has like more time left would still run so typically whenever you call execve it has to like load something from a hard drive which is slow so it would go into that block state and then it might have some time left over but it would go into block state read the actual file do some accounting information replace all the values of whatever I was running let something else run and then eventually it might go back to ready whenever it's done because it has some time left but that's up to the kernel to decide it's a good question we'll get to it later why the hell am I doing this instead of just creating a new process that doesn't like change whatever the hell I'm doing and it is a kind of design thing we'll see benefits of this approach in a bit two strings so we want two strings like you want just another cd two ls's so if this is kind of like the exact same thing as executing or doing bash it should be the exact same thing as if I just type lsls if I type lsls it says cannot access ls they'll file a directory so if I do that let's build it so it's the exact same thing as I run it yep, sorry yes so the natural question is well I give it the path to ls so why the hell do I give it the argument here that's a good question so what do you want it to be let's see does it actually matter so we'll compile that looks the same so this is a funny story so that first argument there this is set by convention it's supposed to be the same string that you use to execute it because sometimes you might have aliases or whatever to actually ls you could rename it whatever the hell you want so typically this is only set by convention and let's see does this one so by convention that thing is just set as the same program name so here it says if I do a help message typically this is the only thing that actually reads that variable so here it's trying to use that to give me a nice help message of like hey I should use ls like this so if that's true then the difference between them is if I change that I compile it oh sorry if I change that and say I'm going to give it that help option so I want to print off some help whenever I exec ve then if I do that well I get that help message I saw before but now when I look at it's usage it just says what so that's just by convention and in fact you might be like wow that's super useful and we might actually crash something but oh no we wouldn't cry so I could just be like well if I screw it I want to just call ls with absolutely nothing so if I do that it's fine because I'm not giving it a help message I'm not giving it anything else so it never reads it doesn't matter funny story though for some of this you can't do that anymore because some security program actually read that variable to see a permission check or something like that and because you can set something as null there that was a way to escalate root privileges and actually get like compromise a machine because if you just call exec ve with null it reads null and that's it so there's some nowadays some things protect against that but it's literally only by convention that the first argument here matches what you call it as typically it's just used for the help message so what we can do to see if I do something like that so if I do something like that that file does not exist so hopefully exec ve gets an error so if it gets an error I should see negative one here and then it should go it saves the error null here in another variable so I just exit with whatever the error is and the reason I do that is because this p error function itself might error and overwrite error null so I want to save the first error that happens so that's probably more indicative of what actually happened so I save it I run p error which unfortunately might also error and then I return which would call exit so if I execute that then I see exec ve filled which was my message there no such file and it would finally exit any questions about that yeah so reset means it just starts executing that new process so it would start executing whatever it start address is for simplicity's sake you can assume that that means it executes its main yep so the error null would get assigned in this exec ve see wrapper so instead of you know how you get errors out of system calls is kind of so as part of the C wrapper what it will always do is if there's an error it will set that error null for you yep so you're only allowed to execute one program here but you can give that program however many arguments you want but you can only execute one process okay so funnily enough this line will actually never execute ever ever ever because either exec ve is successful and I start executing another process or there's an error so I fall into this and then return or exit here so therefore I either become another process or I exit here and funnily enough all this stuff is dead code because it will never ever execute okay so that's the very basic of it we'll see how that works with more stuff later but at the core of it the operating system needs to create and run processes for now we don't even know how to create a process we just know how to replace them which might seem weird for now but I promise we'll get to it but at the minimum your operating system have to load a program and then create a process of it with some context representing all of its dynamic information maintain those process control blocks so including state which would be like the program counter registers things like that then it has to switch between processes using a context switch and then we also saw how it got to replace processes using linux and crap I forgot we'll get better about reading chat sorry about that alright and just remember we're pulling for you