Notes following this MIT course and is based on the xv6 toy operating system.
Operating System Interfaces
- Operating systems serve a number of purposes
- Abstracts away lower level hardware, so that programs don’t have to care about what disk you are writing to
- Allows multiple programs to run “at the same time”
- Provide well defined interface for programs to interact with each other
- For the interface, we want a simple interface that is easy to implement, but allows high sophisticated operations to happen
- UNIX philosophy of composing many simple programs together helps keep this problem trackable
- Can divide operating system into kernel space and user space
- kernel space is the program which provides services to other programs. It’s given hardware level privileges which user programs don’t have access to
- Each running program under the kernel’s manager (called a process) gets it’s own memory which houses the instructions (ie. the CPU assembly instructions), the data (variables created when running the program) and the stack (organizes program procedure calls)
- if process needs a kernel service, it invokes a system call, which briefly transfers ownership to the kernel, which executes the syscall, and then returns ownership to the user program
- Called user space and kernel space
- Typically, people use a shell to allow user input. The shell resides in user space, so you have flexibility in what shell you run
Processes and Memory
- For each running process, there is some user-space memory (instructions, data, stack) and kernel-space memory (which the user’s can’t access)
- One of the kernel-space things is the PID (process identifier)
- Xv6 has it’s own custom syscalls, which are somewhat stripped down versions of the UNIX ones
- Unless otherwise stated, all of these calls return 0 for no error and -1 if there’s an error
Call Sig | Desc |
---|---|
int fork() | Create a process, return child’s PID. Makes an exact copy of user-space. returns in both parent and child process. Returns child’s PID in parent process, and 0 in child process |
int exit(int status) | Terminate the current process; Releases all held resources. Exit status reported to wait(). No return. |
int wait(int *status) | Wait for a child to exit; exit status in *status (out parameter); returns child PID. If caller has no children, immediately returns -1. If parent don’t care about child exit status, pass 0 address (ie. (int *) 0) to wait |
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. This replaces the current process’s memory with that of the file. For Xv6, the ELF format is used. |
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. |
Forking
- As a small case study, look at the following C snippet
int pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}
- After the fork(), the pid let’s you determine what the parent and the child do
- NOTE: While after forking, the memory and register content of both processes is the same, they located in different locations which can evolve independently from each other
- Don’t expect the output to be the same. Whichever process gets to printf first will output first (parent and child output could also be interleaved)
Exec
- Another case study of exec
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
- If echo is functioning properly, then the last printf will never get executed
- Argv[0] is typically reserved for the program name