Operating systems

Portail informatique

File system

The goal of this lab is to implement the equivalent of the function mmap in xv6.

The system call (beginner level)

Start by getting the code from xv6:

  • git clone https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6-riscv.git if you start with a new copy
  • or git checkout master if you already have a local copy of the repository

Then create a local branch to work:

git checkout -b my-mmap

In xv6 add the following system calls:
  • void* mmap(void* addr, int size, int prot, int flag, int fd, int offset). This function implements exactly mmap, but during the first part of the lab, you will ignore the prot and flag parameters.
  • and void munmap(void* addr, int size)

These functions must be added in a new file named kernel/mmap.c. You always need to include the headers types.h, riscv.h and param.h in your file. You must also add the corresponding object $K/mmap.o to the list OBJS in the Makefile.

For the moment, the mmap function must display its arguments on the terminal before returning the pointer -1, which means that an error has occurred, and the munmap function must display its arguments on the terminal. To simplify error handling, create a mmap.h file in which you will add the following macro:

#define MAP_FAILED ((void *) -1)

Add a program to test the mmap function. Your program should:
  • open the file README in read/write mode (remember to include kernel/fcntl.h to have access to the O_RDONLY flags, or O_RDWR),
  • calculate the file size with the fstat function (remember to include stat.h to have the definition of the stat structure),
  • map the whole file to the address 0x10000000,
  • display the content of the memory mapped on the terminal,
  • unmap the file.

For each system call, check the return value and stop the program in the case of an error. For now, the program should terminate with an error since the mmap function returns an error.

Don't forget to add your test program to the UPROGS list in the Makefile.

To access the solution, you must first add the repository which contains the solution with the following command sequence:

git remote add mmap-soluce https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6-riscv-soluces/xv6-soluce-mmap.git git fetch mmap-soluce

Then, you can access the solution with the following command:

git checkout -b mmap-soluce-exo1 -t mmap-soluce/exo1

You can show the difference to the original code with git diff master.

Initial implementation (advanced level)

In this exercise, you do a simple first implementation of the mmap function.

To start with, align addr on a page border, by using PGROUNDDOWN.

In mmap, check that fd matches a file opened by the current process. The list of files opened by a process is stored in the ofile array of struct proc. To check that the file is open, your code can be inspired by argfd found in sysfile.c.

Check that the offset and size arguments of mmap are consistent with the size of the file. If there is an error, display an error message and return MAP_FAILED.

Inspired by your achievement of shared memory in xv6 carried out in TP8, allocate the physical pages allowing to map the file in memory, and associate them with the address addr (the parameter of mmap). At this stage, you are not asked to manage errors generated by kalloc and mappages.
To associate a physical page with a virtual address, you must use the mappages function which is defined in vm.c. You are not instructed to handle the prot parameter for now, so you can map them as you did for the shared memory segments, i.e., with the PTE_R|PTE_W|PTE_U access flags.

You are now asked to handle errors from kalloc or mappages. In the event of an error, you must:
  • delete the entries in the page table that have already been associated,
  • free the physical pages already allocated.
Luckily, xv6 provides the function void uvmunmap(p->pagetable, va, npages, 1) that unmaps npages pages from the process's memory space starting at va; with a truthy last argument, it also frees the unmapped pages.

Use the readi function (refert to its definition in kernel/fs.c) to copy the file content into the memory area you have just allocated. Also modify the return of mmap to return addr.

Verify that your test program correctly displays the contents of the file. In order to speed up the tests you are going to perform, feel free to display only the first 207 characters of the file (this is its first paragraph).

Like for the TP8 on shared memory, your test program will end with a kernel panic "freewalk: leaf". This is expected, for the same reasons that were explained in the past lab: you mapped a non contiguous memory segment that xv6 does not know how to unmap and free. This will be fixed further below.

To access the solution, if it is not already done, add the repository which contains the solution with the following command sequence:

