Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit

CS 367 Fall 2022

Project #3: Task Controller

1. Introduction

For this assignment, you are going to use C to implement a task controller called the KI Task      Controller.  Once running, KI maintains a list of several tasks, which can be executed, inspected, or otherwise managed (e.g. by killing or suspending a running task).  This assignment will help  you to get familiar with the principles of process management in a Unix-like operating system.   Our lectures on processes, signals, and Unix-IO as well as Textbook Ch.8 (in particular 8.4 and  8.5) and 10.3 will provide good references to this project.

2. Project Overview

A typical command shell receives line-by-line instructions from the user in a terminal.  In this  project, the shell is the interface to our task controller.  The shell would support a set of built-in instructions, which will then be interpreted by the shell, and acted on accordingly.  In some       cases, the instructions would be requests for the system to execute other programs. In that case, the shell would fork a new child process and execute the program in the context of the child.

The task controller also has the responsibility to maintain a list of tasks of interest, and to keep    them organized.  The user is able to enter programs, then execute them in theforeground (wait   until the task completes) or background (allow the process to run while moving on to other         things).  The user can also control existing tasks by temporarily suspending them, killing them,   or deleting them from the list altogether. Finally, the user will have some additional capabilities, like redirecting the input or output of a process to or from a file, or piping the output of one task to another.  The task controller will allow the user to check the exit code of completed tasks. The shell provides some built-in instructions to help manage and list tasks, as well as some instructions to execute and control running commands.

For this assignment, your implementation should be able to perform the following:

•    Accept a single line of instruction from the user and perform the instruction.

o The instruction may involve creating, deleting or listing tasks.

o The instruction may involve reading from or writing to a file.

o The instruction may involve loading and running a user-specified program.

•    The system must support any arbitrary number of simultaneous running processes.

o KI is able to both wait for a processes to finish, or let them run in the background.

•    Perform basic management of tasks, whether they are in ready mode, running, or complete;

•    Use file redirects and pipes to read input from or send output to a file or another process.

•   Use signals to suspend/resume or terminate running processes, and track child activity.

We will describe each aspect of the system in more details with some examples below.

Specifications Document: (Chapter 3 at the end has some guidance on starting your design)

This document has a breakdown of each of the features, looking at specific details, required         logging, and sample outputs.  This is an open-ended project that will require you to make your   own design choices on how to approach a solution. Read the whole document before starting.

2.0 Implementation Responsibility

Your project handout consists of several files:

•   The starting template code in taskctl.c.

•   Headers, helper functions and logging functions in logging.c, logging.h, parse.c, parse.h, util.c, util.h, and taskctl.h.

•    A Makefile to build all components of the project.

•    Several utility programs which you can use to help you test your text processing system.

You may take the existing starting template code in taskctl.c and modify it. This is the only file which you should modify, and is the only file which you will be turning in. You should not need to include any additional header files, and you will not be allowed to use headers which     change the project’s linking requirements.

All of your code will be tested on the Zeus system, so Zeus is your expected development       platform.  Even if you have a Linux or Mac system at home, there are subtle differences in the implementation of signal handling and process reaping from system to system. You are responsible for making sure that your code functions correctly on Zeus.

When testing your code, there are some cases where you will be running external commands from within your system shell.  You are allowed to use any normal commands on the system (e.g. ls or grep), any of the provided utility programs, or any programs you have written     yourself.  It is not recommended that you try to execute any commands which have an          interactive interface (e.g. vim) from within your shell.

2.1 Use of the Logging Functions

In order to keep the output format consistent, all ofyour output will be generated by calling the provided logging functions at the appropriate times to generate the right output from your         program.  Do not use your own print statements, unless it is for your own debugging purposes. All logging output is encoded with a unique header which enables us to keep track of the           activities of our shell. The generated output from log calls will also be used for grading.

The files logging.c and logging.h provide the functions for you to call from your code.  Most of the log functions require you to provide additional information such as the Task Number        (task_num), or possibly other info (e.g. Process ID, file name) to make the call. We will explain more details how and when each log function is used in the specifications below.

2.2 Prompt, Accepting, and Parsing User Instructions

Once started, the shell prints a welcome message and a prompt, and waits for the user to input an instruction.  Each line from the user is considered as one instruction.

Logging Requirements:

•    The user prompt must be printed by calling log_kitc_prompt().

o The call to log_kitc_prompt() is already present in the starting template code, so will not need to take any additional action to display the prompt correctly.

If a command line input is empty, it will be ignored by the shell.  Otherwise, you will need to    parse the user input into useful pieces.  We provide a parse() function in parse.h. (the              implementation is in parse.c). The provided template taskctl.c has already included the code which calls parse().  Feel free to use the provided parse() as is, or to implement your own       parsing facility. Check Appendix A for a detailed description of the input, output, and examples of the provided parse().

