Lecture 4: OS Organization
CS111 Scribe Notes
By Edwin Wong
Date of lecture: 1/16/2013
Virtualization
One method of implementing a virtual system with a protected one way transfer
of control is with an interpreter.
Advantages:
- You have a great degree of control over the interpreter, such as gathering performance stats
- It can be made for a non-existant machine
- Can work for different machines that might not officially support it with hardware
- The CPU and memory costs are high because it is basically simulating memory from your actual machine
- The interpreter might contain bugs (eg. bad timings from clocks)
With an interpreter, we want to be able to add on a few instructions to a
machine and run it inside its kernel. An x86 machine can then be simulated ontop
of a non-x86 machine. The following case does not work as intended because the
callee is vulnerable to changes that the caller makes. Remember
that we also want a protected one way transfer of control.
pushl filename
call unlink // a new instruction
addl $4,sp
In the above case, instead of call unlink, we replace it with an invalid instruction, 242. On an x86 machine, the instruction would not be executed, but instead an interrupt or trap would occur. This allows us to switch to kernel mode, look up the appropriate interrupt type in the interrupt table and execute its code. In the example below, an interrupt is called of type int 0x80, a system call. When the interrupt is executed, the hardware pushes the stack segment (ss), the stack pointer(esp), the processor flags (eflags), the code segment, and the instruction pointer (eip). When reti is executed, the state of the computer to returns back to when the interrupt was executed. If we change, the address pointed to by this interrupt, we can make the kernel execute any code that we want; however, this can be easily fixed by making the interrupt table inaccessible if the caller is found to be suspicious.
Priviledged vs Unpriviledged Instructions:
Priviledged instructions can only be executed by the kernel. User
applications are unable to do so, which adds a layer of protection
against malicious attacks or accidentally executing harmful
instructions by a user application. Ultimately the kernel
is free to protect itself from user applications, but not the other way around.
Priviledged Instructions
- insl -> can read anything from the disk
- outb -> write to screen
- intb -> reads a byte from an I/O port
- int -> initiates an interrrupt
Virtualization Processor- The Layers
Abstraction layers in computer architecture allow for individual components to build from each other, and hides unnecessary information. In the image below, the I/O devicer reads a stream of bytes stored in the hardware, which can then be used by user applications.
x86 systems follows the microkernel approach,where there are separate layers for the memory, the driver, file systems, and applications. In Linux and many Unix based systems, there are only two layers, the Kernel and the unpriviledged. The two layer method is only benefitial if the kernel is perfect and has no bugs. The extra layers of the x86 adds more security at the expense of performance and simplicity.
I/O: How to read bytes from disk?
At the C level:
insl(ID, char* buffer, bytes)
At the machine level:
movl $39, %eax
movl $192, %ebx
lea buf, %ecx
movl $256, %edx
int 0x80 //syscall interrupt
In the left assembly code, a few registers save the given immediate values and
byte addresses then the interrupt 0x80 calls the kernel. At that point, the
kernel can then start reading from disk. Although this works, there are
too many operations and is too low leveled for the needs of most programmers.
At the High level:
char * readline(File *f){
movl $119, %eax
movl f, %ebx low level register
int 0x80
}
How do we implement a high level object using low level concepts?
1) use a pointer to an actual object2) use an integer as a handler by the OS
Problems with readline?
- The operating system must keep track of where you are in the file
- It is too high level-> individual words cannot be read at a time
- What happens when a line is too long? How do we define an acceptable threshold length for ending the readline function?
Because the readline function is too high level, we could consider a lower level implementation that reads sectors.
void read_sector (int device_no, char* buf, int size_sectors)
The read_sector function takes a device number specifying the defice, a char buffer to store the retrieved data, and an integer specifying the number of bytes to read. The problem with this implementation is that, the device number is not a portable method of locating the data that you want to read. Instead, use a file descriptor. The function also requires a sector size variable, but it is very difficult to know the number of bytes in a sector. In addition, the kernel would also need to buffer because the physical memory size can be different than the number of bytes allocated; thus slowing down the function.
A Better Implementation:
We can combine the high level function, read, and a low level function, lseek to perfom I/O from disk. With this implementation, a user has the freedom to find exactly where to start reading and the number of bytes to read from that offset.
off_t lseek(int fd, off_t offset, int flag)
- fd: the file descriptor
- offset: is the offset number of bytes
- flag: 0-> the offset is relative to the start of the file
1-> the offset is relative to the current location
2-> the offset is relative to the end of the file
lseek allows the repositioning of the offset in the file specified by the file descriptor. If it succeeds, it returns the offset location, otherwise it returns -1 to indicate an error. In most implementations, such as the x86, off_t is a signed integer, meaning that for one call to lseek, the maximum offset is 2GiB. A user is also given the option to set the offset past the range of the file.
ssize_t read(int fd, char* buf, size_t count) //count=# of bytes to read
The read function attempts to read up to count bytes from the filedescriptor into the buffer; it returns the number of bytes read on success, otherwise it returns -1. Note that the type of count is size_t, which means that a user can specify up to 32 or 64 bit unsigned number of bytes. However, the return type of the read function is ssize_t, which is a signed version of size_t. read can then only read up to 2GiB of data for a 32bit machine, which is only half of the maximum bytes that can be specified in count.
Other problems:
- Race Conditions: What happens when multiple applications read from the same file?
The OS can serialize the access to that file so that one applications write can occur before another's read but not at the same time.