 Okay, everybody, let's get started. So how was the design document last week? Did it all finish it? With no problem, maybe? So starting with this from this week, you should be doing the syscause. So the way we suggest you do is that you first start with file syscause, which is much simpler. And also, later on when you do process syscause, some of the syscause, like fork and exact V, are have dependencies on the file syscause. So we recommend you to first start with file syscause, then continue to process syscause. So before we talk about the contents today, we first do a quick recap of what we talked about last time. So we talked about the big picture of the syscause. The questions, I assume you now have a better idea of these questions. For example, how does a user trap into kernel space? You already know that using the syscall instruction. And how does a kernel know it's a syscall instead of other interruptions? You also discuss that, or you also have to figure that out when you're answering the collaring questions. You will basically have a number assigned for each reason of the interruption. Some of them is timer interruption, others are syscause, we basically have a number to differentiate that. And kernel just check that number to figure out what's the cause of the interruption. And also, how does a user pass the syscall arguments to the kernel space? And we already know that it's through the argument registers, A0 to A3, right? And also, how does a user get the syscall result from the kernel? Yeah, we also already know that the kernel will put the result in the V0 and A3 registers, so user can get them. So here's a big picture of the syscall, basically the lifecycle of the syscall. So suppose user want to make syscall, the user first put the arguments in the registers, this part is done by the compiler. So when you write a function that, say open this file name with this flag, with this mode, the compiler will generate the assembly code that put these registers, sorry, put these values into corresponding registers, right? And then, we have a syscall instruction to trap into kernel, to tell the kernel that we require some service from it. Then, when the interruption happens, the hardware will first enter the privileged mode, and then the hardware will save the context, basically figure out what just happened, and also save the user context. By user context, we mean the trap thread, which contains all the register values of the CPU. And then, the hardware hand over this to the kernel, the kernel first figure out what's the reason for this interruption, if a syscall, we dispatch this syscall to relative handlers, use a big switch cast block, right? Then after we are done with the syscall, we collect the syscall results and put them in A3 and V0. And finally, we go back to user space where the user collected the results from A3 and V0. So this is the whole process of syscall, right? So what you are supposed to do is just one single step of this big picture, which is the syscall handlers, right? But it's important to keep other parts in mind so you don't get lost when you dig deep down into the syscall handlers. You always keep this big picture in mind. So today, we'll mostly focus on the file syscalls. So we first discuss the file table design, and then we discuss a special file, which is a console. And then we work through several, some of the file syscalls. So before we can go on, any questions about this syscall big picture? I mean, by now, I assume most of you have a pretty good understanding of this. I mean, you are supposed to discuss this in the design document, right? So I suspect, I assume you all know what's happening here. So at the very beginning of this semester, I think in the third or fourth lecture, Jeff talked about the file handler of abstraction, right? So we said that there are three levels of interruptions. So basically, we first have file descriptor, which is a green one in the user space. That file descriptor is mapped to a file handle through a data structure called file table in kernel space, right? All the red lines are in kernel space. Then this file handle are mapped to actual file object. And finally, the file object is mapped to some blocks on the disk. So we have this three level kind of indirection. And why is that? I mean, so because different levels have different sharing properties. For example, the file descriptor is privated to each process, right? We already know that file descriptors is nothing but an integer. It can from zero all the way to, say, the maximum file descriptor limit in the system, right? It's just an integer. So every process can have file descriptor from zero, one, two, three, four, right? It's privated to each process. And then we have file handle, which is also provided to each process, but it can be shared among some processes, not all the processes. So this kind of special relationship is through Fox. So we know that one of process A called Fox, after the Fox is called, there will be two processes. One is process A and other is process B, which is A's child, right? And A and B share the same file handles. So that's the only cases where file handles can be shared across process. Otherwise, if no process called Fox, then file handle will also be privated to each process. So for file descriptor, we have exclusive private to each thread. And the file handle can be in some way be shared among processes, but the file object is shared system-wide. So suppose there is a file on disk and multiple thread or multiple user process open that file, they will share the same file object at the lowest level, right? So at the up layer, we have different file descriptors and file handles to be accessed exclusively by that thread, but deep down at the physical layer or at the lower layer, we have the file object that's shared by multiple thread. So this, I assume you already know about. So in OS 161, so the previous slide just talked about some high level concept of the abstractions in file handles or file objects. Now in OS 161, what is these abstractions? So for file descriptor is just an integer, right? And then for file handle, it is supposed to be a structure. The structure is a C kind of jargon. You can imagine as a class in Java and all that. So the file handle should be a structure that contains some process wide file manner information. We will talk about it later of what exactly information you need to keep in the file handle. And finally, we have the file object which is already being provided to you is called VNOTE. We also talk about the VNOTE today. So speaking of VNOTE, so it's defined in this header file. So VNOTE basically represent a physical file on disk. So we have the VNOTE implementation already provided to you. And it will also provide some hyper macros to help you manipulate with the VNOTE, especially VOP read and VOP write. VOP stands for VNOTE operation. So when you implement your system call, especially the read and write system call, you might want to just use these macros to help you do the actual read and write. So let's take a look at where the VNOTE is defined. So it's defined in the VNOTE.OH file and you don't have to understand what's inside a VNOTE. So basically some metadata maintained over there is per file based. We don't need to care about what's inside a VNOTE exactly. What we care about is what kind of interfaces it provides. What kind of options we can do to it. So basically we have a big blob of comments explaining the interface of the VNOTE. Basically you have VOP open, VOP close. These two you won't be using much because we have another VFS layer on top of VNOTE which provides more friendly version of open and close. So what you need here is this one, VOP read, read data from the file to UIO and VOP write, write data from UIO to the file. And also there is a final one called VOP try seek. It's basically try to modify the offset of the file handle which you will be also be using in LSEC. So this is three of VOP macros that are very helpful when you implement your own file system calls. And you can see that it's provided through these macros. So you should be using VOP read, VOP write and VOP try seek. And on top of VNOTE we also have another layer called virtual file system. It provides some other type functions which are very useful for other type of CIS calls. So if you open that, we have a bunch of other useful functions among which you will find that some of them is, where is the VFS open? Does anybody know where is it defined? What's the header file for VFS open? VFS, oh I missed it actually. No, that's type of line by me. Oh here, here they are. So here are some very useful functions that you can use when you implement your own CIS calls like VFS open and VFS close, VFS change directory, and VFS get current working directory. So now you should have a good sense of what you need to do, right? Most of the functionalities are already provided to you. For open, we have VFS open which takes care of actual open file. So what are you supposed to do is actually just add a wrapper to these low level functions. For example, open. You can imagine that most of the functionalities of open are re-handled by VFS open. So what are you supposed to do, right? So first of all, you need to design what is in a file handle. And so basically you need to decide what information you want to keep in the file handle. And also you need to design the file table. So basically, given a file descriptor, how do you find that file handle? And finally, the most important part of this assignment is argument checking because you are dealing with the user and kernel interface. So whatever the value user passed in, you cannot trust, right? You need to check the arguments very carefully and make sure they are correct. And then after this argument checking, you just call the radio functions we have been provided to you to complete the functionality. For example, in this open case, you first check if the arguments is valid. If they are good, then you just call VFS open to actually open the file. Then you just return whatever the file descriptor that you decided to use, right? So that's the pattern for most of the file syscalls. So any questions so far about the Vnode? How to use them? Yeah, yeah. I think it's just to return whether or not it's legal. Remember that Vnode don't have offset information, right? Vnode is just to represent a file on disk. The offset information is kept somewhere else, actually in the file handle, right? Vopishysc just tells you if it's valid to sysc to that point. It doesn't actually forward the file pointer. Any other questions? Okay. So let's talk about the file handle design. I mean, when you design the file handle, what kind of questions need to consider? What kind of problems you are supposed to think about? So first of all, how do you tell where to read and write? I mean, if you look at the read and write function declaration, you will find that first you have a file descriptor, okay? So you know which file to read and write. Then you have a user space buffer. So you know where the data is coming from, or in the case of read, where you should put the data to the user space, right? Then finally you have a length, which tells you how much to read and write. And there is no information to tell you where to read from, right? Suppose user tells the kernel, hey, I want to read 100 bytes from this file. How can you tell where to start, where that 100 bytes start, right? So that information is per process white. Two process may open the same file, but at different location, read from different location, right? So this kind of information that you need to keep in the file handle so the kernel can remember which process read to where, right? So you need to maintain an offset information for each file handle. So whenever you get a read or write system call, you know where to start. So typically when the user first opened the file, you can set the offset to be zero, meaning that because user just opened the file, so every read and write start from here. Then suppose user read 100 bytes, right? So you start from zero, read 100 bytes, return whatever data you have read and forward that offset to 100. And next time, users say, hey, read another 100 bytes. So instead of start from the beginning, you start from 100 bytes because user has already read the first 100 bytes, right? So every time you read or write, you just keep forwarding the file pointer or the offset to whatever the new location is. So next time, you can start from there. So this is one information you need to keep in the file handle. And the second information is that how to prevent invalid read and write. Here invalid, we don't mean that if the file exists and the file has a permission saying read only and user want to read as write. That's not what we mean by permission. So because at this point, we don't have a file system yet, so we have nowhere to check the permission of the file. Here, the valid mean that, okay, user want open a file as read only, right? Okay, we just permit that, we allow that. And later on, user call a write on this file. That we should, we should don't allow it to do because the user open the file and read only and now the user want to write the file. That's not valid, right? You need to check against that kind of violation. Now imagine how would you do that? So you receive a write CIS call. How do you determine if you can actually write that file or not? You need to check the flags, but in order to check that, but in read, you don't have the flags, right? How do you check that? Yeah, that's what the information should go into the file handle, right? So when user open the file, you should remember what the flags was used when user open that file. So later on, when the user read or write that file, you can check the information you remembered in the file handle to determine if this read or write is valid, right? So, so far we have two pieces of information that you need to keep in the file handle. One is the offset, another is the flag or the permission when user open the file. And then, so how do you find the actual file object? I mean, you have a file handle. File handle is supposed to be mapped to a V node, right? So you need to remember which V node is associated with this file handle, in this file handle structure. So now we have the offset, the flag, the V node pointer, which points to the actual file object. And finally, how do you, so that's the first question related to what should be in there in the file handle. And then the final one is actually not quite relevant to the previous one, is when you destroy the file handle. So you basically create a file handle when it's open. That's kind of intuitive to understand, but when you destroy the file handle. So typically, the file handle is only accessed by one thread or one process, right? So when user call close on it, you should be able to just destroy the file handle, right? That's only if, that's possible only if this file handle is not shared to other processes. But whatever the file handle is shared by other processes. Can you just go ahead and destroy the file handle? Suppose two thread, A and B. A is the parent, B is the child. They both have one particular file opened, right? So now B calls close, but A hasn't, right? A should be able to still access the file because A hasn't closed the file, right? So at this point, can you just destroy the file handle? You can't, right? But how can you tell these kind of differences? How do you determine when you can destroy the file handle? Yeah, yeah, exactly. Actually, if you notice that Vnode Ultra actually also have a reference count, but that's called, that's not, that's just the count for the system-wide reference count. The reference count in the file handle should be just the count of how many processes actually share in this file handle. So every time you call fork, when you copy the file table, you need to increment the reference count, right? So initially, when you open the file, the reference count should be one. Now if you call fork once, this file handle will be shared by two processes. So you increment the reference count and so on. So let on when you close, you first decrement the reference count. So you only destroy the file handle when the reference count is zero. So that's the way how you can determine when is the right time to destroy a file handle so that you can reclaim the file descriptor associated with that file handle. That's a good question. Basically, you are referring to Dupet 2. That's only possible through the Dupet 2 system call. So yeah, that's right. So it's also possible that the same process have two file descriptors pointing to the same file handle. In that case, you also need to increment the reference count, right? So yeah, that's a very good question. So file handle can be shared within the same processes by multiple file descriptors or across multiple processes. That's two kind of sharing scenarios. But for both cases, you all need to maintain the reference count, whether it's shared within the process or across the process. Any other questions about the file handle design? So we basically settled one entry of the file table, right? That's the file handle. Now, yeah, the final one is how do you synchronize? Of course, whenever you have something that's shared by multiple processes, you always need to consider synchronization issue, right? And here you may template it to use read and write lock. Because it's natural that we have read, since read, we have since write. So we may use a reader write lock to provide high efficiency or something like that. Actually, that's not correct because what you want to protect is not the file, but the file handle. Remember that whenever you read or write, you need to modify the offset of the file handle, right? So the offset is part of the file handle status. So no matter you read or write, you are always changing the status of the file handle. So in some sense, they are all writers instead of readers. Even you call since read, you still will change the offset of the file handle. So in that sense, the since read is also a writer. So you only have writers here, you don't have reader. So it doesn't make sense to use a reader and write lock. So you just probably want to use a normal lock to protect it. So now we have settled down one entry of the file table. Now let's move on to the file table design. So if you really think about it, you will realize that the file table is nothing but a map, the high level data structure that is a mapping between the file descriptor and the file handle. So the interface or the requirements of this file table is simple. So first we want to find a valuable file descriptor to use. We have from zero, one, two, three, four, all the way up to maybe 1,000, which one is available? Which one has been occupied? The file table should be able to tell that. That's one requirement needed by the open function. Whenever you open the file, you want to allocate a valuable file descriptor. And next, given a file descriptor, the integer, give me the file handle, right? That's the requirement of read and write and actually all other file six calls, right? And the final requirement is that when you close that file handle, you should be able to recycle that file descriptor. So later on, when some process call open again, that file descriptor is available to allocate. So these are the three requirements to the file table. Now it's up to you to determine which kind of data structure you want to use. You can use an array, you can use a link list, you can use a tree maybe, it's all up to you. As long as the data structure you use can provide these three functionalities, you can use that. Any questions on the file table design? So what kind of data structure you want to use? What's that? Array is the most simple one. So how do you find available file descriptor in the array case? What's that? So you just scan the table, scan the array, right? Find the available... So you can search the whole way to find the file? Yeah, actually in both array and the link list, you need to scan anyway. What does it mean? Not necessary. So you have a file handle, right? Which is a structure. And you have a file table. It should be an array of pointers or it can be an array of actual file handle structure. So think about how would you really do it? I mean, how would you find an available slot in the array? Would you... Don't you need to scan it? How would you tell the available file descriptor without scanning it as the next available value? Yeah, in that case, you have a constant time allocation, basically. Every time you want to allocate the file descriptor, you use that value, then increment that value. But the disadvantage of that is that you keep increasing, although there might be some empty slot available in the previous one. Somebody may call close on the previous file descriptors. So you may have some slots available in the previous one, but you keep increasing. Then you need to recycle through that process. That's roughly the same with the... with keep current available file descriptor. But that's implementation details, right? So go back to the array case, given a file descriptor, how do you find the file handle? That's simple, right? Just because the integer can be indexed to that array. So you find that file handle in constant time. And also how to recycle the file descriptor. So whenever you close the file, suppose you are about to destroy the file handle, you just set that entry to be available. What's that? Yeah, if you have array pointers. So there's some implementation details up to you to decide. You mean how to synchronize it? That's right. Then you would have a big lock to protect the whole link list because it's possible that this file table is shared by two processes. And two processes may co-open and close at different times. So you have the whole big sharing issue. In that case, you have nothing but a big lock to protect the whole list. Oh yeah, we will get to that. So that's basically reserved for some special files, which is a console we will talk about later. So when you're advocating the file descriptor, you always start from two, sorry, three, right? You start from three and growing upwards. So the final part is argument checking as we have discussed that whenever you have a user pointers, you always need to use copying and copyout to deal with them. And you also need to check when the user wants to read or write a file, if that file descriptor is valid or not. And also, can you actually do the read or write given the previous flex used in the OpenSys call? And what kind of error code to return? So something may happen, you cannot read a file, user don't have a permission, or the file handle doesn't exist, what kind of error code you should return, right? This kind of information you need to refer to the manual. That's the point of manual which defines on which error conditions you need to return which error code. So that's kind of agreement between the user and the kernel, right? So user know exactly what to expect when something happened, when something wrong happened. Any questions so far about this part? Yeah, didn't we do that last time? I mean, copy or copyout, I won't explain that again. I mean, go back to the read and the slice of last time. Best way is just a memory copy with actual protection. You can imagine copying copyout has a try catch block, right? So try copying this stuff, but if something happens, let me know. And I want to catch that exception and handle it. You can imagine that away. Any other questions? Okay, so we'll move on to the console which is basically the file descriptor 012, right? The console, you can imagine as what is the console? This is the console, right? This is where you can type stuff. This is where you can see the output, right? So basically the input is users type typing and the output is what the program tried to print. This is the console, right? So console is a special file in the sense that the console doesn't have the concept of offset. For normal file, you can specify from where and you read how many bytes. But for console, you don't really have that concept. Whenever you write, you always append to the last of the file. Whenever you read, you always read from the user input. You cannot say read user input starting from where we don't have that kind of concept for the console file. And also the console has fixed file descriptors. So as we already mentioned, for a standard input we have file descriptor 0. For standard output, we have 1. And for standard error, we have 2. So this slide basically tells you two things. First of all, console is a file. So you can deal with console as you would do with other normal files. But the second meaning is that the console is special in the sense that it doesn't have this offset information and has a fixed file descriptor. So now you would, and also the final special thing about a console is that they are considered to be automatically opened for each user process. So for normal files, user would have to open the file first and do read or write on it, right? But the user program should be just right to the console without explicitly open the file. So when you write a C program, just a one line hello word, just a print of hello word, right? You don't have to open the console file and write to it. So it's considered to be automatically open for the user process. That's another special thing about the console file. So how do you initialize it? Well, because it's a file, you can initialize as you would do with other normal files. So you will use VFS open, but with a special file name, which calls con column. This is the file name for the console file, right? So if you call VFS open with this special file name, you will get a V node, right? So later on, when you write it to the V node, something will be printed. That content will be printed on the console instead of writing to some file on disk. And the flags, so we already know what's the file name for the console. The flags for the console, it depends on the, what kind of console you want to open, right? For standard input, obviously, it's read only. You cannot write to it, right? For standard output, it's write only, right? And the same is standard error, it's also write only. And also another question, a final question about the console is when do you initialize it? So you know how to open these console files, right? So because console is considered automatically open for each process, so you may tempt it to just, whenever you create a process, whenever you create a process, you always open these three files for it, right? So for each process, you open these files. That is not a correct, because, so the question to ask here is that, first of all, the console file is only needed by user thread, right? In the kernel, at the kernel, you just do, whenever you want to print stuff to the console, you just use caprintf, right? You just, you don't need to open some file and write to it. You just use caprintf to print some stuff on the screen, right? So this console file abstraction is only used by the user thread. So because whenever you call fork, the file table will be inherited by the child thread. So what you need to do is just initialize these three file descriptors for that first, very first user thread, right? For example, suppose you have opened that three files for the first user thread. And because the thread can be created only using fork, so whenever you call fork, that three file handles will be shared among the child and grandchild of that first user process. This part is a little bit tricky, but it should be clear when you reach the fork syscall where you need to copy the file tables. For now, you just need to remember that you cannot open these files multiple times for each process. You only need to copy them in the fork syscall. So at this point, if you haven't done any process syscall yet, don't worry this part for now. You can just leave it alone. But later on, when you do the process syscalls, you need to consider this issue. This part is tricky, I know, but you will know how to deal with it later on. Okay, any questions on this console part? So you know how to open it, you know when to initialize it, which is for the first user thread. Any questions about this? Okay, so finally, let's go over some of the file syscalls. So first, open. When you open the file, the question you need to consider is that, so first you need to check if the file name is valid. That can be achieved using the copying. You just copy in the string. If anything goes wrong, copying will return error code. You know that the file name is not valid. So you just return some error code. And also is the flag valid. For example, when you receive such a flag, when the kernel receive the flag say, user say, I want to open this file both read-only and write-only. How do you detect that kind of violation? You need to reject this kind of, flags, right? Because you don't really know what to do with it. It's conflicting. Also, so the flag should only contain one of the, one and only one of the read-only, write-only and read-write. You need to make sure that if the user, if the flag contains more than two of them, then you need to return some error code about this invalid flag. And also how do you find available file descriptor that depend on your file table design? And also when you initialize a file handle, one question you need to ask yourself is what's the initial offset? So we said that typically the initial offset is just the start of the file, zero, right? But some, there is one special flag called open append. I say already have a file that is 100 bytes. I want to open the file in the append mode, meaning that I open the file and when I write it to it, I don't want to override the content of it. I just want to append my content to that file. In that case, what's the initial offset? End of the file, right? But you need to figure out how to find out the end of the file, right? There is one function. I don't remember the name. It should be viewVFSState. It's called VFSState, which basically give you statistics of the file and that statistics contain the size of the file. So you need to detect if the flag contains the append flag. If it contains the append flag, you need to set the initial offset to the file size instead of zero. That's one tricky thing about the open. Close is simple except for one thing is one to destroy the file handle. We already discussed this. So you basically need to maintain a reference count to the file handle and only destroy the file handle when the count reaches zero. For read and write, because we already have viewPread and viewPwrite to actually perform the actual read and write for you. So yeah, before that, you need to first check, of course, whether user can read or write, given the flags. And here's the difficult part, which is UIO, right? I guess most of you are confused of how to use the UIO. So the way to use viewPread and viewPwrite is that you first initialize a UIO. Basically, this UIO contains all the information it needed to perform the read or write, right? So we have IO vector. If you go to the definition of the IO vector, we find it has nothing but a base and a length. Basically, IO vector specifies a region of memory, a region of buffer in memory. So it has a base address, it has a length of the buffer is. So this IO vector basically specifies where the data is or where the buffer is. When you read, it specifies where to put the data you have read, right? When you write, it specifies which data need to be written to the disk. This is the IO vector. And typically, we just use one IO vector per UIO structure. So this kind should be one. And we have an offset. This offset basically specified where this UIO should start in the file. So this is offset into the file. And we also have a Reset. We should basically specify how much bytes, how many bytes should be read or write. So IO vector specifies the memory part which is either the data you want to write to disk or the buffer you want the data to be in after the read. And the offset and the Reset specify the file part. Basically, specify where to start, where to read or write and how many bytes to read and write. And then we have segment enumerate which basically can be user instruction, user data and system space, which is for kernel. We have enumerate telling whether this is a read or write operation. Finally, we have address space. So once you understand what's inside the UIO, you can know how to initialize the property to actually perform the IO. For example, say user want to read 100 bytes into some buffer, right? So this IO vector should be pointing to that user buffer. And this offset and the Reset should be, offset should be the offset in the file handle. And the Reset should be 100 because you want to read 100 bytes, right? And segment flag should be user data which is the user space. And RW should be read, right? IO IO space should be the address space of the current thread. So once you initialize a UIO, you can just call a view pure read, use this UIO to do the actual read. Actual read. Any questions of this part? It's very important to understand the UIO structure to you, to implement the read and write as you call. And arrow seek. So the only trick part of arrow seek is the 64-bit argument parsing. We already talked about last time, right? And also, how do you calculate a new offset? That's basically just to read the manual and it will tell you what the whence means where to start, how many bytes to go over or before that. And you will want to use the tri-seg to determine if that seek is valid or not. Dupeto. So Dupeto, in some sense, is very similar to CV in the sense that the implementation is quite simple. If you read the manual, you will know exactly what you need to do inside Dupeto. So the manual says Dupeto clones the file handle belong to the old FD, onto the file handle belong to the new FD. If the new FD is open, just close it. So that's kind of confusing to read at first. But if you really figure out what does it mean, it's actually very simple. So suppose this is your file table looks like before the Dupeto seek call, right? We have old FD, point to one file handle. We have new FD, point to another file handle. After Dupeto, what will happen? What's the file table looks like? So we want the new FD also point to the file handle of the old FD. That's what we call a clone the old FD onto the file handle of new FD, right? So after Dupeto, the file table and file handle will look like this. Both old and new file descriptor point to the same file handle of the old file descriptor, right? And this file handle that previously was pointed by the new FD was closed. Because in this case, this is open. So the manual said, if it's opened, it should be closed, right? So this is the changes before and after Dupeto. And here is one example that why we want such a weird kind of interface, right? Suppose you want to write a simple echo command, which we have already show echo hello. It just print, it's very simple that it just print whatever you typed in, right? So the most simple echo does seem to be like this. We just print the first arguments and we do nothing else, right? A shell does see maybe like this. So I first fork a new thread and in the new thread I call exact V on the echo and in the old thread I still wet on this child. So this is one minimal example of the echo share command, the minimal implementation. Now suppose I don't want to print to the console. Instead of that, I want the string being right to a file. So we already know that we can use IO redirection to redirect the output of the program to a separate file. For example here, if I do this, so you will see nothing being printed out, right? But if you open the file, temp, hello.txt, it's here. So the output, instead of being printing to the console, it's being redirected to another normal file, right? So the question is how do you implement this without changing echo.c? Because shell may have multiple programs that want to do this kind of IO redirection functionality. It's not efficient to implement this functionality in each of the files. For example, you don't want to modify echo.c, oh, you don't want to modify echo.c to taking care of, okay, whether it's redirect or not. If it's redirect, I open the file and instead of do print of, I write to the file, right? You don't want to do that. I mean, so that's why we have such a system called div2, right? Note this highlighted the lines we added here. So in the new thread, instead of just to call exact v, we first manipulate the file table a little bit. So we first open the file, hello.txt, then we call div2. Remember how div2 operates, right? So here, the old fd is the fd point to the file, which is the hello.txt. The new fd is stdout, standard output. If we call div2 like this, the file table will be looked like this, right? So in the child process, in this case echo command, it's still write to the standard output. But that write is actually performed on this file instead of the console. So this is one example of how div2 can be useful to implement such functionality. That's basically what I got today. And we can discuss any questions offline here. I guess we're running out of time. Okay, thanks.