git remote add mmap-soluce https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6--riscv-soluces/xv6-soluce-mmap.git git fetch mmap-soluce

Then, you can access the solution with the following command:

git checkout -b mmap-soluce-exo2 -t mmap-soluce/exo2

You can show the difference with the original code with git diff master (or git diff mmap-soluce-exo1 to see the difference with the previous exercise).

Software segment (adventurer level)

We now want to lazily map the file. Technically, in mmap, instead of mapping the file, you only need to save the association between the virtual address space (addr, size) and the file (fd, offset, size) in a structure. Then, as no page is physically associated with this structure, access to the area will generate a page fault. You must therefore modify how to handle these faults in xv6 to map the requested page if needed.

In the rest of the lab, we define a segment (struct segment) as a virtual memory area associated with a file. Technically, a segment associates a virtual address addr to (i) an inode ip, (ii) an offset offset, (iii) a size size, (iv) a protection and (v) a flag. Define a structure describing the segments of a process in proc.h, and add an array of segments to a process, limiting the size from table to 16 (consider using a macro that you define in param.h).

In mmap, without modifying the functional behavior you currently have, record the segment in the process segment table. In order to allocate a segment, it is necessary to find a free entry. For this, you can consider that if the segment address is 0, the entry is free.

Modify the way to map a file so as to not give user access: remove the flag PTE_U when mapping the page (you can leave read and write permissions, they will be dealt with later). In case of page fault, simply add this permission back for the moment.

To do this, you need to modify the code that handles traps from the user space. Currently, a page fault simply prints a generic message about an "unexpected scause" and kills the process. If the cause is a load or a store page fault, try to load the page.

You can get the cause of the trap by examining the scause register with uint64 r_scause(). On a load page fault, scause == 0xd, and on a store page fault, scause == 0xf.

Note that the faulty address accessed by a process is recorded in the stval register at the time of the fault. You can find this address with the function uint64 r_stval().

You will probably need to use the pte_t *walk() function to find the page associated with the faulty address.

Modify your code further so that pages of segments are lazily allocated, mapped and loaded when accessed, instead of being loaded upfront when calling mmap.

What do you think can happen if the process closes the file associated with the segment? Why, in mmap, instead of directly storing the inode in the segment, it is necessary to actually store the result of the call to idup(ip) in the segment (where ip is the pointer to the inode)?
The process might close the file before accessing the memory segment. At this time, lazy reads would be performed on an closed inode, which would not be functional. By calling idup, we increment the opening counter of the inode, which assures us that the inode will remain in memory even if the process closes it.

Add definitions for the protections PROT_READ and PROT_WRITE in mmap.h, and treat these protections properly.

Remember you can tell whether the page fault occurred on a read on a write access with the scause: it is 0xd on a load page fault (i.e., a read), and 0xf on a store page fault (i.e., a write).
Do not forget about the PTE_U permission bit.

To access the solution, if it is not already done, add the repository which contains the solution with the following command sequence:

git remote add mmap-soluce https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6-riscv-soluces/xv6-soluce-mmap.git git fetch mmap-soluce

Then, you can access the solution with the following command:

git checkout -b mmap-soluce-exo3 -t mmap-soluce/exo3

You can show the difference with the original code with git diff master (or git diff mmap-soluce-exo2 to see the difference with the previous exercise).

Duplication of processes (guru level)

During a fork(), we now want to be able to inherit segments from the parent process. This exercise is optional.

Update your test program to include a call to fork() after the call to mmap(). For now, both the parent and the child should try to read from the segment. What happens?
Reading from the child fails because the segments are not duplicated yet. The segment only exists in the parent's page table, so the page fault upon the child reading cannot be resolved.

In order to be able to manage the sharing of segments between a parent and its child, you have to restructure the code.

To do this, instead of directly storing the segments in the processes, define a global array of segments with a maximum size of 1024. Then, in a process, instead of directly storing segments, store pointers to segments in this global array segments. Beware of concurrency issues, because the pool of segments is now shared among all processes.

