 All right, welcome back to operating systems. So today, basic IPC will be a fun one. So who remembers what IPC stands for? Inter-process communication, right? Because processes, by default, are independent. So we need a way to send data between two processes. You've done inter-process communication before, whether or not you know it. So remember, IPC or inter-process communication is just transferring bytes between two or more processes. You have done this at some point in your life. Reading and writing files is an example of it. The two processes do not have to be active at the exact same time. So if one process reads from a file and then writes to a file and modifies it, and then another process later comes along and reads that file, well, technically, two processes just communicated with each other. And remember, this is also related to the read and write system calls we've seen before, because they just allow you to read and write bytes. The kernel doesn't care what they represent. And we'll see some other fun things to do. So here is a simple example that will be actually fairly useful. And it is a simple process that writes everything it reads. So it reads everything from standard input and writes it to standard output. So let's just dive right into it. So in this program, we're going to create a buffer of size 4,096. Why 4,096? Well, we'll figure out that that is a number the kernel likes whenever we get to virtual memory. But for now, just know it's a big number that the kernel seems to like. Then we'll declare a variable called bytes red and then do a read system call from file descriptor 0. Remember, by convention, file descriptor 0 is standard input. Standard out is file descriptor 1. Standard error is file descriptor 2. So we will read from file descriptor 0, give it the address of that buffer, and then say we can read up to 4,096 bytes. And then we check, write it or update the bytes red variable and then check if it is greater than 0, which means we actually have read some bytes. So we have some number greater than 0 of bytes red whenever we get into the while loop. In the while loop, we will just do a write to file descriptor 1 using that buffer and then the number of bytes we just read in. So we will write out the number of bytes we have read in. And then we will check the return value, which is the number of bytes written, and then check if there's an error, see if it returns negative one. Then otherwise, I'm going to write an assert statement. So you should write assert statements if you are making any assumptions about your code. And here I'm writing a assert statement that bytes red is always equal to bytes written or that the kernel is always going to read or write the number of bytes I tell it to. It does not have to. If you read the documentation, it could write zero or it could write one byte, two bytes, it could just ignore me, write a hundred bytes or do whatever. But here I'm just going to put an assert, what I think is true. And the reason I do that, this is that if it's not true, my program will crash at this line, tell me that it's not true, tell me what line number it happened in, and then I know that I have to rewrite my program a bit and handle that case that I was ignoring before. So you could write the case, I could have written this where like, if read is a partial write, well then I can do some pointer arithmetic and figure out where I have to continue from and then try and write the remaining bytes and then just do that until I write everything. Just makes the code harder to read. So I'll just put an assert there. So if it breaks out of this while loop, it means that read returned a zero or not a zero or less than zero, which if you read the documentation will just be zero or negative one. So if it's negative one, it's an error, I'll print my error message and then I will assert that the documentation did not lie to me and that bytes read is equal to zero if there's not an error and it's not like negative two or something. So this program seems a bit silly. If I run it, it doesn't look like it does anything. How do I make this program do anything? Any guesses if it's just reading from standard input? Sorry? Yeah, well I could just type something, right? And then press enter and guess what? It just repeats it. Why does it repeat it? Well, I type in hello class and then in my terminal, I press enter and then this process would read what I typed in from standard in because my terminal I've done the translation. It would read hello class with a new line from standard in and then it would just write it to standard out. So I get the same thing again. Yeah. Oh yeah, so there's a question about the line break and yeah, it will always read the line break and it won't read won't happen until I hit enter for, there's some implementation reasons for that. But yeah. All right, any questions about that? Seems silly, yep. This? So this is just so for all of the C functions, if there is an error, they return negative one and then set this global variable. In this case, I'm just saving the value of this global variable because this P error function that uses it might also have an error in it and overwrite that. So I just want to return zero and set my exit status equal to error no, just so I know what's going on. So I have to save it before I call it because yeah, guess what? P error can have an error and you know, here even I check if right has an error. Technically printf returns the number of bytes written as well, but no one has probably ever checked the return value of printf ever in their life. So you technically should, but you probably never have. So, but in this case, it's a, I'm trying to set a good example. So, yep. So why is it repeating? So anything I type here, like anything I type here, my terminal is displaying for me and it's not actually being sent to that process until I hit enter and then it sends all that input to the process and then that process is going to read it and then write it to standard out, which is also my terminal, right here. So I'm reading from file descriptor zero which is standard in, yep. Nope. Yeah, so the question is if I just hit enter, we'll just exit the program because I haven't read any bytes. Well, guess what? Enter is a new line character, which is a byte. Yeah, so it won't exit here. So if I want to end this, does anyone know how to actually end this? Probably not. Control what? D, yep. So in your terminal, if you hit control D, that means to your terminal, I'm done interacting with this process. I'm not going to type anything else. Just close it and it's supposed to kind of represent an end of file. You might've been taught before, there's like an end of file character you have to read for a file. Has anyone been taught that before? Yeah, they lied to you. At least on Linux, that's a complete lie. So an end of file character doesn't exist because that one makes sense because whatever byte that represents, you just couldn't write that to a file which would just be really weird. So on Linux at least and Unix and macOS and everything, the only way to know that there's an end of file is if read return zero and that's the only way. So if read return zero, that means you read zero bytes and it also signifies that there is no more input to come. So in the case of my terminal, if I hit control D, that means no more input to come. There's nothing else coming. You can stop. And if this was a real file, well then it would revert to return zero which would signify end of file. There's no end of file character that does not exist. So this might seem silly. Yep, sorry. So how were you reading a file before that had an end of file character? Well, if you were using Linux, it was using read and then whatever library you were using made up an end of file character for you to read. They just made it up. But if you S trace it, you're never gonna find it because they just made it up. So S trace is like the truth of everything, right? So this application seems kind of weird as anyone ever seen an application like this before or ever use anything called cat. Yeah, I see some heads. Well, guess what? If I just run cat, it looks like it does the same thing but cat's useful but this program doesn't really do anything. It just reads from zero and writes to one. I can, turns out that's actually really useful, yep. Yeah, the question is, well, doesn't cat just print out the files? Yeah, like that. Yeah, I could use cat like that and then get a file but jumping ahead a little bit. So let's go see why this is actually useful. So again, just so you have it, read just reads data from file descriptor. There's no end of file character, doesn't exist. Read just returns zero bytes read whenever there's no more possible input. So if it's your terminal, it means the user's not gonna type anything more. If it's a real file, it means you're at the end of the file but again, remember, file descriptors represent more than just files. And same disclaimer, please check for errors. You're just gonna make your life a lot easier even if you're not expecting any errors to happen when you run your program. When you're writing it, trust me, you're gonna want them. And then remember, right, oh yep. So this is in a repository that you all have access to. It's like, you know how the upstream is like student EC35, whatever, three, wow, I forgot the course code 344. This is instead of EC344, it's just called materials. And then there's lectures there, all the codes in there. And I think, I put it in one earlier lectures to all instructions, I think in lecture two there's also a link. All right, so here too, so writes data to a file descriptor, returns a number of bytes written. You should technically shouldn't assume it's always successful. Same thing if you want to save error, no, you have to save it. And then also as a caveat, well, whatever you're interacting with that, so like just interacting with the terminal there, that's inter-process communication. And whatever I am reading from, well, some other process was writing to that file descriptor. So in order to see my output there, well, whenever I typed on my keyboard, my terminal took the keys I actually pressed, converted them into an ASCII string and then wrote to a file descriptor that that process was able to read from. Then it read whatever I typed and then wrote out and then your terminal emulator would have read whatever string you outputted and then converted it to something you can actually read. Yep, yeah, so the question is, what happens if I read before the user hit enter? Yeah, so right now I just, whoops. So right now I just ran the program and I'm in that situation where I haven't typed anything, I haven't hit control D, I haven't done anything. So read is a blocking system call. So it's going to sit there and wait for some data. So that's why this read system call, when I'm not doing anything, if I S traced it, I would see that I have called a read and I haven't returned from it yet. So it's just gonna block here, wait for input and then whatever I hit enter, that's when read returns and then I do my write and then I get input and then it goes back and does a read again. Yeah, write is also a blocking system call but it's actually writing something so it actually returns whenever it's done which hopefully is pretty fast. Okay, so that seems kind of weird but the standard file descriptors are actually really powerful. So there are a few rules with the standard file descriptors. So we already know that zero, one and two there are special ones but there are a few rules the kernel uses when a process tries to open file descriptors. So if you close a file descriptor, so say I close zero because I'm allowed to do that by default it's open so I can just do a close zero and then that process no longer has a valid file descriptor zero. Well, if I go ahead and open a file which gets a new file descriptor, the Linux rule is it will always use the lowest free number. So if I close zero and then I open a new file it will get file descriptor zero. If I didn't close that file it would have got file descriptor three because zero, one and two are already in use. So let's see how I can take advantage of that. So here is the exact same program except I'm going to use some arguments to main. So first I'll check if there's greater than two arguments in which case I will not do anything because I don't want to handle additional arguments and then otherwise I will either have one or two arguments by convention. So if I have a second argument I'm going to assume it's a file name and I'm going to close file descriptor zero. Now file descriptor zero does not refer to anything and then I will open a file that I give as an argument. So because I close zero, whenever I open something as long as it doesn't fail this should get file descriptor zero. So I'm essentially replacing file descriptor zero closing whatever it represented before which in this case was the terminal and replacing it with an actual file. Otherwise I'll check for errors and otherwise the program's exactly the same as I had before. So now if I run this version of the program magically I get the output of the file that I give, right? So all I did was replace what file descriptor zero was without changing the rest of the program. So with just doing that well whatever it is now reading from file descriptor zero it now file descriptor zero now represents a file. So it's just going to read from that file this time over and over again and it's still going to write to file descriptor one. So I'm going to see the output of that file on my terminal. So that gets to your point where like, hey I use cat to read a file. This is cat. So I could have done the same thing with cat as I did with mine. It's the same thing. So actually pretty useful concept. Kind of cool, maybe not. Let's see how it could be cooler. So the nice thing about these standard file descriptors is with your shell. Your shell can manipulate them for you and suddenly a program that didn't deal with files at all can deal with files. So for example, I'll go back to the first version that just read from zero and wrote to one. Well, if I use my shell and do something like this, what does this mean? This means open this file and this arrow means use it to replace file descriptor zero. So if I run this, I should get the same contents of the file without having to write any file system handling code. So my shell will open this file and then use that as file descriptor zero when it ever runs this process. And just using the standard file descriptors, you can interact with files without writing anything that actually handles files, right? That's the power of file descriptors and being super abstract. It could represent anything. And in fact, if I want, if I use this arrow, this will change whatever standard output is to a file. So if I do something like this, well, whatever runs this process, standard input is going to be replaced by this file. Standard output is going to be replaced by this file. And if I do that, I don't get any output because I'm not using the terminal anymore. But if I go ahead and look at copy.txt, well, it's the same contents of that file. It's actually, guess what? It's a copy of that file, right? It's, I read some bytes and then everything I read exactly verbatim I wrote out. So you actually, if you've ever used this command to copy files, you don't really need it. You could have just used cat. So this is just like some nicety to make your life a bit easy, but you could have just used cat to copy files if you really wanted to. All right, any questions with that magic? Yep. Yeah, so the question is if I do something like this, will the content be overwritten or will it just be appended to or something like that? In this case, it will be overwritten each time. So I only have one copy of the file. And in fact, you can change this if you want. So if you do something like double arrow, that means append. So if I do something like that, I'll get two copies into this. So if I open it, right, I have all my header files. I go down, I have it again. So there's some nice shell things you can do, but the reason you can do them are because everything uses the standard file descriptors. So that's, if everyone agrees on the convention, magical things happen. All right, any questions with that? Kind of cool. All right, we agree. So here is that. So your shell lets you redirect standard file descriptors. Really fun. There is this really fun one too that I didn't show that lets you redirect input across multiple processes. So you see this character here, this line or bar. Well, it's called a pipe because it kind of creates a data pipe between two processes. So what this does is takes the output of the process on the left. So whatever this writes the standard output, it connects it to this process. So whatever this process reads from standard input will come directly from this process's standard output. So if I do something like this, let me go back. So if I do something like this, I see the content of the file and it goes between two processes. So this process would write the contents of that file to standard out. And then because of this character here, this pipe, while this process would be reading anything it outputs to its standard in. And then again, it just writes the standard out which is the default terminal. So we go ahead and see it from the terminal. In fact with cat, I can chain this over and over and over again because cat just reads and writes. It doesn't really care. So if I do something like that, I'm essentially just creating a bunch of processes and just moving the information between processes and just wasting a bunch of time. But I get the same output. But yeah, it's kind of fun. Or I guess it depends on your definition of fun. Mine's probably way different than yours. So any questions about that fun? Redirection, all that stuff? Yep. Yeah, so that's the question. So why is the double arrow mean append? Is there a file descriptor specifically for appending? So if you look at the, like if you look at the documentation for open, you're allowed to open a file for appending. So that's just what it does for you if you just do a double arrow. Your shell knows that double arrow means I'll open this file. Instead of opening it brand new, I'll open it for appending. File descriptor zero. No. So it will assign it to, oh yeah, sorry, one, yeah, one. Arrow going that way, yes, one, sorry, yes, one. Okay, so onto something new then. Signals, so a new form of IPC. So they are interrupts. We all love interrupts, don't we? Yeah, okay, yeah, that's about it. So signals should form about the same reaction. They are a pain in the butt and we'll see a bit reason why. So someone told me before, whenever I, if I got stuck running this program, how do I get out of it if I don't know how to press control D? Well, I can press control C. Anyone know what control C actually does? So control C, yeah, control C on Linux. Sends kill, that's a good, a signal interrupt. Something involving signals, so someone knows something about signals already a little bit. Physically cuts off, that sounds aggressive. Yeah, does it kill the process? So in this case, it doesn't quite kill the process. So let's get into it. So what happens when you press control C? Your terminal is going to send a signal to your process and whatever happens there, it really kind of depends. So here control C, it will send a signal. A signal just gets represented as a number in the kernel and if you send control C, that is sig int which is supposed to be sig for interrupt, a signal you want to interrupt the process. In Python, you might have seen like a keyboard interrupt or something like that. It's just a nice way of saying the signal. And we've written interrupt handlers before but we haven't seen any in our C program. So by default, your programs have default interrupt handlers. So the default interrupt handler will, in the case of sig init, it will exit the process, not kill it. So the default signal handler is going to exit the process and set the exit code equal to 128 plus the signal number. Y128 plus the signal number, they just decided that. So if we go back and look at the code, this number at the bottom here actually represents the exit status of the last process I ran. So when I press control C, the default signal, I sent a signal to that process. The default signal handler ran and it called exit with 128 plus the signal number. So if it exited with 130, what was the signal number of interrupt? Hopefully we can do basic math. Two, right? 128 plus two equals 130. So the number for control C or sig interrupt must have been two. And it's just a default handler. So we will go into an example of how to write your own signal handler. This is a way to piss off your friends that haven't taken this course because you tell them control C, determinate your program, that's not true anymore. We can do something fun. So we will declare our own interrupt handler, but here are some numbers just to help us going forward. So two is sig interrupt, which is supposed to mean interrupt from keyboard. You'll see nine, nine is where we get into the term kill. So nine's a nice one, which means terminate immediately, terminate not nicely. And then there's 11 here that represents you de-referencing an all pointer or otherwise corrupting your memory. So that actually makes a signal get sent to your process. And by default, likely you have never written an interrupt handler that intercepts sig seg, but you could if you wanted to. But by default, you know, you'll get an exit code of what? 139 because it is 11. And then you'll see 15 sig term terminate. That basically means you want to terminate the process nicely. So that's like 15 is please terminate. Nine is terminate now, no please, no nice, not being nice. So let's get into the signal example code. So this is going to be the same program I had before, which read from standard in and wrote to standard out. In this case, I'm just going to register two signals for sig int and sig term. And then in the register signal handler, there's a sig action struct. I use this function to initialize it. And basically if you read the documentation, you'll see that it has a field that wants a function to run whenever you get the signal. So I will tell it to call this handle signal function, which just takes a number and returns nothing. And the sig action system call actually informs the kernel, hey, for this signal, I want you to run this handler code whenever I get set that signal. So if there's an error, it wouldn't have been installed, I'll print it. Otherwise, this will run whenever I get a signal and my implementation there is a very nice implementation indeed. It just prints, I'm ignoring you. So if I run this, what happens when I press control C? It should ignore it, right? Oh, it died. This is why it is good to program defensively. So I got an error here, that read interrupted system call. That looks weird. Where did that come from? Well, it got negative one from this read and then came here, bytes read was negative one. It did this, it did this PR, which is this message here. So from that, I learned that, hey, sometimes my system calls might get interrupted and I have to restart them, which is kind of annoying. But because I programmed defensively, I got to see an error instead of just my program dying unexpectedly because I actually checked for errors, which you should check for errors, I actually saw what happened. So I can go ahead and write this version of the program, which checks in the loop that if I got an error, it can check error no, and then this E in error, that just represents interrupted system call. You just have to look at the documentation for this magic number. The developers of the C standard library do not like to make your life easy because that is not obvious and it looks really ugly, but that just means interrupted system call. So in this case, if there's an interrupted system call, I know it's not really an error. I can just continue and then just restart the loop and just call read again. So I can just handle this and carry on, but now I know that, hey, that can happen. So now if I run this version and I hit control C, what's going to happen? Well, hopefully it just ignores me. So if you gave this program to someone you didn't like in APS 105, they probably couldn't quit it. Probably your friends that haven't taken this course yet would have no idea what to do. Anyone want to tell me what to do in this case? Shut down the computers, that would work. Close by terminal, yeah, close by terminal would technically work because of that reason, but soon I ignored that too. Yeah, open another terminal and kill the process. That's generally what I should do. So there's a program called PID of that you should know how to write now if you've done lab one. What it will do is look up a process with this name. I give it and tell you what the process ID of it is. Hopefully you could figure out how to write that if you really wanted to. Let's go through proc, find names, anything that matches, print out the process ID. So if there's multiple matches, it will tell you the process ID of everything that matches. In this case, it just gives us our one process ID of something we cannot terminate. And then you said I should kill it. So yeah, so there is a way to send a process a signal in Linux. You would think the system call would be like send signal or something like that, but for reasons I have no idea why the system call to send a signal to a process is called kill. So if I do kill and then give it the process ID, hopefully it's dead now. So let's go back over and see ignoring signal 15. So kill by default that will send signal 15, which is what I told you before is like, please shut down nicely, but we are allowed to ignore that too. So if I really want to shut it down, do we remember what number is the, please shut down not so nicely? Nine. So if I do dash nine, that means send signal number nine to this process. And if I do that, hey, guess what? It's dead. It's killed. Now we learn the word kill. So now we know how to like kill orphans and children, stuff like that. So yeah. So that's fun. So if I went ahead, oh yeah. So the question is, why don't I always just use kill dash nine to kill processes? So kill nine is violent. Like it just ends the process. So ideally the reason you have please end nicely is for a normal program, if you request it to shut down, well it probably has a bunch of open files and stuff and it doesn't want to have, you know, write the files in a corrupted state. So if you just killed it straight up, it might corrupt things. So the idea is you ask it to shut down nicely, then it writes out any files, does some cleanup code that it needs to do, and then terminates itself nicely after cleaning itself up. So if you do kill dash nine on stuff, you are being very violent. So you might corrupt some stuff, depending on what process you are killing. Yep. Yeah, so that's a question. Does that also mean that, hey, I could just do the same thing and ignore kill? Like, you know, in my register or whatever, I just, so to save us some time, you will get a error from that SIG action thing that says you're not allowed to ignore SIG kill. The kernel will not let you ignore SIG kill. You have no option. You don't get to handle it. The kernel just kills your process. You, yeah, so the question, what about if we change the compiler and let it? So the answer to that is it's going to run as a process, the kernel has control over it, and it has to make a system call to register signal handlers. C doesn't do it for you. So the kernel would just say no. God damn it. Question, what happens if I did? Yeah, I'm not going to type that yet. So we'll type that at the end. So where'd we get to? Yeah, so we got to this where we can, you know, send signals to processes nine. That's the kill signal. Very violent terminates the process. And now it's also stupid that kill has two meanings. So kill means send a signal to a process. And then one of the signals you can send is a kill signal. So you can kill, kill, kill squared, however you want to call it. It's a bit, the terminology is pretty silly, but it is what it is. All right, to go to another topic before we kill things. So before I said, hey, we were using wait before. And I said there's a non-blocking version of it. So most operations are hopefully non-blocking and that means the call returns immediately and then you check whether or not something returns. So let's have an example where we turn wait into a non-blocking call because by default, it's going to wait for the first child process to terminate. If I want to turn it into a non-blocking call, I have to use wait PID and then give it an option called no hang, which basically just means just check for me. And then if no child process has terminated, then it will return zero. So if we go here, we'll go back to the example where we have a child process that just sleeps for two seconds and terminates. And then in the parent, we will declare a variable called wait ID assigned to zero, get our W status. Yeah, it's probably fine. Then we'll declare a variable called count, which we'll just use to see how many times we call wait and increment it each time we call wait and just print how many times we call wait. And here we'll call wait PID with the process ID of our child because we're the parent. So we get a fork returns the process ID of the child so we can wait specifically for our child process. If we want to do the same thing as the wait system call where it's the first process, you don't care, you can just use negative one if you want. So negative one means I don't care, I want the first child, but we can wait for a specific child and then use the no hang option. So if I go ahead and run this, whoops, go ahead and run this, it's going to run a lot. So it attempted, it did 360,000 system calls in order to, that constantly asks, hey, is the child terminated yet? Hey, is the terminated yet over and over and over and over and over again? Which is pretty wasteful, right? That's a lot of calls that didn't do anything. So if I wanted to still pull the status of a child process, but didn't want to do this many attempts, what should I do to bring down the number of attempts here and not waste my CPU? Yep. Yeah, I could probably just wait or sleep or something, right? So do something like that. So if I do something like that, I only get a few attempts, two attempts, two attempts, sometimes three attempts. So is that actually good? What are the drawbacks of doing something like that? If, yeah, sorry? Yeah, so I'm sleeping a bunch of the time, so I might get unlucky. Whatever I call wait, while the child's not terminated yet, and then as soon as I go to sleep, it terminates, but I'm going to have a really bad response time because I'm not going to check again for a second. So instead of knowing instantly, I'm going to be delayed by up to a second, which is going to suck if you really depended on this to do something instantly as fast as possible. So instead of pulling, what's much faster than pulling? Interrupts. And remember before I told you that, hey, whenever your child terminates, the kernel will poke your process. Well, the poke is a signal. So whenever your child, whenever when your child processes terminates, the kernel will send you a signal. What signal will it send? Well, it will send a signal called SIG child. That means whenever that happens, it means one of your child processes has terminated. So to show that it works in my main here, I'm going to simulate hopefully this parent process doing something useful. So I'll just print off, I'm going to sleep, and then I sleep for 9,999 seconds, which is supposed to represent it actually doing something useful instead of being super lazy. And then because of that, whenever this child process dies, the kernel should send a SIG child signal to you. And since we registered it, in order to clean it up immediately in our handle signal, we can do our weight PID here. I don't know why that. So we can do our weight PID here in our signal handler. And then as soon as that child process terminates, it's a zombie. We can clean it up immediately because we call weight PID on it. And in this case, this program is just cleaning it up as fast as possible. And it doesn't really care otherwise about the child process. So you can do that. Main process goes to sleep. And then immediately we get that child signal and then we wait for it, we clean it up. And in this case, we also exit in that signal handler, but we didn't have to. So any questions about that? Nope, okay, so we cleared up the poke. So the last thing is more of a cautionary tale about signals and them kind of being silly. So when you were doing interrupts before and you wrote interrupt handlers, what happened if you got like two interrupts and in the middle of one, you got another one? Yeah, so if you want to make your life easier, you would block all other interrupts while you're handling it so you don't get any recursive behavior or anything like that. In general, you can't really do that. And by default, that doesn't happen. So let's pick up an example where we open a file descriptor and then in our signal handler, we're just going to try nicely to close it. So we're just going to try and nicely clean up all our resources. We'll have a print message here that says I want to close the file and exit with what signal I got. Check for errors, otherwise we will close the file descriptor. And then I'll say print file and then I'll say sleep. And then in the main program is just FD is a global variable and here I just open it. So I have a bunch of print statements so let's go ahead and run that. So if I run that, just to make my life easier, I print out the process ID of the process. So if I go ahead and I try to kill it, it should try and nicely close that file descriptor which I didn't give it as an argument. Okay, sorry. Now I'd open file descriptor four. So if I go ahead and try to kill it, I ask it nicely. It's trying to close the file. That's nice. And then while it's doing that, I can hit control C. So what happened here is when it was handling it, my kill closed it, came here, printed this, closed it and then printed it, closed the file, then slept and while it was sleeping, we got another signal and it just started over again. So the next one printed the same thing, then tried to close that same file descriptor four again. And in this case, it's like doing it double free. You're not allowed to close the same file descriptor twice. So we got an error, bad file descriptor and we exited with an error. So this, if you want to write signal handlers that are safe, you have to account for this and make sure things like this do not happen. So you have to write them in such a way that if in the middle of them, you get another interrupt that runs the same function, it has to be able to run successfully, getting interrupted in the middle of it. And that is something called being re-entrant, which we will get into as a teaser for threads because you will have to write functions like that that behave properly as soon as we get into threads. And that is where your life will get difficult. But that's just a little teaser because we're gonna take a detour before we get to threads so we can wait a bit for that. But yeah, that's just to give you a little taste. And also some other terminology. So we're gonna look at some, I looked at the feedback people wanted to write their own operating system. So we'll look at some code that is an operating system that you should be able to understand completely, that actually works completely and it's written in risk five. So we'll use some terminology. So on risk five, there are three term for interrupts on the CPU, there's like an interrupt which is triggered by hardware, like some interacting with a keyboard or something like that. Then there's an exception which are always triggered by instructions. So like if you divide by zero or have some illegal memory accesses that will trigger an exception which looks like an interrupt. And by default, the kernel is going to handle this but the process can actually handle this through signals. And then a trap is you're just transferring control to a trap handler. And that can happen by an exception or an interrupt and that is basically your interrupt handler code. So an example of this would be like a system call. So that is a requested trap. I can execute an instruction, it doesn't interrupt and the kernel handles it for me. So that's it, we've explored some basic IPC, read and write crawls, redirecting standard file descriptors being useful and then signals and killing processes. So now we can use the word kill which is lots of fun. So just remember, phone for you, we're on this together.