CS3214 Fall 2023 Project 1 –“Customizable Shell”Due Date: See website for due date (Late days may be used.)This project must be done in groups of 2 students. Self-selected groups must have regis-tered using the grouper app (URL). Otherwise, a partner will beassigned to you.
1 Introduction
This assignment introduces you to the principles of processmanagement and job control in a Unix-like operating system. In thisproject, you will develop a simple job control shell.This is an open-endedassignment. In addition to implementing the required functional- ity, we encourage you to define the scope of this project yourself.
2 Base Functionality
A shell receives line-by-line input from a terminal that represents user commands. Some user commands are builtins, which are implemented by the shell itself. If the user inputs the name of such a built-in command, the shell will execute this command. Otherwise, the shell will interpret the input as containing the name of an external program to be executed, along with arguments that should be passed to it. In this case, the shell will fork a new child process and execute the program in the context of the child. Normally, the shell will wait for a command to complete before reading the next command from the user. However, if the user appends an ampersand‘&’to a command, the command is started and the shell will return to the prompt immediately. In this case, we refer to the running command as a“background job,”whereas commands the shell waits for before processing new input are called“foreground jobs.”The shell provides job control. A user may interrupt foreground jobs, send foreground jobs into the background, and vice versa. Thus at a given point in time, a shell may run zero or more background jobs and zero or one foreground jobs. If there is a foreground job, the shell waits for it to complete before printing another prompt and reading the nextcommand. In addition, the shell informs the user about status changes of the jobs it manages. For instance, jobs may exit, or terminate due to a signal, or be stopped for several reasons.At a minimum, weexpect that your shell has the ability to start foreground and back- ground jobs and implements the built-in commands‘jobs,’‘fg,’‘bg,’‘kill,’’exit,’and‘stop.’The semantics of these commands should match the semantics of the same-named commands in bash. The ability to correctly respond to ˆC (SIGINT) and ˆZ (SIGTSTP) is expected, as are informative messages about the status of the children managed. Like bash, you should use consecutively numbered small integers to enumerate your jobs.For the minimum functionality, the shell need not support pipes (|), I/O redirection (< > >>), nor the ability to run programs that require exclusive access to the terminal (e.g., vim).Created by G. Back (gback@cs.vt.edu) 1 September 6, 2023
CS3214 Fall 2023 Project 1 –“Customizable Shell”
We expect most students to implement pipes, I/O redirection, and managing the con- trolling terminal to ensure that jobs that require exclusive access to the terminal obtain such access (see Section 3.3). Beyond that, cush’s customizability, described in Section 5, should allow for plenty of creative freedom.
3 Strategy
3.1 Handling SIGCHLD To Process Status Changes
At a given point in time, a user may have multiple jobs running, each executing arbitrary programs chosen by the user. Because the shell cannot and does not know what these programs do, it has to rely on a notification facility from the OS to be informed when these jobs encounter events the shell needs to know about. We refer to such events as“changing status,”where“status”means whether the job is running1, has been stopped, has exited, or has been terminated with a signal (for instance, crashed).
This notification facility involves a protocol in which the OS kernel sends an asynchronous signal (SIGCHLD) to the shell, and in which the shell then follows up by executing a sys- tem call (a variant of wait(), specifically waitpid(), as shown in the provided starter code).2 3
Thus, you will need to catch the SIGCHLD signal to learn about when the shell’s child processes change status. Since child processes execute concurrently with respect to the parent shell, and since the shell has no knowledge of what these processes are doing, it is impossible to predict when a child will exit (or terminate with a signal), and thus it is impossible to predict when this signal will arrive. In the worst case, a child may have already terminated by the time the parent returns from fork()! You also should not make any assumptions about how a child process might change state: for instance, even if the user issues a kill built-in command to terminate a process, the processes might not immediately terminate (or may not terminate at all), so the shell should not assume that a status change occurred unless and until it has first-hand information from the OS that it did.Because of the asynchronous nature of signal delivery, you will need to block handling of the signal in those sections of your code where you access data structures that are also needed by the handler that is executed when this signal arrives. For example, consider the data structure used to maintain the current set of jobs. A new job is added after a child process has been forked; a job may need to be removed when SIGCHLD is received. To avoid asituation where the job has not yet been added when SIGCHLD arrives, or -1We use the word“running”here not in the sense of thesimplified process state diagram, but rather in the informal sense of having been started, but not having finished, and also not currentlysuspended (stopped) by the user or system.2Such protocols are widely used in systems programming – for instance, an operating system kernel interacts with devices in a very similar way through interrupts.3So far, we have equated jobs and child processes in our discussion. Jobs that include multiple child processes will bediscussed in Section 3.2. worse – a situation in which SIGCHLD arrives while the shell is adding the job, the parent should block SIGCHLD until after it completed adding the job to the list. If the SIGCHLD signal is delivered to the shell while the shell blocks this signal, it is marked pending and will be received as soon as the shell unblocks this signal.
Use the provided helper functions in signal support.c to block and unblock sig- nals, which in turn rely on sigprocmask(2). To set up signal handlers, they use the sigaction(2) system call with sa flags set to SA RESTART. The mask of blocked signals is inherited when fork() is called. Consequently, the child will need to unblock any signals the parent had blocked before calling exec().
3.2 Process Groups
User jobs may involve multiple processes. For instance, the command line input ls | grep filename requires that the shell start two processes, one to execute the ls and the other to execute the grep command. Aside from this example, child processes that a user program may start4 should usually be part of the same job so that the user can manage them as one unit. To help manage these scenarios, Unix introduced a way to group processes that makes it simpler for the shell and for the user to address them as one unit.
Each process in Unix is part of a group. Process groups are treated as an ensemble for the purpose of signal delivery and when waiting for processes. Specifically, the kill(2), killpg(2), and waitpid(2) system calls support the naming of process groups as possible targets5. In this way, if a user wants to terminate a job, it is possible for the shell to send a termination signal to a process group that contains all processes that are part of this job. To facilitate this mechanism the shell must arrange for process groups to be created and for processes to be assigned to these groups.
Each process group has a designated leader, which is one of the processes in the group. To createanewgroupwithitselfastheleader,aprocesssimplycallssetpgid(0, 0).The process group id of a process group is equal to the process id of the leader. Child processes inherit the process group of their parent process initially. They can then form their own group if desired, or their parent process can place them into a different process group via setpgid(). The shell must create a new process group for each job and make sure that all processes that will be created for this job become members of this group. Note that while the process group management facilities are available to all user programs, only shell programs will typically make use of them – for most other programs, the default behavior of inheriting the parent’s process group is a desirable default.
In addition to signals and waitpid, process groups are used to manage access to the ter- minal, as described next.
4For instance, the‘make‘utility program starts many other processes such as compilers and linkers.
5Note the idiosynchracies of the API: kill(-pid, sig) does the same as killpg(pid, sig). You can use either, but make sure to use the correct sign corresponding to the call you use.
3.3 Managing Access To The Terminal
Running multiple processes on the same terminal creates a sharing issue: if multiple pro- cesses attempt to read from the terminal, which process should receive the input? Sim- ilarly, some programs – such as vi – output to the terminal in a way that does not allow them to share the terminal with others. 6To solve this problem, Unixintroduced the concept of a foreground process group. The kernel maintains such a group for each terminal. If a process in a process group that is not the foreground process group attempts to perform an operation that would require exclu- sive access to a terminal, it is sent a signal: SIGTTOU or SIGTTIN, depending on whether the use was for output or input. The default action taken in response to these signals is to suspend the processes in that group. If that happens, the processes’parent (i.e., your shell) can learn about this status change by calling waitpid(). WIFSTOPPED(status) will be true in this case. To allow these processes to continue, their process group must be made the foreground process group of the controlling terminal via a call to tcsetpgrp(), and then the process group must be sent a
The provided base code for the project is available on Gitlab at https://git.cs.vt.edu/cs3214- staff/cs3214-cush,
One team member should fork this repository by viewing this page and clicking the fork link. This will create a new repository for you with a copy of the contents. From there you must view your repository settings, and set the visibility level to private. On the settings page you may also invite your other team member to the project so that they can view and contribute.
Group members may then make a local copy of the repository by issuing a git clone <repository> command. The repository reference can be
4 Code Base
To build the provided code, run make in the src directory. (Don’t forget to build the posix spawn library first.)
The code contains a command line parser that implements the following grammar:
cmd_line : cmd_list
cmd_list :
| pipeline
| cmd_list’;’| cmd_list’&’| cmd_list’;’pipeline
| cmd_list’&’pipeline
pipeline : command
| pipeline’|’command
| pipeline’|&’command
command : WORD
| input
| output
| command WORD
| command input
| command output
input :’<’WORD
output :’>’WORD
|’>>’WORD
|’>&’WORD
5 Builtins
The basic builtins our tests expect include kill, fg, bg, jobs, stop, exit.In addition, you should implement at least 2 builtin commands or a functionality exten-sion, a simple one and a more complex one. Ideas for simple builtins include:A side-note on Unix philosophy – in general, Unix implements functionality using many small programs and utilities. As such, built-in commands are often only those that must be implemented within the shell, such as cd. In addition, essential commands such as’kill’are often built-in to make sure an operator can execute those commands even if no new processes can be forked. Your builtins should generally stay with this philosophy and implement only functionality that is not already available using Unix commands or that would be better implemented using separate programs. If in doubt, ask.
6 Testing
We will provide a test driver to test your project, and tests for the basic and advanced functionality. The tests are part of therepository, which may be updated once before the deadline.The basic and advanced tests are also in the Gitlab repository that you forked to start the project. If updates to the tests come out you will have to pull from the remote repository to update your local copy.Note: you are required to add tests for the builtin commands you add, using the example. 7 GradingRubrics. This project will account for 140 points. 50 points will be assigned for passing the base tests. 50 points for advanced tests, and up to 20 additional points can be earned through builtins. Builtins requires tests to be considered for credit.10 points are awarded for correct use of version control, and 10 points for documenta- tion. In addition, deductions may be taken for deficiencies in coding style and lack of robustness.Coding Style. Your coding style should match the style of the provided code. You should follow proper coding conventions with respect to documentation, naming, and scoping.You must check the return values of all system calls and library functions, with the sole exception of malloc(3) or calloc(3). (Production code would need to check for those as well; this is a simplification for this project.) This requirement includes calls such as kill(2) and close(2).You may not use unsafe string functions such as strcpy() or strcat(), see the website for a complete list.Submission. You must submit a design document, README.txt, as an UTF-