In testing, make sure you use user commands following these rules:

•    Every item in the line must be separated by one or more spaces;

•    Many built-in instructions expect an additional Task Number argument.

•   The pipe built-in expects two Task Number arguments.

•    Some built-in instruction (exec and bg) allow file redirection.

•    Any instruction which is not a built-in represents a command to be executed.

o The command can be any real program, e.g. ls.

o The program name is optionally followed by its command line arguments.

For this assignment, you can make the following assumptions:

•    All user inputs are valid command lines (no need for format checking in your program).

•    You may assume a bounded input line size and number of command arguments.

o The maximum number of characters per input line is 100.

o The maximum number of arguments per program is 25.

o  Check taskctl.h for relevant constants defined for you.

•    A command will not specify the path.

o For example, you may see lsbut not /usr/bin/lsin the input.

After calling parse(), the provided taskctl.c leaves the design and implementation up to you as an open-ended project for you to solve.  You are encouraged to write many helper functions as   well and you may add additional code in to main() as needed.

2.3 Responsible Coding

Our processes management will require us to fork processes and receive signals.  Every process  we create uses system resources sometimes we do not realize how many processes we have left lying around.  To compound the situation, whenever we redefine our signal handlers, it may        sometimes impact our process’s natural ability to shut down cleanly.  Hopefully we won’t write  any unintended fork bombs, but even then, we want to do our best to keep from creating too         many unwanted processes.  Here are some suggestions which will help.

2.3.1 Killing the Currently Running Process

Often, we may be in the middle of running our program, and either got stuck in a loop or simply want to exit out quickly. If this is the case, do not use ctrl-z.  If you do this, it will not kill the  process, it will simply put it to sleep, while still retaining all of its resources in memory.  Instead, use ctrl-c.  If the does ctrl-c not work (often because we have redefined the signal handler),  try ctrl-\ instead.  The former uses SIGINT whereas the latter uses SIGTERM. Either will, by     default, terminate the program.

2.3.2 Checking for Running Process

When we are back at the bash prompt, we may be interested in knowing which processes we are currently running.  That way, we can know if we have left behind any residual processes. The      easiest way to do that is with the ps ux command.  This will list out all processes under our         name (including bash itself).

2.3.3 Killing Residual Processes

If we discover that we have left behind processes, we can use the pkill command at the bash prompt to kill of processes by name.  For example, we would use pkill taskctl in order to kill of all of our taskctl processes.  If pkill fails to kill one of our processes (often due to a             corrupted signal handler), we can instead use pkill KILL taskctl in order to send a firmer     and impossible to override kill message.

2.4 Basic Shell Instructions

A typical shell program supports a set of built-in commands (internal functions that are built-in to the shell itself).  If a built-in command is received, the shell process must execute that directly without forking any additional process. The most basic commands supported by our KI system  can be executed directly without the need to interact with any other parts of the system:

2.4.1 The help Built-In Instruction

help: when called, your shell should print on the terminal a short description of the system, including a list of built-in instructions and their usage.

Logging Requirements:

•   You must call log_kitc_help() to print out the predefined information. Example Run (help instruction):

2.4.2 The quit Built-In Instruction

quit: when called, your task controller shell should terminate.

Logging Requirements:

•    You must call log_kitc_quit() to print out the predefined information.

•   You will then need to exit your shell program, using exit code 0.

Assumptions:

•    You can assume there are no non-terminated background processes when calling quit.

o In other words: you may quit immediately; you are not responsible for clean-up.

Example Run (quit instruction):

2.5 Basic Task Management Instructions

This program is a task management system, so it has the ability to maintain several tasks in          various states at any time.  Each task is assigned a Task Number, which is a positive integer        value.  In addition, every task has some associated metadata, such as its current state and Process ID, and exit code.

Any existing task is in one of five states: Ready, Running, Suspended, Finished, or Killed.      Before it is run, a task will begin in the Ready state.  However, running the program will cause it to enter one of the other states.

A task has both a Task Number and a Process ID, not to be confused.  The Task Number is a        number we have assigned our task in order to make it easier to keep track of.  The Process ID, on the other hand, is a number assigned by the operating system to uniquely define the child

process.

2.5.1 Task Number Assignment

Whenever a new task is created or deleted, we must follow certain rules about Task Numbers:

•    Any new task is assigned the first available currently unused Task Number.

o For example, if current tasks are 1, 3, and 5, the next task will have Task ID 2.

•    If there are no tasks, then the next new task will be assigned Task ID 1.

•   If a task is purged, the remaining Task Numbers are not renumbered.

