 All right, whoops. Welcome back to operating systems. So today we're going to do some basic IPC. Anyone remember what IPC stands for? Inter-process communication, correct. So by default, remember, processes are completely independent. And we need some mechanism to communicate between them if we want to share any data. And that's what IPC is. So we'll look at some basic ways to do IPC and spoiler alert. You've done them already because you've read output from a terminal before. So just remember that IPC at the end of the day is just transferring bytes between two or more processes. So even reading and writing files is a form of IPC. The two processes do not need to be running at any given time. So you hopefully have written programs like this that have opened a file, maybe modified a file, and then later ran again, that opened a file, read a file. So technically, that is IPC, you transfer data between two processes, and they weren't even running at the same time. So primarily, the read and write system calls are a way to just transfer bytes and those files descriptors, well, they could represent a file, and that would be a form of IPC. So let's write a simple process. So a simple process could just write everything it reads. So it could just read from standard in and then write the standard out. That might seem like a silly program, but it's actually very, very useful. So let us go into the code and see this read and write example. So in this read and write example, we'll just declare a buffer of chars of size 4096. We'll see later why this number is important, but trust me, this is a good maximum number to have. And we'll declare a space for a sine size t because read returns a number of bytes read. So we're just going to do a read system call here in a while loop. So it will read from file descriptor 0 and then try and fill that buffer up to a maximum of size of buffer, which is just 4096. And it will assign it to bytes read and then check if it is greater than 0. So if it's greater than 0, that means we read some bytes. Otherwise, if this isn't true, well, we would have read 0 bytes or we would have read negative 1 bytes, which is an error message. So let's assume we actually read some bytes. Well, we're going to write to file descriptor 1 using that same buffer with however many bytes we successfully read in. And then we just check the return value to make sure that there was no errors. If there was an error, we'll just capture it, do the p error, and then do a return from main with that error node so we can actually see what that exit status is and see what the error is. Otherwise, I just have an assert here that the bytes read is equal to the number of bytes written. So I'm not losing any data here. There might be cases where system calls, the write system calls, are guaranteed to actually write everything you ask it to. But for the purpose of this example, I'm going to just assume it does and then write an assert. And you should probably start programming like this because there's a lot of underlying assumptions you might make, and the easiest thing to do is just to put them in an assert if you make an assumption. That way, if you screw up or that's no longer true, instead of being impossible to debug, it will just say that assert is no longer true and you'll have to work around it and you actually know what's going on. All right, otherwise, if it exits a loop, I check if bytes read is equal to negative 1, which indicates an error. And then otherwise, I just assert that bytes read is equal to zero. And you might see here that, hey, this could be a file. I'm not checking for end of file or anything specific. There's no special end of file character. You might have been taught that before. When you get to Linux, that is not true. So the way Linux signifies that there is an end of file is if you call read on it and it just returned zero bytes read. That means there's no more data. It's not possible to get any more data. You're at the end of the file if it represents a file. So any questions about this example? So let's go ahead and run it. So if I go ahead and run it, that's nothing. So that's what I would expect because it is reading from zero, which is standard input. And I haven't told it to do anything yet. So if I go ahead and type, I could type something like high and then press enter. And then suddenly I get it shot back to me. It would read my input, which was the string high with a new line. And then I press enter. The read system call would actually read the data. And then it would do this. And then it would write that data right back out to standard out. So I see it twice. Any questions about that? Or why this would be useful? Has anyone encountered a program that does things like this before? Does this remind anyone of something? Yeah. Copy paste. Copy paste very close. You can actually use this to copy files. If you really want, we'll see that shortly. Has anyone used this command? So guess what? If I run this command and I'd say hi, it does the same thing because this program actually just does what I wrote. It just take reads from standard in and outputs to standard out. And that's all it does. Yep. So for bytes read equals zero, it means I've read every single byte from the file. So in this case, since I'm typing into it and it's not a file, you might be like, okay, well, how do I tell it that I'm done? Is that your question? Yeah. So if you want to tell it you're done in standard input, I can press control D and control D closes your standard input and then that program would have read a zero and then close. So cat would do that. And my read write example does the same thing. If I press control D, gives me my terminal back. So that simple application is actually pretty powerful. So let's see other things we can do with it. And yeah, just to recap too, so read just reads data from a file descriptor and it's an abstraction. So you're not actually sure what that file descriptor represents in the case that I've showed you so far. It just represents me typing into my terminal. And like I said before, there's no end of file character. That is not a thing on Linux. The read system call will just return zero bytes read and that's how you know it's not possible to get any more input. And again, you should probably check for errors here. We'll see a case where checking for errors is probably a very good thing. And yeah, the screen's at a weird place. So yeah, cat's short for concatenate because we can actually use this to concatenate files just by using those file descriptors. So the write system call just writes data to a file descriptor, same thing as read returns the number of bytes written and you shouldn't always assume it is successful. And again, if you want to see what the error know is that caused it, well, you probably have to save it because that P error function might set it, other functions might set it. So if you want to capture that error, you have to save it before you call any other function from the C standard library. So, and also with this, I'm reading from standard input. So another process is writing to that same file descriptor which is why I can read from it. So my terminal is taking the keyboard input I put in and then writing to that file descriptor, translating it to ASCII and then that process can then read from whatever it wrote in. So both sides of this, they're going to have a read and a write side. In this case, we're just dealing with read from standard in and write to standard out. So these are pretty powerful. So what we could do is we could close file, the standard output, just freeing file descriptor zero and then we can use that open system call to open a file instead. And Linux has a rule here with file descriptor so it will always use the lowest available number whenever it needs to allocate a new file descriptor. So if I close file descriptor, so by default, zero, one and two will be open. If I close file descriptor zero, then suddenly zero is available, that's the lowest number. If I open a new file, it will just get file descriptor zero. So we'll essentially replace standard in. So let's look at that example. So this code is going to be the exact same except I take another argument from main. So if I have a second argument that's supposed to represent a file, I will close file descriptor zero and then I will open, try to open whatever that file name is as read only because I'm just doing read system calls on it. And then here I would check for an error. If there's not, then I would have a file open as file descriptor zero. So if I go ahead and run that, I can go kind of meta here and I can tell it to read the source code file of itself. So if I execute this, what should I expect to see? Yeah, yeah, I should just expect to see the whole source code. So there's the whole source code for it. So now because of that, the rest of the program didn't change. It's just reading from zero and writing to one. But all I did was replace zero instead of being the default thing where you just type in your terminal and that's what represents file descriptor zero. I replaced it with an actual file. Yeah, so the normal convention is that file descriptor zero, one and two are open and you don't close them. Yeah, even in this case because I use it until the end. So it's like that whole thing where we don't have to necessarily call free because the process is gonna die and everything gets cleaned up. So this, we don't have to do it. Yep. So yeah, the question is if here I close file descriptor zero and then I open it again, what would happen? So we actually don't know what file descriptor zero represents in general in this case. So it gets like a special file that represents my terminal. And if you knew what that was, you could reopen again and nothing bad would happen but you would have to guess it. So if that's my only way to refer to file descriptor zero and I close it, I can't get it back without knowing how it got opened originally. Yep. Yeah, so you can check whether or not there's file descriptors open. In fact, that's what one of your test cases does in lab one. So I checked that you close your file descriptors because you should, they're like memory. You should close them whenever you're done using them. So you should, but by default, no one closes the default file descriptor zero, one and two. So, all right. Any other questions with this? So this, yep. What the difference between argc and argv is. So here the C way of referring to things in main into argc is the size of this array. So it's however many command line arguments you have in it. So in this case, by default, the first one is always going to be like how you ran it. So this would have, this would be the zeroth argument. This would be the first and there'd be two elements in this case. If I did this, then argc would just be one. Okay, so that actually also behaves exactly the same as cat. I can cat the file, I get its output. Doesn't really do anything special. In fact, if you use the standard file descriptors, you don't even have to write open and closed to deal with files. You can do some hacks with the shell. So in the shell, I can do something fun like just use my first version that just used two file descriptors and I can use this fun arrow and give it a file. So what this will do is your shell will actually open that as a file and then replace file descriptor zero with that file for you. So now suddenly, without changing your application at all, it can deal with files if you just use the standard file descriptors. So if I run that, it's the exact same thing. And then I could also do something like this. So the other arrow will replace standard output. So this goes to whoever said it's kind of like copying. I could do something like this. So I could replace standard output with a file as well. So this will read from one file and then write to another file, which is AKA copying a file. So if I do that and then I look at copy.c, I should see that it is exactly the same. So using this, you can use cat to actually copy files if you really wanted to, which is kind of neat. At least I think so, but you know, I'm me. All right, any questions? Yep. So the question is where did I write the logic for this? So the nice thing about it is you don't have to write it so your shell will do this replacement. So this is just a shortcut. So before it creates this process, it will open file descriptor, it will open this file and then use this as file descriptor zero for that process I'm about to make. And then it opens this file and replaces file descriptor one in the process I'm about to make. Yep. No, it doesn't change RGC, RGV or anything like that. It just changes the standard file. What the standard file descriptors actually refer to. So if I do this, instead of before standard input was my terminal and standard output was my terminal, now standard input is openexample.c and standard output is copy.c. So if you wanna capture the output of any program to a file, you know, if you haven't seen this before, you can just do like that. I might have showed you that before and not explained it, but this way just captures the output of any program by just writing it to a file instead of the terminal. Yep. Yeah, so if your program has scanf or something, that's just gonna read from file descriptor zero. So if you give it a file, it will try and scanf whatever match the format. If it doesn't match the format, it'll give you an error. So it'll work the same way scanf works fine. Yep. Yeah, so this will create it if it doesn't exist for you. So that's part of your shell. These are just fun shell extensions that they've written over the years if you want to. You know enough by now you could write your own shell if you really wanted to. It's kinda cool. Yep. Yeah, so this arrow replaces file descriptor zero. This arrow replaces file descriptor one. Yep. So if I have another open, whoops, how do I screw that up? Like if I just open something else here, int fd one equals open. So it will just open that file, give you a new file descriptor. In this case I can argue that it would be file descriptor three because that's the lowest available number. And if I want to write through that file, if I wrote to file descriptor zero or three, it would do the same thing. They both represent the same file. Yeah, so you could have multiple ones. You could have it really screwy where standard input and standard output refer to the same file, in which case bad things will probably happen. We'll save that for later. But yeah, these standard file descriptors, they're actually pretty powerful. And that's why they designed Unix like this. So yeah, so here's what we saw. We saw that the shell lets you redirect the standard file descriptors. So I didn't actually have to write a program that did any open system calls. If I'm just using file descriptor zero and one, well, I can just let my shell replace them for me. I don't have to write any additional code. And you don't have to like, if you want a program to start using files instead, you don't have to modify it or anything. You just use shell commands. There's also this fun operation here, which is a little pipe operator. So this actually feeds the output of this process into the input of this process. So anything that it writes out gets read by the next one in the sequence. So if I run something like this, so if I run something like this, I'm going to get the same output, but it's going to be a bit more roundabout. So cat open file dot c, well, that will print the file to standard output and then this operator would redirect that process's standard output to the next process's standard input. So this would read all that information again and then write it out to my terminal. So this is how you can chain commands together to do some special stuff. So like for example, if I did like ls dash l, that gives me the new line for every file in my directory. If I want to count how many there are, well, there's this other program called word count and you can give it dash l and it counts however many lines it reads in. So if I do that, I can see, hey, I have 12 lines here and you could do that for anyone. In fact, that's what I use to figure out how many thousands of system calls JavaScript did. So I just counted the lines using that program. I never wrote a program that counted lines. I just reuse that one and just move the outputs around. Okay, so do, do, do. All right, so now we get into signals which is another form of IPC and they basically behave like interrupts. So has anyone ever ran a program before and it ran too long or your thing got an infinite loop and you press control C? We've got some. Does anyone know what control C did? No, we just know to hit it. Yeah, it does interrupt your program but specifically it will send a signal. So let's see. So if we go here, let's just see that it works. So, whoops, build open example. So I'm going here, oh no, I don't know how to exit. I press control C and it exits. Well, I can see it didn't actually reach and a file or anything. If I put some print statements at the end, I can see that it didn't finish my process or anything. It just kind of ended and that seems a bit weird. That's not super, that's not super satisfying. So what actually happens is when you press control C, the kernel will send a signal to that process and in this case, it gets a name where it's called signal interrupt. So it's a special signal to represent the user interrupted the process through using the keyboard in Python. It's just called keyboard interrupt, I think. And all the signal is, basically the kernel generates an interrupt to your process and then each different signal is just assigned a number. So sig int will just be a specific number and the default handler. So there's a default handler if you don't write one and by default it will exit the process with the exit code equal to 128 plus whatever the signal number is. So if I go back to this, I can see this is my exit code way in the bottom right here, this 130 and I didn't call exit with 130. That was the default signal handler that actually exited my process with that exit status. So since it's 128 plus whatever the signal number is, I can probably, hopefully we can do some math and argue that sig int is signal two because 128 plus two is 130 which is what I saw. So we can see that this is what's actually happening. So we've all written interrupt handlers. Well, you have written interrupt handlers so signals and dealing with those are the same thing. You can write your own interrupt handlers. So you write your own signal handlers and then you register them through a sig action system call and we'll see an example of that. Basically you just declare a function that doesn't return a value or anything. You should be fairly familiar with this for any interrupts you have because they just get called asynchronously and all it has is an argument that represents the number of the signal number. So some of the numbers are non-standard. Below are some of them from Linux. So two is interrupt from keyboard. Nine is sig kill, which is a fun one that we'll get into later. Here's another fun one that you've probably encountered before sig v, so segmentation fault that is you dereferencing an all pointer and those fun stuff. So that is just a signal and by default that's why your program dies because the default signal handler just exits your process and it would exit with 128 plus 11, so 139 if you've ever actually checked it whenever you say fault. And then there's this 15, which is sig term which is terminate. That's supposed to represent graciously shutting down and we'll see what not graciously shutting down looks like. So let's go into this example. So in signal example, so I'll just keep the rest of the program. So it's just going to read from standard in and write to standard out. And in here, I just have a register signal that just creates a sig option struct. So they give you a struct to fill in. You can use this function to zero initialize it. And then the only argument we really care about is this essay handler. So it says what function should I call whenever I get receive a signal and here I just have this handle signal and I'll just do something stupid and I will just say, I will ignore you because if we register a signal handler we can do whatever we want. So this is actually a good way to annoy your friends. So in here, we do the sig action call register a signal number and then say the action which has our handler we want to run and then we just check for errors. So in our program, we're going to register a signal for sig int which is pressing control C and generating that, hey, I want to interrupt you, I probably want to quit and we'll also register the signal handler called sig term. So if I run something like this and I run it, well, it behaves as before but now what happens when I press control C and your friend wants to get out of your process? Well, just ignores it, that's fine. So this is a good way to annoy your friends because hey, it's not going to exit. So one way we can actually try and exit, we can try to exit it nicely so we can ask pid of and then give it a name so it was called signal example. This program gives you the process ID of any processes with the matching name. So here, this is a process ID for that program which is still running and you can actually send signals to any process you would like and this is where our friend, sending a signal is called kill. Why is sending a signal not called send signal and why is it called kill? I have no idea but by default it's called kill. So I can say kill this process and then go look over at and that says ignoring signal 15. So kill by default that says please close nicely that's that sig term which is 15 but as luck would have it, I ignored that too because yeah, why not? Right, I could ignore it. So there is a special signal that you are not allowed to ignore that will kill your program and that is this dash nine. So dash nine, this dash and then number will send a signal of that number. Dash nine is sig kill. That is a signal you are not allowed to handle so the kernel handles this signal for you and has its own handler for it and you are not allowed to overwrite it so if I do kill dash nine to it and then I go back over to it, I see that it was killed. So now we have the word kill in our vocabulary so we can now kill our children, yay. So any questions about that? So that is a fun thing to do and by default sometimes whenever you're writing this so before I rebooted my computer that example behaved a little bit differently. So whatever I hit control C before it actually stopped running because the read system call returned an error. There is sometimes system calls can return an error that say they got interrupted and they don't automatically restart by default. In this case it did restart by default. So we should have saw something like ignore signal two in that it would have gone back to executing our program and we would have saw that read got an error and its error would be interrupted system call in which case I modified it in another example that checked for that error and then would restart the read system call since that's actually not a bad error that doesn't mean anything's gone wrong that just means I have to retry the system call. And then so that's where that example is and now if I fix it it does the same thing I showed you where I just press control C over and over again and I can't get out of it and here is the explanation of what I did before that. So sending a signal you can the system call is called kill and someone wrote a program called kill that just essentially does that kill system call for you. So it takes the number and then the PID of what to kill and I showed you how to find the process ID with PID of that is another program you can run and then after kill was just a PID send sig term which is supposed to just say end graciously please and the idea behind that is you can register a signal handler and do some cleanup and be sure you don't mess up your files you write anything you need to write before you exit and then you just exit. Then we saw kill dash nine which that's like you're telling the kernel to kill that process and it doesn't have any rebuttal to it. It just kind of dies and yeah and then there's a question can you override SIG, SIG faults sure you can ignore them but then the kernel if you catch it the kernel thinks you that you should fix it because it will try and resume running your program. So if you catch it you do nothing and then it tries to resume your program and just SIG faults again. It'll probably get very angry at you and just kill it because it can't run anymore. But if you really wanted to you could catch SIG fault if you want so it probably won't help you it's really hard to recover from that but you can if you want and yeah. Yeah, so if I get PID of and I have two processes with the same name it'll return all of them so it would return both of them. Yeah, okay so that's fun. So I talked earlier too about blocking and unblocking operations and that wait system call. So let's see that so a non-blocking system call is going to return immediately. Remember by default the wait system call is going to wait for your first child to terminate before it returns but if you don't want to wait for that you can do a non-blocking wait. So you can turn it into non-blocking by using wait PID and then there is another argument for options and one of these options is this W no hang. So W no hang means not blocking I just want to see if I have any terminated children and to react to these well there's two options that you should probably recall from architecture polling and interrupts. So let's go ahead and see what the polling example would look like. So here's code we had before where we have main we immediately fork and then the child sleeps for two seconds and then exits and now in the parent we'll set up wait PID we'll initialize it because why not and then set up some space for the status and now I'm just going to see how many wait system calls we actually need to make. So here I just have a count I initialize to zero and then while wait PID equals zero which means for the non-blocking version a child has to initialize it so I'll just have a loop that says well I don't have a child terminated yet just go ahead and call wait PID with the status that we used before and then no hang and in this case I'm specifically waiting for a certain child to terminate because in this example well this child process just has one or sorry this parent process has one parent process or sorry this parent process just has one child anyways so I can just wait for it specifically if I wanted to do a non-blocking wait that waited for the first children to die like wait well I would just do something like negative one so negative one means wait for the first child but in this case they're both going to do the same thing so it doesn't really matter so if I run the pull example well it's going to ask over and over again it should be asking for two seconds and let's just see how wasteful that is so it's just asking out over and over and over and over again and pulling and it asked 370,000 times so it did 370,000 system calls which are pretty slow and just asked hey is a child terminated yet is a child terminated yet which seems really bad so how would I make this better so this is wasting a lot of time what could I do to make this not waste so much of my CPU yep while still pulling over and over again yeah you could just like sleep like that yeah so this will work better right I type out the other one which one did I break oops there we go so if I do that I'll get one attempt, another attempt attempt, attempt oh yay it finally worked but this is going to be also really slow because I sleep for a second I won't get I won't immediately get notified whether or not the child terminates I might be up to a second late hopefully on average I'm like half a second late but that delay is probably still not to is probably bad so the optional thing to do is to do an interrupt version of that and in user mode the interrupt version of that is signal so remember how I told you that hey when one of your children terminate the offering system pokes you well that poke is it sends you a signal that says hey you have a child that is dead or terminated sorry not dead yet so in here I can have a handle signal so I'll just register a signal handler and there's a special signal called SIG child which has another number associated with it so that is the signal you get when the curl wants to tell you that your child has terminated so in here in my main my child's going to do the same thing so wait for two seconds and then terminate and then here in the parent just to show that I can do useful work I'll just print off I'm going to go to sleep and then sleep for 9999 seconds just to show that hey this could represent a long running task and an interrupt is instant so it will interrupt this sleep so I register the SIG child signal handler and in the function that's where I actually call wait so I'll call wait with wait PID give it negative one because I don't care it's the first child in this case I'm not keeping track of the child's PID I created so I don't know what it is in this function unless I create a global variable or something like that but I didn't so I'll just use negative one to say just the first child even though there's only one and then here I just call wait PID with the no hang and then check if there's an error otherwise I'll do the same thing and just print off whatever I got so if I run this it should be a lot faster so I just say time to go to sleep then two seconds the kernel generates a signal for me I handle it I wait immediately I'm a good parent I immediately you know took care of my child even though I was doing something else that you might have thought it was more important so any questions about that yep yeah so the questions why did I set to negative one here so I set to negative one here because in this signal handler right it just gets the number of the signal I got it doesn't get any other arguments I can't pass any other arguments to it so I don't know I don't have any access to this variable so I capture PID in the parent but in that signal handler I don't have access to it if I wanted access to it I could make it a global variable or something like that but I decided well I can just wait for any old child I don't have to create a global variable or do anything like that so I could have but I didn't want to alright any other questions with this yep and good parents yeah if you want to be a parent you just have to challenge your child you can neglect it it's entire life you don't have to constantly hover over it just wait for you know a sensible adult like the colonel to tell you that you should take care of it alright any other questions with that cool so everyone knows signals yep sorry like the handle yep so it's pretty much what I had in all the examples where I just call say I'm calling wait and just call wait in this the only difference is instead of just calling wait I called wait PID with negative one and use the no hang version so if I didn't have a dead child it would just immediately return with zero to say it's still running but in this case the curl already told me that it's done right so I can just do the no block version and it should return immediately yep yeah so if I have multiple children I could get multiple signals yep yeah negative one would still work in fact if you have multiple children you don't know which one is going to terminate first unless you really wanted to wait for one to finish before the other for some reason you could do that by waiting specifically on their process ID but if you don't care you just want the first one just negative one yeah so in this example my process could do useful stuff instead of just calling wait and then waiting for the kernel to wake me up sometimes you might want to call wait sometimes you're just managing processes and you just want to wait for that specific one to finish and you don't have anything useful to do this one I had useful things to do and I wanted to be notified immediately so it just depends what you want to do okay so yeah so we saw the interrupt version, the polling and so now we have to talk about interrupt handlers and they run the completion and you can generate multiple of them and they're not great so let's see an example so let's have our open example well we might want to write it in such a case that if we get a control C we might try and be good and close our file descriptor that represents the file before we exit so in this case if I have an open file descriptor in my signal handler that will catch me pressing control C or asking to terminate I'll put a message that says I'm going to close this file descriptor in exit check if the file descriptor is equal to negative one and the file descriptor is a global variable so I can access it in this handler so my main function will set this file handler if I give it a file argument and then in here I'll just say close basically I want to do is close that file descriptor in the signal handler and then I'll check if there was an error otherwise I'll say you know closed file then I'm going to artificially slow it down to illustrate something so all looking at this we could just say hey if I press control C or I ask it to terminate everything should be fine right I just close the file and then close the process I should never get an error from close well in fact that is probably not a thing so let's signal close example so here I say what the process idea is just so I can send signal to it easier and we'll say hey it opened file descriptor for so I didn't actually close file descriptor zero then here I can be nice and say kill that process that says hey I close the file but I can press control C and now that signal handler runs again so in the middle of that signal handler a new signal handler runs before that one is finished and it starts doing the exact same thing again so the first signal handler would have printed then close the file and then print closed file and then been in the sleep and then while it was in the sleep we started another signal handler while this one was sleeping that did the same thing so it printed this then it tried to close and guess what it got an error so it hit this and then did close bad file descriptor exit why was it bad because we closed it already right so this kind of sets the stage for threads later this is an example kind of an example of concurrency and things you have to be careful of so things signals like this things like this are possible which makes your life very difficult so we'll get into that just this is just kind of a warning as something not to do technically the terminology when we get to it so just so you remember this is called your signal handlers have to be reentrant so in the middle of calling it you need to be able to call it again and it needs to be able to successfully finish in this case not really the case so before we close that was real quick so your hardware interrupts are a bit different so on risk five there's like three terms for an interrupt it depends how it got generated so there's an interrupt which is triggered by real external hardware like a keyboard or something the kernel handles that you don't see it in user mode then there is an exception which is essentially the same thing as an interrupt but that's caused by you executing instruction you shouldn't have like divide by zero or referencing memory and the kernel may pass that up to your user process in the form of a signal and then there's a trap so you're just requesting control that can be caused by an instruction or something like that and a system call would be an example of a requested trap so you are triggering an interrupt that the kernel handles alright so before we leave so I always get this and I always hate this part but you might ask hey process ID one pretty special kill-9 pretty cool what happens if I do that I just kill-9 one anyone want to guess what happens yep nothing shut down why is that uh oh I didn't get a good warning anyways so the kernel actually protects you from kill-9 in it because it knows that's a silly it has a shutdown command for a reason so newer kernels won't let you do something silly like this but I can do something sillier and still break it so system D remember that's the thing controlling my whole root or my whole user so I could wreck this VM and do something I don't know which one switch I could do this and I meant it alright that was the wrong one hey it's still going oh it just restarted itself cool alright cool it's resilient to this so I'm not actually logged in directly to it visual studio codes doing some magic which actually saved me from it it's a simple success here but sorry alright just remember pulling for you we're all in this together