Operating systems provide interfaces to user programs through system calls.

1. xv6 system calls

The following are the system calls provided by xv6.

function Description
int fork() Create a process, return child’s PID.
int exit(int status) Terminate the current process; status reported to wait(). No return.
int wait(int *status) Wait for a child to exit; exit status in *status; returns child PID
int kill(int pid) Terminate process PID. Returns 0, or -1 for error.
int getpid() Return the current process’s PID.
int sleep(int n) Pause for n clock ticks.
int exec(char *file, char *argv[]) Load a file and execute it with arguments; only returns if error.
char *sbrk(int n) Grow process’s memory by n bytes. Returns start of new memory.
int open(char *file, int flags) Open a file; flags indicate read/write; returns an fd (file descriptor).
int write(int fd, char *buf, int n) Write n bytes from buf to file descriptor fd; returns n
int read(int fd, char *buf, int n) Read n bytes into buf; returns number read; or 0 if end of file
int close(int fd) Release open file fd.
int dup(int fd) Return a new file descriptor referring to the same file as fd.
int pipe(int p[]) Create a pipe, put read/write file descriptors in p[0] and p[1].
int chdir(char *dir) Change the current directory.
int mkdir(char *dir) Create a new directory.
int mknod(char *file, int, int) Create a device file.
int fstat(int fd, struct stat *st) Place info about an open file into *st.
int stat(char *file, struct stat *st) Place info about a named file into *st
int link(char *file1, char *file2) Create another name (file2) for the file file1
int unlink(char *file) Remove a file.

2. shell

The shell (like sh or bash) is just a user-space program that:

  1. Reads user input (commands).
  2. Uses fork() to create a new process. (except from cd)
  3. Uses exec() to run a program.
  4. Waits for it using wait().

For xv6, the shell is directly connected to console, it does not have ssh. So read and write (stdout, stderr) are directly go to console device The following codes show you the stdin, stdout, stderr opened when shell process start. Remember that fd created by open system call, it’s underlying fd tables are different, so these three fds uses different offset. You may also wondering how to keep the stdout and stderr write to console in order, so that output won’t jumbled. The write to console is specially handled with consolewrite, which has a lock to keep the output in a good order.

user/sh.c shell fds connected to console device

int
main(void)
{
  ...
    // Ensure that three file descriptors are open.
  while((fd = open("console", O_RDWR)) >= 0){
    if(fd >= 3){
      close(fd);
      break;
    }
  }
 ...

user/sh.c print prompt and read input from console

int
getcmd(char *buf, int nbuf)
{
  write(2, "$ ", 2);
  memset(buf, 0, nbuf);
  gets(buf, nbuf);                  // <<< get a whole line as input
  if (condition) {

  }(buf[0] == 0) // EOF
    return -1;
  return 0;
}

user/sh.c shell loop framework

int
main(void)
{

  // Read and run input commands.
  while(getcmd(buf, sizeof(buf)) >= 0){
    // cd command
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Chdir must be called by the parent, not the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(2, "cannot cd %s\n", buf+3);
      continue;
    }

    // other types of command, fork and run in child process
    if(fork1() == 0)
      runcmd(parsecmd(buf));    // parsecmd helps to build a tree from the input, runcmd will help to go down the tree and run the commands

    // shell wait for child process
    wait(0);
  }
  exit(0);
}

There are 5 types of command that can be run by xv6, which are defined in the following:

//e.g. xxx
#define EXEC  1
//e.g. xxx > xxx
#define REDIR 2
// e.g. xxx | xxx
#define PIPE  3
// xxx ; xxx
#define LIST  4
// xxx &
#define BACK  5

The parser and tree wal through is the harder part for the shell process. The following video explains how the tree is built and walked in details: xv6 Shell Program Explained (runcmd walk the cmd tree)

Shell Code– More Detail (from cmd line input to cmd tree)

Example of a tree build from the command

<file1 pgm opt1 > file2 opt2

Tree:
| redir cmd | -> | redir cmd | -> | exec cmd |

<file1            >file2           pgm opt1 opt2

The command will be run from the top of the tree, so the fd will be opened first before the command running

3. I/O and File Descriptors

Each process have their own file descriptor table, so that they have their own private space of fd starting from 0. A newly allocated file descriptor is always the lowestnumbered unused descriptor of the current process.

stdin: 0. stdout: 1. stderr: 2.

Fork copies the parent’s file descriptor table along with its memory, so that the child starts with exactly the same open files as the parent. The system call exec replaces the calling process’s memory but preserves its file table. Although fork copies the file descriptor table, each underlying file offset is shared between parent and child.

Two file descriptors share an offset if they were derived from the same original file descriptor by a sequence of fork and dup calls. Otherwise file descriptors do not share offsets, even if they resulted from open calls for the same file.

4. Pipe vs Redirection

echo hello world | wc

could be implemented without pipes as

echo hello world >/tmp/xyz; wc </tmp/xyz

Pipes have at least four advantages over temporary files in this situation.

  1. Pipes automatically clean themselves up; with the file redirection, a shell would have to be careful to remove /tmp/xyz when done.
  2. pipes can pass arbitrarily long streams of data, while file redirection requires enough free space on disk to store all the data.
  3. Third, pipes allow for parallel execution of pipeline stages, while the file approach requires the first program to finish before the second starts.
  4. If you are implementing inter-process communication, pipes’ blocking reads and writes are more efficient than the non-blocking semantics of files

5. Lab1