ℹ Notice: This article serves as a DLC for Lab 5 Reflection. The main stuffs are there.
⚠ Warning: If you are confused about some concepts in this article, or feel contents missing, please refer to that post first. (You can open two browser tabs at the same time.) 😉
As we know, File System is an independent process in user space, so it has a main function. Let’s see what does it do.
So, we can see that after two initialization, it will start to server other processes.
File System’s initialization can be divided into two parts as we see above.
This part is quite simple, but it involves another concept -
opentab. It is the global record for open files. And here we just initialized all the
So let’s see what the hell
opentab is. And oh, here it is. With every bit set to zero, except
As we know, if we do not initialize
opentab, it will be placed to
.bsssection. So here we make a intentionally nonsense initialization to make it into
Open::o_mode here is the same as the open mode in file. The available modes are defined in
This part is more complicated, and can be divided again into three parts.
Here, we simple read the super block from the disk and validate its content.
Well, I think this serves as a self diagnose?
We’ve already introduced it in that post, so here is the code again. 🤪
After initialization, we can start to serve other processes. It is a never ending loop. See it by your self.
The involved macros and request structures are defined in
So this is just a collection of requests, huh? (
Why not use a function pointer table like exceptions? )
As user processes, we simply send a request to File System to get the corresponding service, and waiting the response from File System. For File System, as mentioned above, we just receive the request, handle it, and send the response back to the sender. Nice communication, right? 🤨
In the last Lab, we implemented IPC related system calls, but we didn’t mention their user library interface. So… Here they are.
In case you get confused, I’d like to have a brief explanation on its parameters. The most confusing ones may be
ipc_recvmust be called by the receiver to enable IPC receiving. Then, it will block the process until a message is sent. Here,
permare as a return values. The only in parameter is
dstva, it indicates, if you want, the virtual address of the receiving page.
Then, the sender can send the message by
ipc_send. Here, all parameters are in, with
whomindicates the target process ID,
permfor the page and permission if you want. This function will call
syscall_ipc_try_send, which will return
-E_IPC_NOT_RECVif target and result in a busy wait.
Important! When page is delivered, it will not create a new physical page, but only map the original page to
ipc_recvwith a new permission
permwe set in
ipc_send. So the receiver and sender may view the same page with different permissions.
To simplify IPC to File System, user library provided some interface for such IPCs. However, the are used as auxiliary functions and are not exposed directly to user process. (Although declared in
user/include/lib.h, it doesn’t seem to be a good isolation.)
I just… just don’t understand why not specify parameter name in header file. 🤬 It is really annoying. And it doesn’t seem to have optional definitions. (Perhaps there will be?)
These functions just works as a parameter wrapper. They simply wrap parameter to corresponding request structures, and send it to File System. Actually, they ended up by calling another auxiliary function to actually send the request.
typeis just the IPC enumeration, such as
FSREQ_CLOSEas mentioned in
permwill be combined with
PTE_Vto be the permission bits of the page at
dstva. It intend to make the page writable because this is where our file descriptor locates (I’ll talk about this later), and we want File System to initialize this page for us.
fsipc_xxx functions, they will wrap request parameters, and finally call
fsipc to send the request.
The memory exchange during file IPC may be a little confusing. Take
open as an example. The interface for user is only
open. It will first call
fsipc_open to send
fsipcbuf to file system, which contains
Fsreq_open as raw data. Then, File System receives this page, (only one physical page, but two references!), and create a physical page to store the newly create file descriptor, and make pointer
Open::off points at it. Finally, File System deliver this page to user. Similarly, user only share the reference. 😉
After a request is received, File System will call corresponding handler functions to do the actual work. There are quite a lot request handlers, as you can see in
serve function, but generally of the same pattern. So here I just want to give several examples. They are all defined locally in
serve_open will initialize file description page the user process passes to it. Of course, it will record open file in global tab.
void serve_open(u_int envid, struct Fsreq_open* rq)
❗ Important: Here is the first time we use
PTE_LIBRARYto mark a page. That’s because file descriptor is shared between parent process and child process. With this flag,
forkwill not mark this page
COWwhen duplicate pages of parent process.
ℹRefer to this post if you forget about
fork: Lab 4 Reflection. 😱
Open::o_fileidis not the return value of
open, which will be introduced later.
serve_map will map the given page to requested virtual address. This is used to load file from disk. You’ll get a clear view of it later in the user library function
void serve_map(u_int envid, struct Fsreq_map* rq)
Well, nothing to say, just remove the file.
void serve_remove(u_int envid, struct Fsreq_remove* rq)
If you go through the implementations of these IPC interfaces, you may see some essential functions that I never mentioned before. You can skip this part if you’re not interested at such details.
In this handler function, we’ll open a file, and initialize the file descriptor page user passed to us. You can find the body of
serve_open above. There are two functions that are important here,
file_open. Let’s see ‘em.
open_alloc simply allocate a
struct Open from global open tab -
Here, we just allocate a page to store file descriptor. A little waste of memory, but makes it easier for memory exchange. And you can see that, if this open tab is used, it simple add a offset to
Open::o_fileidto record the time it used. Interesting, huh. After this, the open file will be recorded in File System’s global open tab.
opentabarray is always there, what we allocate is the page at
Open::o_off. Only the first allocation of a
Openrequires memory allocation, the page will not be freed, and will be reused next time.
After a open tab is allocated, it can be looked up.
int open_lookup(u_int envid, u_int fileid, struct Open** po)
Here is a explanation for
pageref check in
open_lookup, read it as you will. 😉
explanation on `pageref`
open_alloc, we use
pageref to determine whether a open tab is allocated or not. If not, then the reference is sure to be 0, and will be created, which will make this value 1. Then, when
serve_open ends, it will use
page_insert to deliver the return value, which will make it 2. Only when user process close all files can this value become 1 again. So reference 1 on allocation means it is to be used by a new file and should initialize again.
open_lookup, we validate the
open. Why 1 is invalid? Because only a successful open will complete all steps and return the page back to user process, make this reference 2. Thus 1 may indicate a failure of open! Or, the user process just closed the file. More than 2 just means many processes share the same file, e.g. parent and child process. 🥱
Then, file open will get the corresponding
struct File. Don’t be deceived by the name of this function. It is not literally “open file”, but only find the corresponding
struct File with the given path. It’s so simple to be put here with out hide block.
In case you forget, I put this figure here again. Am I nice? 🥰 So what this function get is actually just the
So, what is
Some explanation to its parameters. Here, only
pathis a in parameter, and all the others are out. We assume that
NULL, meaning that it is a required parameter. (
If not, why you use this function?Though you may just want to get
ℹNotice: In my implementation, I altered this function to make
If we successfully find the file (can be directory or regular file),
pdirwill be set to its parent directory, and
pfilethe file we find. In this case,
lastelemis not used.
pfileare set to
walk_pathwill return a error, and set
lastelemto the last successful found file name. Not the full path of the last file. (I doubt if this is of any use.)
This function is long but not complicate. It is a good example of traverse file structure. 🥱
int walk_path(char* path, struct File** pdir, struct File** pfile, char* lastelem)
file_get_blockis just like a simplified
file_block_walk, to get the block content of the corresponding file block id. Just refer to Lab 5 Reflection for more information. 🥴 Just be reminded that it will map block into memory if not loaded from disk yet.
You may see this function in
serve_close. Again, don’t be fooled by its name, it doesn’t really close the file, it only save the file to disk using
Why we should flush parent directory? Well, you know, content of directory is its children’s
struct File, so perhaps due to file size change or some else reason, this
struct Filemay change. Therefore, update directory, we should.
In the previous post Lab 5 Reflection, we’ve learnt some library functions, which doesn’t rely on File System IPC. And here, I’d like to introduce these two that depend on such IPC. 😯
First, let’s see the definition of this function.
So, you can have a complete view of open process now. We first find a suitable page to store the incoming file descriptor (only find, no allocation of memory). Then, we just call
fsipc_open, which will end in
serve_opento prepare such file descriptor page for us. Then, we can initialize some properties using this page.
Then, to get the content of the file, we just use
fsipc_mapthat will call
serve_mapto load data from the disk, as you’ve known previously.
Finally, we return
fd2num(fd)to the caller. So,
fileidand the actual id user got are different.
open is only for file device, here
close can be used to any device. As you can infer from the source file it located. As you can see, a function pointer does the dynamic job.
Here, for file device, this
file_close. A little long, so sorry about not make it inside a hide block.
You can see that in this function, we call
fsipc_close that will eventually invoke
serve_close to flush the file to the disk as a save job. Then, we just un-map the page for memory release.
Then, we have
fd_close, as we known, it simply un-map the file descriptor page.
❗ Important: So now, it’s time to review
serve_openagain! There, we check
pageref, and sharing the file descriptor make its page reference be 2 or more. And here, as we close the file, user process will un-map this page one by one (if there are more than one process using this file), thus the reference will decrease until 1, which is the File System’s reference. And once it becomes 1, it means that all user processes have lost connect with this page, indicating the file is ultimately closed.
Oops, I guess there are nothing to say anymore. I wonder if this is of any point. 😔 But, enjoy.
- BUAA 2023 Spring OS
- 1. Workflow
- 2. File System IPC
- 3. File System Handler
- 4. Library File Function