Repo: ovfor4/tiny-shell
An advanced version ovfor4/ccshell
Not a good idea to paste every line of code on the website if you have a github account
Some useful notes here, not cheating:
pid_t getpid(void);
returns PID of current process pid_t getppid(void);
returns PID of parent process void exit(int status);
terminates with status code
called once, never returns pid_t fork(void);
returns 0 to child process, and child PID to parent process
called once, returns twice pid_t wait(int *child_status);
suspends current process until the one of child processes finishes
returns child PID, and set child_status if (child_status != NULL) pid_t waitpid(pid_t pid, int *status, int options);
when pid is -1, it means any child processes
WNOHANG return immediately if no child has exited
when waitpid() returns negative value, error occurs
VERY IMPORTANT, CHECK TEXTBOOK int execve(char *filename, char *argv[], char *envp[]);
keep PID, opened files …
called once, never returns (except returns -1 when no file) %rdi (argc) the number of arguments
%rsi (argv) argv[0] this array contains pointers
%rdx (envp) envp[0] this array contains pointers
environ (global var) same as envp[0] argv[0] is path of executable file, and argv[1…] are arguments int setpgid(pid_t pid, pid_t pgid);
returns 0 for successful operation, otherwise -1 pid_t getpgrp(void);
returns PDIG of calling process int kill(pid_t pid, int sig); typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
modifies signal action
SIG_IGN
SIG_DFL
or address of user-level signal handler function
you can use the following if you don’t understand the gibberish
void mySignalHandler(int sig) { … } these things configure a set
sigset_t set;
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum); and this applies the set to real env
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK async-signal-safe: everything is local so these things aren’t changed by another code, or non-interruptable int sigsuspend(const sigset_t *mask);Notes
Intro
So relatively speaking it’s a simple lab, and I only spent 3 days (6 hrs in total), but signals are tricky
And I’m gonna go beyond the syllabus, code more stuff and optimize user experience
And I used C++ instead of C, but they are fairly similar, and the key of this lab is POSIX stuff, so just ignore this point
By the way, I plan to rewrite the whole stuff in C++ to reduce bad habit (eg **argv if you aren’t communicating with the system), and maybe add some features?
Utils
Easy, wrap your own write so that it can output both numbers and strings without typing strlen and STDOUT_FILENO
Handlers
At the beginning of the handler, backup errno and block other signals that can race, in this case, SIGINT SIGCHLD SIGTSTP
SIG INT/TSTP handler
Find the foreground process and send SIGINT/SIGTSTP to the whole group
SIG CHLD handler
Signals aren’t queued, and when the program receives a signal, perhaps there are many children terminated/suspended, so while is used to process all of these
And in this lab, the job status is changed for termination and suspension. WNOHANG means return immediately if there aren’t any more children to process, and WUNTRACED means consider SIGTSTP
If return value is >0, there is at least 1 child
And this expression can be obtained
((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
And then we need to determine the reason, and print corresponding message. Just check the code if interested
These might be useful:
WIFSTOPPED(status)WTERMSIG(status)WSTOPSIG(status)WTERMSIG(status)
waitfg
This one is VERY tricky
SIGCHLD MUST be blocked before child is forked and CANNOT be unblocked manually
Before waiting for SIGCHLD, we need to unblock other signals. Otherwise, for example, SIGINT is blocked and thus the child cannot receive, and thus cannot terminate, but waitfg is still waiting for SIGCHLD, which can never happen
int sigsuspend(const sigset_t *__set) changes the block set to set, waits for signal, and restores to the previous set before this instruction runs. And it’s noticeable that it’s atomic, so it’s impossible that SIGCHLD is unblocked and received before we are ready to catch this change (that’s why we can’t unblock manually, otherwise there can be a gap)
do_bgfg
First, do some validation to make sure the input is legal
And get both job pointer and pid, so we can do something easily afterward
fg
- change status to
FG - block
SIGCHLDand sendSIGCONT(sig continue) to the process GROUP waitfg
bg
Same, but without waitfg
eval
int parseline(const char *cmdline, char **argv) requires argv[][], we need to convert cmdline[] to argv[][] format first, and check whether it’s foreground or background command
And test whether it’s built-in command or not
And block all signals to prepare to fork child
Remember, fork() returns twice, 0 to the child, and the pid of child to the parent
Inside the child, signals must be unblocked otherwise it will be blocked even after execve (bad, the program inside doesn’t know all signals are blocked LOL)
Inside the parent, it does different actions based on whether it’s foreground or background, and this is similar to do_bgfg. just check that chapter
And that’s it! It’s working
Issues in “official” solution
It’s official perhaps, because tshref.c was decompiled from the binary and I hope it’s correct (you just ask claude or what)
But there are some problems
[212]
sigprocmask(SIG_UNBLOCK, &mask, NULL);is before [217]waitfg(pid);so how can you make sure SIGCHLD is not received in this gap? The “solution” issleep(1)and accessjob[]every time! (sigsuspend is better, because it atomically switch to the set and wait for signals, preventing any gaps)And
errnoisn’t saved in another variable and restored before returing, and it’s a good habit to always do soAnd
printfare used everywhere even inside signal handler, and we know signal handler should use async-safe functions (man 7 signal-safety), sowriteshould be used instead (that’s why some people feel really uncomfortable when doing signal handlers, because you want the main thread to check something and output, and really, outputing in handler isn’t good anyway, just simple, becausewriteitself is simple and ignores buffer and lock and other meaningful stuff) 👉 TIP: if you are using C++,...arguments can be implemented easilyAnd [23]
#define MAXJID 1<<16is not safe (macro trick). Considercout << MAXJID;? It would becout << 1 << 16;(ie116). And thisMAXJIDonly appears once in the file, perhaps mistakeAnd autograder doesn’t work (see my environment later, but anyway it’s 2003 work)
And even more? Perhaps
Linux version 6.12.95+deb13-amd64 ([email protected]) (x86_64-linux-gnu-gcc-14 (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44) #1 SMP PREEMPT_DYNAMIC Debian 6.12.95-1 (2026-07-04) This is perl 5, version 40, subversion 1 (v5.40.1) built for x86_64-linux-gnu-thread-multi
(with 48 registered patches, see perl -V for more detail)Env