Written Component
Everyone has now turned in their answers and they have been graded. You can find the grading by looking at the graded-written branch of your repository on GitHub, where you'll find your written.md file has been updated with comments, and there is a new file WRITTEN-GRADING.md that provides a summary of your grade.
DO NOT SHARE OR DISTRIBUTE THE MODEL SOLUTIONS.
A: Loading and Running User-Level Programs; Dealing with User-Space Memory
-
What are the ELF magic numbers?
The ELF magic numbers are a sequence of bytes at the beginning of an ELF (Executable and Linkable Format) file that identify it as an ELF file. They are defined in
elf.has follows:#define ELFMAG0 0x7f #define ELFMAG1 'E' #define ELFMAG2 'L' #define ELFMAG3 'F'These magic numbers form the byte sequence
char(0x7f)+"ELF"at the start of every ELF file. They are used for consistency checking and to quickly identify ELF files. -
What function forces the processor to switch into usermode? Is this function machine dependent?
The function that forces the processor to switch into usermode is
mips_usermode(), which is defined inkern/arch/mips/locore/trap.c. This function is indeed machine-dependent, as evidenced by its location in thekern/arch/mipsdirectory and its call to MIPS-specific assembly code.The process of switching to usermode involves the following steps:
enter_new_process()inrunprogram.ccallsmips_usermode()to switch to usermode.mips_usermode()then callsasm_usermode(), which is written in MIPS assembly.asm_usermode()sets up the necessary CPU registers and uses theeret(Exception Return) instruction to switch to usermode.
-
In what file are
copyinandcopyoutdefined? Why can't we just copy directly (e.g., usingmemcpy)?copyinandcopyoutare defined inkern/vm/copyinout.c. We can't usememcpydirectly for several reasons:- Safety:
copyinandcopyoutperform important checks to ensure the memory being accessed is within the proper userspace region. This is done using thecopycheck()function. - Fault handling: These functions use
copyfailas a recovery point if a fatal fault occurs during copying. This allows the kernel to handle errors gracefully without crashing.
(After performing these checks and setups,
copyinandcopyoutdo actually usememcpyfor the data transfer.) - Safety:
-
What (briefly) is the purpose of
userptr_t? What's the difference betweenuserptr_tandvaddr_t, both technically and in intent?userptr_tis a type used to represent pointers to user-space memory. Its purpose is to:- Clearly distinguish pointers to user memory (and originating from user code, thus not entirely trustworthy) from pointers to kernel memory.
- Prevent accidental dereferencing of user pointers in kernel code.
userptr_tis defined as a pointer to a dummy struct containing a single char. This allows pointer arithmetic (with no implicit multipler, since the object size is 1) but prevents accidental dereferencing as the result will be a dummy struct that is unlikely to be what the erroneous code expects.vaddr_t, on the other hand, is defined as an unsigned 32-bit integer (uint32_t) and represents a virtual memory address. It's used more generally in memory-management operations. By being defined as an integer (that's the same bit width as a pointer), we can perform arithmetic on it, but we cannot dereference it.The key differences are:
- Intent:
userptr_tis specifically for referencing user-space memory, whilevaddr_tis used for general virtual address manipulation in both user and kernel space. - Trust:
userptr_tvalues are considered untrusted and require careful handling, whilevaddr_tvalues are generally trusted within the kernel. - Usage:
userptr_tis primarily used withcopyinandcopyout, whilevaddr_tis used in various memory management operations.
Deeper dive: Why is one an integer and the other a pointer? The answer lies in the existance of
const_userptr_tand non-existance ofconst_vaddr_t. In C code, we want to be able to pass non-const pointers into functions that take const pointers, but not the converse. By makinguserptr_tan actual pointer, it'll follow the usual C rules for const-ness. Because we don't make the “pointing to const memory” distinction forvaddr_t, we don't need to make it a pointer to ensure the appropriate const-correctness.
B: Virtual File System
-
As you were reading the code in
runprogram.candloadelf.c, you probably noticed how the kernel manipulates the files. Which kernel function is called to open a file? Which macro is called to read the file? What about to write a file? Which data structure is used in the kernel's VFS subsystem to represent an open file?- To open a file:
vfs_open()function - To read a file:
VOP_READmacro - To write to a file:
VOP_WRITEmacro - Data structure representing an open file:
struct vnode, which we always refer to using a pointer (struct vnode *) since the VFS layer manages the actual memory holding thevnode.
(Note:
vfs_opentakes astruct vnode **as its last argument, which is a pointer tostruct vnode *. This is how it returns the vnode of the opened file to the caller, because the function uses its normal return value to return an error code.)Deeper dive: The
vnode(virtual node) structure is an abstract representation of a file or directory in the VFS. It includes: - A reference count - A lock for the reference count - A pointer to the filesystem it belongs to - Filesystem-specific data - Function pointers for operations on this vnode - To open a file:
-
What is the difference between
UIO_USERISPACEandUIO_USERSPACE? When should one useUIO_SYSSPACEinstead?UIO_USERISPACE: Refers to user process code (instruction space)UIO_USERSPACE: Refers to user process data
Both are used to indicate that the memory being accessed is in user space. The distinction between instruction and data space is not (currently) significant in OS/161, but it's likely good practice to make the distinction anyway (e.g., it could be important in systems with separate I and D caches or with a Harvard architecture).
UIO_SYSSPACEshould be used when the memory being accessed belongs to the kernel (kernel space). This is important because:- It allows the system to use more efficient copying methods (like directly calling
memmove) instead ofcopyin/copyout. - It indicates that the memory is trusted and doesn't need the same level of checking as user space memory.
-
Why can the
struct uiothat is used to read in a segment be allocated on the stack inload_segment? (i.e., where does the memory read actually go?)The
struct uiocan be allocated on the stack inload_segmentbecause:- It's a small, fixed-size structure that doesn't contain the actual data being transferred.
- It only contains metadata about the transfer, such as pointers, offsets, and sizes.
- The
uiostruct is only needed for the duration of theload_segmentfunction call.
The actual memory read goes to the location specified by the
vaddrparameter in theload_segmentfunction, which is typically in the user's address space. Theuiostruct just describes how this transfer should occur. -
In
runprogram, why is it important to callvfs_closebefore going to user mode?It's crucial to call
vfs_closebefore entering user mode because:- The
enter_new_processfunction doesn't return, so this is the last chance to close the file properly. (What is on the kernel stack at this moment will be lost. Although the user program will likely reenter the kernel to perform system calls or because of interrupts, that will be a new entry into the kernel.)
Of course, in general, it is always good to close a file when you are done with it for all the usual resource-management reasons.
- The
-
What are
VOP_READandVOP_WRITE? How are they used? What doesVOP_ISSEEKABLEtell you about a vnode?VOP_READandVOP_WRITEare macros that provide an abstract interface for reading from and writing to files, respectively. They are part of the VFS (Virtual File System) layer and allow uniform access to different types of file systems.Usage:
VOP_READ(vn, uio): Reads data from the file represented by vnodevninto the memory described byuio.VOP_WRITE(vn, uio): Writes data from the memory described byuioto the file represented by vnodevn.
These macros handle the necessary locking and call the appropriate file system-specific functions.
VOP_ISSEEKABLEis a macro that checks if a file is seekable. It returns a boolean value indicating whether the file supports random access (seeking to arbitrary positions). - Regular files and directories are typically seekable. - Some devices (like keyboards or network sockets) may not be seekable.This information is useful for determining what operations can be performed on a file and how to handle it in the VFS layer.
-
The
pathargument tovfs_openis notconst. Investigate, and determine whether there are any potential gotchas with usingvfs_open.The
pathargument tovfs_openis notconstbecause it may be modified during the process of opening the file.Specifically, in the
getdevicefunction (called byvfs_lookup, which is called byvfs_open), the path string may be modified if it contains a colon. The colon is replaced with a zero byte (NUL terminator) to separate the device name from the rest of the path. (FWIW, this approach is used to avoid duplicating the path string, which would be inefficient, especially for long paths.)The possible gotcha is that if the caller of
vfs_openexpects to use thepathstring after the call, they it may have been altered, potentially causing bugs or unexpected behavior.To avoid these issues, callers of
vfs_openshould be aware that the function may modify the path string, and should use a copy of the path if they need to preserve the original.
C: Traps & System Calls
-
What is the numerical value of the exception code for a MIPS system call?
The numerical value of the exception code for a MIPS system call is 8. This is defined in
kern/arch/mips/include/trapframe.has:#define EX_SYS 8 /* System call */ -
How many bytes is an instruction in MIPS? (Looking at the end of the code for the
syscallfunction inmips/trap.cwill give you the answer.)An instruction in MIPS is 4 bytes. This can be deduced from the following code in the
syscallfunction inkern/arch/mips/locore/trap.c:/* * Now, advance the program counter, to avoid restarting * the syscall over and over again. */ tf->tf_epc += 4;Here, the program counter (
tf_epc) is incremented by 4 bytes to move to the next instruction. -
What does the
syscallfunction do with the trapframe that is passed into it? And where is the trapframe that is passed intosyscallstored?The
syscallfunction uses the trapframe to:- Extract the system call number and arguments from the saved registers.
- Execute the appropriate system call based on the call number.
- Store the return value and error status in the trapframe.
- Advance the program counter to the next instruction.
The trapframe is stored on the kernel stack of the current thread. It's initially created in the assembly code in
exception-mips1.Swhen an exception occurs. The assembly code saves all the necessary registers onto the stack and then callsmips_trap, which in turn callssyscallfor system call exceptions.(Note: In lab I helped some students see that the stack was used, but didn't draw their attention to the fact that it was the kernel stack. Sorry about that. If you got this aspect wrong, we'll still give you full credit but do be aware of this detail as it matter for the assignment.)
-
Why do you “probably want to change” the implementation of
kill_curthread?It's important to realize that
kill_curthreadis notthread_exit. It's not called when a thread exits normally, it's called when the thread encounters a fatal error, such as accessing invalid memory or executing an illegal instruction.The current implementation of
kill_curthreadsimply prints an error message and then panics, causing the entire kernel to shut down. This is obviously problematic, as we never want the entire system to crash just because one thread misbehaved.A better implementation would:
- Terminate the process containing the current thread as something has clearly gone badly wrong in the process.
- This likely involves noting the reason the process was killed wherever we track the process's state for
waitpidand such.
- This likely involves noting the reason the process was killed wherever we track the process's state for
- Schedule another thread to run, so the system can continue operating.
These steps are somewhat akin to calling
sys_exitwith a non-zero status.This approach allows the system to continue running even if one thread encounters a fatal error.
(Note: If you thought a process could continue after one thread crashed, you'll get full credit, but understand that a thread crashing is a sign that the process is in a bad state and should be terminated. Also, understand that when a user process is in a bad state, we can't really trust anything inside it, so the kernel should probably not try to read any data from it or write any data to it, or cause it to continue running.)
- Terminate the process containing the current thread as something has clearly gone badly wrong in the process.
-
What would be required to implement a system call that took more than four arguments?
System calls follow the standard MIPS calling convention (a.k.a. the MIPS ABI [Application Binary Interface]), which passes the first four arguments in registers
a0-a3, and from that point on, arguments are passed on the user stack. We don't implement that ourselves, that's just how it works already. We just have to deal with that being the way things are.Thus, to implement a system call with more than four arguments, we need to:
- Read the first four arguments out of the registers as usual.
- Read additional arguments from the user stack using
copyin.
They would be placed at
sp+16and upwards, so we would use code like:int arg5; err = copyin((userptr_t)tf->tf_sp + 16, &arg5, sizeof(int)); -
User-space code refers to a global variable called
errno. How doeserrnoget set?The
errnovariable is set in user space, not directly by the kernel. The process works as follows:- When a system call fails, the kernel sets the error code in the v0 register of the trapframe and also sets the a3 register to 1 to indicate an error occurred.
- When control returns to user space, the assembly code in
syscalls-mips.Schecks the a3 register. If a3 is set to 1 (indicating an error), the assembly code:- Stores the value from v0 into the global
errnovariable. - Sets v0 to -1 to indicate failure to the C code that called the system call.
- Stores the value from v0 into the global
This approach allows C programs to check the return value of system calls for -1 and then check
errnofor the specific error that occurred. It also ensures thaterrnois only set when a system call fails, not when it succeeds. -
Our MIPS processor is a 32-bit CPU, but
lseek()takes and returns a 64-bit offset value. Thus,lseek()takes a 32-bit file handle (arg0), a 64-bit offset (arg1), a 32-bit whence (arg3), and needs to return a 64-bit offset value. Invoid syscall(struct trapframe *tf), where will you find each of the three arguments (in which registers) and how will you return the 64-bit offset? (Hint: Readsyscalls-mips.Sandsyscall.c.)The arguments for
lseek()are passed as follows:arg0(32-bit file handle): Register a0arg1(64-bit offset):- Lower 32 bits: Register a2
- Upper 32 bits: Register a3
arg3(32-bit whence): On the stack (since a0-a3 are used)- specifically, it is located at
tf->tf_sp + 16, which should be cast to auserptr_tand then used withcopyinto read the value.
- specifically, it is located at
To return the 64-bit offset:
- Lower 32 bits: Register v0
- Upper 32 bits: Register v1
This arrangement ensures that 64-bit values are aligned on 64-bit boundaries (pairs of registers), which is a requirement of the MIPS ABI.
(Note:
kern/include/endian.hdefines to handy functions,join32to64andsplit64to32, that can be used to combine and split 32-bit values into a 64-bit value. You might want to use them in your implementation. For a clean solution, it may be helpful to add aint64 retval64;general variable to your syscall function to hold 64-bit return values, and a boolean, defaulting to false, to indicate if the return value is 64-bit, and some logic at the end of the function to decide whether to return a 32-bit or 64-bit value.)
D: Process Support
-
struct threadincludest_proc, a pointer to astruct proc. What code inthread.cuses or manipulatest_proc, to do what?In
thread.c,t_procis used in several functions. The most relevant to us isint thread_fork(const char *name, struct proc *proc, void (*func)(void *, unsigned long), void *data1, unsigned long data2);where we can supply the desired process for the new thread to be associated with, which is done by calling
proc_addthread()to add the new thread to the process's thread list.(Note: If we don't supply a process, the new thread will be associated with the current process, but that is less relevant to us in the current assignment, as all new threads we create will be associated with new processes we've just created, because we aren't supporting multi-threaded processes. The only “process” that has multiple threads is the kernel process, which is a special case in several ways, and posibly shouldn't be called a process at all.)
In addition,
t_procis used in following places:thread_bootstrap(): Sets the kernel main thread'st_procto the current process (likely the kernel process).thread_create(): Initializest_proctoNULL. (Note, BTW, that confusinglythread_createdoes not create a running thread; it only initializes a newstruct thread.)thread_exit(): Callsproc_remthread()to disassociate the thread from its process.thread_detach()andthread_destroy(): Asserts thatt_procisNULL, ensuring the thread is not associated with any process when detached or destroyed.cpu_create()also callsproc_addthread(), but this is largely irrelevant to supporting user processes.
These uses of
t_prochelp maintain the relationship between threads and their associated processes, ensuring proper process management and preventing errors like destroying threads that are still linked to processes. -
The
enter_forked_processfunction exists to “return” from thefork()system call in a newly created child process. Why is return in quotes? Where will the trapframe it needs come from? What's the missing code needed in this function so that in the child process, the return value offork()is 0?“Return” is in quotes because
enter_forked_processdoesn't actually return in the traditional sense. The child process is brand new, so technically it is starting up, not returning. However, because it has complete copy of its parent's memory, because the parent just called the kernel with a system call, the memory state of the child looks the same way. So, the child process is in the same state as the parent was when it calledfork(), and expecting to be returned to.So,
enter_forked_processneeds to deal with user-mode code that is expecting to return from thefork()system call.The trapframe needed by
enter_forked_processcomes from:- The parent process's trapframe, which needs to copied during the
fork()operation. - This copied trapframe is then modified to set up the child process correctly.
To ensure that the child process's
fork()call returns 0, the following code needs to be added toenter_forked_process:// Set return value to 0 for the child process tf->tf_v0 = 0; // Indicate no error tf->tf_a3 = 0;This code modifies the copied trapframe to set the return value (in
v0) to 0 and indicates no error (ina3), mimicking a successful return fromfork()in the child process.(Note: Copying the trapframe is absolutely necessary. The original trapframe from the parent process has vanished from the kernel stack by the time we're in the child process, so we can't just use that one. We need to use
kmallocand make a copy before we create the child thread, and then free it in the startup code for the child process, making yet another copy, back on the kernel stack, that we can use forenter_forked_process.) - The parent process's trapframe, which needs to copied during the
-
What does
kill_curthreaddo right now? What will it need to do when process support is implemented?[Opps, this is a bit of a repeat of question 4 from part C. Sorry about that!]
Currently,
kill_curthread:- Sets a signal code based on the type of exception that occurred.
- Prints an error message with details about the fault.
- Calls
panic(), which halts the entire system.
When process support is implemented,
kill_curthreadshould terminate current process. It should note the correct crashed-due-to-signal code in the process's state, and then schedule another thread to run. This allows the system to continue running even if one thread encounters a fatal error. -
There isn't currently an analogous
proc_exitto matchthread_exit. If there were, which one should call the other (and be called by yourexitsystem call)?If a
proc_exitfunction were to be implemented:- The
exitsystem call should callproc_exit. proc_exitshould clean up any resources associated with the process, particularly closing any open files, record is exit status, and then callthread_exitto destroy the current thread.
(Note: It might be more complex if processes had multiple threads.
thread_exit()takes no arguments and exits the current thread and does not return. You cannot call it in a loop! There is no mechanism to kill other threads in OS/161 right now. If we had multiple threads per process, we'd need to add that mechanism, perhaps a newthread_terminatefunction that took a thread as an argument. Then we'd callthread_terminatefor each additional thread in the process inproc_exit. Writingthread_terminatecorrectly would likely be tricky. Be glad you don't have to do this! If you thought we could have multi-threaded processes, that's understandable, and full credit, but understand that we're not doing that right now in OS/161. However, if you explicitly spoke of callingthread_exitin a loop, we can't give you full credit, because that shows a misunderstanding of howthread_exitworks.) - The
(When logged in, completion status appears here.)