You can amend the function procinit to initialize any lock you need.

You may encounter a kernel panic sched locks when adding locks. It can happen if you hold a non sleeping lock when sched() is called, for instance because you go to sleep when taking a sleep lock.

It comes from the implementation of locks: in an effort to avoid deadlocks, xv6 disables interrupts while holding a lock. It effectively prevents preemption, so sched() ought to never be called on this core while the lock is held. However, sched() can be called explicitly, for instance via a call to sleep().

The solution is to use only sleep locks, or to manage to release the lock before going to sleep.

Update the function fork() to copy the segments from the parent to its child. Take into account whether the pages of the segments have already been loaded by the parent before calling fork().

Add a MAP_SHARED flag to mmap.h.

When a segment is noted as MAP_SHARED, the segments are shared between a parent and its children instead of being duplicated upon fork. Otherwise, the child must duplicate the parent's segments as is currently done.

Update your test program to check that the parent and the child can communicate through the segment. For instance, make the child wait for some signal from the parent.

Handle the concurrency between a parent and its children when trying to lazily map a page of a file.

To access the solution, if it is not already done, add the repository which contains the solution with the following command sequence:

git remote add mmap-soluce https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6-riscv-soluces/xv6-soluce-mmap.git git fetch mmap-soluce

Then, you can access the solution with the following command:

git checkout -b mmap-soluce-exo4 -t mmap-soluce/exo4

You can show the difference with the original code with git diff master (or git diff mmap-soluce-exo3 to see the difference with the previous exercise).

Deleting a mapping (guru level)

The goal of this exercise is to be able to delete mappings. This exercise is optional.

Implement munmap. Think you can actually delete a segment only once no more process uses it. Also remember that you should write the pages that have changed to disk. For this, you must add the definition of the constant PTE_D = 0x40 in mmu.h, and know that this bit is activated in the page table as soon as the processor modifies the content of the page.

Modify exit() so as to automatically delete the mappings of a process before it terminates.

Segmentation (guru level)

The purpose of this exercise is to complete our implementation. This exercise is optional.

Add a MAP_ANONYMOUS flag to mmap.h. When a segment is marked as anonymous, the arguments fd and offset are ignored, and memory should be initially filled with zeros.

Modify the code of xv6 so that your segments are also used to build the initial image of a process. A process must have 5 initial segments (see the end of the proc.h file):
  • A text segment containing the program code,
  • A data segment containing the initialized data of the program,
  • A bss segment containing the data not initialized, i.e. initialized with 0s, of the program,
  • A stack segment of fixed size,
  • A heap segment that can grow.

All these segments must be defined as not being of the type MAP_SHARED so that they are duplicated automatically during a fork().

To build these segments, you need to analyze and modify the exec function (file exec.c), especially starting from line 42.

Also modify the fork() function to stop calling uvmcopy which takes care of copying the memory of a process (the memory of the segments which you have just defined). Instead, you should always call proc_pagetable() which allows to create a new page table in which the kernel is mapped. If your code is correct, depending on the MAP_SHARED flag, the parent's segments will all be copied to the child, or shared with the child.

Modify mmap so that, if addr is equal to 0, mmap automatically finds a free area in the process addressing space.

Copy-on-write fork, (master guru level)

We now want to implement a fast fork(). The principle of this fast fork() is not to copy the parent's memory at fork() time, but only when either the parent or the child modifies it(see Copy-on-write). Do not hesitate to ask questions to your teachers to find out how to design your solution. This exercise is optional.

We only provide a solution to the first four exercises. To access the solution, if it is not already done, add the repository which contains the solution with the following command sequence:

git remote add mmap-soluce https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6-soluces/xv6-soluce-mmap.git git fetch mmap-soluce

Then, you can access the solution with the following command:

git checkout -b mmap-soluce-exo3 -t mmap-soluce/exo3