CS111 Fall 2014

Lecture 4 Scribe Notes: Operating System Organization

Scribes: Sandra Suttiratana, Gina Kwong and Sarah Suttiratana

Table of Contents

  1. Operating System Organization
  2. The 3 Fundamental Abstractions for Operating Systems
    1. Memory API
    2. Interpreters API
    3. Link API
  3. Hardware
  4. Layering
  5. The Virtualizable Processor
  6. Accessing Memory
  7. The exit() Function
  8. Fork Bomb
  9. Pipe

I. Operating System Organization

Goals

We want interface, use, simplicity, reliability, robustness (able to still run even if there is an invalid command), durability, efficiency (not waste RAM, battery, time), flexibility (able to be changed later), and security in our OS.

Application Programming Interface (API)

How a software works based on their inputs, outputs, and underlying types, as well as how these components should interact with each other and how they are used.

The 3 Fundamental Abstractions for Operating Systems

1. Memory API

Ex)

*p = v; (write to p)

(*p); (read p)


The issues with Memory API are the limitations on the total size, the word size, and the speed (throughput and latency). It is also volatile, has linear vs. associative addressing, and has memory coherence (or the ability to atomically read and write to memory).

2. Interpreters API

Ex) v = f(p)

where v is the answer, f is the interpreter, and p is the program. In other words, the Interpreters API focuses on interpreting data sent and received by the programs we want to run. The positive aspect of this abstraction is the efficiency due to the hardware (HW) support and interrupts by the kernel, making this relatively safe to use for normal users who may accidentally mess with important parts of the system if allowed. It also allows us to use instruction and environmental pointers to various data, requiring little memory to use. This, however, requires hardware support (i.e. a virtualizable processor). This is in fact what Microsoft implements in Windows.

3. Link API

The Link API is based message passing, where the network is abstracted away and allows the kernel to be anywhere. This makes the OS simpler and more general. With Link API, one can send data (e.g. link names and buffers of data) and receive data (i.e. I/O buses receive data). Data is sent using a process known as serialization, where data is reformatted to fit the receiver (encode data into a buffer). The limitations of this method is that we cannot send pointers to data, as reformatting pointers would be meaningless. There are also performance issues involved with abstracting systems this way (e.g. copying data to a buffer, and then having to copy it back). This is what Apple implements in OS X.

How would a programmer (with CS 32 level knowledge) design an O.S.?

They would most likely create an object oriented program with different classes for I/O buses, memory, and the interpreters (C++), along with multiple pointers. However, this implementation is not as flexible, as say, the C Language.

Hardware

The parts of the hardware in a CPU (in decreasing performance order):

In the CPU

In the CPU:

In the CPU

Layering

Instructions are not all created equal. Some instructions, especially the user-written code, do not allow you to directly access the kernel, for fear of the user destroying some part of the computer; this is unprivileged code. Other instructions, usually instructions given by the kernel, allow access to almost anything in the computer, such as directly accessing a memory location and storing it in a variable (e.g. int 0xf0023); this is privileged code. Below are diagrams of such code:

Layered Code

The unprivileged instructions are run at full speed, and the program has full access to registers.

Ring Diagram

Ring Diagram: A diagram of the levels of authority granted by the kernel with:

0 being the highest level, and

3 being the lowest level.

0 is essentially the supervisor mode, and allow access to privileged code. 3 is the user code, and allows access to only to unprivileged code, which is used to make sure the user doesn’t accidentally destroy the computer.

The Virtualizable Processor

The Virtualizable Processor lets you support a process, which is a program running in isolation (on a virtual interpreter), to create a process, and to destroy a process. A process has its own virtual memory (which includes registers, etc.) and runs on a virtual interpreter.

Ex) Creating a process:

pid_t fork(void);

if(fork() == 0)

printf (“I’m child\n”);

else

printf (“I’m not\n”);

pid_t is the process id, which is a signed bit in a 64-bit machine. The fork() system call creates a process which is a copy of the process itself.

We have only one CPU, so the other processes’ states are saved before a process is run.

Ex) Destroying a process

Noreturn void exit(int) //exit status

1 physical CPU

>1 process

Suppose a process does a:

read(fd, buffer, 1000)

Then:

int 0xf0; //Saves this to processes’ registers

//Then resumes some other process (this process uses CPU instead)


Therefore, this process is saved into registers, and another process is resumed. The process runs in a virtual machine.

Stack

Shown above is a process table. While a child process is running, the parent process continues to run. When the child process exits. it returns 0 to the parent.

Accessing Memory

Each process is individual and has its own view of memory:

Ex) According to Process 1:

While according to Process 2:

Memory

Each process thinks that it has a machine, but it is actually in virtual memory. Everything in each separate process is kept track of by the physical RAM process table.

Stack

Can a program run without an exit status? Yes!

true.c:

int main(void)

{

return 0;

}

pid_p = fork();

_exit(2);


_exit(2) is a system call. 2 is the exit status that the kernel will put into the process table. If the exit status is 2, it means that the process succeeded. If the exit status is 1, it means that process did not succeed.

pid_t waitpid(pid_t, int*, int);


waitpid waits for a child to complete its execution. pid_t is the pid for the waited-for process and int is the location where it stores its status.

flag(0) or WNOHANG tells the process not to wait.

The exit() Function

exit() is a library function that flushes its output to the physical RAM. It takes a process and kills it. It stores the exit status as an entry in the process table, but it does not reclaim the memory required for the process. The technical term for a killed process (in Linux) is a “zombie”. In comparison, _exit() does not flush the output buffers. It kills the process, but does not reclaim the process table id.

Below is the process table. The exited process stores its exit status, and waits for another process to call waitpid so it can be “buried”.

Process Table

Fork Bomb

while(fork() == 0)

continue;

This kind of program is called a “fork bomb”. The fork() will create a child that will create another child which will create another child... and so on until the process table is full. When the process table is full, fork() will return -1 and set errno.

Even though there is controlled isolation between programs, there is at least one way programs can communicate. One is by using a pipe.

Pipe

A pipe is a bounded queue of bytes that is stored in the kernel memory. You create it by using the following:

int pipe(int fd[2]);

fd is actually int *fd . It is a pointer to an array of 2 bytes (two file descriptors). fd has 2 file descriptors (one for reading and one for writing to the pipe). The reading fd will hang if there is no data, and it will give data if some data is read. Therefore, if the parent pipes and then forks, the parent and child can communicate.