2.5.2 User Commands and the list Built-In Instruction

Any command which is not a built-in instruction should be interpreted as a user command. When a user command is entered into KI, this will create and initialize a new task entry which               describes the command. Entering the command will not immediately result in executing the         command, but merely creating a task entry.

Logging Requirements:

•    Call log_kitc_task_init(task_num, cmd) to indicate which Task Number was assigned.

o The cmd is for the complete command line string which was provided as input.

Assumptions:

•    The Task Number of the new task should be assigned consistent with the rules from 2.5.1.

•    When a new command is entered, it is not immediately forked or executed.

o Entering the command will only create a new task entry.

o We would later use the exec, bg, or pipe built-ins to run the task.

•   The newly created tasks will be in the Ready state.

Implementation Hints:

•   You are required to have the ability to maintain an arbitrary number of tasks.  As we have seen in an earlier example, a new task may be numbered in between two existing task, and arbitrary tasks may be deleted.  Consider using a data structure which lets you add an        unlimited number of elements, and to add or remove arbitrary elements easily.

list: lists all of the currently existing tasks.

Includes the total number of tasks.

Logging Requirements:

•    First call log_kitc_num_tasks(num) to indicate the current number of tasks.

•    Call log_kitc_task_info(task_num, status, exit_code, pid, cmd) once per task.

o Tasks should be listed in order of increasing Task Number.

o The status should be one of the LOG_STATE_* constants found in logging.h.

o The exit_code should be 0 unless the process has already completed execution.

o The Process ID (pid) should be 0 while in the task is in the Ready state.

o The cmd is the complete command line of the task.

Assumptions:

•    Tasks will begin in the state LOG_STATE_READY – but see 2.6.

•    The pid and exit_code can be 0 for now but see 2.7.2

•   The number of tasks begins as 0, but increases if new user commands are entered.

Example Run (new command and list instructions):

2.5.3 The purge Built-In Instruction

purge TASKNUM: removes TASKNUM from the list of tasks.

Logging Requirements:

•    On a successful delete, call log_kitc_purge(task_num).

•   If the selected task does not exist, call log_kitc_task_num_error(task_num) instead.

•    If the task is currently busy, call log_kitc_status_error(task_num, status) instead.

o A busy task is a Running or Suspended task; do not delete the task in this case.

o The status would be LOG_STATE_RUNNING or LOG_STATE_SUSPENDED.

Assumptions:

•    Only a buffer which is idle (Ready, Finished, or Killed) can be deleted.

•   Once purged, the task no longer exists and information about the task need not be retained.

Example Run (purge instruction):

2.6 Process Execution Instructions

Our system would not be a task controller without the capability to execute tasks.  In order to   enable this capability, we will give our shell the ability to run external commands as separate   processes.  We can run an external command by first forking a child process, and then within the context of the child using one of the exec() variants to actually run the program.

Our system will allow us to run multiple concurrent processes.  In fact, every existing task in the list is allowed to have its own corresponding process running as a child.  If a task is currently       running, it will be in the Running (or Suspended) state.  Before running, it starts in the Ready state.  After the process completes, it will be in the Finished or Killed state, depending on how it terminated.

Assumptions:

•   Any task will not have more than one associated child process at a time.

2.6.1 Execution Paths

When we execute external commands using execv or execl, we will also need to know the full    path of the command to satisfy its first argument.  To generate this, we will need to check two     different paths for each command. These are: " ./" and "/usr/bin/".  Both of these paths must be checked, in this order, for an entered command.  A command as entered on the command line      will not have any path to begin with.

For example, if the user enters the command "ls -al", we will try both " ./" and "/usr/bin/" as the path argument to execv or execl, in that order.  We would first try to execute " ./ls", and     failing in that, we then execute the correct path in "/usr/bin/ls".  Check the error code on execv or execl to see if the path was not found before checking the next one.  If neither path leads to a  valid program, then we would handle it as a path error and issue the appropriate log function.

Since the path argument of execv or execl needs to be modified from the original command by  concatentating in " ./" or "/usr/bin/", we will simply keep the original command name as argv[0].  So, if the user inputs "ls -al", then the path may be either " ./ls" or "/usr/bin/ls"     depending on which one works, but our argv[0] will still need to be "ls", which is what the user typed in.

2.6.2 The exec Built-In Instruction

exec TASKNUM [< INFILE] [> OUTFILE]: executes an external command.

•    The command is the external command associated with task TASKNUM.

•    Runs as a foreground process, meaning that the shell waits for the process to finish.

•   If INFILE is specified, then the child process’s input should be redirected from the file.

•    If OUTFILE is specified, then the child process’s output should be redirected to the file.

