Logo  

CS456 - Systems Programming

Creating a Unix Shell part 1:

To really begin to understand Unix it is perhaps necessary to create a Unix shell, once having done so you will understand among other things:

  • Process creation and program execution (fork() / clone()?, execve())

  • Signals (signal handlers, sigaction(), kill())

  • Pipes (pipe() / socketpair())

  • Job-control and the controlling terminal (wait() / waitpid(),SIGCHLD/SIGTSTP/SIGCONT, process groups, ioctl(), termios)

  • Resource usage and limits (getrusage(), getrlimit(), setrlimit())

  • The environment (getenv()/setenv())

  • Globbing (i.e. Wildcard file-name expansion, glob(), scandir(), fnmatch())

And you'll probably get reasonably proficient at dealing with strings. Though time probably won't allow the creation of a complete shell I hope to touch on most of the above in the time remaining.

Process Creation

In Linux there are two primary system calls to create a new process, fork() which creates a full-blown process with a complete copy of the parent processes memory, descriptors and signal handlers and clone() which allows fine control over the aspects the parent that are copied/accessible to the child, and is usually used to create threads which usually share the same memory and other resources of the parent rather than copies of them.

There is also a vfork() call which differs from fork() in that it actually shares the parent processes memory (suspending the parent until a call to execve() is made or it exits,) and in that respect is more like a temporary thread (and is actually just a wrapper around clone(),) but for now we'll avoid any use of vfork() or clone() and focus entirely on fork() for process creation.

The fork() System Call:

  • Reading: man 2 fork
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

From the prototype we see that fork takes no parameters and returns a pid_t type which is a signed integer. When fork is successful it creates a new child process, in which the return value will always be 0, in the parent process the return value is the process id of the new child process. After the fork code is executed in both the parent and child process following the fork call as if there were no other difference than the return value of the fork.

If fork should return less than 0, then fork has failed to create a new process which could be because the system has either run out of process table space (in which case the system is screwed,) or you have reached your process creation limit ('limit maxproc' in tcsh or 'ulimit -u' in bash,) note that each thread created by programs also count toward your process creation limit.

Example fork() code:


pid_t pid = fork();
if (pid < 0) {
  perror("fork");
} else if (pid == 0) {
  // This code would only be executed in the "child" process:
  printf("I'm the child\n");
  exit(0);
} else {
  // This code would only be executed in the "parent" process:
  printf("I'm the parent process, child PID = %d\n", pid);
}

Example of really bad fork() code:

while (1) fork();

This is called a fork bomb, which will very quickly (exponentially) create so many processes as to exhaust the system process table space (around 128K processes) or your personal process limit. Note that it creates processes exponentially, as each child process runs it's own fork loop, so for n loop iterations, 2n number of processes are created. It is example of an easy Denial Of Service (DOS) attack, however it can be unusually easy to accidentally create a fork bomb when using fork() in a loop, so some care should be taken when programming around fork() to not let your forks get away from you.

Program execution via execve():

  • Reading:

    • man 2 execve

    • man 3 execl

#include <unistd.h>

int execve(const char *pathname, char *const argv[], char *const envp[]);

The execve() function is a system-call that replaces the current processes memory/code with that of another program the location of which is specified by the first parameter (pathname). The command line parameters to the program are given in the character pointer array argv and the environment to be given to the program are specified by the character pointer array envp. Both the character pointer arrays should have NULL as their terminating value.

Example of using execve():

char *program = "/bin/ls";
char *argv[] = { "ls", "-l", "-a", NULL };
char *envp[] = { "HOME=/u1/h7/sbaker", "PATH=/bin:/usr/bin:/usr/local/bin", NULL};

execve(program, argv, envp);


It should be noted that the execve() call will replace the current process with the new program, so it is often invoked in a child process created by a fork() call. Note also that argv[0] is the name of the program itself, however it need not be, the entirety of the command line parameters and environment are up to the parent process to define as they see fit, only the first parameter is used to define the program to execute. It is however cumbersome to provide the environment or even the command line parameters as a string vector, so there exist a number of wrapper functions to execve:

int   execl(const char *pathname, const char *arg, ... /* (char *) NULL */);
int  execlp(const char *file,     const char *arg, ... /* (char *) NULL */);
int  execle(const char *pathname, const char *arg, ... /* (char *) NULL, char * const envp[] */);
int   execv(const char *pathname, char *const argv[]);
int  execvp(const char *file,     char *const argv[]);
int execvpe(const char *file,     char *const argv[], char *const envp[]);


The execl* functions allow the command line parameters to be given as a sequence comma separated strings terminated by a NULL value (a bit like printf()):

execl("/bin/ls", "ls", "-l", "-a", NULL);

The execv or execvp functions exclude the environment parameter as it will copy the callers environment so the caller need not do so.

execv("/bin/ls", (char *[]){"ls", "-l", "-a", NULL});

The execlp, execvp or execvpe functions first parameter need not point to the full pathname of the program to be executed, but instead the PATH environment is used to search for the given program:

execlp("ls", "ls", "-l", "-a", NULL);

The execlp() function is probably the most friendly to a programmer wanting to exec a specific program, but for purposes of writing a shell the execv*() functions are probably better as the command line parameters are provided by the user and will likely be stored in an array of strings.

Combining fork() and execlp():

pid_t pid = fork();

if (pid < 0) {
  perror("fork");
  exit(1);
}

if (pid == 0) {
  execlp("ls", "ls", "-l", "-a", NULL);
  // This code would only be reached if the above execlp fails to replace this
  // process with the ls program:
  perror("exec");
  exit(1);
}

/**
 * The following is parent code to wait until the child process finishes. The
 * wait function gets the childs exit status (i.e. "reaps" the dead child
 * process):
 */
int status;
wait(&status);

printf("Process has exited with exit code = %d\n", WEXITSTATUS(status));