xv6 Fork Bomb
In this lab, you will create a fork bomb in xv6. Recall that a shell (like bash on Linux) is a user program that reads commands entered by you (e.g., ls -l), parses that command text, and use system calls (fork, exec, wait) to execute the commands.

- Foreground (e.g.,
sleep 10): The shell runs the command and waits for the command to finish. It callswait()and blocks untilsleepfinishes before it gives you a new$prompt. - Background (e.g.,
sleep 10 &): The&tells the shell: “Run this command, but do not wait for it. Give me a new$prompt immediately.”
The current xv6 shell We need a system call that lets the shell ask the kernel, “Hey, do I have any finished (zombie) children right now?” without being forced to wait. You will implement Now that we have This lets the user ask the shell, “What background processes are you currently managing?” You will implement xv6’s shell lacks the ability to run shell script. Modify You will create a script that calls itself in the background, over and over, recursively. This will create new processes exponentially until the kernel has no more process slots and panics. Please fork or clone the starter code from Github: Please DO NOT continue developing on the previous project, xv6-lab-syscall-tracing. You must clone or fork the new repository “xv6-lab-forkbomb” specifically for this lab, otherwise you will be missing the necessary grading programs and script files. Read this article. Type Read this article. Before your shell can monitor background job termination, you need a new system call to check for exited child processes without blocking. Add a new system call First, modify In In the above code, we loop through all processes and check if they have entered zombie state (the child has ended but the parent doesn’t call In Then, register the syscall in Think About It: Can you trace and explain the complete life cycle (or ‘journey’) of a system call, starting from the moment it’s triggered in a user program, to the moment the user process gets the return value? xv6 already parses commands ending with When a background job is run, print BEFORE printing a new command prompt Clarification for “AFTER inputting a command”: immediately after getcmd() returns Clarification for “BEFORE printing a new command prompt Example Implementation for polling Create a file Add new path Make sure you can You’ll notice that in xv6, The followings are examples about how your xv6 should behave and how your xv6 should not behave. ✔️ CASE 1: Run three background job ✔️ Run three background job This undesired output is caused by xv6’s original Even though you only want one background process, this default behavior creates two new processes. Since xv6’s Please change ✔️ CASE 2: After the user has entered a second command, the shell calls ✔️ CASE 3: After the user has entered the fourth command, the shell calls ✔️ CASE 4: The user execute two commands connected with a pipe as background process. Print the pid of the first command in the pipeline. ❌ Print a new command prompt This is likely due to incorrect control flow within We use this command to test if you print the Think about it: Printing only one ✔️ CASE 5: You execute an non-existing binary (e.g., “asdfgh”) as background job, please still print its PID and exit status. (For the sake of simplicity, it’s ok in this lab that you don’t return exit status 127 as command not found) ❌ “asdfgh” does not exist but you execute it, but you don’t find it as a exited background job and you don’t print it: ❌ Even though This is the hardest test case in the entire lab. If you get stuck on it for too long, we recommend you finish implementing the other features first and then come back to this one later. ✔️ CASE 6: If you run a foreground process before background jobs exit, then after that foreground process finishes (and before the $ prompt is displayed), the exit status of the background jobs should be displayed. ❌ We expect reaping background jobs with ❌ We don’t see any Sometimes, or in this case, you may find that you are not reaping background processes but instead the foreground process as a zombie. Why? What happened to background processes? WHEN and WHERE do background processes being reaped? Can you modify Make good use of debugger to carefully inspect and learn how Be careful with the process you are currently in if you don’t get any result from polling If you still can’t figure out how to correctly polling Which process becomes orphan in Can you tell about why orphaned processes may exist? How can you modify Now we will add a new command In the example above, Please add the following code to Add You should track background PIDs in a fixed-size array. Remember to remove already exited background jobs from DO NOT print anything if no background job is running. For the sake of simplicity, you don’t have to parse To support Parse the Create a fixed-size array Before executing background jobs, add the new child PID into When Here comes the most exciting part: We will add support to Let’s look at the example about how to run a shell script file in xv6: We have a file In xv6, we In Bash or other shells, if you run the exact same shell script, you will not get the exact same result as above, so please don’t refer to or rely on the output of other shells too much in Step 4. First, copy and paste the following script into In to Now, it’s your turn to implement remaining features of executing shell scripts. We will describe in detail what you need to do in Requirements section. In If you don’t know how to open a file, looking around and investigate how other user programs achieve that. When opening a non-existent file (more precisely, if your program can’t open a file), please print Example Output Read and parse each line as a shell command. Execute them in the same logic as interactive input. For the sake of simplicity, you don’t have to parse You might run into this problem if you are developing on Windows. This is because Windows handles line endings (newlines) differently. Please change every Congratulations on completing all the features! With all the features you extend, we can finally do something crazy to xv6 now! In from to Create a file Copy and paste the following script into Don’t forget to add new paths Run Think About It: A fork bomb will drain all the CPU on an unprotected OS like xv6, but modern OS like Linux has protection to prevent bad program (like the fork bomb) from taking all the resources. What are the mechanisms in Linux that enforce this CPU resource management? In the The question is: Why must we use 1. Please answer this question from the perspective of Virtual Memory. 2. Please answer this question from the perspective of Protection / Security. In the first example under Step 2: More Examples, we asked you to change the original xv6 behavior where running a background job creates TWO processes, so that it only creates ONE process now. 3. Please briefly explain how you changed the control flow in In Step 3, we gave this specific hint: Is there any additional char at the tail of jobs strings when you type jobs and press ENTER in shell? 4. Please briefly explain why parsing the user/sh.c partially supports background job. It will run the command and give you a new prompt. But it reaps the background job. When the job finishes, it becomes a “zombie”. We can’t just “fix” this by having the shell call wait(). The normal wait() system call is blocking. If the shell called wait(), it would freeze until the background job finished. Let’s add a new system call to fix this behavior.Overview
Step 1: Add Non-blocking Wait
wait_noblock(). This new system call will check for zombie children:Step 2: Support Background Execution
wait_noblock(), let’s implement our background job processing as follows.fork()s twice to execute a command. You will modify this behavior to fork only once.[pid].$ prompt, it will call your new wait_noblock() to reap background jobs that have finished.Step 3: Add
jobs Commandjobs as a “built-in” command (part of the shell itself, like cd).wait_noblock(), you remove its PID from the array.jobs, you just print the contents of that array.Step 4: Shell Script Execution
user/sh.c so that: If the shell is run with no arguments ($ sh), it reads commands from the keyboard (interactive mode). If it’s run with an argument ($ sh script.sh), it will open that file and read/execute commands from it.Step 5: Fork Bomb Script
Implementation
Build and run xv6
prompt> make qemuHow to quit qemu
C-a x (that is, hold down control and while doing so, press a, then let go of control, then press x). When you see (qemu), type quit to quit qemu.Step 1: Add Non-blocking Wait (Warm-up)
Requirements
uint64 sys_wait_noblock(void):*exit_status with exit status of zombie childHow
kernel/proc.c and kernel/sysproc.c:proc.c
kernel/proc.c, here’s an example implementation of non-blocking wait. You can directly copy and paste this entire code block into a suitable location:int wait_noblock(uint64 exit_status) {
// the current process, which is the shell
struct proc *p = myproc();
// loop through all processes
for(struct proc *pp = proc; pp < &proc[NPROC]; pp++){
if(pp->parent == p && pp->state == ZOMBIE){
int pid = pp->pid;
// Get the child's xstate (exit code) to user space:
if(copyout(p->pagetable, exit_status, (char *)&pp->xstate, sizeof(int)) < 0)
return -1;
// Free the process entry from the proc table
freeproc(pp);
return pid;
}
}
return 0; // no zombie child
}wait() to get its exit status).sysproc.c
kernel/sysproc.c, here’s an example implementation of system call for non-blocking wait. You can directly copy and paste this entire code block into a suitable location:uint64
sys_wait_noblock(void)
{
uint64 exit_status;
argaddr(0, &exit_status);
return wait_noblock(exit_status);
}kernel/syscall.h, kernel/syscall.c, kernel/defs.h, user/usys.pl and user/user.h. (Try it yourself)Step 2: Support Background Execution
& and runs them in the background (see BACK command type in user/sh.c). However, it does not print the child PID, does not track or clean up exited background jobs.Requirements
[pid] of the child in a new line and print a new command prompt $.$ and AFTER inputting a command, poll wait_noblock() to check if there is any exited background job. If there is, print it:[bg pid] exited with status X$”: immediately before printing a new command prompt $wait_noblock():int status;
int pid;
while ((pid = wait_noblock(&status)) > 0)
printf("[bg %d] exited with status %d\n", pid, status);Create a user program
sleepuser/sleep.c:#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char* argv[])
{
if(argc != 2)
{
printf("no argument or too many arguments.\n");
exit(1);
}
sleep(atoi(argv[1]));
exit(0);
}$U/_sleep\ to UPROGS in Makefile.$ sleep 20 without running into error messages exec sleep failed in xv6.$ sleep 50 doesn’t actually sleep for 50 seconds, but approximately 5 seconds. This isn’t an anomaly. you can investigate the specific reason and mechanism for this behavior.Expected Behavior
sleep, pid increases by one:$ sleep 1000 &
[3]
$ sleep 1000 &
[4]
$ sleep 1000 &
[5]
$sleep, pid increases by two:$ sleep 1000 &
[4]
$ sleep 1000 &
[6]
$ sleep 1000 &
[8]
$ user/sh.c implementation, which fork()s twice for a single command.forks() once to create a child process.forks() again to create a grandchild process that actually exec()s the sleep command.allocpid function increments the PID for each new process (nextpid = nextpid + 1) and does not reuse PIDs, each background command consumes two PIDs. This is why you see the job IDs jumping by two (e.g., [4], [6], [8]).user/sh.c so that starting a background job creates only one process, not two.wait_noblock() and finds that pid=3 is finished. It printed [bg 3] exited with status 0 before executing the second command.$ sleep 10 &
[3]
$ sleep 10 &
[bg 3] exited with status 0
[4]
$ sleep 10 &
[bg 4] exited with status 0
[5]
$ wait_noblock() and finds that all three previous jobs have finished. It printed [bg 3] exited with status 0, [bg 4] exited with status 0, [bg 5] exited with status 0 before executing the fourth command.$ sleep 50 &
[3]
$ sleep 50 &
[4]
$ sleep 50 &
[5]
$ sleep 10
[bg 3] exited with status 0
[bg 4] exited with status 0
[bg 5] exited with status 0
$ $ ls README | cat &
[3]
README 2 2 2292
[bg 3] exited with status 0
$$ more than once instead of EXACTLY ONCE:$ sleep 10 | sleep 10 &
[3]
$ $ $main(). You can debug this by finding out when and where the $ prompt is being printed.$ prompt only once. If this command looks strange to you, just imagine it is running two long-running programs in background.[pid] for this command is the correct behavior. But why is only one [pid] printed? Does this [pid] represent either one of the sleep processes in the pipe?”$ asdfgh &
[3]
exec asdfgh failed
[bg 3] exited with status 0
$$ asdfgh &
[3]
$ exec asdfgh failed
$[bg 3] exited with status 0 printed successfully, the exec asdfgh failed message appeared after the $ prompt, and you still have to press ENTER again just to get a new $ prompt.$ asdfgh &
[3]
$ exec asdfgh failed
[bg 3] exited with status 0
$xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$ sleep 50 &
[3]
$ sleep 50
[bg 3] exited with status 0
$PID == 3 but instead we end up reaping foreground process PID == 4:xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$ sleep 50 &
[3]
$ sleep 50
$
[bg 4] exited with status 0[bg 3] exited with status 0, but it should be there:xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$ sleep 50 &
[3]
$ sleep 50
$
$user/sh.c to properly wait for the foreground process?A hint for one of the possible solutions
jobs[NPROC] in Step 3 will provide you information about who is background process and who is NOT.Hints
sh works when you are executing foreground or background commands (for example, $ sleep 60 and $ sleep 60 &).wait_noblock().wait_noblock(), observe that:sh?sh.c to avoid orphaned processes?Step 3: Add
jobs Commandjobs to show all currently running background jobs!$ sleep 50 &
[3]
$ sleep 50 &
[4]
$ jobs
3
4
$ sleep 50 &
[5]jobs command prints all currently running background jobs’ PID.Prerequisites
user/sh.c first:#include "kernel/param.h"Requirements
jobs command that show all currently running background jobs.jobs command is not a system call nor a standalone user program. It’s a shell built-in command. You should implement all the functionality of jobs command ONLY within user/sh.c.jobs[NPROC]. Do not print any background PID which was already reaped.jobs & command in this lab. It’s ok to get exec jobs failed if you input jobs & command.How
jobs command that prints all live (not-yet-reaped) background PIDs, you should modify sh.c:jobs command.jobs[NPROC] as global variable. (NPROC has been defined in kernel/param.h so you don’t have to declare it yourself).jobs[NPROC].jobs command is called, print all PID of background jobs currently running.Hints
jobs command, observe how cd command was parsed in user/sh.c. BUT for jobs command, things are a little different. (Is there any additional char at the tail of jobs strings when you type jobs and press ENTER in shell?)Step 4: Shell Script Execution
user/sh.c so it can run a simple shell script file😁Demonstration
user/script.sh:echo hello
sleep 40 &
sleep 30 &
jobs
sleep 50
echo Im awake
sleep 60 &
jobs
echo done$ sh script.sh to run shell script file script.sh, and we get the following result:xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$ sh script.sh
hello
[5]
[6]
5
6
[bg 6] exited with status 0
[bg 5] exited with status 0
Im awake
[9]
9
done
$How
user/script.sh:echo hello
sleep 2 &
echo donexv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$ sh script.sh
hello
[5]
done
$user/sh.c, modifyint
main(void)
{
// some code here...
}int
main(int argc, char* argv[])
{
// some code here...
}Requirements
main() of user/sh.c, open argv[1] as input if argc > 1.sh: cannot open xxx.sh.$ sh asdfgh.sh
sh: cannot open asdfgh.sh
$$ ./script.sh in this lab. It’s ok to get exec ./script.sh failed if you input $ ./script.sh only instead of $ sh script.sh..sh file from CRLF mode to LF mode. If you are using VSCode, you can switch the file from CRLF to LF in the bottom-right corner of the editor.Step 5: Fork Bomb Script
Prerequisites
kernel/param.h, please change the value of NOFILE from 16 to 24:#define NOFILE 16 // open files per process#define NOFILE 24 // open files per processuser/dummy.c:#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char* argv[])
{
while(1);
}user/bomb.sh:./dummy &
./dummy &
sh /bomb.sh$U/_dummy\ to UPROGS in Makefile.💣 Let’s Go… 💣
$ sh bomb.sh in your xv6 and see what happen.

Run grade script
prompt> python3 grade-lab-forkbombDemo
Questions 1 and 2
int wait_noblock() implementation we provided in Step 1, there is this line:if(copyout(p->pagetable, exit_status, (char *)&pp->xstate, sizeof(int)) < 0) copyout? Why can’t we just use the = (equals) sign to assign the value of xstate to exit_status?Questions 3
user/sh.c to achieve this.Questions 4
jobs command is slightly different from the cd command.