•    The process status changes as it runs and eventually completes; see 2.5.

•   When the process completes, the task should record the process’s exit code; see 2.7.2

Logging Requirements:

•    If the selected task does not exist, call log_kitc_task_num_error(task_num).

•    If the task is currently busy, call log_kitc_status_error(task_num, status) instead.

o A busy task is a Running or Suspended task; do not execute the task in this case.

o The status would be LOG_STATE_RUNNING or LOG_STATE_SUSPENDED.

•    Call log_kitc_status_change(task_num, pid, LOG_FG, cmd, LOG_START).

o This should be called by the parent after forking a new process.

o Use the tasks full command line as cmd.

•    If the command cannot be executed (exec failed), call log_kitc_exec_error(cmd).

o Use the tasks full command line as cmd.

o Terminate the child with exit code 1.

•    If a redirection is performed, call log_kitc_redir(task_num, redir_type, filename).

o redir_type is LOG_REDIR_IN or LOG_REDIR_OUT depending on which type.

•    If a redirect file cannot be opened, call log_kitc_file_error(task_num, filename).

o Terminate the child with exit code 1.

•   When the process terminates, there should be a log message; see 2.7.2.

Assumptions:

•    The exec instruction can only be successfully run if the task is not currently busy.

•    The exec will not be used on interactive programs (e.g. vim).

•   All commands will be entered without a path (the path is entered by the shell).

Implementation Hints:

•    We can use fork() to create a new child process.

o See Appendix C for information about something you should do after forking.

•    We can use either execl() or execv() to load a program and execute it in a process.

•    Though execl or execv do not normally return, they will return with a -1 value on error.

o Example: if the path or command cannot be found.

o Use the man pages for the command you wish to use to see the details.

o Check both valid paths ( ./ and /usr/bin) with a command before calling it an error.

•    If a file is specified, open the file and redirect the child’s standard in or out to the file.

o Use dup2() to change the standard input or output the child after forking.

o It is legal to request a redirect of both input and output at the same time.

o Implement exec built-in without redirection before adding the redirect feature.

•    Use wait() or waitpid() to wait for the child process to finish.

o Either way, signals are relevant to process completion; see 2.7.5

•   Yes, running a completed task would result in re-running the command.

Example Run (exec instruction):

2.6.3 The bg Built-In Instruction

bg TASKNUM [< INFILE] [> OUTFILE]: executes an external command.

•    Runs as a background process, meaning that the shell does not wait for the process to finish.

o Signal handling will be necessary to detect process completion; see 2.7.5.

•   The command is the external command associated with task TASKNUM.

•   If INFILE is specified, then the child process’s input should be redirected from the file.

•    If OUTFILE is specified, then the child process’s output should be redirected to the file.

•    The process status changes as it runs and eventually completes; see 2.5.

•   When the process completes, the task should record the process’s exit code; see 2.7.2.

Logging Requirements:

•    This logging requirements are nearly identical to those of the exec command.

•    The exception: log_kitc_status_change(task_num, pid, LOG_BG, cmd, LOG_START).

o LOG_BG instead of LOG_FG.

•    With background execution, process termination is logged only when the process completes.

o This is not necessarily before returning to the shell prompt.

Implementation Hints:

•    The bg built-in is like the run built-in, but without waiting for process termination.

•   It’s normal for a background process to print output after returning to the prompt.

Example Run (bg instruction):

2.6.4 Pipes

When we want to send output from one process to another process, we can use a pipe. Similar to file redirection, output of one process can be redirected to a pipe, and input to another process      can be redirected from a pipe. If one process sends to a pipe, and a second process reads from the same pipe, this lets the first process send data to the second process.

A pipe is like a double-ended file: we can write to one end of the pipe and read from the other end of the pipe.  If we create a pipe before forking children, then both children will inherit the pipe and be able to use it to send data from one to the other. Consider this scenario:

•    Parent process P has two children, A and B.

A wants to send data to B.

P creates a pipe before forking A or B; thus, both A and B gain access to the pipe.

A redirects its output to the write-end of the pipe and closes its read-end.

B redirects its input from the read-end of the pipe and closes its write-end.

P closes both its read-end and write-end, because it is not directly using either.

•   Now, any output from A gets sent as input to B.

It is vitally important to close all unused pipe ends as described above. Without doing so,     child B might never terminate, because it has no way of knowing when it has come to the end of its input (the unused pipe ends could potentially still be used to send input to B).

2.6.5 The pipe Built-In Instruction

pipe TASKNUM1 TASKNUM2: executes two external commands connected by a pipe.

•   Creates a pipe to allow output to be redirected from TASKNUM1 and to TASKNUM2.

•    Executes TASKNUM1 as a background process (see bg).