BRIDGE Exam 4
6 OS Designs
1. Monolithic 2. Layered 3. Microkernels 4. Client-server 5. Virtual machines 6. Exokernels
Items Required for a Deadlock
1. Mutual Exclusion 2. Hold and Wait 3. No Preemption 4. Circular Wait
5 Generations of OS's
1. Vaccuum Tubes (1945-1955) 2. Transistors and Batch Systems (1955-1965) 3. ICs and Multiprogramming (1965-1980) 4. Personal Computers (1980-Present) 5. Mobile Computers (1990-Present)
Graphical Processing Unit (GPU)
A GPU is a processor with, literally, thousands of tiny cores. They are very good for many small computations done in parallel, like rendering polygons in graphics applications. They are not so good at serial tasks. They are also hard to program. While GPUs can be useful for operating systems (e.g., encryption or processing of network traffic), it is not likely that much of the operating system itself will run on the GPUs.
Circular Wait
A closed chain of processes exists, such that each process holds at least one resource needed by the next process in the chain.
Buddy System
A compromise between the fixed and dynamic partitioning. There's not a lot of OS's that do this today, in fact I don't know any of them, but for example, Linux uses this for what it calls its kernel slab allocator. So, when the kernel itself needs to create some memory for the kernel, it will use a chunk of memory and divide that chunk of memory as a buddy system, just to make smaller partitions of a larger chunk of main memory. So, while we don't do this on a large scale for all of main memory, there are certainly situations where even Linux today will do this for parts of its slab allocator for creation of, or for allocation of smaller parts of main memory for kernel purposes. Anyhow, what it does is we take main memory and divide it in multiples of two. And the reason of course that we're doing multiples of two is because it makes it easier on the calculations; we don't have to necessarily record the starting in the ending point, we can record what multiple of two this is and so on and so forth. Let's take for example, a two megabyte memory allocation, so let's say the the Linux kernel has a two megabyte memory allocation for it and wants to divide it up for purposes for storage of an IO table, or storage of a main memory management table, or who knows. It's got to store some portion of information but two megabytes would be a huge waste, it needs something much smaller. The kernel means is let's say one hundred kilobytes, for whatever it's going to do. The process then is that we take that two megabytes and start breaking it down into divisions, so you can see that what we've created initially, we take the two megabytes and divide it in half so that we end up with the a one thousand and twenty four kilobyte allocation and another one thousand twenty four kilobyte allocation. But that's way over what we need of hundred kilobytes, so we break down one of the one thousand twenty-four kilobytes into two five hundred twelve kilobyte allocations... We might end up with some extra fragmentation if we have a lot of these small slivers but one of the nice features of the buddy system is what's called coalescing. We can take those two hundred twenty-eight kilobit allocations, and recombine them up. Coalescing allows us to bring these back into a larger hole. So, we can we can create a lot of one hundred twenty-eight kilobyte partitions out of this original two megabytes. So, we end up decreasing the amount of internal fragmentation, overall, because if we had only two megabyte allocations there would be one point nine megabytes of completely wasted space, because we'd have to allocate two megabytes for one hundred kilobyte allocation. It makes the operating system structures easier, because they're beginning and ending on a two to the N boundary it. It actually does work quite well, the only problem is coalescing back does take some effort. And so Linux actually delays that, it's called delayed coalescing. It waits until much later on, when the OS is not very busy and it says: "you're probably not going to use these anymore" and then it recombines them back up. But ultimately this does work; it doesn't work on a grand scale, it works on a micro scale. But it is a solution that involves both fixed and dynamic partitioning and it is a compromise between the two.
State
A condition that a program might be in and the process is going to spend quite a significant amount of time in each state. So each state has to be recognized and has to be recorded by the operating system.
User Level Threads
A leftover of a bygone era although that still very much in use today. The biggest downside of a user level thread is that the OS doesn't recognize that the threads are in use. There is a library of code inside our program, which creates or simulates creating of threads and switching between these threads. So, what would be required would be some scheduling algorithms and some ability to create new threads and destroy threads, inside a user's program. But all of this is a simulation and the OS doesn't really know that multiple threads, if you want to call them that, are being created here. Instead what it does is recognize that one process is being created. And the biggest downside to that is that if any of those threads, and I use quotations, those threads cause a blocking system call then all of those threads are going to be blocked because the OS just sees it as a single process. The advantage of a user level thread is that you can choose to do whatever scheduling algorithm you want; you're not at the behest of the OS. In a kernel level thread the operating system decides when your threads are going to run, but in a user level thread you, the programmer, decide when that thread is going to run and how often it's going to be run. So, you could have three threads and give priority to one of the three threads to run more frequently than the other two, and you can't do that with kernel level threads easily.
Hardware Abstraction Layer (HAL)
A low-level system driver in Windows OS that controls communication between Windows and the computer hardware. A set of functions that the kernel could call on to perform tasks on different, on the various hardware. So, rather than directly programming the timer in the system to say: let this process run for twenty milliseconds. The hardware abstraction, the operating system would call a function inside the hardware abstraction layer to say program the timer to let this process run for twenty milliseconds. And the reason that that's essential is because Microsoft can change out the HAL and produce an operating system that can now run on a completely different hardware. Without making any other changes to the operating system, it can change just the functions inside the HAL and those functions will, those features will take effect on the new hardware.
Child Process
A process (parent) can create one or more other processes (referred to as child processes) and these processes in turn can create child processes.
Process vs Thread
A process is an executing instance of an application. A thread is a path of execution within a process.
Process & Address Space
A process is basically a program in execution. The most central concept in any operating system is the process: an abstraction of a running program. Everything else hinges on this concept, and the operating system designer (and student) should have a thorough understanding of what a process is as early as possible. Processes are one of the oldest and most important abstractions that operating systems provide. They support the ability to have (pseudo) concurrent operation ev en when there is only one CPU available. They turn a single CPU into multiple virtual CPUs. Without the process abstraction, modern computing could not exist. A process is just an instance of an executing program, including the current values of the program counter, registers, and variables. Associated with each process is its address space, a list of memory locations from 0 to some maximum, which the process can read and write. The address space contains the executable program, the program's data, and its stack. Also associated with each process is a set of resources, commonly including registers (including the program counter and stack pointer), a list of open files, outstanding alarms, lists of related processes, and all the other information needed to run the program. A process is fundamentally a container that holds all the information needed to run a program.
Hold and Wait
A process must be holding at least one resource and waiting to acquire additional resources that are currently being held by other processes. While the thread is waiting, it is holding onto the other locks.
Daemon
A process that performs some useful tasks in the background.
Operating System (OS)
A program that controls execution of user programs and acts as an interface between applications and computer hardware. OS have two purposes: to provide abstractions for otherwise difficult to deal with hardware; and to manage hardware resources for applications.
Program Counter
A register that contains the memory address of the next instruction to be fetched. After that instruction has been fetched, the program counter is updated to point to its successor.
Stack Pointer
A register that points to the top of the current stack in memory. The stack contains one frame for each procedure that has been entered but not yet exited. A procedure's stack frame holds those input parameters, local variables, and temporary variables that are not kept in registers.
System Call
A request to the OS - a request for an OS service. To obtain services from the operating system, a user program must make a system call, which traps into the kernel and invokes the operating system. The TRAP instruction switches from user mode to kernel mode and starts the operating system. When the work has been completed, control is returned to the user program at the instruction following the system call.
No Preemption
A resource can be released only voluntarily by the process holding it, after that process has completed its task, i.e. the OS cannot remove the resource from the thread forcibly.
Virtual Memory
A scheme that makes it possible to run programs larger than physical memory by placing them on the disk and using main memory as a kind of cache for the most heavily executed parts. This scheme requires remapping memory addresses on the fly to convert the address the program generated to the physical address in RAM where the word is located. This mapping is done by a part of the CPU called the MMU (Memory Management Unit). If we're using a paging or segmentation system, we have to recognize that the process is only going to be making one memory reference at any given time. Now I know that's an overly simplistic view of things, it's actually going to be making a lot of memory references. But one of the things that you can see is that the memory references are all going to be pretty much clustered together. If we, for example, start a process and the process throws a splash screen up on the screen. You know starting Word or PowerPoint, that splash screen will never appear again during that run of the program. But that splash screen takes a memory, there's code that tells it how to display that splash screen, and there's the splash screen itself probably an image. So, that memory is loaded into main memory and it's never going to be used again. So the question then becomes: why should still be loaded in main memory, which is a finite quantity very much in demand, why can't we take it out of main memory and put it on the hard drive? Somewhere that we can bring it back if we ever need to, if the process never needs it again then it can just be deleted off the hard drive and never recovered. But what if the the process was never going to use that splash screen for that run of the process again. And what if we just took it out of main memory and left it on a hard drive. And if we did that we would free up some main memory and we would allow that main memory to be used by other processes. So, this is the concept known as virtual memory; what we're doing is we're using a separate portion of the hard drive, it's not where the process is normally stored. It's a separate portion of the hard drive; Windows calls a virtual memory, I think Mac OS calls it the swap space, most Linux and Unix clones call it a swap space. But we have this area of a Hard drive dedicated for storing pages of main memory, which are no longer in main memory, which are no longer going to be used. Now the OS can decide, dynamically, which pages to save in main memory, which pages stay in memory and which pages come out. And there has to be some operations that has to go back and forth between main memory and the secondary storage device, but it's a mechanism for actually saving a lot of main memory for more processes than would normally be allowed to fit. So, now if we say have only four gigabytes of main memory, and we want to run one hundred processes, each of which needs a gigabyte of main memory, under normal circumstances there's no way that we could do it. But with virtual memory, we would be allowed to do that; each of the hundred processes would have a portion of the four gigabytes, a portion of itself loaded in the four gigabytes, the rest of it would be on secondary storage and available. So, we have to move these pages in and out into main memory, and a lot of discussion to talk about how that's done, but we have that available now. We can use more memory than is actually in the system. Each process even, can view main memory as completely available purely to itself. In a 32-bit address space, each process is going to think that it has 4 gigabytes of main memory available; it doesn't. There may not even be 4 gigabytes of main memory available in the system. But that process is going to think that it has 4 gigabytes because it's using virtual memory. In a 64-bit operating system, programmers are going to perceive a 18-exabyte memory space which is just enormous. Even if the system doesn't have that much memory the program can be written as if it were available. The OS can remove unused pages like a splash screen from main memory that are not going to ever be used again. More processes can run in the system and that means we have better performance, that means more processes generally means that there's a higher probability that a process will find itself in the ready state, so that will find a process that's in the ready state. So, ultimately we can result in better system performance just by having more main memory available. Huge benefits available for virtual memory, which is why it's the standard.
Mutex
A shared variable that can be in one of two states: unlocked or locked. Consequently, only 1 bit is required to represent it, but in practice an integer often is used, with 0 meaning unlocked and all other values meaning locked.Tw o procedures are used with mutexes. When a thread (or process) needs access to a critical region, it calls mutex lock. If the mutex is currently unlocked (meaning that the critical region is available), the call succeeds and the calling thread is free to enter the critical region. On the other hand, if the mutex is already locked, the calling thread is blocked until the thread in the critical region is finished and calls mutex unlock. If multiple threads are blocked on the mutex, one of them is chosen at random and allowed to acquire the lock.
Client-Server OS Design
A slight variation of the microkernel idea is to distinguish two classes of processes, the servers, each of which provides some service, and the clients, which use these services. This model is known as the client-server model. Often the lowest layer is a microkernel, but that is not required. The essence is the presence of client processes and server processes. Communication between clients and servers is often by message passing. To obtain a service, a client process constructs a message saying what it wants and sends it to the appropriate service. The service then does the work and sends back the answer. If the client and server happen to run on the same machine, certain optimizations are possible, but conceptually, we are still talking about message passing here. An obvious generalization of this idea is to have the clients and servers run on different computers, connected by a local or wide-area network. Since clients communicate with servers by sending messages, the clients need not know whether the messages are handled locally on their own machines, or whether they are sent across a network to servers on a remote machine. As far as the client is concerned, the same thing happens in both cases: requests are sent and replies come back. Thus the client-server model is an abstraction that can be used for a single machine or for a network of machines.
Pipe
A sort of pseudofile that can be used to connect two processes. If processes A and B wish to talk using a pipe, they must set it up in advance. When process A wants to send data to process B, it writes on the pipe as though it were an output file. In fact, the implementation of a pipe is very much like that of a file. Process B can read the data by reading from the pipe as though it were an input file. Thus, communication between processes in UNIX looks very much like ordinary file reads and writes. Stronger yet, the only way a process can discover that the output file it is writing on is not really a file, but a pipe, is by making a special system call.
Memory Hierarchy
A structure that uses multiple levels of memories; as the distance from the processor increases, the size of the memories and the access time both increase. Fast memory close to CPU is more expensive.
Disabling Interrupts
A way to avoid two processes being in a critical section at the same time. Not a good solution.
Supplier-Demander Problem
Aka Producer-Consumer, aka Bounded-Buffer Problem. problem). Two processes share a common, fixed-size buffer. One of them, the producer, puts information into the buffer, and the other one, the consumer, takes it out. (It is also possible to generalize the problem to have m producers and n consumers. Multiple threads running of either a supplier thread or demander thread. Think about a factory for example where things are coming off the production line there's a lot of people producing things, there's a lot of people taking those things and moving them on to the next stage in the production. This has a critical section because there are shared variables: the buffer and the buffer count. The buffer count tells us how many items there really are in the buffer. The buffer can store five hundred items. The buffer count let's say it's initialized at zero there's nothing in the buffer. When the morning starts the suppliers come in and they start producing things that go into the buffer. Now the supplier just has a simple piece of code that says as long as I'm not finished then I'll put something in the buffer. Now the reason that we have that while not done is because it's possible that the buffer is full and if the buffer is full we want this thread to just kind of pause, pause for maybe half a second and let one of the demanders take something out of the buffer so it makes space and then we can put this thing in the buffer. We can update the buffer count and then we can be finished. So the suppliers a very simple piece of code the demander is almost the same thing except it checks to see that there's actually something in the buffer. As long as there is something in the buffer we'll decrement the buffer count and return the item that was in the buffer at the position. So that's the supplier demander idea. Now we do have multiple supplier threads and multiple demander threads they're all active they're all running in the system. However we've got a one processor system so we can only run one thread at a time we choose either supplier or we choose demander and we have to remember that there are still going to be interrupts they're still going to be preemption there still going to be situations where the thread is interrupted and we don't know when it's going to be interrupted. Now the core idea of this is that the buffer count should never exceed five hundred. We should never exceed five hundred because there are only five hundred places to store in the buffer. So we can recognize that the buffer count can get as high as five hundred but it can't ever exceed five hundred. Unfortunately as we'll see because of asynchrony because we haven't taken care of the fact that the code is protected by a critical section that buffer count can easily go higher than five hundred. So here we can see the steps that we can run through to produce this problem. The supplier thread checks to make sure that the buffer count is less than 500, and the buffer count is 499 so that thread is allowed to enter that if statement. It goes past that if statement and then right at that point for whatever reason either because of preemption or because of a hardware interrupt we get an interrupt. So, now we have an interrupt and when we run from the interrupt, when we return from the interrupt the operating system has to choose a thread to run and it's not guaranteed that it's going to choose thread one again. Unfortunately, it chooses to run thread two, which is another producer. The buffer count is 499, so it enters into the if statement., increments the buffer count now to 500. The last time thread one ran we didn't take into account the critical section. we didn't take into account the possibility for asynchrony, so now we don't go back and recheck the buffer count so the buffer count now is 500 but for thread one the last time it checked it was 499 so when it runs again it increments buffer count to 501. So asynchrony can cause us to run out of buffer space is it's not accounted for.
New State
All processes start out in a new state. Now we have to recognize that the process is going to spend quite a significant amount of time in each of these states. So, why would a process spend a significant amount of time in the new state. We have to think about the process being created and the procedure that we would go through to create a process inside the operating system. And the new state is when that process is being executed by the operating system. Now one thing that we need to recognize is that a state is an atomic element. And if the operating system can complete everything atomically, then there's really no reason to have a state. So, the new state happens because the process is being created and the code for that process, one of the reasons is the code for that process will have to be loaded from a secondary storage device into main memory. And that's an IO operation and I operate generally take an enormous amount of time, and we'll see this in a later model, but for now we just say that an IO operation takes a really long time. And the operating system doesn't want to sit in wait for the IO operation to complete; it wants to go on to do other things. But it needs to keep track of where this process is in the act of creation and the way to do that is the new state. So all processes in the new state are in the act of being created and their code being loaded into main memory.
Process Table
All the information about each process, other than the contents of its own address space, is stored in an operating system table called the process table, which is an array of structures, one for each process currently in existence.
Kernel Level Threads
All the work of thread management is done by the kernel. The OS recognizes that a thread has been created and the OS will create a TCB for that thread and the OS can choose to run that thread or can choose to block that thread and all the states that go along with that thread.
Page Fault
An event that occurs when an accessed page is not present in main memory.
Multiprocessor OS
An increasingly common way to get major-league computing power is to connect multiple CPUs into a single system. Depending on precisely how they are connected and what is shared, these systems are called parallel computers, multicomputers, or multiprocessors. They need special operating systems, but often these are variations on the server operating systems, with special features for communication, connectivity, and consistency. With the recent advent of multicore chips for personal computers, even conventional desktop and notebook operating systems are starting to deal with at least small-scale multiprocessors and the number of cores is likely to grow over time. Luckily, quite a bit is known about multiprocessor operating systems from years of previous research, so using this knowledge in multicore systems should not be hard. The hard part will be having applications make use of all this computing power. Many popular operating systems, including Windows and Linux, run on multiprocessors.
UNIX
An operating system originally conceived in 1969 by Ken Thompson and Dennis Ritchie of AT&T's Bell Labs. In 1974, the UNIX code was rewritten in the standard programming language C. Today there are various commercial versions of UNIX. Open source, multi-user, multi-tasking.
Exit State
And when it's done the process moves to the exit state. And in the exit state the process is just waiting to be cleaned up and terminated. And there's one key element that has to be returned back to the parent process, which we'll talk about later and linkages, and that is the return value. Remember return zero from int main, well that has to be given back to the parent process. And a process that in the exit state has not yet returned back its element, its return value, to the parent process and that's why we have the five states that we have.
Mounted File System
Another important concept in UNIX is the mounted file system. To provide an elegant way to deal with removable media UNIX allows the file system on the disk to be attached to the main tree. UNIX does not allow path names to be prefixed by a drive name or number; that would be precisely the kind of device dependence that operating systems ought to eliminate. Instead, the mount system call allows the file system on the disk to be attached to the root file system wherever the program wants it to be.
Trap
Any single-CPU computer can execute only one instruction at a time. If a process is running a user program in user mode and needs a system service, such as reading data from a file, it has to execute a trap instruction to transfer control to the operating system. The OS then figures out what the calling process wants by inspecting the parameters. Then it carries out the system call and returns control to the instruction following the system call. In a sense, making a system call is like making a special kind of procedure call, only system calls enter the kernel and procedure calls do not. This is a situation where the process has done something that causes it to need the OS, even though it doesn't know it needs the OS. And we can also have a blocking system call; these are obvious like the program is trying to open up a file. If the program is trying to open up a file, it's going to need to invoke components of the OS and so the responsibility switches. The process will switch to running the OS and when it does that the OS will take care of opening up the file and reading in the data. Now a process which involves quite a lot of steps, most importantly, we need to save the context of that process. We're gonna save that in the PCB. And then we need to move that PCB into the appropriate queue whether it's the blocked queue or the ready queue to indicate what we're going to next do with this process, and then we need to service whatever was requested; deal with the interrupt, deal with the blocking system call or deal with the trap. And then choose another process that's ready to run so we're going to look to the ready queue and choose one of the processes on the ready queue to move into the running state. And then we're going to actually run that new process by restoring its context.
Working Directory
At every instant, each process has a current working directory, in which path names not beginning with a slash are looked for. Processes can change their working directory by issuing a system call specifying the new working directory.
BIOS
BIOS (Basic Input Output System). The BIOS contains low-level I/O software, including procedures to read the keyboard, write to the screen, and do disk I/O, among other things. Nowadays, it is held in a flash RAM, which is nonvolatile but which can be updated by the operating system when bugs are found in the BIOS. When the computer is booted, the BIOS is started.started. It first checks to see how much RAM is installed and whether the keyboard and other basic devices are installed and responding correctly. It starts out by scanning the PCIe and PCI buses to detect all the devices attached to them. If the devices present are different from when the system was last booted, the new devices are configured. The BIOS then determines the boot device by trying a list of devices stored in the CMOS memory. The user can change this list by entering a BIOS configuration program just after booting. Typically, an attempt is made to boot from a CD-ROM (or sometimes USB) drive, if one is present. If that fails, the system boots from the hard disk. The first sector from the boot device is read into memory and executed. This sector contains a program that normally examines the partition table at the end of the boot sector to determine which partition is active. Then a secondary boot loader is read in from that partition. This loader reads in the operating system from the active partition and starts it. The operating system then queries the BIOS to get the configuration information. For each device, it checks to see if it has the device driver. If not, it asks the user to insert a CD-ROM containing the driver (supplied by the device's manufacturer) or to download it from the Internet. Once it has all the device drivers, the operating system loads them into the kernel. Then it initializes its tables, creates whatever background processes are needed, and starts up a login program or GUI.
Register
Because accessing memory to get an instruction or data word takes much longer than executing an instruction, all CPUs contain some registers inside to hold key variables and temporary results. Thus the instruction set generally contains instructions to load a word from memory into a register, and store a word from a register into memory. Other instructions combine two operands from registers, memory, or both into a result, such as adding two words and storing the result in a register or in memory.
File Descriptor
Before a file can be read or written, it must be opened, at which time the permissions are checked. If the access is permitted, the system returns a small integer called a file descriptor to use in subsequent operations. If the access is prohibited, an error code is returned.
Hardware Solutions - Mutual Exclusion
Built into the CPU. First is the disabling interrupts. Not a great idea because interrupts are a necessity. If an interrupt does occurs it means a piece of hardware needs immediate servicing. Imagine a situation where a thread disables interrupts and then crashes; the whole system is stuck you can't get out except with a big power switch. So, disabling interrupts is something that we use very sparingly. The other options are built into the instruction set on the processor. So, the first one is called a test and set where we check a memory location and change its value as a Boolean flag variable, which says that somebody is in the critical section. This is an atomic machine level instruction. So, the CPU takes control of the system bus, goes and checks the location in memory, and then if it is the zero, goes resets that value to one in one atomic instruction; nobody else will be allowed to access that memory location. So, even if we have another processor it will not be allowed access to the memory location during a test and set because it has to happen atomically. A process is allowed to go into the critical section when the variable has been tested to make sure nobody's in their critical section, and then it's set it to indicate the critical section is now occupied. If we have a failure, the thread has to pause and delay and try it again. The test and set actually works fairly well. The other option is very similar. It's called exchange. It is a very common one that we use today, in which we have a swapping of a location in main memory with that of a register. So, we can again use this is a Boolean flag variable, except now we can put a one in a particular register, call exchange for the memory location and we literally get a swap of the value in the register with the value in the memory location. So, if the memory location had a zero, it will now be set to one and we can go back afterwards and check to make sure that it was a zero, if it was a one then we'll know that it was a one because we'll get it back. But the nice feature is that when we do the exchange, even if we're interrupted immediately after the exchange instruction, the indication that we're in the critical section has already been set and now we just have to check to make sure whether it was successful or not. So, the exchange instruction is a very common one that's in use today and it's relatively easy to understand.
Monolithic OS Design
By far the most common organization, in the monolithic approach the entire OS runs as a single program in kernel mode. The OS is written as a collection of procedures, linked together into a single large executable binary program. When this technique is used, each procedure in the system is free to call any other one, if the latter provides some useful computation that the former needs. Being able to call any procedure you want is very efficient, but having thousands of procedures that can call each other without restriction may also lead to a system that is unwieldy and difficult to understand. Also, a crash in any of these procedures will take down the entire OS. To construct the actual object program of the operating system when this approach is used, one first compiles all the individual procedures (or the files containing the procedures) and then binds them all together into a single executable file using the system linker. In terms of information hiding, there is essentially none—every procedure is visible to every other procedure. Even in monolithic systems, however, it is possible to have some structure. The services (system calls) provided by the operating system are requested by putting the parameters in a well-defined place (e.g., on the stack) and then executing a trap instruction. This instruction switches the machine from user mode to kernel mode and transfers control to the operating system, shown as step 6 in Fig. 1-17. The operating system then fetches the parameters and determines which system call is to be carried out. After that, it indexes into a table that contains in slot k a pointer to the procedure that carries out system call k.
Interprocess Communication
Communication between related processes that are cooperating to get some job done to synchronize their activities with one another.
Multiprogramming
Conceptually, each process has its own virtual CPU. In reality, of course, the real CPU switches back and forth from process to process, but to understand the system, it is much easier to think about a collection of processes running in (pseudo) parallel than to try to keep track of how the CPU switches from program to program. This rapid switching back and forth is called multiprogramming.
Dining Philosophers Problem
Considered a classic synchronization problem not because of its practical importance but because it is an example of a large class of concurrency-control problems. It is a simple representation of the need to allocate several resources among several processes in a deadlock-free and starvation-free manner. Dr. Dijkstra presented it as a concurrency control problem. There's a house where we have five philosophers numbered 0 through 4. And they alternate between eating and thinking. There's a circular table and it has a place setting for each philosopher. And the place setting consists of one fork on the left side, and one plate in front of the philosopher. The meal is a particularly difficult to eat kind of spaghetti. You need to have two forks to eat this kind of spaghetti. And unfortunately, each philosopher is only given one fork. But we recognize that there's a fork on the left of the philosopher and a fork on the right of the philosopher. The way that we can think about this is that when the philosopher comes he can pick up the fork on the left, and he can try and pick up the fork on the right, the unfortunate problem is that if all five philosophers come to the table at exactly the same time, all of them will pick up their left forks and there will be no right forks. In other words, all of them are holding a lock where there's mutual exclusion because the philosopher doesn't let go of his fork. There's hold and wait because all of them are holding the lock on the left fork and waiting for the lock on the right fork. All of them have no preemption so nobody's going to reach across the table and start grabbing a fork these are nice philosophers. And we have a circular wait, since the last philosopher in a sequence waiting for the first philosopher on the sequence. So, ultimately what we have is all four requirements and we have a deadlock. In this scenario all the philosophers will starve to death, because none of them will put down the fork and allow somebody else to eat. What's the solution? Dijkstra proposed the solution of resource ordering. That's the easiest one. And he said, let's order the forks 0 through 4. If you pick up fork 0, you're not allowed to pick up fork 4, and that leaves fork 4 for the philosopher on his right to pick up, and that means that the fourth philosopher will be allowed to eat because he has his own fork plus the last fork. We can't have a larger fork and a smaller number fork at the same time unless you ask for the smaller fork first then the larger fork second. So ultimately what this means is that one of the philosophers will get both forks, will eat, and put both forks down, allowing more philosophers to eat and more philosophers to eat and ultimately, everybody eats. So that's the resource ordering solution. There's another solution with a semaphore. We're going to have semaphores for each of the forks, you can see that already cause we need mutual exclusion on each of the individual forks. What we're talking about is adding yet another semaphore, and saying that there's a semaphore that has to be met that has to hold mutual exclusion for the entire room. So with the semaphore solution, we have four signals that go into the queue under semaphore; the first 4 philosophers are allowed to enter the room, and they're allowed to eat. The last philosopher, of course the fourth philosopher, will pick up the fork from the fifth philosopher because he's not allowed in the room at all. And that allows the fourth philosopher to eat, and then he'll put his fork down, allowing the third philosopher to eat, and then he'll put his fork down, and so on and so forth. Eventually, after the fourth philosopher leaves the room, the fifth philosopher will be allowed in and he'll eventually be allowed both forks; everybody is allowed to eat and the problem is resolved. So, we've got two scenarios here and there's actually even more solutions that get even more complex, for example using a monitor. The easiest to understand are that we order the resources (forks), or we use a semaphore to prevent one of the philosophers from even entering the room.
Suspended Process
Consists of its address space, usually called the core image (in honor of the magnetic core memories used in days of yore), and its process table entry, which contains the contents of its registers and many other items needed to restart the process later.
COW
Copy-on-Write. Allows both parent and child processes, or any two process that want to share pages, to initially share the same pages in memory. If a page gets updated, then a copy is made, otherwise there is no need to make multiple copies of shared pages.
EEPROM
EEPROM (Electrically Erasable Programmable Read-Only Memory) and flash memory are also nonvolatile, but in contrast to ROM can be erased and rewritten. However, writing them takes orders of magnitude more time than writing RAM, so they are used in the same way ROM is, only with the additional feature that it is now possible to correct bugs in programs they hold by rewriting them in the field. Flash memory is also commonly used as the storage medium in portable electronic devices. It serves as film in digital cameras and as the disk in portable music players, to name just two uses. Flash memory is intermediate in speed between RAM and disk. Also, unlike disk memory, if it is erased too many times, it wears out.
Levels - Microkernel vs. Macrokernel
Each layer provides services to the layers above. The levels above use the services of the levels below. Microkernel are the 3 needed parts for an OS: primitive processes, secondary storage, virtual memory. Primitive processes are very low level pieces of code mostly scheduling and resource management. Secondary storage used to access all the other code and components of the operating system not in main memory. Virtual memory or memory management to control what's in main memory. Macrokernel has everything else: systems communication, subsystems for getting information into and out of the computer, the file systems, etc.
Sensor-Node OS
Each sensor node is a real computer, with a CPU, RAM, ROM, and one or more environmental sensors. It runs a small, but real operating system, usually one that is event driven, responding to external events or making measurements periodically based on an internal clock. The operating system has to be small and simple because the nodes have little RAM and battery lifetime is a major issue. Also, as with embedded systems, all the programs are loaded in advance; users do not suddenly start programs they downloaded from the Internet, which makes the design much simpler. TinyOS is a well-known operating system for a sensor node.
Deadlock Prevention
Eliminate one of the four requirements for a deadlock. Mutual exclusion - really hard to get rid of mutual exclusion because it's usually a requirement so get- ting rid of mutual exclusion is usually not a possibility. Hold-and-wait - we can say that all locks are required to be obtained at the same time and either they're all approved or the thread is blocked. Now if we do that then there's no additional lock that might occur. If a thread holds a lock, it cant obtain another lock, which means that we can't get into a situation where we're holding a lock and we're asking for a new lock and therefore, we're blocked because we're asking for the new lock. So, if we make the threads ask for all of its locks at the same time, then we've eliminated the possibility of hold-and-wait. No preemption - we can say that if a thread requests a new lock and the new lock is denied then the thread has to release all of its existing locks. It's got to require a callback from the OS to the thread to say if the request for the lock is denied unlock everything else. The OS might also have the authority to remove existing locks individually, but that's a bit of a bad idea. Circular wait - an easy one to deal with especially if we're talking about locks on individual items that the programmer can control; one of the easiest solutions for dealing with the circular wait is to order all the resources. We can number them and order them so that there's never a possibility of holding a higher level resource and requesting a lower level resource. So, one of the problems of the circular weight is that the last thread is waiting on the first thread. What that means is that the first thread holds a lower lock and the last thread holds a higher level lock and if we prevent the last thread from accessing its lower level lock, because it holds the higher lock number then we've prevented the situation of the circular wait. Either that higher level, that last thread will either release the higher level lock and then request the lower level lock allowing other threads to continue or it won't be allowed to do it. So, the circular wait is actually an easy one and it's the one that's recommended by a lot of computer scientists because the programmer can protect his own program internally, just simply by ordering the locks, ordering resources. So, that we never ask for a lower lock while we're holding a higher lock.
Embedded OS
Embedded systems run on the computers that control devices that are not generally thought of as computers and which do not accept user-installed software. Typical examples are microwave ovens, TV sets, cars, DVD recorders, traditional phones, and MP3 players. The main property which distinguishes embedded systems from handhelds is the certainty that no untrusted software will ever run on it. You cannot download new applications to your microwave oven—all the software is in ROM. This means that there is no need for protection between applications, leading to design simplification. Systems such as Embedded Linux, QNX and VxWorks are popular in this domain.
Path Name & Root Directory
Every file within the directory hierarchy can be specified by giving its path name from the top of the directory hierarchy, the root directory. Such absolute path names consist of the list of directories that must be traversed from the root directory to get to the file, with slashes separating the components.
I/O Subsystem
Every operating system has an I/O subsystem for managing its I/O devices. Some of the I/O software is device independent, that is, applies to many or all I/O devices equally well. Other parts of it, such as device drivers, are specific to particular I/O devices.
Process Creation
Four principal events cause processes to be created: 1. System initialization. 2. Execution of a process-creation system call by a running process. 3. A user request to create a new process. 4. Initiation of a batch job.
Partitioning Strategies
How to break down main memory into different sections so that the programs can all be in their own individual sections: 1. Fixed Partitioning 2. Dynamic Partitioning 3. Buddy System 4. Paging (Simple) 5. Segmentation (Simple)
Process Hierarchies
In UNIX, a process and all of its children and further descendants together form a process group. When a user sends a signal from the keyboard, the signal is delivered to all members of the process group currently associated with the keyboard (usually all active processes that were created in the current window). Individually, each process can catch the signal, ignore the signal, or take the default action, which is to be killed by the signal. In UNIX, all the processes in the whole system belong to a single tree, with init at the root. In contrast, Windows has no concept of a process hierarchy. All processes are equal. The only hint of a process hierarchy is that when a process is created, the parent is given a special token (called a handle) that it can use to control the child. However, it is free to pass this token to some other process, thus invalidating the hierarchy. Processes in UNIX cannot disinherit their children.
ROM
In addition to the main memory, many computers have a small amount of nonvolatile random-access memory. Unlike RAM, nonvolatile memory does not lose its contents when the power is switched off. ROM (Read Only Memory) is programmed at the factory and cannot be changed afterward. It is fast and inexpensive. On some computers, the bootstrap loader used to start the computer is contained in ROM. Also, some I/O cards come with ROM for handling low-level device control.
Machine Architecture
Instruction set, memory organization, I/O, and bus structure. The machine-language level is primitive and awkward to program, especially for input/output.
Atomic Instructions
Instructions that run from start to end without interruption. If there is a need for a section of code to run atomically, then that section is also a critical section, and should be protected using a mutual exclusion method.
Deadlock Detection
It's really easy going. All it does is say we have all the requirements, there's a possibility for a deadlock, so we'll check periodically to see that the deadlock is not occurring. It's not at all restrictive; we're not saying that we were going to stop any situation from occurring. We're saying deadlocks will occur and when they do occur we're going to have to deal with them. Once we detect the deadlock, we can roll back the deadlock process or thread to a previous unlock state. That's useful for things like SQL servers and database servers, but it requires transaction logs. We could also just say let's abort all the deadlock threads. We'll kill them all. We could abort a single thread and see if the deadlock is being resolved, so we can arbitrarily just pick one of the deadlock threads and say it's now killed, its resources are released, and now see if the system is back to a stable state, let it run again and see if there's a deadlock or not. Or we can start preempting resources. Unfortunately, in all of these situations we have the potential, except for rolling back, we have the potential for catastrophic loss of data or corruption of data. Basically we're saying that we don't think that the system is in a consistent state, but we really have no other resolution other than to just say we have to take some emergency action and that means we recognize that if the data is corrupt we have to deal with that. So, deadlock detection says, just let it happen and then we'll deal with it afterwards, we hope it doesn't happen. And believe it or not this is the one that's most common today. So what happens we kill it. We are the deadlock detection algorithm; we have recognized that a deadlock has occurred and we abort all the deadlock threads by killing the entire process. So, congratulations everybody has been promoted to a deadlock detection algorithm.
Clock Replacement Algorithm
Keep a pointer that goes around and that's why we call it the clock page replacement algorithm. Now when we load up a page, we're going to set it's use bit equal to one and that's fine. So, we're going to have N frames and let's just say it's ten frames, just for easy math. We have ten frames and as the frames, as the pages are loaded up into those frames, we said all their use bits equal to zero and the pointer is pointing at let's say a page, frame number two, as you see it in the the left side image. When it's pointing at page number two, you can see that the use bit for frame number two page forty-five is set to one, the use bit for frame number three page one-ninety-one is set equal to one but the use bit for frame number four page five-hundred fifty- six to set equal to zero. So, what happens now when we have a page fault, we're going to have to remove a page and we're going to have to load up the new page. So, to do that what we do is real look for advancing the pointer; we're looking for use bits of zero. But if we find a use bit of one, we're going to reset its value back to zero. We're not going to steal it quite yet. We just set the use bit back zero. So, frame two and frame three as you can see they their page numbers didn't change but their use bits didn't change to zero. Now we need to load page seven-hundred twenty-seven but page five-hundred fifty-six, which is loaded in frame four is unused. So, when the pointer gets around to that point, we're going to remove that page stored in a second or a storage device if the modify bit or whatever, and load up page seven twenty-seven and set its use bit equal to one. The reason that this is effective is because if page forty-five is no longer in use, then next time the clock pointer comes around to frame two, the use bit will still be set to zero because it's not in use and page forty-five will be stolen now. So, the clock page replacement algorithm does have a very high efficacy rate; it's very effective in not throwing out pages which are actively in use, and it does have a pretty good rate of throwing out pages which aren't in use. So, we can save a lot of memory using this and it's really very simple to implement doesn't require a lot of effort to implement.
Fixed Partitioning
Main memory is divided into a number of static partitions at system generation time. A process may be loaded into a partition of equal or greater size. When the system boots up, main memory is divided into some static number of parts. Those could be equal or unequal sized partitions. If main memory is broken down into 16 equal partitions, it means we have a maximum limit of running 16 different processes. Another problem is if we start up a small program like notepad that doesn't use much of its partition, then we're wasting main memory, which is due to internal fragmentation. We could use unequal sized partitions to minimize the amount of internal fragmentation, but we're always going to have some internal fragmentation with these solutions. If we use unequal sized partitions, we're going to have to decide which partition to place each process in, and we may have to relocate processes, so there are some real disadvantages to having fixed partition. But one of the big advantages is that it is really simple, really requires very little effort on the part of the OS. But the downside is that we have very limiting factors on the number of processes that we can run, and we also waste a lot of memory in internal fragmentation.
RAM
Main memory is usually called RAM (Random Access Memory). Old-timers sometimes call it core memory, because computers in the 1950s and 1960s used tiny magnetizable ferrite cores for main memory. They hav e been gone for decades but the name persists. Currently, memories are hundreds of megabytes to several gigabytes and growing rapidly. All CPU requests that cannot be satisfied out of the cache go to main memory.
Lookup Problems - TLB
Memory management is always going to require two look ups. When accessing memory space, any logical address, the MMU is going to have to first look up in the page map table where this is located, and then translate that into a frame number, and then access that frame number. So every main memory access now, by a process, is going to result in two main memory accesses. Now main memory is fast; There's no question it's that. But when we're doubling the price of everything, it really starts to add up and it slows down the CPU. What the CPU designers implemented was what's called a Translation Lookaside Buffer (TLB), which is a type of content addressable memory, which stores a cache of those entries in the PMT which have been retrieved recently. TLB is a table where we're looking up the Virtual Page Number (VPN) associated with a physical page number. So that VPN to physical page number mapping is only temporary. It's only there for a short period of time. In fact once we have a context switch, we basically have to dump the TLB. But that VPN to physical page number mapping can be looked up because this is content addressable memory in a time a big O of one, constant time search through the entire table. So we can use that immediately inside the C.P.U. without accessing main memory. So it's just for speeding things up, so that we can avoid one of those memory references that we can avoid going to main memory and asking for that value. if we don't have the value from the PMT already loaded in the TLB then we have to go look at the page map table and there's no way of to avoid that. But the the the PMT is not going to change while the process is running, of course because we can't do relocation while the process is running. So the TLB is a way to avoid that double memory access for multiple accesses to the same VPN. It's not a huge savings but over the course of a run of the program, it does cut the memory references not in half but certainly significantly less.
Win32 API
Microsoft has defined a set of procedures called the Win32 API (Application Programming Interface) that programmers are expected to use to get operating system services. This interface is (partially) supported on all versions of Windows since Windows 95. By decoupling the API interface from the actual system calls, Microsoft retains the ability to change the actual system calls in time (even from release to release) without invalidating existing programs.
OS as Client and Resource Manager
Most of the time the OS services client applications, but since the OS needs to run some processes for itself, it can also be its own client. Either way the OS has to manage all available hardware resources between itself and applications. Modern OS have modular designs.
Shared Bus Architecture
Multiple devices use the same wires to transfer data. Thus, when multiple devices have data to send, you need an arbiter to determine who can use the bus.
Benefits of Paging
No external fragmentation; every frame of main memory is equally good, and we can put any page in any frame and there's no detriment. There's no reason to keep pages together, which means any pages, any frame is available for use as long as of course it's not occupied. There's a minimum of internal fragmentation. We're talking the maximum amount of internal fragmentation we could have is four kilobytes minus one byte. A process wasting four kilobytes of main memory is not terribly significant, even if we had one hundred processes running on the system that means a maximum of four hundred kilobytes of main memory wasted and that's insignificant in today's environment. Protection is really easy because if the process is making a reference beyond the end of its page map table, the hardware memory management unit won't be able to convert that into a physical memory address. We know there's a problem and we can interrupt and call the OS immediately to say that something's gone wrong. The process literally cannot access anything outside its own memory space. Relocation is easy. We don't necessarily have to relocate the entire process; we could if we want, but if all we need to do is relocate one page, we can just update the page map table to indicate where the new the page is now located what the new frame number is and we're done. It really doesn't take a lot of effort, of course we have to copy those four killer bytes, but copying four kilobytes is not terribly significant anyhow. Besides why would we do relocation? There's very little reason in an simple paging system, there's very little reason to move a page from one to the other. Sharing is also easy. If we just have two processes that want to share a page and it's allowed to do so, doesn't violate protection, if they're allowed to share a page then we can simply enter the same frame number into their page map tables at the same location and we don't have any problems with that. There's a huge number of benefits to paging and that's why we'll never go back to the old fixed and dynamic partitioning systems.
Degree of Multiprogramming
Number of processes in memory.
Layered OS Design
OS is a hierarchy of layers, each one constructed upon the one below it.
Ready State
Once the process finishes loading and can be run, then the process moves not to the running state, unfortunately, but to the ready state. And the ready state is that point where the operating system has all the processes which actually have all their parts ready to go. These are only waiting for a processor to become available so that we can load the process onto the processor and actually let it run. Remember from our hardware discussion, that the processor is the only location where code can actually be run in the system. So, the processes that are on the ready state have everything that they need in order to run but they're not quite running yet because there's no processor available.
Running State
Once the processor becomes available, the process is going to move from the ready state into the running state and then it will actually be running on the processor and doing some real work. This is called a dispatch operation. Once the processor either times out, or the process makes a blocking system call, or the process ends altogether, the process is going to stay in the running state until one of those three things happens and then it's going to move to the ready state, the block state, or the exit state accordingly.
Program Status Word (PSW)
PSW register contains the condition code bits, which are set by comparison instructions, the CPU priority, the mode (user or kernel), and various other control bits. User programs may normally read the entire PSW but typically may write only some of its fields. The PSW plays an important role in system calls and I/O.
PFF Algorithm
Page Fault Frequency algorithm. PFF algorithm tries to look at the frequency of the page faults by looking at the time between the current page fault and the last page fault. And we have some value that we'd like to hit, we have some value we call F. And the F says if we're page faulting more frequently than F, in other words, if the time between page faults is less than F, then the solution is to add a frame. And what that's going to do is if we're page faulting too often it should decrease the page fault rate because now we've given it more memory. But if the page fault rate is greater than F, then we can use those use bits look at all the used bits of zero discard those pages they're not use anymore, reset all the used bits of the remaining pages to zero and load up the the frame, the page, that we need into an available frame. So one of the points of the PFF algorithm is that we can manage the resident set, how many pages are loaded into main memory, by I looking at how often the algorithm is or how often the process is page faulting. One of course the difficult values is how to figure out a value for F. One of the difficult things is how to figure out a value for F. How do we set a value for F? How do we know what a reasonable F is? And in fact there isn't a reasonable value for F. As you saw in the previous slide it would be better to have an upper bounds and a lower bounds, so that we're not looking for a moving target, we're not looking for F and deciding whether it's too frequent or too infrequent. We can use an upper bounds and lower bounds to say we might add a frame, or we might steal from the current process, or we might steal from the global set. So, there's a number of different variations on the PFF algorithm that we can use to to make it more effective. One problems of PFF though is that when the process moves from one locality to a completely different locality; so, it's starting in one area of its code and it jumps very far to do some other work. And it's going to be in the new locality for a long period of time, during a locality shift those pages in the new locality probably are not loaded. So, we'll have to load all those pages from the new locality and then when we're in the new locality, the page fault rate will decrease significantly because we have all the pages that we want. And so now we would, the next time try to unload those old pages from the old locality but unfortunately if we never fault again, for example, unlikely. But if we never fault again we're never going to clear out those old pages. So, the page fault frequency during a locality shift can result in both the old locality and the new locality using memory and that means double the memory and that's a huge waste. So, there are some downsides to using the PFF algorithm.
PMT
Page Map Table. The table used by the OS to keep track of page/frame relationships.
Controlling Page Faults with Resident Set Size
Page fault rate as compared to the number of frames allocated to the process. Now if we take a look, obviously if we only have one frame for the process it's going to create an almost infinite number of page faults; every memory access, almost, will need a page fault. And on the other side if we have an infinite number of frames, enough that it fits the entire process into main memory, then we have no page fault. But where it's interesting is that there's a non-linear progression between those two points. If we look at the process that has all the memory that it could possibly want and we remove just one frame, we only cause a very small number of page faults. and on the flip side if we look at a process which only has one Frame and we double the number of frames, that's going to result in a massive decrease in the number of page faults. So, there's a non linear progression between the two. And one of the things that we can extrapolate is that there is a sort of optimal location, where the process is not faulting too much but also is not is faulting a little bit. In other words, we're saying that page faults are not necessarily a bad thing; that since main memory is a quantity which is not infinite that we would like to have some number of page faults to indicate that the process hasn't been given too many frames, in other words that were starving other processes. And we can use a couple of algorithms to try and cause the process to try, and manage the resident set, to cause the process to fall in between the upper and lower bounds. If it's above the upper bounds, we can increase the number of frames that are available to that process thereby, effectively decreasing its page fault rate. And if it's below the lower bounds, then we can decrease the number of frames in the process give them to somebody else to hopefully lower its fault rate. And we can leave it in between this upper and lower bounds so that it has enough frames to do the work that it needs to do, but not so many friends that it's starving other processes.
Dynamic Partitioning
Partitions are made the exact size of the process. Causes external fragmentation when a process is pulled out. Ask the process exactly how much memory it needs and let's give it exactly that much memory. If a process is in a particular memory space, it can grow as much as it wants inside that memory space. So, once we allocate a memory space for that process, it really can't grow but it can use up everything that that's inside that partition. But what this causes is external fragmentation, which is wasted memory between allocations. Say, we've got a little chunk of memory free, because a process has exited, but what we would like to do is run a program that's just slightly larger than the space that was allocated there. And unfortunately because it was larger, I can't allocate that space; because the new process is larger than the space that was allocated, I can't put that new process into that memory space. So, what I have to do is put it at the end and that means we're we're using up more and more and more at the end. And then of course what happens if all we have are these little slivers of memory left over and I want to run a big process, which is not so large that it would that we don't have enough memory free memory for it, we have enough free memory for it but we have as these little segments of free memory. And we have to compact all the running processes now into the beginning of main memory to make a big space at the end, so that we can run the new process and that of course requires a lot of copying of memory, a lot of CPU time and that's a big waste of time. We have an issue with dynamic partitioning in that the data structures for the OS get rather complex because we have to record both the start and the ending location of the process. And then the last problem is where do we choose to put a process if we have enough space available, assuming that we have enough space available, where to be choose to put a new process into its into what partition. So, we could have the best fit which is the area size closest, and of course larger, than what the process is asking for. We could use first fit, which just says the first spot that we becomes available when we start looking, so once we start looking from the beginning of main memory. If a spot is available that's larger than what the process is asking for we'll just put it in there. And the next bits as we begin looking from where we last left off. Of all the options it actually turns out that next fit has the minimum CPU time with the best average utilization of memory, but the point is moot because we don't do this anymore. Dynamic partitioning has so much overhead, that we just can't do it in today's environment.
Process Image - PCB
Process Control Blocks. This entry contains important information about the process' state, including its program counter, stack pointer, memory allocation, the status of its open files, its accounting and scheduling information, and everything else about the process that must be saved when the process is switched from running to ready or blocked state so that it can be restarted later as if it had never been stopped. The PCB is a really critical component of the operating system; it's usually implemented as a simple struct with quite a bit of information.
PID
Process Identifier. Sixteen bits large and has to be unique for all the processes. The maximum number of processes in the system is 65,535 because that's the maximum number of process identifiers that we have.
Process Segments
Processes in UNIX have their memory divided up into three segments: the text segment (i.e., the program code), the data segment (i.e., the variables), and the stack segment. The data segment grows upward and the stack grows downward. Between them is a gap of unused address space. The stack grows into the gap automatically.
PSW
Program Status Word. Register condition variable that indicates to the CPU what mode (kernel or user) instructions need to be carried out in.
Peterson's Algorithm
Protects two threads from accessing the same resource at the same time using a Boolean flag variable. There's an array of Boolean flag variables for each of the threads and each thread sets its Boolean to true if it wanted to access its critical section. Now, unfortunately that's not enough because we're going to have to go and check the other threads flag to make sure it's not in its critical section. And we run the risk if we set our flag and then go check the other thread, that the other thread is doing exactly the same thing at the same time, and now we have both threads have their flags up and both threads think that the other thread is in it's critical section and in fact, nobody is in a critical section. So, what Peterson did to solve that problem specifically was introduce another variable he called the turn variable. So what the threads do is they offer the turn to the other thread so thread zero would offer the turn to thread one and thread one would offer the turn to thread zero. So, in that very rare situation where both threads want to access the critical section, both would raise up their flags and one would offer the turn to the other and the second one would win. So, either thread zero or thread one would be allowed access into its critical section, and then of course eventually it would unset its flag and allow the other thread to access its critical section because it no longer has its flag up. If we have more than that we have a bit of a problem, but Peterson's algorithm allows us to deal with the situation where we have two threads and we can provide mutual exclusion for those two threads using only software solutions.
Exokernal OS Design
Rather than cloning the actual machine, as is done with virtual machines, another strategy is partitioning it, in other words, giving each user a subset of the resources. Thus one virtual machine might get disk blocks 0 to 1023, the next one might get blocks 1024 to 2047, and so on. At the bottom layer, running in kernel mode, is a program called the exokernel (Engler et al., 1995). Its job is to allocate resources to virtual machines and then check attempts to use them to make sure no machine is trying to use somebody else's resources. Each user-level virtual machine can run its own operating system, as on VM/370 and the Pentium virtual 8086s, except that each one is restricted to using only the resources it has asked for and been allocated. The advantage of the exokernel scheme is that it saves a layer of mapping. In the other designs, each virtual machine thinks it has its own disk, with blocks running from 0 to some maximum, so the virtual machine monitor must maintain tables to remap disk addresses (and all other resources). With the exokernel, this remapping is not needed. The exokernel need only keep track of which virtual machine has been assigned which resource. This method still has the advantage of separating the multiprogramming (in the exokernel) from the user operating system code (in user space), but with less overhead, since all the exokernel has to do is keep the virtual machines out of each other's hair.
Memory Management Requirements
Relocation - we have a lot of programs running, and we don't know what the order of the programs is that are going to start; we don't know what's free when a program starts, we don't know what's occupied when a program starts. The OS is going to have to be responsible for putting a running program into a particular space in main memory and during its lifetime even while the program is running, not actively running on the CPU, but maybe in the ready state or in the block state or even in one of the suspended states. The OS should be able to relocate that process to a completely different section of main memory, I mean actually take it and move it to a different place in main memory. Protection - the OS will be the only entity allowed to access all of main memory. A program cannot be allowed to access another program's memory space, and that's a prime tenet of memory management. So, that we need to make sure that one program cannot interfere with another running program in the system. In fact, one program shouldn't even know that another program is necessarily running without the intervention of the OS. Now obviously, two programs may need to communicate and we have facilities for doing that; we have the inter-process communication facility. But overall, we should say that protection should be enforced, such that one program cannot access another program's memory space. One of the problems with this is that the OS can't prescreen. We can't go through and look at all the memory accesses that your program is going to do before you actually do them, because that would take just too much time. It has to be done dynamically. So as you make a memory request, the CPU, the hardware actually has to look at the memory request and decide whether it's in your memory space, your program's memory space, or outside your program's memory space and should be terminated. Sharing - We also have some situations where the process is actually going to share code with another process. Fork is an example. Logical organization - where we create modules. So, a lot of programs are written these days using shared objects or dynamically linked libraries and what this is is rather than creating every possible function. So, for example when you were writing your "hello world" program, your simple "hello world "programs, you didn't overload the output operator for the IO stream class. You had a library that did that for you and all you had to do was #include <iostream>. Well what that did was tell the OS that you're going to use a library and one of the libraries is the Microsoft Visual C++ library MSVCRTDLL. Inside that library is the code for the function on how to output to C-out to the O-stream class. So you don't write that code, it's already in Windows and every program that uses C++ has use for that code. Well we don't want to load that code with every program. We want to load that code once and have it shared between every program that was written in C++. So that might be seventy programs and we only have one copy of that MSVCRTDLL, it's not a large DLL maybe it takes up only 1-2 megabytes. But if we're talking about 70 different programs using it, that means we're saving between 70-140 megabytes of memory, which is significant. Physical organization - how we map a logical memory addresses to physical memory addresses.
Deadlock Avoidance
Requires that the operating system be given additional information in advance concerning which resources a process will request and use during its lifetime. In the deadlock avoidance scenario, the OS has to know everything that the thread is going to do before it actually does it. What the OS does is it takes all the information for all the threads that are running, and says that whenever a thread makes a new request, the OS is going to run a banker's algorithm and the idea is that we know all the resources that the OS has, we know all the resources that the threads have, and we have some premonition about what the resources the thread is going to want in its entire lifetime. So, we can tell which threads are going to run to completion just by knowing what we currently have available and what the threads are going to want before they're finished. And what we can do is constantly check, the OS is going to run the bankers algorithm to try and figure out if the allocation of that request would cause the system to be in an unsafe state in which any thread cannot complete. If that allocation keeps us in a safe state, we make the allocation, and if we can't then we deny the allocation. So, the system is by definition always in a safe state, but we're checking every single time to make sure that the allocations will not put the system in an unsafe state. It's a lot of work and we usually don't have information about what the threads are going to ask for ahead of time, until they actually ask for it. So, that makes dead lock avoidance a very rare strategy.
Deadlock Resource Types
Reusable and consumable
Server OS
Run on servers, which are either very large personal computers, workstations, or even mainframes. They serve multiple users at once over a network and allow the users to share hardware and software resources. Servers can provide print service, file service, or Web service. Internet providers run many server machines to support their customers and Websites use servers to store the Web pages and handle the incoming requests. Typical server operating systems are Solaris, FreeBSD, Linux and Windows Server 201x.
Semaphore
Semaphores have concurrency issues internally so they use some hardware level instructions, but the semaphore will provide a software solution for mutual exclusion. Semaphore is a nautical term for a signal flag. There are two functions used inside the semaphore. Semaphore is constructed and sent a signal. A semaphore is a message passing structure, so one thread could send a signal and the other thread could receive that signal. Now the way we receive the signal is by calling wait. But here's where this is very useful for mutual exclusion, the wait function is blocking. So, if there is no signal that has been sent, then the wait function causes a block to wait for the signal to be sent, and that's where we can really use it for mutual exclusion. So, initially when we create the semaphore we send a signal to the semaphore, so that's an initial queuing of the signal. Now assuming that we just created it I'm going to assume that nobody's waiting on the semaphore, so the signal is just going to sit there and wait to be received. When we enter a critical section the first thing that we do is call wait. Now as long as there's a signal already there, then wait is non blocking and the thread is just going to be allowed to enter its critical section. But the key feature is that the wait will consume the signal that's queued, so now there's no signal that's queued. If another thread calls wait to enter its critical section then it will not have a signal and it will be blocked and the operating system will keep a queue of the blocked processes so that it can start those processes again as the signals come in. When we're done, when the thread is finished with its critical section, it's going to call signal and signal is going to either queue up the signal if there's no threads waiting or if there's a thread waiting then the signal is going to release the next thread from the queue. So, this allows us to create a mutual exclusion protection mechanism using just a simple signal passing routine. We're going to piece together what happens inside a semaphore, without actually going through the nitty-gritty of how the OS creates and maintains a semaphore. We create a counter inside the semaphore that indicates how many signals have been sent. Each wait causes the counter to decrement. If the counter ever becomes negative, then the thread that caused it to go negative, and all subsequent threads, are blocked and placed on a queue. The OS maintains the queue, and it's a queue of blocked threads waiting for a signal on that semaphore. The only thread that's allowed to run is the one thread that consumed the one and only signal. Now when that thread is finished, it has to send a signal, and if there are any threads on the queue then the signal will cause the counter to increment; first signal always causes the counter to increment. But it also releases the next thread in the queue, if the counter actually goes positive that means that there's nobody in the queue waiting for a signal so there's nothing to be done there, but we recognize that the signal in the wait function allow us to create this mutual exclusion environment. Unfortunately, internally to the semaphore, there exists the critical section; the act of looking at the counter and then changing the counter is itself a critical section. So, here we have a mechanism that we're trying to use to protect against critical sections, and we created a critical section! So, that's a real problem so we have to use a hardware solution to prevent asynchrony when the semaphore has to both check the counter and set the counter. So, this is where the semaphore is going to use someone of the hardware options, either disable interrupts or test and set, probably exchange, and it's going to take care of it internally. So, we have to recognize that semaphore itself has a critical section and it has that in order to solve our critical section.
Multiplexing
Sharing resources in two different ways - in time and space. Time multiplexing is used for the CPU, space multiplexing is used for memory.
Time Sharing & Multitasking
So the operating system does this (preemption - stopping and restarting processes) very, very quickly and it really does this hundreds or even sometimes thousands of times per second inside the system, and in doing so it creates an environment that we call a time sharing system - processes take turns running so they share time on the CPU. And it allows us to run very many applications, which is multitasking.
Mutual Exclusion
Some way of making sure that if one process is using a shared variable or file, the other processes will be excluded from doing the same thing. A difficulty occurred because process B started using one of the shared variables before process A was finished with it. The choice of appropriate primitive operations for achieving mutual exclusion is a major design issue in any OS.
Critical Section
Sometimes a process has to access shared memory or files, or do other critical things that can lead to races. That part of the program where the shared memory is accessed is called the critical region or critical section. If we could arrange matters such that no two processes were ever in their critical regions at the same time, we could avoid races. We can select what code needs to run atomically, i.e. those pieces of code that once started can't be interrupted. Now unfortunately we can never say that we can't be interrupted! So the better solution is to say that we if we are interrupted that no other thread is allowed to go into that piece of code that could conflict with us. The critical section is a piece of code that once entered prohibits all other threads from entering the critical section on the same resource. If thread two wants to enter it's critical section it has to be blocked it has to be paused. The critical section should be as small as is possible because we don't want to spend a lot of time blocking all the other processes from accessing sections of code. We want this these pieces of code to be absolutely as small as possible.
Special File
Special files in UNIX are provided in order to make I/O devices look like files. That way, they can be read and written using the same system calls as are used for reading and writing files. Two kinds of special files exist: block special files and character special files. Block special files are used to model devices that consist of a collection of randomly addressable blocks, such as disks. By opening a block special file and reading, say, block 4, a program can directly access the fourth block on the device, without regard to the structure of the file system contained on it. Similarly, character special files are used to model printers, modems, and other devices that accept or output a character stream.
Hybrid Threading
Takes benefits from Kernel and User Level Threading. First implemented by Solaris OS, which created both kernel level threads and they created a user level thread, and tied the two together by means of what was known as a lightweight process, and the lightweight process is simply a container. So, we can put a number of user level threads inside of a lightweight process and we can choose to run that lightweight process on one kernel level thread, so when a kernel of a thread becomes available the lightweight process runs. And all of the user level threads in that process are run according to the scheduling library of that lightweight process. So, you as the programmer would create a lightweight process; you'd creates a number of user level threads. You'd assign the user level threads that you wanted to run to the lightweight process, but you can create any combination of user level threads and lightweight processes. So, if you had a very important user level thread, you would create one lightweight process for that one user level thread and any time that lightweight process ran that one user level thread would be the only one running. Then you could take three other user level threads, that are not critical, and put them on one lightweight process. So that when that lightweight process runs any of the user level threads inside could run. Of course, the same downside occurs as with user level threads, if one of the user level threads in the lightweight process blocks; all of the threads in the lightweight process will be blocked. As far as thread scheduling is concerned, there's both a global scheduling algorithm that chooses to run the kernel level threads and then there's a local scheduling algorithm, which you can implement you can manage yourself as the programmer, that would choose which of the user level threads associated with that lightweight process would be chosen to run.
Fundamental Mutual Exclusion
The built-in hardware mutual exclusion protection mechanism via the system bus, since it can only be used by one processor at any given time. So, even in a scenario where there is symmetric multiprocessing, with multi processors in the system, we it's impossible for two processors to change the same memory location at the same time, because one of those processors will gain access to the system bus to change that memory location and then the other will have to wait. Having that fundamental hardware mechanism is very useful, is helpful, but ultimately we need something a lot easier to deal with on a high level.
Logical vs. Physical Address Space
The concept of a logical address space that is bound to a separate physical address space is central to proper memory management. The computer system understands a physical address and expects to be communicated with physical addresses. Unfortunately, the programs will only be able to use logical addresses. And that's because the program has no idea where it will be loaded physically. This is really just a relocation problem, if we can use the offset from the beginning of the program, since the program should have no access outside of its own memory space. If the addresses that the program is going to use for all the pointers, and all the code, and all the jumps, and everything that it's going to do, if those addresses could be logical addresses relative to the offset from the beginning of the program, then the program doesn't really need to know where it's physically loaded in main memory. Those logical addresses need to be converted dynamically at run time into physical addresses by the hardware MMU. The MMU knows the starting address of the program, and then when it sees a memory reference, when it sees a pointer, it will convert it by adding the base address of the program into the logical address that the program is trying to reference to produce a physical address. And then the CPU will actually access that physical address, rather than accessing a logical address that the program is asking for. So, at run time without the OS's intervention (remember the OS is asleep while the program is running) the program will make a reference to a logical address and then the MMU will convert that logical address into a physical address. The program doesn't have to know anything more about where things are physically located. That meets our relocation and protection requirements because we can now physically move the entire process to a completely different location while it's not running, and then update the MMU the next time it runs to tell it where the process is now.
Context Switch
The exchange of register information that occurs when one process is removed from the CPU and another takes its place
Virtual Machine OS Design
The heart of the system, known as the virtual machine monitor, runs on the bare hardware and does the multiprogramming, providing not one, but several virtual machines to the next layer up. However, unlike all other operating systems, these virtual machines are not extended machines, with files and other nice features. Instead, they are exact copies of the bare hardware, including kernel/user mode, I/O, interrupts, and everything else the real machine has. In practice, the real distinction between a type 1 hypervisor and a type 2 hypervisor is that a type 2 makes uses of a host operating system and its file system to create processes, store files, and so on. A type 1 hypervisor has no underlying support and must perform all these functions itself. After a type 2 hypervisor is started, it reads the installation CD-ROM (or CDROM image file) for the chosen guest operating system and installs the guest OS on a virtual disk, which is just a big file in the host operating system's file system. Type 1 hypervisors cannot do this because there is no host operating system to store files on. They must manage their own storage on a raw disk partition. When the guest operating system is booted, it does the same thing it does on the actual hardware, typically starting up some background processes and then a GUI. To the user, the guest operating system behaves the same way it does when running on the bare metal even though that is not the case here.
Modified Bit
The modified bit is useful because we're going to make copies of pages in secondary storage before we realize that the the process doesn't need them anymore. So, what we would like to do is make duplicates of the main memory into the secondary storage device, so that when it's time to decide to remove a page from main memory we can look for a page that has the modified bit unset. And what that means is that the copy on the secondary storage device Is the exact same as the copy in main memory, which means we don't have to go to IO to actually do the write operation we can simply erase the page out of main memory and trust that the copy on the secondary storage device is valid. The MMU has to know what happens when it finds a present bit set to zero, and what it does is whenever it looks at the page map table and finds the present bits at zero, it's what's called a page fault, and the CPU stops running the process and instead switches to the OS, very much in the way that an interrupt would occur, and begins running code to handle the page fault. And then the OS can make the decision on whether the page fault was caused by a page which is no longer present, or perhaps was caused by a page which the process should not have any access to to begin with. And that's a page fault that's going to result in an exception error; that's going to shut down the process. That hardware memory management unit also has to recognize that the modified bit has to be changed any time we make a change to that page in main memory, so any write operations to a page in main memory would cause the modify bit to get set. But now the hardware memory management unit knows all about that and as the OS we're just responsible for writing the code that brings in and out pages, and updates and maintains the present bits, and works with the modified bits.
Preemption
The operating system will be responsible for stopping and restarting running programs and this is what we call preemption.
Kernel Mode
The operating system, the most fundamental piece of software, runs in kernel mode (also called supervisor mode). In this mode it has complete access to all the hardware and can execute any instruction the machine is capable of executing.
Mainframe OS
The operating systems for mainframes are heavily oriented toward processing many jobs at once, most of which need prodigious amounts of I/O. They typically offer three kinds of services: batch, transaction processing, and timesharing. A batch system is one that processes routine jobs without any interactive user present. Claims processing in an insurance company or sales reporting for a chain of stores is typically done in batch mode. Transaction-processing systems handle large numbers of small requests, for example, check processing at a bank or airline reservations. Each unit of work is small, but the system must handle hundreds or thousands per second. Timesharing systems allow multiple remote users to run jobs on the computer at once, such as querying a big database. These functions are closely related; mainframe operating systems often perform all of them.
Suspended State
The process is sort of still ready to run but it isn't actually running or runnable yet. In suspension, the process will be completely removed from main memory; so, obviously its code can't run because this code isn't loaded in main memory. So, in suspension we completely take the process out of main memory and move it to a secondary storage device. Done to free up main memory. If we get to the point where main memory is starting to fill up, with so many processes or with such large processes, that we no longer have a lot of processes that are ready to run we might get into a condition where the process list has nothing on the ready state; everything is in the block state and there's no more memory available to allocate to new processes. So, we aim to free up that memory by suspending one or more processes and this is done by what's known as the medium term scheduling algorithm. And the medium term scheduling algorithm will choose which process to suspend, and then it will take that process and put it on the secondary storage device. Now, this doesn't mean that the process is terminated because we can certainly reload all the code data and context from the secondary storage device back into main memory at a later time and then run that process, just as if nothing ever happened. So it's very possible for any of your programs your 'Hello World' style programs from previous modules, that could have been suspended at some point. We might have suspended the process just simply for debugging purposes; we might have suspended the proper process at the request of the user doing a control Z on a Unix system. But the point is that we need to add States to our five state process model to recognize the possibility that the process is suspended. And we actually need to add two different suspended states, because we need to recognize if a process is suspended and ready to run or if it's suspended and blocked for some reason. And the movement into and out of the suspended states is relatively easy. If the process was in the block state than during suspension it would be moved to the block suspended state, and if it was in the ready state during suspension it would be moved to the ready suspended state. If it some point in time in the block suspended state, the event that we were waiting for occurs then the process would be moved from the block suspended state to the ready suspended state. So, we evolve our concept of the process model from a five state process model into a seven state process model, where we add these two suspended states.
Paging
The process of swapping data or instructions that have been placed in the swap file for later use back into active RAM. The contents of the hard drive's swap file then become less active data or instructions. Paging is what is done today, which is to break down main memory into a lot of equal size frames, and each of the frames are the same size. And it's something small like four kilobytes; it's not something significant. Then what we do is we take down a process, take a process and we break that down into the exact same size pages. So, we'll take the process and break it down into four kilobyte pages. Now I'm sure you can see where this fits, and that is that one page fits exactly into one frame. This is RAM okay, and what RAM means is that we have random access memory, that means that every single frame, in fact every single byte, can be accessed all at the same amount of time. So, there's no benefit to keeping the process as one coherent entity. There's no reason we should keep the process in sequential memory; that wasn't a failed assumption that we had for fixed and dynamic partitioning. That we had to keep the process all together; we don't. If we have bits of the process at the beginning of main memory and bits of the process the end of main memory, it doesn't matter as long as we can keep track of them in order. We can access the front of main memory and the back of them in memory all at the same time. So, there's no reason that the process needs to be continuous. Now the OS is going to have to keep track of where each page is located in main memory; what frame number each page is loaded into. And it can do that, it does that, in what's known as a page map table or some books call it or just simply a page table. And the page map table is simply in an array of frame numbers and if we look at index three, for example into the page map table. That's going to tell us where the fourth page, zero one two three, where the fourth page of the process is located; what frame number the fourth page of the process is loaded into. So, we can look now into the page map table and find out where each of the pages in the system is in physical memory, and that's exactly what the hardware memory management needs to do. Whenever any logical memory request is made, to a logical address, basically to the offset from the beginning of the program, the hardware memory management unit needs to calculate how to convert that logical memory address into a physical memory address. And it doesn't take a lot of effort to do so it can simply do a number of bit shift left and bit shift right to get the the physical address from the logical address. and one of the features that it's going to have to do is a look up into the page map table. So, the format of the page map table now is going to be defined by the hardware manufacturer and not by the OS designer, because the hardware needs to know first where the page map table is stored and each process is going to need its own page map table; each process has its own page map table. And the hardware memory management unit is programmed, during a switch, during a context switch, it's programmed for where that page or app table is. And the hardware memory management unit will look up in the page map table where the appropriate page is loaded and do the conversion between page number and frame number and then look at the offset. So, this conversion process is a little bit involved.
Resident and Working Sets
The resident set is the portion that's in main memory. The working set is the portion of the process we want to use. Now in order to run, the working set has to be in the residence set. So, the working set must contain the resident. The page map table has a present bit updated by the MMU that indicates if the page is in main memory. If the page is not in main memory, then the present bit will be unset and then we know that we have to look for that page on a secondary storage device if the process wants it.
Replacement Policy
The rules used by the Virtual Memory Manager to determine which virtual page must be removed from memory to make room for a new page. Stealing is the choice of pulling a page out of main memory. And unfortunately if we choose poorly we can really cause a lot of performance issues because if we remove a page which is going to be used, that's going to require an I/O operation, it's going to require us possibly writing that page to the secondary storage device if the modified bit is set, we're going to have to write it to the secondary storage device then remarked the present bit to zero. And when the process runs again and wants to use that page, We're going to have to have a page fault and bring that back in. So, one one way of looking at the performance of virtual memory is to count the total number of page faults that are happening perception and if that number is is too high then one of the problems might be, the first obvious problem is that we have an insufficient amount of main memory. But the second obvious problem might be that we're choosing the wrong pages to remove; we're choosing pages that the process will immediately will want. Least Recently Used (LRU) algorithm - need a timestamp on each page to tell us when the last time that page was accessed; not modified, accessed. And unfortunately, we don't have time stamps on every page and it's too much overhead inside the CPU to timestamp every page. So the LRU is not feasible. FIFO algorithm - really simple to implement but not feasible. We just throughout the oldest page that we've brought in. But that doesn't say anything as to whether or not we're actually still using that page, and if we're still using that page and we throw it out it's going to immediately cause a page fault. In other words, it might be the oldest because the one that's needed the most. Clock Page Replacement algorithm - easy to implement, but requires USE bits. MMU updates those used bits whenever a page is used.
Interrupt
The second I/O method is for the driver to start the device and ask it to give an interrupt when it is finished. At that point the driver returns. The operating system then blocks the caller if need be and looks for other work to do. When the controller detects the end of the transfer, it generates an interrupt to signal completion. Once the CPU has decided to take the interrupt, the program counter and PSW are typically then pushed onto the current stack and the CPU switched into kernel mode. The device number may be used as an index into part of memory to find the address of the interrupt handler for this device. This part of memory is called the interrupt vector. Once the interrupt handler (part of the driver for the interrupting device) has started, it removes the stacked program counter and PSW and saves them, then queries the device to learn its status. When the handler is all finished, it returns to the previously running user program to the first instruction that was not yet executed. During an interrupt, some piece of hardware has indicated that it needs immediate servicing, and so the operating system must be invoked in order to take care of that hardware. And what happens in an interrupt is the CPU will finish executing the instruction that it's running, and then it will switch to running the OS. And the OS has a specific point that it uses, it's called the Interrupt Service Routine (ISR). The ISR runs whenever an interrupt occurs; the processor knows to switch to the OS's ISR, and of course since it's switching from a running program to the kernel; it switches from user mode to kernel mode and it runs the ISR.
Busy Waiting
The simplest I/O method. A user program issues a system call, which the kernel then translates into a procedure call to the appropriate driver. The driver then starts the I/O and sits in a tight loop continuously polling the device to see if it is done (usually there is some bit that indicates that the device is still busy). When the I/O has completed, the driver puts the data (if any) where they are needed and returns. The operating system then returns control to the caller. This method has the disadvantage of tying up the CPU polling the device until it is finished.
Smart Card OS
The smallest operating systems run on smart cards, which are credit-card-sized devices containing a CPU chip. They have very severe processing power and memory constraints. Some are powered by contacts in the reader into which they are inserted, but contactless smart cards are inductively powered, which greatly limits what they can do. Some of them can handle only a single function, such as electronic payments, but others can handle multiple functions. Often these are proprietary systems.
DMA
The third I/O method makes use of special hardware: a DMA (Direct Memory Access) chip that can control the flow of bits between memory and some controller without constant CPU intervention. The CPU sets up the DMA chip, telling it how many bytes to transfer, the device and memory addresses involved, and the direction, and lets it go. When the DMA chip is done, it causes an interrupt.
Segmentation
There's a lot of systems that do it today; it's very popular. Fortunately, it's not substantially different than paging. The only key difference here is that we're now allowed to have unequal sized partitions. Segmentation says, in a simple segmentation system, you could use any segment size and we don't call it a page anymore, we call it a segment; you can choose any segment size. But now the logical addresses really have to change because we need to talk about not just the the logical address, we need to break the logical address into two parts: a segment number and an offset into that segment. And then the OS can keep different segments in different locations, of course, we need space for different segments. So, this is kind of leading to a more dynamic partitioning strategy, but the segments don't necessarily have to be contiguous, so we can now divide them up. This isn't as as universal as you might see a paging system. What usually happens in a segmentation system is the CPU designer will give you a few options of segment sizes to choose from and so you might not have just four kilobyte pages, you might have four kilobyte pages, sixteen kilobyte pages and sixty-four kilobyte pages. The reason for this is that the OS if it knows it's going to need a lot of main memory for this process, we can use all sixty-four pages and then we have less of them. So, the page map tables are smaller and simpler to maintain. So, we don't really have the exact dynamic partitioning or the design of a simple segmentation system, where we have an infinite number of segments sizes. Instead we just have a few limiting segments sizes and the CPU designers decide what segment sizes we have as options.
Real-Time OS
These systems are characterized by having time as a key parameter. If the action absolutely must occur at a certain moment (or within a certain range), we have a hard real-time system. Many of these are found in industrial process control, avionics, military, and similar application areas. These systems must provide absolute guarantees that a certain action will occur by a certain time. A soft real-time system, is one where missing an occasional deadline, while not desirable, is acceptable and does not cause any permanent damage. Digital audio or multimedia systems fall in this category. Smartphones are also soft realtime systems. Since meeting deadlines is crucial in (hard) real-time systems, sometimes the operating system is simply a library linked in with the application programs, with everything tightly coupled and no protection between parts of the system. An example of this type of real-time system is eCos.
Thread States vs. Process States
Thread only has three states: ready, running, and blocked. Threads don't have a new and an exit state, because we don't need to load code. For example, our old example of loading the code and that was why we needed the new state, we don't have that anymore because the thread already has all of its code loaded in main memory. We're just creating a new thread and how does a new thread get created? Well the operating system creates a Thread Control Block (TCB) to recognize this thread, initializes the linkages initializes the TCB, and then it starts to run. So, it's really an atomic element creating a new thread can be done atomically, and the thread just begins running inside the context of the process. So, we don't need a new state and likewise, we don't even exit state because when a thread completes there's nothing returned back to the running process; the thread just records that it's done. Likewise, the idea of suspension is really a process level concept; if we've removed all of the main memory the none of the threads are able to run. So in suspension, we have to recognize that all of the threads have been blocked or all the threads have been stopped altogether. But at the same time, there's no real recognition of the suspended state inside of a thread; that's more of a process level concept.
POSIX
To make it possible to write programs that could run on any UNIX system, IEEE developed a standard for UNIX, called POSIX, that most versions of UNIX now support. POSIX defines a minimal system-call interface that conformant UNIX systems must support. In fact, some other operating systems now also support the POSIX interface.
Race Condition
Two or more processes are reading or writing some shared data and the final result depends on who runs precisely when. Debugging programs containing race conditions is no fun at all. The results of most test runs are fine, but once in a blue moon something weird and unexplained happens. Unfortunately, with increasing parallelism due to increasing numbers of cores, race condition are becoming more common.
Double Update Problem
Two pieces of code: a deposit function and a withdraw function. Imagine a situation where you have two ATM cards for the exact same bank account and you go to the bank, at exactly the same microsecond in time, and one person is doing a deposit and the other person is doing withdrawal. And when we do that, we have two transactions that are happening at the same time on different processors. So, now we have the possibility of asynchrony because we're dealing with this with separate processors and symmetric multiprocessing. We're going to assume that the bank has a good enough system that we have more than one CPU, so we can expect that this these pieces of code are running on two separate processors at exactly the same time. The biggest issue is that they share a balance, and the balance starts out at $100 and the first transaction is the deposit which is $50 and the second transaction is the withdrawal of $100. Ultimately, we should see is the bank balance is $50. What we start off with is the deposit function which is going to put in $50. The new balance is $150. This new balance variable is local to the deposit function, because it made a copy of the balance which was $100 and it adds the $50, so we have $150. Unfortunately, we get an interrupt or we're running on a separate CPU, so we go to the other CPU and we check out what's happening there. Now we look at the withdrawal function now and it's running on the other CPU, and in that function the new balance is $0. We return to the deposit function, which updates the balance to $150 and the deposit thread exits. But when we go back and finish out the withdraw thread, we get into trouble because we're going to update the balance to $0. So, our $50 deposit disappeared, because of asynchrony between the deposit and withdrawal functions/threads. We're going to take care of this with critical sections.
Load Control
Use the number of page faults that are happening in a system to make a decision of whether or not to swap a process altogether out. The benefit, of course, of swapping a process is that we get all of its memory, not just a portion of its memory. But that whole amount of main memory is going to be removed and reallocated to the other running processes and that process can be brought back at a later time. We can do that we've always been able to do that but now we can do it on a much grander scale. So, now we can use the number of page faults to tell us when it's time to choose a process to swap. But the question then becomes which process do we swap? Load control can be done by looking just at the number of page faults and it is a reasonable solution for the medium term scheduling algorithm: 1. Lowest Priority. 2. Faulting process as it's the greatest probability that it doesn't have its working set in the resident. So, the faulting process of course, if we swap that out, that means we don't have to recover a page; we don't have to deal with the page fault, we effectively deal with the page fault by swapping the entire process. 3. Last process activated is certainly the least likely to have its working set resident. 4. Smallest local resident set because it's the one that's going to be easiest to offload and easiest to onload later on, when we decide to run this process and finish it up. 5. Largest process, the exact the opposite, because we get the most amount of main memory and it's most likely that we're going to be able finish those other processes if we get back a large chunk of main memory. 6. Largest remaining execution window, if we know how long we expect this process to run for, because if we aren't going to finish this process for another five hours, who cares if it's another five hours and five minutes more.
Parallel Bus Architecture
Used in traditional PCI means that you send each word of data over multiple wires. For instance, in regular PCI buses, a single 32-bit number is sent over 32 parallel wires.
Batching
Used on mainframes that ran jobs. One program had complete access to all of the system resources; it had access to all of system memory, save for a small little area for the operating system. It had access to all of the CPU time. This one program was started, processed all of its data, ran through to completion, and when it ended another program was started. In order to do that you require some Job Control Language, which tells the operating system what facilities: what printers, what files, what network connections, anything the program needed, before even the program started.
UID
User Identifier of the person who started that process. The UID is important because it leads to the security perimeter restrictions that are placed onto that program. What can this program do is based on who activated this process.
Layers of Interaction
Users interact with applications, applications interact with the OS, OS interacts with hardware
Device Drivers
Utility software used by the operating system to communicate with peripheral devices. Written by hardware manufacturers. Provides function for OS to call to access device.
VSWS Algorithm
Variable Interval Sampled Working Set algorithm tries to solve PFF's problems by setting a number of different parameters. The first one is an M value, a minimum value for clearing out the unused pages. If we're ever below M, we're always going to add on a new page. If the page fault ever happens below M, we always add on a new page. Then we set a Q value, and we say if we're above M and we've reached Q page faults. We've had Q page faults since the last time we reset all the used bits and threw out the unused pages, then let's go ahead and do that algorithm again. So between M, above M and above Q, we would throw out all the unused pages and reset the use bits. Even if we're above M, but we've only had a very small number of page faults then it's too much work to go through the whole algorithm of resetting the used bits and throwing out the unused pages; so, we'll just add a page. But once we get to L, L is the maximum limit. Once we get to L time units then it's time. It doesn't matter how many page faults we've had; this process had its chance. It's now time to throw out the unused pages, reset the use bits ,and let the process run again. So, even if we don't get a page fault by the time we get to L, we now say it's time to run this algorithm and clear out the old waste. And that's the point that VSWS solves the PFF shortcomings by through setting the L value to say that even if we haven't had a large number of page faults by the time we reach L, there's got to be some stale information there and we'll throw out that that old locality's information. So, it does actually solve the problems of PFF and it actually works fairly effectively.
Shared Pages
We have duplicate entries, duplicate frame number entries in a page map table. So, since the code doesn't change, for example, if we opened up five copies of Chrome would we need five different copies of the code for Chrome. The answer is no, not necessarily. The OS may be smart enough, may be capable, of recognizing that multiple copies of the same program are running and it will share the code pages, not the data pages, not the context, just the code pages of that process because the code is not going to change from one process to the next process as long as they are the same program. So, if we're running Chrome twice, we should probably only load it's code once; it will, each copy will have a different data section but who cares because at least we're saving the memory for the code. Now there's also the possibility that a process will want to share data pages, but unfortunately if we're going to share data pages any changes to those data pages have to result in duplicates of those pages of data; not necessarily the entire portion of data but just those pages that are going to change. So, even if you're just changing one byte inside a page, we have to make an entire copy of the page. Now this is what's called copy on write, or COW, and it allows the sharing of the pages. The page table is now extended to the to have a read only attribute and if the page is marked as read only any attempt to change that page is going to result in a call to the OS. And the OS can then say this process is trying to change a page which has COW enabled. And when we change that page the OS will make a duplicate of just that page, update the appropriate page map tables, unsetting the read only bit and then re running the instruction. So, that the OS can intervene only of course when necessary, make the copy of the page, and then the processes can run again not realizing that the pages are now separate and different.
Converting Logical to Physical Memory Address
We're given a logical address that is the offset from the beginning of the program, and the program doesn't know that it's memory is not completely sequential. The program sees logical addresses that are all completely sequential. So, what we're going to do is use a very simple calculation, which is going to be that the physical address is equal to a look up in the page map table for the logical address divided by the page size multiplied by the page size plus the logical address mod the page size. And you see that that's a divide and a mod operation and you say, well wow that's got to be a lot of work; it would be if the page size were something other than a multiple of two a factor of two. And that's what makes this really easy because the divisions and the mod operation are now simply going to become bit shifts. So, because we have the bit shift, and bits shift really doesn't take much effort inside the CPU certainly not in comparison to a divide operation. To calculate the page number: we'll do bit shifts to find the page number, we'll look up the page number in the page map table, we'll find out what the frame number is, do the bit shift again as is the multiple of the page size, and then we'll add the offset. And to calculate the offset we simply looking for the latter half of the logical address. So, we can do an XOR to wipe out the early parts of the logical address and end up with just the offset, and that tells us where the page is located (what frame number it's in) and how far into that page the offset we're looking for for that particular byte of memory that we need. And that's how we convert a logical to a physical address in a paging system.
Blocked State
Well what happens if the process makes some sort of request which is going to take a really significant amount of time. The operating system doesn't want to stand and wait, waiting for that process to do what it's asking to do. And so it moves the process into the block state to indicate that this process is no longer ready to run; it is not running but it's not quite done yet. And the process is going to wait in the block state until whatever action it's requested finishes, and then it's going to move from the block state instead back into the ready state.
Cache Hit
When the program needs to read a memory word, the cache hardware checks to see if the line needed is in the cache. If it is, called a cache hit, the request is satisfied from the cache and no memory request is sent over the bus to the main memory. Cache hits normally take about two clock cycles. Cache misses have to go to memory, with a substantial time penalty.
Thread Table
When threads are managed in user space, each process needs its own private thread table to keep track of the threads in that process. This table is analogous to the kernel's process table, except that it keeps track only of the per-thread properties, such as each thread's program counter, stack pointer, registers, state, and so forth. The thread table is managed by the run-time system. When a thread is moved to ready state or blocked state, the information needed to restart it is stored in the thread table, exactly the same way as the kernel stores information about processes in the process table.
Asynchrony
When two threads are running seemingly at the same time. We might have a running thread that is interrupted due to some hardware considerations and a different thread is chosen to run. We have one thread that's running on the CPU and it stops running and then when we come back we're choosing a different thread to run. We could also have a thread that runs out of time. Remember we're in a preemptive system. So we're not allowing threads to run until they're finished. We're allowing them to run only for a certain period of time and when that period of time runs out whether or not the thread is finished we have to pause the thread we have to bring the thread out of the running state bring it back into the ready state and then we can go on to running either this thread or we can choose a different thread to run. If we have multiple CPUs we can have two of these threads running on the CPU all at the same time. The work that the threads are doing it is not an atomic instruction. We don't do one function call and finish the function call and then pause to run another thread. We could be in the very middle of an instruction or in the very middle of a line of code and we could stop and run a different thread altogether so we always have to keep that in the back of our minds when working with threads that we never really know when the thread is going to be stopped and when a different thread and possibly a conflict thread would be run.
WHQL
Windows Hardware Quality Labs. A service provided by Microsoft to hardware developers and vendors to test their hardware and device drivers with Windows. Bad device drivers were common cause of "Blue screen of death."
Microkernel OS Design
With the layered approach, the designers have a choice where to draw the kernel-user boundary. Traditionally, all the layers went in the kernel, but that is not necessary. In fact, a strong case can be made for putting as little as possible in kernel mode because bugs in the kernel can bring down the system instantly. In contrast, user processes can be set up to have less power so that a bug there may not be fatal. The basic idea behind the microkernel design is to achieve high reliability by splitting the operating system up into small, well-defined modules, only one of which—the microkernel—runs in kernel mode and the rest run as relatively powerless ordinary user processes. In particular, by running each device driver and file system as a separate user process, a bug in one of these can crash that component, but cannot crash the entire system. Thus a bug in the audio driver will cause the sound to be garbled or stop, but will not crash the computer. In contrast, in a monolithic system with all the drivers in the kernel, a buggy audio driver can easily reference an invalid memory address and bring the system to a grinding halt instantly.
Threads
With threading, a process now no longer means an execution path, we're now talking about a unit of resource ownership. The process becomes the unit of resource ownership but the thread becomes the unit of execution. And we might have multiple threads that run inside a single process and each thread has its own code. Although the code is memory and the memory is owned by the process. So, there might be multiple threads that are running the same code, or the threads might be running different code. All the threads inside a process share all the resources of the process and that includes all the memory, so threads can share the same memory unlike processes; processes can't share memory they're restricted because they're in user mode. But threads all exist inside of a process and the operating system is going to recognize that multiple threads are running inside a process, and it's going to be able to execute any one of the individual threads inside the process. Synchronous processing. Multiple threads run separately but at the same time. If there's a very large data set and we'd like to process portions of it separately, we can process those portions separately using multiple threads. And a block that happens in one thread will not obscure the other thread from running. Asynchronous processing. The threads need to do some sort of work cohesively so that when one thread ends the next thread begins, or infrequent tasks, where one thread doesn't run very often. Advantages of Threading: 1. The ability for the parallel entities to share an address space and all of its data among themselves. This ability is essential for certain applications, which is why having multiple processes (with their separate address spaces) will not work. 2. Since they are lighter weight than processes, they are easier (i.e., faster) to create and destroy than processes. In many systems, creating a thread goes 10-100 times faster than creating a process. When the number of threads needed changes dynamically and rapidly, this property is useful to have. A 3. Threads yield no performance gain when all of them are CPU bound, but when there is substantial computing and also substantial I/O, having threads allows these activities to overlap, thus speeding up the application. 4. Finally, threads are useful on systems with multiple CPUs, where real parallelism is possible. Disadvantages: Concurrency!, which means possible deadlocks.