Skip to content

rvk7895/RaSH

Repository files navigation

Operating Systems and Networks Assignment 3

RaSH:

Build and Run:

The file already contains the precompiled files and you can launch the shell just by running ./cash. If however, you wish to recompile the shell, run the following commands

make
./rash

Specification Implementation Details

Specification 1: Prompt String

The prompt string is generated by the printf statement method in the main while loop. The Prompt string consists of 3 parts.

  1. The name of the user. An instance of struct passwd is populated with the current user's details using getpwuid(getuid()). The current user's name is in passwd->pw_name.
  2. The hostname of the system. Copied into a buffer using the gethostname() method.
  3. The current working directory of the shell. Copied into a buffer using the getcwd() method.

Important: If any of the above functions fail, the prompt string cannot be generated, and the shell exits abnormally due to potentially insufficient run-time permissions.

Specification 2: Builtin Commands

The following built in commands were required to be handled:

  1. echo: Since by the time echo() is called from builtin_commands.c, the string has been stripped of extra white spaces, the function just prints the command, skipping the first 5 characters ('e', 'c', 'h', 'o', ' ').
  2. pwd: Gets the present working directory of the shell using getcwd().
  3. cd: Skipping the first 3 characters of the input, ('c', 'd', ' '), leaves us with the path to cd to. If the path is absolute, or relative to the current working directory, a single chdir() suffices. However, if the path is relative to ~, we fist chdir() to ~ using the absolute path to ~ which is stored as a global variable, then we skip the first two characters of the provided path ('~', '/') and chdir() again to the rest of the path.

Specification 3: ls Command

Implemented in ls() in functions.c. Here is how it works:

  1. Assign a pointer to each argument in the command.
  2. Iterate over the pointers and identify the flags. Set l_flag and a_flag as required.

Specification 4: System commands with and without arguments

If the given command is not recognized as one of those mentioned above, or pinfo (next specification) or some others (mentioned later), it is treated as a system command. If the last character of the command string is &, the background_process() method is called to launch it as a background process, otherwise the foreground_process() method is called to launch it as a foreground process. Both background_process() and foreground_process() are defined in functions.c.

In both these functions, a pointer is assigned to each space seperated argument. If one of the arguments provided has a path realtive to ~, it is expanded into its equivalent absolute path.

We then use fork() to create a child process and check if it was successful. In the child process, if it is in background_process(), we change the child's process group using setgid(0, 0), so that it is sent to the background, and cannot read from the terminal. It can however still write to the terminal if needed.

We then use execvp() to load a new executable image onto the child process. If it is successful, the process (foreground/background) starts execution running. If not, an error message is printed regarding the given command and the process is exited from.

On the other hand, in the parent process, if it is in foreground_process(), we wait for the child to exit and then claim the resources using wait(). In background_process() however, we insert the process into the child pool (explained in specification 6) and return to the prompt.

Specification 5: pinfo

Implemented in pinfo() in functions.c. It is first checked if there is a single argument or two arguments. If it is just one, then we need to get information of the current process, whose pid is obtained using the getpid() method. Otherwise, the pid is extracted from the command provided. Then contents of /proc/<pid>/stat are read into a buffer, and each space seperated value is assigned a pointer.

  • The first pointer points to the pid, but this can also be read from the command given.
  • The third pointer points to the process status.
  • The twenty third pointer points to the memory.

We then read the link whose path is /proc/<pid>/exe to get the path to the executable and print it. If however, the mentioned process is a zombie process, this link won't exist, so we print an error message in its place.

Specification 6: Finished Background Processes

The way this shell has been implemented, sets a cap on the maximum number of child processes that it can have running simultaneously to 512. A custom structure, struct child, defined in definitions.h, stores a process pid and corresponding name. We initilize an array of 512 such stuctures and set all their pids to -1 using the init_child_list() method in functions.c, indicating that that particular process is empty. This array is referred to as the child pool.

In main(), one of the first things we do is install a signal handler using the init_child_process_handler() method defined in utilities.c, that will run each time a SIGCHLD signal is sent to the shell. This is done using an instance of struct sigaction. Note that no extra functionality that is provided by sigaction but not signal() has been used. sigaction has been chosen because it is the newer, and arguably better approach to define custom signal handlers.

