 All right, it's noticeably quieter today. Is this everyone that did lab one already? Did we do lab one, hands up? OK, so I assume everyone else is panicking doing lab one. All right, well, anyways, so I synchronized with Ashton about quiz one. We are on track. We're ahead of the other section in most regards. And we're behind in a few. But for the quiz, it'll be everything we can both answer. So I'll look at the quiz. If there's anything we haven't covered yet, I'll throw it out. So yeah, it's mostly multiple choice and true, false. It's like 80% multiple choice, true, false. There's some written things. They'll probably explainations. Like why is this? What's this trade off? Blah, blah, blah. I believe you can just type it. Yeah. Is it like a Quarkis quiz? Yeah, it's a Quarkis quiz. All right, so today, this stuff will not be on the quiz because it's just tying together everything we've learned. There's some stuff that will be on the quiz towards the very end, like very conceptual things. But for the most point, we can relax for like the first half of this lecture. Yeah. Sorry? Unfortunately not. Because it's not a direct conflict, so there's an hour gap, so he's hesitant to change it. So the quiz two got changed. But quiz one, unfortunately, remains the same. All right, so let's get into some fun stuff today that might even help with lab one a bit or at least help with kind of unixy stuff. So today, we're going to talk about IPC, which is just transferring bytes between two processes. And you've been doing that already if you don't know it. Even printing hello world to the terminal is IPC because you're transferring the bytes hello world into something that can actually show them. So even reading and writing files is a form of IPC. Some process will come, be created, write to a file, and then another process that comes along much later comes, reads that file, and that's a form of IPC. The processes don't even have to be alive at the same time. And remember, from the beginning of the lecture, the read and write system calls, they just take a file descriptor and read or write bytes, and they don't really care about that. So we could make, yeah, inter-process communication. And that's just communicating between two processes. Because otherwise, remember what we saw? By default, processes are completely isolated from each other, even their memory, their address spaces are completely different, so they can't affect one another. But in some cases, you want processes to communicate with each other. And you have to do that through IPC or inter-process communication supported by the operating system. So you're actually talking to the process you want to be talking to. So we'll dive right into a code example that all it does is read from standard in. And if we remember the standard file descriptors, that's just file descriptor 0. And then it writes the standard out or file descriptor 1. And we'll see what it does and see if it kind of reminds us of something we may have used before. So here is the entire program. It declares a buffer of size 4096, which is an important number, which we'll see when it comes to memory management. But for now, it just declares a fairly big buffer, and that's all on the stack. And then it declares some variable that keeps track of the number of bytes read. And then it just calls read with passing in the address of the buffer and then the size of the buffer. So it will read at most 4,096 bytes. And then it just has it in a while loop. So it loops over and over this as long as there are some bytes left. OK. So someone complained about that before or said it was better just that. This is overwhelmingly voted for. OK. Well, sorry to whoever suggested the other thing. So this read, so it just returns the number of bytes read from it. So it's just going to infinitely go over this loop while it's still reading some bytes. After it gets some bytes read, we just write out the number of bytes that was read in. So we're just reading it in from file descriptor 0, spitting it back out on file descriptor 1. And then it just checks if bytes written, it's a standard C wrapper for that system call. So we have to check if it returns negative 1. If it does, we do the error handling stuff. And then here I was lazy. The kernel cannot write out all the bytes you give to it, and in which case you should handle it and do another write call. But I was just lazy, and I just checked that it did what I expected it to do. And if it doesn't, instead of silently failing, which is like things Java likes to do, which is really annoying to debug, I just explicitly put an assert there, and I will see something. So another neat thing about this is there's no explicit end of file character because standard end could be a file. So there's no end of file character. The only way you know if you've reached the end of input is if read returns 0 bytes read. And that means you've reached the end of the file or the file descriptor or whatever it represents. So if we go ahead and execute this program, it's going to read in things from standard in and then print them back on standard out. So I didn't do anything special. So if I just type, it should be on standard in, and then I can hit Enter. And then it just spits it back out at me. So has anyone seen this program before or used a program that behaves like this? Echo, close, anything? Scan F to C function. Although I guess we might not have, has anyone used this command cat? Oh yeah. Yeah, so if I do cat and I does the same thing, right? So we actually just wrote cat. So cat's a very, very simple program. It barely does anything. All it does is read from standard in, write to standard out, and in the unixie way, you can do a lot with that. So read write, yeah. Bites written less than bytes read, yes. It might, in this case it doesn't because I have this assert and the assert never hits. So I check if that is true. I just didn't want to write more code if I didn't have to. And this, I haven't seen this assert got been hit yet. So typically there's a limit, but it's less than that 4,000 limit, which is why I picked it, yeah. Yeah, the default C handler for an assert essentially just prints an error message related to that and then calls exit with like one, right? So that's what assert does. All right, cool. So if we go back, if we just read the documentation, right? Read again, just reads from a file descriptor by default. We just use the default ones. We use zero and one, like that's just a unix convention that those file descriptors exist. Zero, one and two are always open and they're just defined as the standard ones. So there's no end of file character. That is actually not a thing. Read just returns zero bytes read and that's how you know you're at the end of the file or the end of your network connection or at the end of whatever that file descriptor happens to represent. And again, if you do this, you need to check for errors, right? Save error number because if you use that P error to print it out, that also might contain an error because that would just do a right system call essentially we know now and it could fail for whatever reason. So you should, if you want to show the first error you just have to save it. And write does the same thing in reverse. It just returns the number of bytes written. You shouldn't always assume it's successful if you're doing big writes but we've all used printf before printf calls write. It actually returns the number of bytes it has written for you but I would bet no one's ever checked. The return value of printf. So you should but if it's small enough you don't really have to. But in this too, both ends of the read and write have a corresponding write and read. So something is writing those bytes that are read from our process as standard in and then something is reading the bytes we are writing out namely our terminal or our shell in this case. And we'll see how to actually make this communication channel later but for now it can remain a mystery. So the standard file descriptors are quite powerful, right? We could, if you wanted to you could close the standard input which closes file descriptor zero and open a file instead and you had to do an open system call for the lab one I assume, right? So if you're opening file descriptors open just returns a file descriptor and the way it works is it returns the lowest unused file descriptor. So if I close file descriptor zero and I open a file it will get file descriptor zero because it's not currently in use so it'll use the lowest number. So let's see that example. So the core code is going to be exactly the same where we have this, right? I'm not going to change even the file descriptor number the core code is going to stay exactly the same. So I'm just going to read in an infinite loop and then write at the end. And then at the beginning here I will open my file so I just take the second argument yeah index, yeah the second argument which is index one I open the file as read only and I get a file descriptor out of it. And now at this point if I printed it off here it would be file descriptor zero but I know it's file descriptor zero because I closed it right before. Yeah, it's returning the file descriptor number but the kernel as part of that open with a file name it's going to check that you have permissions that correspond to this. So this is I want to open a file for reading. So if you can't open that file for reading it'll give you an error. So no, so file descriptors are unique to a process. Yeah, so by default the only thing you know is the convention is you'll have three file descriptors open when your process starts and that's it. You don't know what they're connected to you just know that there are three valid numbers you can use and you're supposed to use them standard in, out, and error. So without changing the core code I can go ahead and be kind of meta and I can essentially if I do that I get the file back out as input. So now I change file descriptor zero to be the file and now if I write or if I run execute that same code the read is now going to read from the file instead of my terminal and it's going to read until it gets the whole file and then just dump it out on standard out, right? And this is exactly what would happen if I just catted the file, right, same idea. If I catted the file I just get the file back out. So having open files is kind of a long solution you don't have to do that if you're using a shell because there's standard file descriptors and your shell will let you change what the file descriptors are. So instead of running open example and then specifying the file and then within there I have the open that will open the file I could instead just use my shell and do this. So this is just the executable you run and then this symbol means that whatever is on the right I should feed that into standard in. So this is just replacing standard in with the file so I didn't actually need to write that open system call if I didn't want to, all right. I could have just used our first read write example and just let the shell redirect the file as standard input and then I kind of would have saved myself having to do a file handling code in my program. Other fun things you can do you can also redirect across multiple processes so I could cat that file and then do this pipe character to feed it into another one. So this basically just connects these two programs together. So anything that the command on the left outputs will get fed to the command on the right. So if I do that I could cat open example and then I could essentially that character is called a pipe which we'll see later but that just connects the standard output and standard input to the standard input of the next process, right. And I could chain those indefinitely so echo will just say whatever I put in. So if cat just like reads what it just writes whatever it reads and I do that it just should be the exact same message, right. It essentially just does nothing and I could pipe that as many times as I want I'm gonna get the same answer. I'm just creating a bunch of processes because each of these commands the shell's gonna create a process for them, right. Okay, so we'll get, yeah. So the line thing connects two processes together and the arrow to the, yeah. So there's gonna be two arrows. There is read right. So there's like the arrow to the left which is standard input. So if I put open example.c it's going to open that file and replace that process's standard input with that file, right. And then there's also, I could go the other way around. I should do a better command. Open example. So this will just read that file and spit it to standard output but I could use this arrow and this arrow will redirect standard output to whatever file name you want, right. Which I used before in another class without really explaining it. So if I do that instead of going to the terminal it's going to create a file called output.txt which should have the contents of open example.c which it does. Okay, hopefully that is okay, oops. Yeah, so you don't have to worry about the pipe things just yet. We'll see how to actually do pipes. We'll see it come up again. Yeah, yeah. And you can just chain and main them together as you want. Exactly, yeah. And one of the labs you might do is you will implement that. We'll not implement that but create pipes between different processes and launch processes and it's lots of fun. All right, so now we have to talk about signals. You'll be using signals I believe in lab three. Signals are kind of awful but this gets into our whole fun terminology where we actually get to learn the word kill today. So if I go ahead and open that open example or the read-write example that just takes input from like by default standard in and if that's my terminal, instead of pressing control D which kind of closes that connection like sends kind of end of file I could press control C, right? Who's here has ever hit control C? Okay, who knows what control C actually is? Okay, one person, two people. All right, so we'll figure out what control C actually is but for now all we know is that like kind of just interrupts our program and then hopefully stops it. We'll, sorry? Yeah, well, so we'll see. I will show you something that cannot be stopped. Yeah, that sounds more fun than it actually is. So in here, right, if I do read-write example so if I press control D that just sends end of file so read would return zero, it exits normally but if I hit control C, it does this. So it actually dies but it didn't die from any code we wrote, right? We just had an exit zero and we now know that that's the only way to end a process and this is the exit status number that got set. So some mystery happened in some code we did not write that exited with this weird number 130. So we will figure out what that actually means today. So the kernel, it's another form of IPC. It can send a signal to processes and when you press control C that actually sends a signal and we'll see how you actually handle that and what the default is but by default the kernel or C, yeah the kernel will have default signal handlers for you if you don't write them yourself and by default the control C signal handler will just kill your program. So if you don't do anything by default it will work in your case where control C didn't stop it they probably overwrote it and just ignored you they didn't, unfortunately they didn't listen to you. So if you press control C you're going to send a signal to a process and they're just like everything else in computers they're just names given to numbers it's just a number associated with something that's supposed to represent something. So one signal is called sig init which is just supposed, we're terrible at naming stuff it's just supposed to be short for interrupt so it's just interrupt from keyboard came in. So the default handler codes will just call exit for you and will set the exit status to 128 plus the signal number so we can even work backwards from this and figure out that sig init is number two because we got 130 back. So let's see the fun of writing signal handlers. So you can write your own signal handlers that is a perfectly fine thing to do and you do that with a kind of system call called sig action so we will see that in an example but basically you just declare a function that doesn't return a value and has an in argument so you can figure out what signal was sent to it and that integers is the signal number you can, some of the numbers are non-standard but on here are a few from Linux so two is that interrupt, nine which is sig kill which is like super serious you're gonna die that can't be ignored by your program so the kernel just kills your process immediately that's number nine then everyone's favorite one is this one so the kernel will actually just send your process a signal you didn't write any default signal handlers and your program just died and it returned whatever it would return error code 139 in that case but the fun thing about this is since it sends a signal and you can handle it if you wanted to you could just ignore your sag fault and keep trucking on but yeah so it depends what you do in your handler ideally you would be like oh I know why I sag faulted I will allocate memory for that but if you just ignore, if you wrote a signal handler for your sag faults and you didn't do anything it would just try and run that and then go restart that code again and then just kind of fall into an infinite loop and you'll probably be in a worse situation than you were with the sag fault ironically and then last one is sig terminate that's the nice way of saying hey program you should exit you can do some cleanup so let's go ahead and see an example of that yep, so sig term yes that's the way you're supposed to handle exiting that's like please exit now clean yourself up sig kill you can't do anything about sig kill is kernel someone from the kernel wants to kill you and you're dead yeah so that's a good point and that's one of the main things we are going to see at the end that's like valid for the quiz that's like interrupts come back so these are like interrupts except they are use or interrupts for users so this is like user land interrupts so they behave in pretty much exactly the same way except they do nothing but cause problems so in this so in this program I just make sure that there are some arguments yeah so this is essentially our open file example but I'm just going to install a signal handler in it so I registered the signal sig init and then sig term cause that is ones that I can handle and inside of the signal handler so this is just code that it's boilerplate you're just going to essentially have this provided to you or only write this once or look it up on stack overflow or something like that and never have to look at it again but basically all it does is it registers this handle signal function to run whenever whatever signal you give as an argument comes in so I installed this signal handler to run if there is a sig init which is supposed to be control C or a sig term which is supposed to be like a nice way of saying hey shut down and then here is my handle signal code isn't it wonderful so all I do is print off what signal I got and otherwise I just ignore it so let's go ahead and run that now so if I run this something weird is going to happen so I press control C it says ignoring signal two which is what I expect but I also get an error from my read system call which is kind of weird so this is why you should check the errors for all your system calls because some of them will just return immediately if they get interrupted by a signal and if you want to, you know if this was reading from standard out and you want to continue again you just have to recall read over and over again so I was nice in my head and fix that so the fix for that is that I call read and then I check if bytes read is negative one in which case there is an error and then I check it's error number and if it's error number is this error that says the system call has been interrupted I just restart the loop again and ignore it so I restart the system call so I just do another read system call because I know it's like a benign error so this is why you should be very proactive in catching your errors so you can actually figure out that the kernel does stuff like this so if I run signal example two now which is our fixed one and then I press control C it just says ignoring signal two which is probably like what your program did so no matter how many times I hit control C I really want out of this program oh okay well if you hit it too many times the kernel kind of forces the issue but if I hit it once it just sits here and it's ignored so yeah your shell can be kind of mean but otherwise this just ignores it doesn't matter even if I go into it and see where the PID of what's it called signal so I can go ahead use this command to look up its process ID and the way you send signals to processes on Linux or any other Unix system is violently you just say kill so if I want to try and end this program nicely whoops that is not the control let's type it so you just say kill process ID and that will send a signal to it to nicely ask it to end but in this case it says ignoring single 15 so it refused to do what I asked it to so the heavy handed approach is to do kill dash nine which sends that say kill so it's like kill say kill double kill like ultra kill so if I ultra kill it you'll see if I switch back it says it was killed give some weird error message from the shell and then gives this error code and it didn't have a choice to ignore it even if I went ahead and registered a signal here called SIG kill and tried to ignore it I would very much doubt that it will let me do that yeah so it says SIG action invalid argument you're not even allowed to try and install a signal handler for SIG kill because you don't get to control what happens in that case yep it's just a number it's two include signal.h so that'll just define all the numbers for you so all the signals are it's just a number and then just like everything computers right we just give meaning to a number so signals are just a number we give meaning to them so it's just right here it's the signal number so this is like your interrupt service routine so you as part of this register signal I register this function to essentially be my interrupt service routine so whenever a signal comes in this gets called and you don't know when a signal is coming in as the process so this is your interrupt handler code that you can install so this is exactly like an interrupt for a normal process I think that might be a nice idea or maybe the kernel gets uppity about it I'm not I need to double check that because last it might be the kernel because last time I bashed control C it didn't die so yeah yeah so this just does a lookup of any processes that exist and then look at their name to see if they match this and then show you the process ID see if the name matches so this is why by convention the first argument name is the name of the program because it checks the name of the program so like I could do pit of Z shell and there's three Z shells running right now so return the process ID of any name that matches that that's currently running yes but if you want to kill yourself you could just call exit yeah that just gets into how Z shell works so one's the one I'm typing in one's this one so I have two shells open right now and then one is I think my login shell so there's just three shells running there no they're they're all independent they're not like they don't depend on each other there's just three independent Z shells running okay so so the very kind of pain thing is that the signal will pause your process wherever it's running and then run that signal handler so again exactly like an interrupt and you can't control when an interrupt happens to your process you just have to deal with it so and your process can be interrupted that any single point of its execution and then as soon as your signal handler code returns it just picks up right back where it left off so this is an example of forced concurrency right where it's not exactly concurrency because this the signal handler code or your interrupt service routine has to run until it's ended it can't actually switch back and forth but the concurrency is when it switches to that program and you don't know when that is so you have to actually be like super careful about what you write there and we'll see an example of that and here is just the interrupt system call example where I didn't restart the read call and I got an error from my read and this is yeah this is how you send signals to processes through the kill command because calling it send signal would have been too clear we want to be violent and it just takes the process ID number we saw how to get the PID of any process we want and we could kill them so by default if you just kill a process ID it sends sig term which is supposed to be the nice please shut down but kill-9 is the violent please stop so this is back to the wait so most operations are non-blocking when we saw wait before it was a blocking system call right it waited for one of the children to die but if we want most of the calls you encounter and most of the ones you want to make are non-blocking system calls and they return immediately and you just check if something has happened or not so if we want to turn wait into a non-blocking call we can use wait PID which pretty much has the same semantics as wait by itself except it takes a process ID as an argument if you want to wait for some specific process to die but if you want to wait for any process to die so it behaves like wait you just put in a negative one there and we'll see that that'll be in the code just so there's no surprises when I show that and then you just give it this w no hang in the options and then suddenly it is a non-blocking system call so it will not wait until the process will die and that's how we get a process ID of zero back out of it so zero means you have a child but it's not dead yet so you should probably check again so here let's see the example signal pull example where the hell are you oh wait pull example sorry all right so in the wait pull example all I'm going to do is immediately fork because that's fun and then in the child I just sleep for two seconds and then it's going to exit for zero now we're going to have the same code as we had before where we wait except in this case we're going to have a fun while loop so we're going to while PID equals to zero which means we still have a child and it is not dead yet we'll just count how many times we call wait so every time through this loop we will increment calling wait and then say how many attempts we have and in here we're going to call wait process wait PID in this case since we know the ID of our one child and we know we only have one child because there was only one fork call we can just give it the process ID directly and say I know the ID of my child just wait for that one to die the w status as we saw before and then here's this no hang so at the end of it when we come outside of that infant while loop while it's zero wait PID will be not zero so either it'll be an error or hopefully it exited normally so if we go ahead and do that we so does this thing does this look like a smart thing to do yeah so if I did that um that waste a lot of time right so each time I call wait and it's the process isn't dead I'm essentially just wasting CPU time doing nothing very productive and this is one of the techniques you have to do if you are an operating system and essentially your your hardware will change some memory address somewhere and the only way you can see if there's a change is you have to check yourself so this is a technique called pulling which is valid for the quiz by the way and this is one technique you'd have to do to identify hardware changes so you can see the obvious drawback is it wastes a lot of time so anyone want to give me idea is how to make this you know not waste so much of my CPU time yeah unfortunately not so yeah like turn it into an interrupt yeah so that's a good idea so that we'll see later so the suggestion was to turn weight into an interrupt so we don't have to just keep on pulling it but if we had to keep on pulling it what's probably a better strategy than just trying as hard as we can if we don't want to waste a ton of time yeah yeah well in this case I could go ahead and just in this case I'll just put myself to sleep for I don't feel like typing at the whole microsecond call but the option you could have is hey just put yourself to sleep every time so I'm not trying so this is attempt one, attempt two, attempt three so I'm not wasting that much CPU time but what's the drawback of this yeah it's not instantaneous I'm almost waiting a second so if you have to pull something there's going to be some trade-off with how often you pull and how much time you waste and you know the least or the less often I pull the more responsive it's going to be if I wait forever it's not going to be responsive but it's not going to waste that much time so there's always going to be a trade-off yeah so if I do this and give it no options if I get rid of that you know that W hang it's a blocking system called like what we saw in the last lecture where it just waits until a child to die so blocking means it won't return until whatever you're waiting for has occurred so it just sits there and waits until a child has died yeah so blocking means the kernel is just preventing your process to run because you're waiting for something and you can block on weight you could block on read if it was like a slow file and so on yep uh it's on the github the the link is was in the first lecture I guess I should I'll post it on quarkis too on like the main page thing okay well the other suggestion was a good one where signals might actually save us for this because if I had right maybe I don't want to pull maybe I don't want to block on a system call maybe I just want to be interrupted so we could do that there's actually something called weight interrupt example so what happens when a child dies remember last lecture I said the kernel would kind of nudge you that a child has died while the nudging is a signal so there's a special signal called sig child don't ask me why they're missing the eye programmers are very lazy yeah no noticed it but if you try to type it you know you get compiler error so I just install a signal handler to also handle sig child and then in the signal handler I check the signal number if it's not sig child so I want to keep ignoring any interrupt I want to just run forever but if it is a sig child it means that one of my children has died and thanks for the heads up and I can call weight so I can do this no hang option because I don't need to block in this case you know it's died anyway so weight should return immediately and here's the example to turn weight PID into the exact same as weight so if I don't care about waiting on a specific child I can just give it a negative one argument that says wait for any process to die but again I only have one process in this case so I can wait for it in my interrupt essentially my interrupt handler and I should get you know weight return for that process and then I'm just gonna and then I'm just gonna also exit the current process because as far as it's concerned it's done I only wanted to wait on that process so in this case yeah so in the main loop just to see that main's not actually doing anything like the main process isn't doing anything it calls fork and then if it is the parent it just says time to go to sleep and sleeps itself for nine hundred nine thousand nine hundred ninety nine seconds so if a child didn't die in that time it would actually exit after a very long time but this shows that the process actually dies whenever the child dies so it says time to go to sleep and then two seconds later we see the calling weight and the weight return so that is immediate and that is you know that's a way to be very very responsive and pretty much if you have an option that's generally the way to go yeah yes like how it got here so the main process would be a sleep and it's an interruptable sleep and then when a signal comes in it just forces the signal handler code to run doesn't matter what your program's doing at the time doesn't matter if it was running doesn't matter if it was sleeping it's going to just be forced to run the signal handler code it will finish any atomic operation it's doing so if it's like in the middle of adding two numbers it'll finish that but otherwise it'll just get booted back up to the signal handler execute that and then it will resume from exactly where it left off all right so a bit more housekeeping for the quiz so that was our interrupt example and here I'll go back to this in a sec just to make sure that I say this so on a CPU which is what the kernel has to do there's only one kind of interrupt in user space it's just signals but on a CPU you can like classify interrupts into three different categories so an interrupt like a true interrupt is triggered by external hardware and it needs to be handled explicitly by the kernel because it needs to respond quickly so it's some external hardware source that the kernel has to deal with and you were kind of familiar with interrupts before the other category one other category is called a trap and a trap is what is triggered by users so as a user you are allowed to essentially interrupt the kernel but it's given a special name because they happen in user mode so they're handled by the user or they're handled by the kernel but user processes initiate them right the calling process gets suspended the kernel executes handles the code and doesn't doesn't need to be super super responsive because it's just your code and it doesn't really care that much about it the third category is called an exception which is usually triggered by abnormal control flow divide by zero illegal memory accesses so seg faults and fun stuff like that the default handler is in the kernel and again the calling process is going to be suspended while the kernel executes whatever code it needs to and this is where signals come in so the kernel will optionally if you install a signal handler for seg faults the kernel will allow you to handle them yourself so you're also allowed to handle divide by zero yourself even though no one's probably ever done that yeah so let's see an example of why it is tricky because again when signal handlers run they have to run to completion so let's see this close example where in our interrupt handler we open a file and then in our interrupt handler we decide to close it and be nice and say we're cleaning up our program or something in this but it's going to fail kind of badly so which one is this so in our close example all we do in our main is we say im process id and it opens a file and it really doesn't do anything else after that we'll just say what file descriptor we opened and then in the file handler code we try to close the file so we check if the file descriptor is even valid then we call close on it we error check to see our return value print and error close failed and then say we close the file and then we sleep for 10 seconds and then try and exit the program so if we do this wait interrupt example or no this is called signal close so if we do this it says hey i'm process id you know 9000 something I could send a kill to it was that its process id yeah so if I send a kill to it it's going to get a seg term and then try and close that file and do all that good fun stuff and then sleep for 10 seconds after it closes the file and then tries to exit the process so if I do that and switch over oh I didn't open a file my bad okay so now I open the file and it says it's file descriptor 3 which makes sense because it's the first one after the standard ones so if I go ahead try and kill it I'll see that says hey I'm closing the file and exiting but I could also press control C so I sent another signal to it while it was handling the first signal and these are things you're going to have to consider and this is what makes operating systems so difficult so here I opened it I essentially the main process went to sleep I tried to kill it from another you know I tried to kill it by sending a signal to it it made it to I'm closing the file and exiting the file descriptor was valid right it was file descriptor 3 got here to the close closed successfully and it said it closed the file then it slept for 10 seconds before calling exit and then I hit control C while this one was sleeping so then this interrupt handler got interrupted again so it starts executing again so it would again print off closing file and exiting the file descriptor is valid because it's still 3 now it would call close on 3 again which is an error because I've already closed it and that's why it says close bad file descriptor already closed right so that wraps it up for the day remember pulling for you we're all in this together