File System IPC
BUAA 2023 Spring OS
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.) 😉
1. Workflow
1.1 Main Loop
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.
1 | // fs/serv.c |
So, we can see that after two initialization, it will start to server other processes.
1.2 Initialization
File System’s initialization can be divided into two parts as we see above.
1.2.1 serve_init
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 opentab
.
1 | // fs/serv.c |
So let’s see what the hell opentab
is. And oh, here it is. With every bit set to zero, except opentab[0]
.
1 | // fs/serv.c |
As we know, if we do not initialize
opentab
, it will be placed to.bss
section. So here we make a intentionally nonsense initialization to make it into.data
section.
And, the Open::o_mode
here is the same as the open mode in file. The available modes are defined in user/include/lib.h
.
1 |
1.2.2 fs_init
This part is more complicated, and can be divided again into three parts.
1 | void fs_init(void) |
read_super
Here, we simple read the super block from the disk and validate its content.
1 | // fs/fs.c |
check_write_block
Well, I think this serves as a self diagnose?
1 | void check_write_block(void) |
read_bitmap
We’ve already introduced it in that post, so here is the code again. 🤪
1 | // fs/fs.c |
1.3 Serve
After initialization, we can start to serve other processes. It is a never ending loop. See it by your self.
1 | // fs/serv.c |
The involved macros and request structures are defined in
user/include/fdreq.h
So this is just a collection of requests, huh? (Why not use a function pointer table like exceptions? 🫤)
2. File System IPC
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? 🤨
2.1 IPC Library Function
In the last Lab, we implemented IPC related system calls, but we didn’t mention their user library interface. So… Here they are.
1 | // user/include/lib.h |
In case you get confused, I’d like to have a brief explanation on its parameters. The most confusing ones may be
srcva
,dstva
andperm
.At first,
ipc_recv
must be called by the receiver to enable IPC receiving. Then, it will block the process until a message is sent. Here,whom
andperm
are as a return values. The only in parameter isdstva
, 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, withwhom
indicates the target process ID,val
the value,srcva
andperm
for the page and permission if you want. This function will callsyscall_ipc_try_send
, which will return-E_IPC_NOT_RECV
if 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
dstva
inipc_recv
with a new permissionperm
we set inipc_send
. So the receiver and sender may view the same page with different permissions.
ipc_send/recv
1 | // user/lib/ipc.c |
2.2 File System IPC Interface
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.)
1 | // user/include/lib.h |
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.
1 | // user/lib/fsipc.c |
type
is just the IPC enumeration, such asFSREQ_OPEN
,FSREQ_CLOSE
as mentioned in1.3
.
perm
will be combined withPTE_V
to be the permission bits of the page atdstva
. 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.
For fsipc_xxx
functions, they will wrap request parameters, and finally call fsipc
to send the request.
fsipc_open
1 | // user/include/fsipc.h |
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. 😉
3. File System Handler
3.1 Handler Function
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 fs/serv.c
.
serve_open
will initialize file description page the user process passes to it. Of course, it will record open file in global tab.
serve_open
1 | void serve_open(u_int envid, struct Fsreq_open* rq) |
❗ Important: Here is the first time we use
PTE_LIBRARY
to mark a page. That’s because file descriptor is shared between parent process and child process. With this flag,fork
will not mark this pageCOW
when duplicate pages of parent process.ℹRefer to this post if you forget about
fork
: Lab 4 Reflection. 😱⚠ Reminder:
Open::o_fileid
is not the return value ofopen
, 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 open
. 😌
serve_map
1 | void serve_map(u_int envid, struct Fsreq_map* rq) |
Well, nothing to say, just remove the file.
serve_remove
1 | void serve_remove(u_int envid, struct Fsreq_remove* rq) |
3.2 Auxiliary Function
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.
2.3.1 serve_open
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, open_alloc
and file_open
. Let’s see ‘em.
First, open_alloc
simply allocate a struct Open
from global open tab - opentab
.
1 | // fs/serv.c |
open_alloc
1 | // fs/serv.c |
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_fileid
to record the time it used. Interesting, huh. After this, the open file will be recorded in File System’s global open tab.Notice:
opentab
array is always there, what we allocate is the page atOpen::o_off
. Only the first allocation of aOpen
requires 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.
open_lookup
1 | int open_lookup(u_int envid, u_int fileid, struct Open** po) |
Here is a explanation for pageref
check in open_alloc
and open_lookup
, read it as you will. 😉
explanation on `pageref`
In 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.
Then in open_lookup
, we validate the pageref
of 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.
1 | // fs/fs.c |
In case you forget, I put this figure here again. Am I nice? 🥰 So what this function get is actually just the struct File
.
So, what is walk_path
again?
1 | // fs/fs.c |
Some explanation to its parameters. Here, only
path
is a in parameter, and all the others are out. We assume thatpfile
is neverNULL
, meaning that it is a required parameter. (If not, why you use this function?Though you may just want to getpdir
) andpdir
andlastelem
is optional.ℹNotice: In my implementation, I altered this function to make
pfile
nullable too.If we successfully find the file (can be directory or regular file),
pdir
will be set to its parent directory, andpfile
the file we find. In this case,lastelem
is not used.Otherwise,
pdir
andpfile
are set toNULL
andwalk_path
will return a error, and setlastelem
to 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. 🥱
walk_path
1 | int walk_path(char* path, struct File** pdir, struct File** pfile, char* lastelem) |
file_get_block
is just like a simplifiedfile_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.
2.3.2 serve_close
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 file_flush
.
1 | // fs/fs.c |
file_close/flush
1 | // fs/fs.c |
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, thisstruct File
may change. Therefore, update directory, we should.
4. Library File Function
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. 😯
1 | // user/include/lib.h |
4.1 open
First, let’s see the definition of this function.
1 | // user/lib/file.c |
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 inserve_open
to 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_map
that will callserve_map
to load data from the disk, as you’ve known previously.Finally, we return
fd2num(fd)
to the caller. So,fileid
and the actual id user got are different.
4.2 close
While 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.
1 | // user/lib/fd.c |
Here, for file device, this Dev::dev_close
is file_close
. A little long, so sorry about not make it inside a hide block.
1 | // user/lib/file.c |
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.
1 | // user/lib/fd.c |
❗ Important: So now, it’s time to review
serve_open
again! There, we checkpageref
, 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.
Epilogue
Oops, I guess there are nothing to say anymore. I wonder if this is of any point. 😔 But, enjoy.