When a new background process is created, it is first checked that current number of child processes is less than the maximum limit. If not, the child process is not created and an error is shown. Otherwise, we fill the pid and process name in the first empty location in the array.

When a background process is terminated, the handler is called. Within the handler, we loop over all the children who have exited since the shell recieved the SIGCHLD signal, using waitpid(). By default waitpid() is blocking, which is not desirable in this situation, so we make use of the WNOHANG flag to make it non-blocking. For each pid that we get from waitpid(), we look for it in the child pool, and get its name by matching its pid. We print the termination message with the name and pid of the process and "normally" or "abnormally" depending on the exit status of the process. We then replace the pid of that element in the array with -1 to indicate that that position in the pool is free to be occupied by another child process.

Finally, the prompt string is printed again as an aesthetic feature.

Specification 7: Input/Output Redirection

The execute_command() method in functions.c has been modified to parse the command and check if any kind of I/O redirection is required. If it is, then the required file is opened and the required streams are connected to them. Then the functions corresponding to the command is executed. After returning from the command specific function, at the end of the execute_command() function, the files are closed and streams are set to default.

Specification 8: Command Pipelines

A new function has been added in the call stack, called handle_pipes(), which is called by parse_input() and inturn calls execute_command() to execute it acfter handling piping logic.

Inside handle_pipes(), we first calculate the number of pipes. If it is 0, we directly send the input string to execute_command(). Instead, if there are some pipes, we split to commands. Say there are n commands, then we create n pipes, meaning 2n file descriptors and populate them using the pipe() function. We then create n child processes of the shell. We link the STDOUT of the ith child to the ith write end of the pipe and the STDIN of the ith child to the (i-1)th read end of the pipe. So, the STDIN of the first child is still default and the STDOUT of the last child is also still default. This creates a pipe chain in which the ith child reads the input from the output of the (i-1)th child.

Specification 9:I/O Redirection within Command Pipelines

The way the first two specifications have been implemented, this specification has inherently been implemented.

Specification 10: User-defined Commands

  1. jobs: Implemented in jobs() in functions.c. Iterates over the children in child pool and fetches each one's status from the processes' proc/<pid>/stat file.

  2. fg <job number>: Implemented in fg() in userdefined_commands.c. Checks command usage, then verifies the given job number. Once it is confirmed that the job is a valid one, it gets its pid from the child pool, gets its process group, removes the child from the pool, tells the shell to ignore all STDIN and STDOUT related events and then gives terminal control to the process group belonging to said job. The job is also sent the SIGCONT signal, incase it had stopped in the background. While the process runs in the foreground, the shell waits on it using the wait() syscall. Once the foreground process terminates or has been stopped, the terminal control is returned to the shell and the default response to STDIN and STDOUT events is restored.

  3. bg <job number>: Implemented in bg() in userdefined_commands.c. Checks command usage, then verifies the given job number. Once it is confirmed that the job is a valid one, it gets its pid from the child pool. Then it sends the SIGCONT signal to that process using the kill() method, to tell it to change its status from stopped to running.

  4. sig <job_index> <signal>: Implented in sig() in userdefined_commands.c. Checks command usage, then verifies the given job number. Once it is confirmed that the job is a valid one, it gets its pid from the child pool, and sends the signal specified by the user

Specification 11: Signal Handling

For both the signals, a new action was defined using the sigaction structure.

  1. Ctrl-Z: The hanlder for this is relatively straightforward. It makes a new line and prints the prompt string again. To make sure that when a Ctrl-Z is hit on a foreground process, it is added to the job pool, one bit of modification needed to be made in foreground_process() and fg(). The wait() was replaced with waitpid() and the WUNTRACED flag was set. This way, we could inspect the status code to check if the process had been stopped or terminated using the WIFSTOPPED macro. If the process has been stopped, it is added to the child pool, because it has been sent to the background.

  2. Ctrl-C: The hanlder for this is essentially the same as that for the previous one. It prints a new line and prints the prompt string again.

Releases

No releases published

Packages

No packages published