# Shell Lab
- [Shell Lab](#shell-lab)
- [Introduction](#introduction)
- [How to launch(Using docker)](#how-to-launchusing-docker)
- [How to validate](#how-to-validate)
- [Trace 01](#trace-01)
- [Trace 02](#trace-02)
- [Trace 03](#trace-03)
- [Trace 04](#trace-04)
- [Trace 05](#trace-05)
- [Trace 06](#trace-06)
- [Trace 07](#trace-07)
- [Trace 08](#trace-08)
- [Trace 09](#trace-09)
- [Trace 10](#trace-10)
- [Trace 11](#trace-11)
- [Trace 12](#trace-12)
- [Trace 13](#trace-13)
- [Trace 14](#trace-14)
- [Trace 15](#trace-15)
- [Trace16](#trace16)
- [Review](#review)
- [Appendix](#appendix)
## Introduction
In this lab, we'll become more familiar with the concepts of process control and signal by writing a simple Unix shell program that supports job control.
## How to launch(Using docker)
Source from [Yansongsongsong](https://github.com/Yansongsongsong/CSAPP-Experiments)
Firstly using a docker:
`docker run -d -p 9912:22 --name shelllab yansongsongsong/csapp:shelllab`
Then using vscode plugin **remote ssh**
`ssh
[email protected] -p 9912`
password: `THEPASSWORDYOUCREATED`
## How to validate
`./sdriver.pl -t trace01.txt -s ./tsh -a "-p"` or `make test01`
You can compare the result with `tshref`
`./sdriver.pl -t trace01.txt -s ./tshref -a "-p"` or `make rtest01`
## Trace 01
To shutdown the program when meeting `EOF` (`ctrl-D` in linux)
It's developed already.
## Trace 02
To shutdown the program using `quit` command. Only need to update `eval` function here.
```c
void eval(char *cmdline)
{
char * argv[MAXARGS]; /* args list */
parseline(cmdline, argv); /* parse command line to argv */
if (argv[0] == NULL) return; /* ignore empty command */
if (!strcmp(argv[0], "quit")) {
exit(0); /* exit the shell */
}
}
```
## Trace 03
Here we need to call `/bin/echo` binary and output `"tsh> quit"`, then `quit`
This can be realized by using `execve`
```c
void eval(char *cmdline) {
char *argv[MAXARGS]; /* args list */
parseline(cmdline, argv); /* parse command line to argv */
pid_t pid;
if (argv[0] == NULL) return; /* ignore empty command */
if (!builtin_cmd(argv)) {
/* not a builtin command */
if ((pid = fork()) == 0) {
/* sub process execute here */
if (execve(argv[0], argv, environ) == -1) {
/* not found */
printf("%s: command not found \n", argv[0]);
exit(0);
}
}
}
}
```
Note that we've move the `quit` function to `builtin_cmd`
```c
int builtin_cmd(char **argv) {
if (!strcmp(argv[0], "quit")) {
exit(0); /* exit the shell */
}
return 0; /* not a builtin command */
}
```
## Trace 04
Here we need to support background job: `./myspin 1 &`
So we should use `addjob()`, `deletejob()` to manage jobs. And parse `&` by using `parseline()` to distinguish foreground job and background job.
What's more, we need to realize `sigchld_handler` to reap zombie subprocess.
```c
void eval(char *cmdline) {
char *argv[MAXARGS];
int bg; /* whether runs in background */
pid_t pid;
bg = parseline(cmdline, argv); /* parse command line to argv */
if (argv[0] == NULL) return; /* ignore empty command */
if (!builtin_cmd(argv)) {
if ((pid = fork()) == 0) {
if (execve(argv[0], argv, environ) == -1) {
printf("%s: command not found \n", argv[0]);
exit(0);
}
} else {
/* parrent executes here */
addjob(jobs, pid, bg == 1 ? BG : FG, cmdline);
if (!bg) {
waitfg(pid); /* wait foreground job */
} else {
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}
}
}
void waitfg(pid_t pid) {
while (pid == fgpid(jobs)) {
sleep(0);
}
}
void sigchld_handler(int sig) {
pid_t pid;
/* pid_t waitpid(pid_t pid, int *status, int options);
pid == -1 means waiting for arbitrary sub process
WHOHANG means that if no process exits, return 0 */
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
deletejob(jobs, pid);
}
}
```
Looks all good, right? **However, `fork()` may execute so fast that it exits before parent calls `addjob`, which can cause trouble.** The parent may think that the subprocess is still running and keep waiting for it, while the subprocess has already finished.
So we should block `SIGCHLD` signal before parent calls `addjob` to avoid this problem.
```c
void eval(char *cmdline) {
// ...
sigset_t sig_mask_child, oldset; /* signal set, unsigned long actually */
if (!builtin_cmd(argv)) {
sigemptyset(&sig_mask_child); /* set sigset all zero*/
sigaddset(&sig_mask_child, SIGCHLD); /* add SIGCHLD to sig set*/
/* block signal SIGCHLD */
sigprocmask(SIG_BLOCK, &sig_mask_child, &oldset);
if ((pid = fork()) == 0) {
/* recover from blocking signal for child, we should do this because
children inherit the blocked vectors of their parents */
sigprocmask(SIG_SETMASK, &oldset, NULL);
// execve()...
} else {
/* parrent executes here */
addjob(jobs, pid, bg == 1 ? BG : FG, cmdline);
/* recover from blocking signal for parent */
sigprocmask(SIG_SETMASK, &oldset, NULL);
// ...
}
}
}
```
## Trace 05
Trace 5 is quite simple here, the only thing we need to do is to implment the `jobs` command.
```c
int builtin_cmd(char **argv) {
// ...
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs); /* print jobs */
exit(0);
}
return 0; /* not a builtin command */
}
```
## Trace 06
We need to realize `sigint_handler` here.
The easiest way is like this:
```c
void sigint_handler(int sig) {
/* find foreground pid */
pid_t pid;
if ((pid = fgpid(jobs)) > 0) {
printf("Job [%d] (%d) terminated by signal 2\n", pid2jid(pid), pid);
kill(pid, sig);
deletejob(jobs, pid);
}
}
```
Note: we only kill the foreground process using `kill(pid, sig)` here, but not for the whole **group** by using `-pid` in `kill` function. This is an issue and we'll solve it later.
## Trace 07
We should forward `SIGINT` only to foreground job.
As we use `kill(pid, sig)` above instead of using `kill(-pid, sig)`, we can still pass this trace.
This is an issue since the subprocesses of foreground job are not cared, we'll solve it later.
## Trace 08
We should forward SIGTSTP only to foreground job here.
If we realize this like:
```c
void sigtstp_handler(int sig) {
pid_t pid;
if ((pid = fgpid(jobs)) > 0) {
printf("Job [%d] (%d) stopped by signal 20\n", pid2jid(pid), pid);
kill(pid, sig);
struct job_t * job = getjobpid(jobs, pid);
job->state = ST;
}
}
```
we'll find that the process is hanging, traping into a deadlock. Let's figure out what's happening
```bash
ps ajf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
131 8983 8983 8983 pts/11 9748 Ss 0 0:00 /bin/zsh -i
8983 9748 9748 8983 pts/11 9748 R+ 0 0:00 \_ ps ajf
131 679 679 679 pts/9 9666 Ss 0 0:01 /bin/zsh -i
679 9666 9666 679 pts/9 9666 S+ 0 0:00 \_ make test08
9666 9667 9666 679 pts/9 9666 S+ 0 0:00 \_ /usr/bin/perl ./sdriver.pl -t trace08.
9667 9668 9666 679 pts/9 9666 R+ 0 0:02 \_ ./tsh -p
9668 9672 9666 679 pts/9 9666 T+ 0 0:00 \_ ./myspin 5
```
We can find that only `pid=9672` is suspended, while other processes are still running! And since we are `waitfg(pid /* 9672 */);`, here we are trapped.
The solution of this problem is by using `kill(-pid, sig)` to send signal to the process **group**.
However, we should forward SIGTSTP **only** to foreground job. So we need to make a change of our code, furthermore, using `setpgid()` to put subprocess into a new process group.
Here is the code:
```c
void eval(char *cmdline) {
// ...
if (!builtin_cmd(argv)) {
// ...
if ((pid = fork()) == 0) {
/* set subprocess into a new process gro