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
2
3
4
5
6
7
8
9
10
11
12
// fs/serv.c
int main()
{
user_assert(sizeof(struct File) == BY2FILE);
debugf("FS is running\n");

serve_init();
fs_init();
serve();

return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
// fs/serv.c
void serve_init(void)
{
// Set virtual address to map.
u_int va = FILEVA;
// Initial array opentab.
for (int i = 0; i < MAXOPEN; i++)
{
opentab[i].o_fileid = i;
opentab[i].o_ff = (struct Filefd*)va;
va += BY2PG;
}
}

So let’s see what the hell opentab is. And oh, here it is. With every bit set to zero, except opentab[0].

1
2
3
4
5
6
7
8
9
10
11
// fs/serv.c
struct Open
{
struct File* o_file; // mapped descriptor for open file
u_int o_fileid; // file id
int o_mode; // open mode
struct Filefd* o_ff; // va of filefd page
};

// initialize to force into data section
struct Open opentab[MAXOPEN] = { {0, 0, 1} };

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.

How I suppose to know? 😭

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
2
3
4
#define O_RDONLY  0x0000  /* open for reading only */
#define O_WRONLY 0x0001 /* open for writing only */
#define O_RDWR 0x0002 /* open for reading and writing */
#define O_ACCMODE 0x0003 /* mask for above modes */

1.2.2 fs_init

This part is more complicated, and can be divided again into three parts.

1
2
3
4
5
6
void fs_init(void)
{
read_super();
check_write_block();
read_bitmap();
}
read_super

Here, we simple read the super block from the disk and validate its content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//  fs/fs.c
void read_super(void)
{
int r;

// Step 1: read super block.
if ((r = read_block(1, &blk, 0)) < 0)
user_panic("cannot read superblock: %e", r);

void* super = blk;

// Step 2: Check fs magic nunber.
if (super->s_magic != FS_MAGIC)
user_panic("bad file system magic number %x %x", super->s_magic, FS_MAGIC);

// Step 3: validate disk size.
if (super->s_nblocks > DISKMAX / BY2BLK)
user_panic("file system is too large");

debugf("superblock is good\n");
}
check_write_block

Well, I think this serves as a self diagnose?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void check_write_block(void)
{
super = NULL;

// backup the super block.
// copy the data in super block to the first block on the disk.
read_block(0, 0, 0);
memcpy((char*)diskaddr(0), (char*)diskaddr(1), BY2PG);

// smash it
strcpy((char*)diskaddr(1), "OOPS!\n");
write_block(1);
user_assert(block_is_mapped(1));

// clear it out
syscall_mem_unmap(0, diskaddr(1));
user_assert(!block_is_mapped(1));

// validate the data read from the disk.
read_block(1, 0, 0);
user_assert(strcmp((char*)diskaddr(1), "OOPS!\n") == 0);

// restore the super block.
memcpy((char*)diskaddr(1), (char*)diskaddr(0), BY2PG);
write_block(1);
super = (struct Super*)diskaddr(1);
}

Again! How I suppose to know? 🥺

read_bitmap

We’ve already introduced it in that post, so here is the code again. 🤪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// fs/fs.c
uint32_t* bitmap;
void read_bitmap(void)
{
void* blk = NULL;

// Step 1: Calculate the number of the bitmap blocks, and read them into memory.
u_int nbitmap = super->s_nblocks / BIT2BLK + !!(super->s_nblocks % BIT2BLK);
for (u_int i = 0; i < nbitmap; i++)
read_block(i + 2, blk, 0);
bitmap = diskaddr(2);

// Step 2: Make sure the reserved and root blocks are marked in-use.
user_assert(!block_is_free(0));
user_assert(!block_is_free(1));

// Step 3: Make sure all bitmap blocks are marked in-use.
for (u_int i = 0; i < nbitmap; i++)
user_assert(!block_is_free(i + 2));

debugf("read_bitmap is good\n");
}

1.3 Serve

After initialization, we can start to serve other processes. It is a never ending loop. See it by your self.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// fs/serv.c
void serve(void)
{
u_int req, whom, perm;

for (; ; )
{
perm = 0;
req = ipc_recv(&whom, (void*)REQVA, &perm);
// All requests must contain an argument page
if (!(perm & PTE_V))
{
debugf("Invalid request from %08x: no argument page\n", whom);
continue; // just leave it hanging, waiting for the next request.
}

switch (req)
{
case FSREQ_OPEN:
serve_open(whom, (struct Fsreq_open*)REQVA);
break;
case FSREQ_MAP:
serve_map(whom, (struct Fsreq_map*)REQVA);
break;
case FSREQ_SET_SIZE:
serve_set_size(whom, (struct Fsreq_set_size*)REQVA);
break;
case FSREQ_CLOSE:
serve_close(whom, (struct Fsreq_close*)REQVA);
break;
case FSREQ_DIRTY:
serve_dirty(whom, (struct Fsreq_dirty*)REQVA);
break;
case FSREQ_REMOVE:
serve_remove(whom, (struct Fsreq_remove*)REQVA);
break;
case FSREQ_SYNC:
serve_sync(whom);
break;
default:
debugf("Invalid request code %d from %08x\n", whom, req);
break;
}

/*
* IPC receiver does not map page to va by itself, this map is done
* by sender at the end of transaction. So... to save memory? Receiver
* here has to unmap this page by itself.
*/
syscall_mem_unmap(0, (void*)REQVA);
}
}

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
2
3
// user/include/lib.h
void ipc_send(u_int whom, u_int val, const void* srcva, u_int perm);
u_int ipc_recv(u_int* whom, void* dstva, u_int* perm);

In case you get confused, I’d like to have a brief explanation on its parameters. The most confusing ones may be srcva, dstva and perm.

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 and perm are 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 whom indicates the target process ID, val the value, srcva and perm for the page and permission if you want. This function will call syscall_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 in ipc_recv with a new permission perm we set in ipc_send. So the receiver and sender may view the same page with different permissions.

ipc_send/recv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// user/lib/ipc.c
void ipc_send(u_int whom, u_int val, const void* srcva, u_int perm)
{
int r;
while ((r = syscall_ipc_try_send(whom, val, srcva, perm)) == -E_IPC_NOT_RECV)
{
syscall_yield();
}
user_assert(r == 0);
}

u_int ipc_recv(u_int* whom, void* dstva, u_int* perm)
{
int r = syscall_ipc_recv(dstva);
if (r != 0)
user_panic("syscall_ipc_recv err: %d", r);
if (whom)
*whom = env->env_ipc_from;
if (perm)
*perm = env->env_ipc_perm;
return env->env_ipc_value;
}

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
2
3
4
5
6
7
8
// user/include/lib.h
int fsipc_open(const char* path, u_int omode, struct Fd* fd)
int fsipc_map(u_int fileid, u_int offset, void* dstva)
int fsipc_set_size(u_int fileid, u_int size)
int fsipc_close(u_int fileid)
int fsipc_dirty(u_int fileid, u_int offset)
int fsipc_remove(const char* path)
int fsipc_sync(void);

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
2
3
4
5
6
7
8
// user/lib/fsipc.c
static int fsipc(u_int type, void* fsreq, void* dstva, u_int* perm)
{
u_int whom; // redundant
// Our file system server must be the 2nd env.
ipc_send(envs[1].env_id, type, fsreq, PTE_D);
return ipc_recv(&whom, dstva, perm);
}

type is just the IPC enumeration, such as FSREQ_OPEN, FSREQ_CLOSE as mentioned in 1.3.

perm will be combined with PTE_V to 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.

For fsipc_xxx functions, they will wrap request parameters, and finally call fsipc to send the request.

fsipc_open
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// user/include/fsipc.h
struct Fsreq_open
{
char req_path[MAXPATHLEN];
u_int req_omode;
};

// user/lib/fsipc.c
u_char fsipcbuf[BY2PG] __attribute__((aligned(BY2PG))); // shared by all fsipc_xxx functions

int fsipc_open(const char* path, u_int omode, struct Fd* fd)
{
u_int perm;
struct Fsreq_open* req;
req = (struct Fsreq_open*)fsipcbuf;

if (strlen(path) >= MAXPATHLEN)
return -E_BAD_PATH;

strcpy((char*)req->req_path, path);
req->req_omode = omode;

return fsipc(FSREQ_OPEN, req, fd, &perm);
}

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. 😉

FS-IPC


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void serve_open(u_int envid, struct Fsreq_open* rq)
{
int r;
struct Open* o;

// Find a file id.
if ((r = open_alloc(&o)) < 0)
{
ipc_send(envid, r, 0, 0);
}

// Open the file.
struct File* f;
if ((r = file_open(rq->req_path, &f)) < 0)
{
ipc_send(envid, r, 0, 0);
return;
}

// Save the file pointer.
o->o_file = f;

// Fill out the Filefd structure
struct Filefd* ff = (struct Filefd*)o->o_ff; // redundant conversion
ff->f_file = *f;
ff->f_fileid = o->o_fileid;
o->o_mode = rq->req_omode;
ff->f_fd.fd_omode = o->o_mode;
ff->f_fd.fd_dev_id = devfile.dev_id; // 'devfile' is the global dev struct.

ipc_send(envid, 0, o->o_ff, PTE_D | PTE_LIBRARY);
}

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 page COW 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 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 open. 😌

serve_map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void serve_map(u_int envid, struct Fsreq_map* rq)
{
int r;
struct Open* pOpen;
if ((r = open_lookup(envid, rq->req_fileid, &pOpen)) < 0)
{
ipc_send(envid, r, 0, 0);
return;
}

u_int filebno = rq->req_offset / BY2BLK;
void* blk;
if ((r = file_get_block(pOpen->o_file, filebno, &blk)) < 0)
{
ipc_send(envid, r, 0, 0);
return;
}

ipc_send(envid, 0, blk, PTE_D | PTE_LIBRARY);
}

Well, nothing to say, just remove the file.

serve_remove
1
2
3
4
5
6
7
8
void serve_remove(u_int envid, struct Fsreq_remove* rq)
{
// Step 1: Remove the file specified in 'rq' using 'file_remove' and store its return value.
file_remove(rq->req_path);

// Step 2: Respond the return value to the requester 'envid' using 'ipc_send'.
ipc_send(envid, 0, NULL, 0);
}

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
2
// fs/serv.c
int open_alloc(struct Open** o);
open_alloc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// fs/serv.c
int open_alloc(struct Open** o)
{
int i, r;

// Find an available open-file table entry
for (i = 0; i < MAXOPEN; i++)
{
switch (pageref(opentab[i].o_ff))
{
case 0:
if ((r = syscall_mem_alloc(0, opentab[i].o_ff, PTE_D | PTE_LIBRARY)) < 0)
{
return r;
}
case 1:
opentab[i].o_fileid += MAXOPEN;
*o = &opentab[i];
memset((void*)opentab[i].o_ff, 0, BY2PG);
return (*o)->o_fileid;
}
}

return -E_MAX_OPEN;
}

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 at Open::o_off. Only the first allocation of a Open 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
2
3
4
5
6
7
8
9
10
int open_lookup(u_int envid, u_int fileid, struct Open** po)
{
struct Open* o = &opentab[fileid % MAXOPEN];

if (pageref(o->o_ff) == 1 || o->o_fileid != fileid)
return -E_INVAL;
*po = o;

return 0;
}

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
2
3
4
5
// fs/fs.c
int file_open(char* path, struct File** file)
{
return walk_path(path, 0, file, 0);
}

In case you forget, I put this figure here again. Am I nice? 🥰 So what this function get is actually just the struct File.

File-Layout

So, what is walk_path again?

1
2
// fs/fs.c
int walk_path(char* path, struct File** pdir, struct File** pfile, char* lastelem);

Some explanation to its parameters. Here, only path is a in parameter, and all the others are out. We assume that pfile is never NULL, meaning that it is a required parameter. (If not, why you use this function? Though you may just want to get pdir) and pdir and lastelem 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, and pfile the file we find. In this case, lastelem is not used.

Otherwise, pdir and pfile are set to NULL and walk_path will return a error, and set lastelem 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
int walk_path(char* path, struct File** pdir, struct File** pfile, char* lastelem)
{
char name[MAXNAMELEN];
int r;

// start at the root.
path = skip_slash(path);
struct File* file = &super->s_root;
struct File* dir = NULL;
char* p;

name[0] = '\0';

if (pdir)
*pdir = NULL;
if (pfile)
*pfile = NULL;

// find the target file by name recursively.
while (*path != '\0')
{
dir = file;
p = path;

while (*path != '/' && *path != '\0')
path++;
if (path - p >= MAXNAMELEN)
return -E_BAD_PATH;

memcpy(name, p, path - p);
name[path - p] = '\0';
path = skip_slash(path);
if (dir->f_type != FTYPE_DIR)
return -E_NOT_FOUND;

if ((r = dir_lookup(dir, name, &file)) < 0)
{
if (r == -E_NOT_FOUND && *path == '\0')
{
if (pdir)
*pdir = dir;
if (lastelem)
strcpy(lastelem, name);
*pfile = NULL;
}
return r;
}
}

if (pdir)
*pdir = dir;
if (pfile)
*pfile = file;

return 0;
}

int dir_lookup(struct File* dir, char* name, struct File** file)
{
int r;

// Step 1: Calculate the number of blocks in 'dir' via its size.
u_int nblk = dir->f_size / BY2BLK;

// Step 2: Iterate through all blocks in the directory.
for (int i = 0; i < nblk; i++)
{
// Read the i'th block of 'dir' and get its address to 'blk'.
void* blk;
if ((r = file_get_block(dir, i, &blk)) < 0)
return r;
struct File* files = (struct File*)blk;

// Find the target among all 'File's in this block.
for (struct File* f = files; f < files + FILE2BLK; ++f)
{
if (strcmp(f->f_name, name) == 0) // Karabast! '== 0'!
{
f->f_dir = dir; // set this every time it is accessed?
*file = f;
return 0;
}
}
}

return -E_NOT_FOUND;
}

file_get_block is 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.

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
2
3
// fs/fs.c
void file_close(struct File* f);
void file_flush(struct File* f);
file_close/flush
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// fs/fs.c
void file_close(struct File* f)
{
// Flush the file itself, if f's f_dir is set, flush it's f_dir.
file_flush(f);
if (f->f_dir)
file_flush(f->f_dir);
}

void file_flush(struct File* f)
{
int r;
u_int bno;
u_int diskno;

u_int nblocks = f->f_size / BY2BLK + 1;
for (bno = 0; bno < nblocks; bno++)
{
if ((r = file_map_block(f, bno, &diskno, 0)) < 0)
continue;
if (block_is_dirty(diskno))
write_block(diskno);
}
}

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 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
2
3
// user/include/lib.h
int open(const char* path, int mode);
int close(int fd);

4.1 open

First, let’s see the definition of this function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// user/lib/file.c
int open(const char* path, int mode)
{
int r;

// Step 1: Alloc a new 'Fd' using 'fd_alloc' in fd.c.
struct Fd* fd;
if ((r = fd_alloc(&fd)) < 0)
return r;

// Step 2: Prepare the 'fd' using 'fsipc_open' in fsipc.c.
if ((r = fsipc_open(path, mode, fd)) < 0)
return r;

// Step 3: Set 'va' to the address of the page where the 'fd''s data is cached.
char* va = fd2data(fd);
struct Filefd* ffd = (struct Filefd*)fd;
u_int fileid = ffd->f_fileid;
u_int size = ffd->f_file.f_size;

// Step 4: Alloc pages and map the file content using 'fsipc_map'.
for (int i = 0; i < size; i += BY2PG)
{
if ((r = fsipc_map(fileid, i, va + i)) < 0)
return r;
}

// Step 5: Return the number of file descriptor using 'fd2num'.
return fd2num(fd);
}

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_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 call serve_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// user/lib/fd.c
int close(int fdnum)
{
int r;
struct Dev* dev = NULL;
struct Fd* fd;

if ((r = fd_lookup(fdnum, &fd)) < 0 || (r = dev_lookup(fd->fd_dev_id, &dev)) < 0)
{
return r;
}

r = (*dev->dev_close)(fd);
fd_close(fd);

return r;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// user/lib/file.c
int file_close(struct Fd* fd)
{
int r;

struct Filefd* ffd = (struct Filefd*)fd;
u_int fileid = ffd->f_fileid;
u_int size = ffd->f_file.f_size;

// Set the start address storing the file's content.
void* va = fd2data(fd);

// Tell the file server the dirty page.
for (u_int i = 0; i < size; i += BY2PG)
fsipc_dirty(fileid, i);

// Request the file server to close the file with fsipc.
if ((r = fsipc_close(fileid)) < 0)
{
debugf("cannot close the file\n");
return r;
}

// Unmap the content of file, release memory.
if (size == 0)
return 0;
for (u_int i = 0; i < size; i += BY2PG)
{
if ((r = syscall_mem_unmap(0, (void*)(va + i))) < 0)
{
debugf("cannont unmap the file.\n");
return r;
}
}

return 0;
}

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
2
3
4
5
// user/lib/fd.c
void fd_close(struct Fd* fd)
{
syscall_mem_unmap(0, fd);
}

Important: So now, it’s time to review serve_open again! 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.


Epilogue

Oops, I guess there are nothing to say anymore. I wonder if this is of any point. 😔 But, enjoy.