Operating Systems

CS 160-0
CRN 9705
Drake University
Spring, 2004






Basic information

Instructor: M. Q. Rieck
Office: Howard 219
Phone: 271-3795
E-Mail: Michael.Rieck@Drake.edu
Homepage: www.drake.edu/mathcs/rieck (to get to ongoing course info)
Office hours: Tues. 1-4, Wed. 3:15-5:15, and by appointment

Text: A. S. Tanenbaum and A. S. Woodhull, Operating Systems, Design and Implementation, 2nd ed., Prentice-Hall, 1997



Course objectives

From the catalogue: Introduction to the design, development and implementation of operating systems. Problems of resource allocation, concurrency, file systems design, networking and the interface between hardware and software.

In order to gain hands-on experience, students will study the implementational details of the MINIX operating system (forerunner of LINUX). Numerous projects will involve using and altering this system to further reinforce the understanding of it and of general OS principles.



Policy

All assigned work should be submitted on time. Late assignments will not be accepted. Anticipated absences from exams must be discussed with and approved by the instructor well in advance. Emergency absences should be supported by documentation with the name and phone number of a professional involved in the emergency.



Grading

    Final Exam (Thurs., May 6, 9:30-11:20)         25%    
    1st Segment Test (Wed., Feb. 11)         15%    
    2nd Segment Test (Mon., Mar. 15)         15%    
    3nd Segment Test (Wed., Apr. 21)         15%    
    Projects, HW, quizzes, participation         30%    




Homework Assignments

    Chapter One         Exercises 2, 3, 4, 6, 8, 12, 13, 15, and the following: What happens when the code    
    for (i=0; i<3; i++) fork(); is execute? How many processes will be created?    
    Due: 1/26    
    Chapter Two, First Batch         Exercises 2 (give two different reasons), 28, 30, 31, 32 ("starve" means stuck forever in the ready queue without running), 38 (handwritten code is OK - indicate where to insert it), plus the following:

    • Suppose that the clock causes an interrupt to occur and that this occurs while a user process is running. Indicating line numbers in the Minix code, describe the program flow as the interrupt handling proceeds. You should follow this from the beginning of interrupt handling until the moment interrupt handling finishes and some process resumes execution. Note that during system initialization, irq_table[0] is set equal to the starting address of clock_handler (page 672). Don't worry about what happens in this code, except for the potential call to the interrupt function. Assume that this occurs, and that the clock task is currently suspended, waiting for a message.

    • Assuming that all system tasks are currently suspended, exactly why will the clock task now be the running process, upon completion of the interrupt handling?

    • What duty (duties) would you expect the clock task to perform at this point, before suspending itself?

  1. The ready queues in Minix are structures as linked lists via a pointer p_nextready field in each of the process entries in the process table. Suppose that Tannenbaum decided to change this to a doubly linked list instead, using p_nextready and p_previousready to point forward and backwards through the list. Alter the ready function on page 609 to work with this doubly linked list.

    Due: 2/9    
    Chapter Two and Chapter Three         Exercises 2.5, 2.8, 2.9, 2.10, 2.11, 2.16, 2.17, 3.11, 3.13, 3.14, 3.16, 3.17, 3.19         Due: 3/10    
    Chapter Four         Exercises 2, 4, 7, 8, 11, 13, 14, 16, 20, 22         Due: 4/14    
    Chapter Five    
    3, 7, 8, 10   plus
  • Compare (1) contiguous allocation, (2) linked list allocation, and (3) linked list allocation using an index in the context of (A) random access usage, and (B) in the context of memory overhead and fragmentation issues.
   
    (not collected)   



Projects

  1. Project One (due 2/2)


  2. Project Three (due 4/12)

  3. Project Four (due 4/28)




Homework and Exam Solutions

  1. Here are my solutions to the first batch of Chapter Two homework. (Your mileage may vary.)

    • 2.2. The main reason is that you can't alter CPU registers directly using high level code. This makes it hard to change the active stack for example. It is also impossible to directly change the FLAGS register and so enable and disable interrupts at appropriate times. And so forth. Another reason is that a good assembly language programmer can produce the most efficient (fastest) possible code. Compilers may try to produce efficient code, but the code is generally sub-optimal.
    • 2.28. Why not? It's a fast way to get your hands on all the relevant info concerning the currently running process. (Am I missing something here?)
    • 2.30. I don't see it disabling interrupts any place!! (???) Can anybody explain this. The message must be sent via mini_send (not interrupt) since it is being sent to a user process. The line here where ready is called is if (...) ready(dest_ptr); Certainly interrupts are enabled at the hardward level. And there are no calls to the lock function (which somehow "disables" them at the software level). Also there is no such call in ready.
    • 2.31. Oh good. Here's one I can do. We discussed this for loop at the top of page 608. We check to see if the caller ("intended receiver") has any messages waiting to be delived to it. If so, there will be a linked list of proc structs (in the process table) conaining all of the processes that have message that need to be delivered to the caller. Scan this list for an entry for a specific source (whose PID is the second argument passed to this function). If found, remove this entry, copy the message from the source to the receiver's "mail box", and then "set the source free" (i.e. unblock it and make it ready). Also, if the source is ANY then just process the first message in the queue.
    • 2.32. An important point about tasks and server processes: they are never preempted. They voluntarily block (suspend) themselves whenever they are done doing whatever they were called upon to do (and wait for further instructions via a message). Also, they are guaranteed to finish and block within a certain definite time (cause Tannenbaum wrote them to do so). Now follow this reasoning (proof by contadiction) that a user process will never get starved:
      • Suppose a user process get's starved. Because of the round-robin nature of the user processes' queue, this would mean all the processes in this queue are starved because the server processes and/or tasks are pigging out on the CPU.
      • But server processes only get called in response to some need from user processes (via the system call mechanism), and so if the user processes aren't running at all, then these will eventually settle down with nothing to do, and so will suspend waiting for the next user level request, which will never occur.
      • That leaves the tasks. It is true that some of these get woken up by hardware interrupts (like clock and keyboard interrupts), and each of these could take a while to service. But Tannenbaum was pretty careful (I think!) to make sure that these can easily keep up with all the demands of an idle system. The clock only interrupts at regular intervals and even the fastest typer can't keep the keyboard that busy. So unless you "artificially" rig up some device (a fake keyboard maybe) that generates interrupts at an alarming rate, the tasks too will settle down and suspend.
      • At some moment a clock interrupt will cause the clock task to be woken up for rescheduling purpose. It will look around and see that no other tasks want to run (with high probability), and no other server processes either. Since there are user processes queued up, it must now let one of them run.
    • 2.38 (sketch). Naturally you'ld set a a doubly indexed array of integers as counters. In mini_send you would get the PID of the caller by using proc_number(caller_ptr). Use this as the first index, and use dest as the second index. Increment the corresponding counter around line 7092. Do something similar in mini_recv. Why? I'm assuming we're suppose to count the messages that actually get delivered (you might interpret differently). An important thing to notice is that in any message passing situation between two processes, both mini_send and mini_recv get called once. Also, the one that gets called last does the actually copying of the message via a call to CopyMess. Why????????????
    • My first question. Begin at line 6168 on page 593, where "pre-assembling" has resulted in a copy of the block from 6143 to 6160 to be pasted in. Let's now speak in reference to this block. Line 6144 causes program flow to go to line 6261 on page 595. Continue down to line 6277, whereupon a strange and magical return to line 6145 occurs. At line 6152 an indirect call is made to the C function clock_handler at line 11374 on page 672. (Note: I didn't tell you this, but observe that this function has an integer parameter and returns an integer. Before the call is made at line 6152, the value of the parameter is pushed onto the stack, and later, after returning from the call, the returned value will be popped off the stack.) Now that we're inside clock_handler, I told you to ignore most of it, but assume that you ultimately arrive at the call to interrupt that occurs at line 11459. Interrupt begins at line 6938 on page 604 (as you've seen). Assume that it gets past the initial checks and so gets down to the end where it is ready to put the clock task into the ready queue. I told to assume that all tasks are currently suspended, so the task ready queue is empty. Thus, line 7000 will be executed, and the clock task will not only be put in the ready queue, but will also be made the currently running process (well, as soon as interrupt handling is done). Soon interrupt returns to clock_handler, which soon returns to line 6153. When line 6160 is reached, this causes a branch to line 6322 on page on page 596. We soon end up at line 6345, whereupon we are officially finished with the "interrupt handling". However, the clock task will now execute and may well take further action in connection with the interrupt that occurs, continuing the "interrupt handling" in a broaded sense.
    • 07207	/*===========================================================================*
      07208	 *                              ready                                        *
      07209	 *===========================================================================*/
      07210	PRIVATE void ready(rp)
      07211	register struct proc *rp;       /* this process is now runnable */
      07212	{
      07213	/* Add 'rp' to the end of one of the queues of runnable processes. Three
      07214	 * queues are maintained:
      07215	 *   TASK_Q   - (highest priority) for runnable tasks
      07216	 *   SERVER_Q - (middle priority) for MM and FS only
      07217	 *   USER_Q   - (lowest priority) for user processes
      07218	 */
      07219
      07220	  if (istaskp(rp)) {
      07221	        if (rdy_head[TASK_Q] != NIL_PROC)
      07222	                /* Add to tail of nonempty queue. */
       MQR                    rp->p_previousready = rdy_tail[TASK_Q];
      07223	                rdy_tail[TASK_Q]->p_nextready = rp;
      07224	        else {
       MQR                    rp->p_previousready = NIL_PROC;
      07225	                proc_ptr =              /* run fresh task next */
      07226	                rdy_head[TASK_Q] = rp;  /* add to empty queue */
      07227	        }
      07228	        rdy_tail[TASK_Q] = rp;
      07229	        rp->p_nextready = NIL_PROC;     /* new entry has no successor */
      07230	        return;
      07231	  }
      07232	  if (!isuserp(rp)) {           /* others are similar */
      07233	        if (rdy_head[SERVER_Q] != NIL_PROC)
       MQR                    rp->p_previousready = rdy_tail[SERVER_Q];
      07234	                rdy_tail[SERVER_Q]->p_nextready = rp;
      07235	        else
       MQR                    rp->p_previousready = NIL_PROC;
      07236	                rdy_head[SERVER_Q] = rp;
      07237	        rdy_tail[SERVER_Q] = rp;
      07238	        rp->p_nextready = NIL_PROC;
      07239	        return;
      07240	  }
      07241	  if (rdy_head[USER_Q] == NIL_PROC)
      07242	        rdy_tail[USER_Q] = rp;
      07243	  rp->p_nextready = rdy_head[USER_Q];
      07244	  rdy_head[USER_Q] = rp;
       MQR      rp->p_previousready = NIL_PROC; /* adding at head here, so .... */
      07245	/*
      07246	  if (rdy_head[USER_Q] != NIL_PROC)
      07247	        rdy_tail[USER_Q]->p_nextready = rp;
      07248	  else
      07249	        rdy_head[USER_Q] = rp;
      07250	  rdy_tail[USER_Q] = rp;
      07251	  rp->p_nextready = NIL_PROC;
      07252	*/
      07253	}
      


  2. Here are my solutions to the Exam One:

    1. Any two of the following: (1) integrated circuits, (2) software compatable line for business and scientific computations (string manipulation versus number crunching), (3) multiprogramming.

    2. Something like this is a bit thin, but acceptable:

              while (TRUE) {
                 read_command(command, parameters);
                 if ( fork() != 0 ) {
                    if ("&" not in parameter list)
                       waitpid(-1, & status, 0);
                 } else {
                    execve(command, parameters, 0);
                 }
              }
          

      It should be noted that the child never returns from the execve system call. It just dies in the other program at some point. (Notice I said "other program", not "other process". Be clear about this.) Heck, it couldn't return because the machine code that corresponded to the above has been written over (in main memory) by the machine code for another program. This is so in the child's world but not in the parent's world. (Children and parents often see things differently.)

    3. First process creates second process. Second process creates third process. Etcetera until there are five processes altogther. Each processes (except last one) waits on its child to finish before it quits. Each time a process is created, the parent (i.e. process that exectues the fork) says "hi", while the child process (newly created process) says "hello". However, we cannot predict which is displayed first. Most of the processes (all except first and last) first play the role of a child and say "hello" and later play the role of a parent and say "hi". The first process never says "hello" and the last process never says "hi". All together four "hello"s are displayed and four "hi"s are displayed, in some order.

    4. First, let me say that the close(m) and close(n), blacked out at the bottom of the page, were suppose to be dup(m) and dup(n), but this doesn't affect my question. Still, for the following discussion, assume the dup's are there.

      • After the first two dup's, the keyboard has two file decriptors associated with it, namely 0 and some other integer stored in m. Likewise the screen has two file descriptors, namely 1 and n.
      • The closes free up 0 and 1. They are no longer file descriptors.
      • The first open opens the file 1.dat and assigned 0 as a file descriptor to it (the first available non-negative integer). In this way, 1.dat has just become the "standard input". Likewise, 2.dat is opened and assigned the file descriptor 1, and so becomes "standard output".
      • An integer is then read from 1.dat and written to 2.dat.
      • The files are closed, freeing up 0 and 1 as file descriptors.
      • dup(m) would now assign 0 as the file descriptor of whatever m is the file descriptor for, namely the keyboard. Likewise, after dup(n), 1 again becomes the file descriptor for the screen. So "standard input" and "standard output" are again the usual things.


    5. The interrupt handling begins on page 593, hops over to page 595, and hops back to 593, as you know. At line 6152, execution branches to the clock_handler function on page 973. (Note this function is excuted by the kernel, not by the clock task!!) At line 11459, the interrupt function is called. This is on page 604. Before returning, at line 7000, proc_ptr is set to point to the clock task's entry in the process table. At this point the clock task has officially become the "running process". It won't actually execute though until the kernel finishes handling the interrupt. After returning to clock_handler and then to page 593, "return to" restart on page 596. At line 6333, the stack pointer is set to point to the "tiny stack" in the clock task's entry in the process table. Registers (including esp and eip) are now popped off of this, and execution now continues inside the clock task's memory space. The clock task is NOW really executing.

    6. (Most gave a good discussion. You can read about this in the book in Section 2.6.8.)

    7. The best solution was slightly nicer than my own. Here it is:

              if (rdy_head[TASK_Q] == NIL_PROC) {
                 proc_ptr = rdy_tail[TASK_Q] = rp;
              } else {
                 rp->p_nextready = rdy_head[TASK_Q]->p_nextready;
                 rdy_tail[TASK_Q]->p_nextready = rdy_head[TASK_Q];
                 rdy_tail[TASK_Q] = rdy_head[TASK_Q];
              }
              rdy_tail[TASK_Q]->p_nextready = NIL_PROC;
              rdy_head[TASK_Q] = rp;
              return;
          
    Very slick!

  3. Solution to original part one of Project Two:

    Take a careful look at main at the end of heap.c. I'm going to write (m,n) to mean the node whose ID is m and whose priority is n. Even though these numbers are stored in two different arrays (in corresponding positions), I will write a list of such pairs, and you can infer the two arrays, and the conceptual tree we discussed. During the insertions and extractions and priority changes (and all the heapifying the accompanies these) here are some snapshots of the situation along the way:

    • (1,13), (2,22), (3,45)
    • (3,45), (2,22), (1,13)
    • (3,45), (2,22), (1,13), (4,81)
    • (4,81), (3,45), (1,13), (2,22)
    • (4,81), (3,45), (1,13), (2,22), (5,13)
    • (4,81), (3,45), (1,13), (2,22), (5,13), (6,22)
    • (4,81), (3,45), (6,22), (2,22), (5,13), (1,13), (7,13), (8,45)
    • (4,81), (3,45), (6,22), (8,45), (5,13), (1,13), (7,13), (2,22), (9,81)
    • (4,81), (9,81), (6,22), (3,45), (5,13), (1,13), (7,13), (2,22), (8,45)
      Now setPriority(4,13) results in
    • (9,81), (3,45), (6,22), (8,45), (5,13), (1,13), (7,13), (2,22), (4,13)
      Then setPriority(10,10) does nothing, but extractFromTopOfHeap removes (9,81) and produces
    • (4,13), (3,45), (6,22), (8,45), (5,13), (1,13), (7,13), (2,22)
    • (3,45), (8,45), (6,22), (2,22), (5,13), (1,13), (7,13), (4,13)
      Then extractFromTopOfHeap extracts (3,45) and builds
    • (4,13), (8,45), (6,22), (2,22), (5,13), (1,13), (7,13)
    • (8,45), (2,22), (6,22), (4,13), (5,13), (1,13), (7,13)
      Then extractFromTopOfHeap extracts (8,45) and builds
    • (7,13), (2,22), (6,22), (4,13), (5,13), (1,13)
    • (2,22), (7,13), (6,22), (4,13), (5,13), (1,13)
      Etcetera. See the display when you execute ./heap and check that it looks right to you.

  4. Solutions to HW submitted 3/10:

    • Exercies 2.5. (See page 58.)

    • Exercies 2.8. Certainly this approach still suffers from the need to "strictly alternate" so that property 3 on page 59 is still violated. However, what about property 1, which is more important!? There is no reason why this should fail to be satisfied now, just as it was originally. I'm saying that it doesn't matter whether you use two processors or share one processor. Things are essentially the same.

    • Exercies 2.9. The solution that comes to mind is to replace the TSL instruction with two instructions, the first puts a one in the register, and then the second is the swap instruction described here. The only danger would be if the process was interrupted in between executing these two instructions. But this won't matter, because when the process starts running again, the 1 that was in the register will have been restored, as a result of the final part of interrupt handling.

    • Exercies 2.10. Here is some pseudo-code for implementing DOWN:
      1. Disable interrupts.
      2. Check counter. If greater than zero, then skip ahead to Step 6.
      3. Enable interrupts. (Real important!)
      4. Suspend self, "waiting on the semaphore".
      5. Go to Step 1.
      6. Decrement counter.
      7. Enable interrupts.
      8. Return
      Here is some pseudo-code for implementing UP:
      1. Disable interrupts.
      2. Increment counter.
      3. See if some process is waiting on this semaphore. If not, skip ahead to Step 5.
      4. Arrange for one such process to be unsuspended.
      5. Enable interrupts.
      6. Return

    • Exercies 2.11. The idea is to use just one semaphore of the mutex sort to allow only one process at a time to execute critical section code. This code could easily now manage a shared counter. The trick again is that a process might need to suspend itself. If it does, then it needs to do an UP on mutex before it suspends itself, in order to allow other processes to execute in a critical section. Basically, my solution would be the same as my solution to Exercise 2.9, except that I would replace "disable interrupts" with "DOWN(mutex)" and replace "enable interrupts" with "UP(mutex)".

    • Exercies 2.16. If the process gets blocked and cannot eat because one of its neighbor is currently eating, then later this neighbor will be able to detect that the first process is "hungry" and deduce that it was therefore blocked and should now be unblocked, by doing an UP on its semaphore.

    • Exercies 2.17. At the time that put_forks is called, the process's state will be EATING. If this is not changed, then when the process tests its neighbors, if one of them is blocked (and waiting only on the first process), the first process won't unblocked the second process, as it should. Here are the details of what could happen using Process 1 and Process 2 as examples:
      1. Process 1 calls take_forks(1) which ends up calling test(1), and we'll assume that the condition is satified, so it does an UP of its semaphore, resulting in its value (counter) being 1. Process 1 also changes its state to EATING. Process 1 returns to take_forks and does a DOWN on its semaphore, but does not get blocked, and so goes on to execute eat.
      2. Assume Process 2 goes through much of the same thing, except that when it calls test(2), and gets to the condition, part of the condition is state[1] != EATING, which is false. So Process 2 does not do an UP on its semaphore. So later, when it does a DOWN on its semaphore, it gets blocked inside take_forks.
      3. Eventually, Process 1 finishes its call to eat. It then calls put_forks(1). This in turn calls test(0) and test(2). During the call to test(2), this checks state[1] and state[3]. But if the state[i] = THINKING statement didn't occur in put_fork before the test calls, then the check on state[1] being made now would come back FALSE. As a result, Process 1 would not end up unblocking Process 2 and putting it into the EATING state, as it should, at least it the other conditions are also TRUE.
      This would be a problem as long as there was at least two processes.

    • Exercies 3.11. Notice that the situation in Fig-11(b) is safe because Marvin might request at most 2 more widgets and this request could be fulfilled, and if Marvin is then allowed to complete and terminate, then the number of available widgets would become 4. At this point either Barbara or Suzanne's maximum request could be fulfilled, etcetera. Returning to the questions that were posed,
      1. "If Suzanne asks for ....?" : This would mean that only one widget is still available (unallocated), by Andy might request 5 more, Barbara might request 4 more, Marvin might request 2 more, and Suzanne might request 2 more. This situation is UNSAFE.
      2. "What if the request came from Marvin....?" : Again, only one widget would be available. But now Marvin would be holding 3 and would potentially request only one more. If he did, that request could be fulfilled. If Marvin is then allowed to run to completion, this would end up being the situation I described earlier. So, this situation is SAFE.


    • Exercies 3.13. Notice that fulfilling this request would change the Resources Assigned table to
          A   4 0 1 1
          B   0 1 0 0
          C   1 1 1 0
          D   1 1 0 1
          E   0 0 0 0
          
      and would change the Resources Still (Potentially) Needed table to
          A   0 1 0 0
          B   0 1 1 2
          C   3 1 0 0
          D   0 0 1 0
          E   2 1 1 0 
      and would result in P = (6 3 2 2) and A = (0 0 2 0). This situation is SAFE. D could be allowed to make all its requests, and run to completion. At that point the Resources Assigned table would be
          A   4 0 1 1
          B   0 1 0 0
          C   1 1 1 0
          E   0 0 0 0 
      The Resources Still Needed table would be
          A   0 1 0 0
          B   0 1 1 2
          C   3 1 0 0
          E   2 1 1 0 
      Would also have P = (5 2 2 1) and A = (1 1 2 1). It is now OK to let Process A run to completion since its maximum possible requests can be fulfilled. After A terminates, the Resources Assigned table will be
          B   0 1 0 0
          C   1 1 1 0
          E   0 0 0 0 
      The Resources Still Needed table would be
          B   0 1 1 2
          C   3 1 0 0
          E   2 1 1 0 
      Would also have P = (1 2 1 0) and A = (5 1 3 2). It's pretty easy from here, since you can let anybody run to completion now.

    • Exercies 3.14. Note that that tape drives are regarded as identical, i.e. instances of the same resource type. So you can't just see what would happen by drawing diagrams with n circles and 6 boxes and arrows between them. Consider the following situations:
      1. What if 6 processes each held one tape drive, and then one of them requested another tape drive. Deadlock? No. But we do have potential deadlock in this situation. They could all request one more tape drive, all the requests would be denied, and they would all be stuck. So 6 processes is too many. (The Banker's algorithm could be used to avoid getting into a situation where each process held a tape drive though, but the point is that deadlock is possible.)
      2. Now try 5 processes. What if one of them held two tape drives? Then it would never request another, and could be allowed to complete, freeing up both tape drives. Then it would be easy to let the other processes finish too. But now if each process is holding at most one tape drive, there is an available tape drive if one is requested. Anyone holding a tape drive already will request at most one, and can get it and then finish. Anyone holding no tape drives can request two and get these, since there would have to be at least two tape drives available. No deadlock here.
      So with five or fewer processes, deadlock cannot happen.

    • Exercies 3.16. A could be waiting to get a message from B, and B could be waiting to get a message from A. Neither has sent a message. Deadlock.

    • Exercies 3.17. An important thing we didn't get to today is the list on page 168. There are four things that need to be true in order to have deadlock. If something can be done to eliminate one of the conditions, then deadlock is impossible. In this problem, we can prevent the circular wait condition. How? The solution is discussed on page 174 ("global numbering" of resources). In the present context, give each bank account a unique integer ID. When it comes time for a process to "lock" two accounts (i.e. hold them as resources), always lock the one with the lower ID first.

    • Exercies 3.19. (Ignore this. I've lost my enthusiasm for it.)

  5. Solutions to Second Exam

  6. Here are my solutions to the Chapter 4 HW (Let me know you spot any mistakes):

    • Exercise 2.
      • First Fit:
        • Fill request for 12K so holes left are now 10K, 4K, 8K, 18K, 7K, 9K, 12K, 15K.
        • Fill request for 10K so holes left are now 4K, 8K, 18K, 7K, 9K, 12K, 15K.
        • Fill request for 9K so holes left are now 4K, 8K, 9K, 7K, 9K, 12K, 15K.
      • Best Fit:
        • Fill request for 12K so holes left are now 10K, 4K, 20K, 18K, 7K, 9K, 15K.
        • Fill request for 10K so holes left are now 4K, 20K, 18K, 7K, 9K, 15K.
        • Fill request for 9K so holes left are now 4K, 20K, 18K, 7K, 15K.
      • Worst Fit:
        • Fill request for 12K so holes left are now 10K, 4K, 8K, 18K, 7K, 9K, 12K, 15K.
        • Fill request for 10K so holes left are now 10K, 4K, 8K, 8K, 7K, 9K, 12K, 15K.
        • Fill request for 9K so holes left are now 10K, 4K, 8K, 8K, 7K, 9K, 12K, 6K.
      • Next Fit:
        • Fill request for 12K so holes left are now 10K, 4K, 8K, 18K, 7K, 9K, 12K, 15K.
        • Fill request for 10K so holes left are now 10K, 4K, 8K, 8K, 7K, 9K, 12K, 15K.
        • Fill request for 9K so holes left are now 10K, 4K, 8K, 8K, 7K, 12K, 15K.
    • Exercise 4.
      • 20 = 0000 0000 0000 1100 (binary), so page 0, so frame 2, so physical address is 0010 0000 0000 1100 (binary) = 8192+20 = 8212.
      • 4100 = 0001 0000 0000 0100 (binary), so page 1, so frame 1, so physical address is 0001 0000 0000 0100 (binary) = 4100.
      • 8300 = 0010 0000 0110 1100 (binary), so page 2, so frame 6, so physical address is 0110 0000 0110 1100 (binary) = 6*4096 + 108 = 24576 + 108 = 24684.
    • Exercise 7. A 32-bit addressing means 4G = 232 possible addresses, and so this many memory locations. I'm assuming that each memory location holds one byte of information (as with the usual RAM). Each page in the paging system contains 8K = 213 bytes (memory locations). So there are 232 / 213 = 219 entries in the page table, each of which is a 32 bit word. It takes 100nsec = 100 x 10-9 = 10-7 seconds per entry to load the table from main memory into a TLB (or similar cache) for fast use by the MMU. This entire load therefore takes 219 x 10-7 seconds. This is approximately 0.5 x 106 x 10-7 seconds = 0.05 seconds. When a process runs, it onle runs for 100msec = 0.1 seconds. So half the time will be spent loading the page table into the TLB (or similar cache).
    • Exercise 8. 32 - 9 - 11 = 12, so 12 bits are used as an offset into a page, and we can infer that each page has size 212 = 4K, the usual deal. There are 220 pages in the virtual memory space. The fact that the page table has two levels is irrelevant.
    • Exercise 11. We should have talked about HIT RATIO, but presumably you read about this. The hit ratio is the fraction of the time that the page-to-frame conversion information you want to find is in the TLB. Let h denote the hit ratio. Every time the MMU needs to do a page-to-frame conversion, it must first check the TLB. This takes 100 nsec. The fraction of time that it fails to find what it's looking for is 1-h. On these occasions, a lookup in the page table (in main memory) will be required. This takes 500 nsec. On average the time it will take to do the page-to-frame conversion will be 100 + (1-h)500 = 600 - 500h nsec. Set this equal to 200 nsec and solve. So 500h = 400. So h = 4/5. This means that a TLB "hit" must occur 4 out of 5 times, to meet the specs.
    • Exercise 13. 48-bit addressing, so 248 addresses, so virtual memory contains 248 bytes. These are organized into pages containing 213 bytes each. So the number of pages is 248 / 213 = 235. That's how many entries the page table needs.
    • Exercise 14.
      • The only one that has its reference bit cleared is page 0, so NRU will choose that.
      • Page 2 was loaded first, so FIFO will choose it.
      • According to the "last referenced" time stamp, page 1 was referenced the furthest in the past, so LRU chooses it.
      • Second chance starts off thinking like FIFO. But it sees that page 2's reference bit is set. So now it looks at page 0. It's reference bit is cleared, so 2nd Chance chooses page 0.
    • Exercise 16.
      • After first clock tick:
        • Page 0's shift register ("counter"): 00000000
        • Page 1's shift register ("counter"): 10000000
        • Page 2's shift register ("counter"): 10000000
        • Page 3's shift register ("counter"): 10000000
      • After second clock tick:
        • Page 0's shift register ("counter"): 10000000
        • Page 1's shift register ("counter"): 01000000
        • Page 2's shift register ("counter"): 11000000
        • Page 3's shift register ("counter"): 11000000
      • After third clock tick:
        • Page 0's shift register ("counter"): 11000000
        • Page 1's shift register ("counter"): 00100000
        • Page 2's shift register ("counter"): 11100000
        • Page 3's shift register ("counter"): 01100000
      • After fourth clock tick:
        • Page 0's shift register ("counter"): 11100000
        • Page 1's shift register ("counter"): 10010000
        • Page 2's shift register ("counter"): 01110000
        • Page 3's shift register ("counter"): 10110000
      • After fifth clock tick:
        • Page 0's shift register ("counter"): 01110000
        • Page 1's shift register ("counter"): 01001000
        • Page 2's shift register ("counter"): 10111000
        • Page 3's shift register ("counter"): 01011000
      • After sixth clock tick:
        • Page 0's shift register ("counter"): 10111000
        • Page 1's shift register ("counter"): 00100100
        • Page 2's shift register ("counter"): 11011100
        • Page 3's shift register ("counter"): 00101100
      • After seventh clock tick:
        • Page 0's shift register ("counter"): 11011100
        • Page 1's shift register ("counter"): 10010010
        • Page 2's shift register ("counter"): 01101110
        • Page 3's shift register ("counter"): 00010110
      • After eighth clock tick:
        • Page 0's shift register ("counter"): 01101110
        • Page 1's shift register ("counter"): 01001001
        • Page 2's shift register ("counter"): 00110111
        • Page 3's shift register ("counter"): 10001011
    • Exercise 20. In the original setup, the 15000 page faults each took 2 msec = 0.002 to handle, so all together it took 30 seconds to handle the page faults. This means the other 30 seconds was spent executing the code. We don't need to know how many instructions were executed, but heck, let's find out anyway. Each takes 1 microsec = 10-6 seconds to execute. 30 divided by 10-6 = 3 x 107 = 30,000,000 instructions. But this doesn't matter. Consider doubling the amount of RAM and so halving the number of page faults, and so halving the amount of time spent on page fault overhead. It would now take only 15 seconds to deal with the page faults. It still takes 30 seconds to execute the program code, ignoring the page fault overhead. Together it now takes 45 seconds to execute the program (including page fault overhead).
    • Exercise 22. Internal fragmentation essentially means wasted allocated memory while external fragmentation essentially means wasted unallocated memory. Pages are allocated to processes. A process might only need and use a portion of some page. The rest of the page is wasted. Internal fragmentation. In a pure segmentation system, segments of "just the right size" are allocated. No waste here. The waste occurs because small fragments of unallocated memory are never big enough to be of any use. External fragmentation.

  7. Here are my solutions to Exam 3:

      • (a) The 5 in 573 leads us to location 05 (in primary table) where we find 09, which is 00001001 in binary. The 1 in bit position 3 indicates that this is a valid entry in the table. So, 001 (the lowest three bits) gives the page number of the entry in the secondary table that we must find. This together with the 7 in 578 leads us to memory location 17 where we find 0C, which is 00001100 in binary. Again the entry is valid, and we extract the page number 4 from this. Together with the 3 in 573 we get the address 43 which is the physical address sought.
      • (b) The E in EB8 leads us to location 0E (in primary table) where we find 0A, which is 00001010 in binary. This is valid, so now need to go to page 2, in fact to address 2B. Here we find 03, which is 00000011 in binary. Since the bit in position 3 is 0, this entry in the secondary table is invalid. Page fault in order to go fetch desired page.
      • (c) The F in F29 leads us to location 0f (in primary table) where we find 04, which is 00000100. Since the bit in position 3 is 0, this entry is invalid. Therefore, there will be a page fault in order to go fetch the needed page of the secondary page table.

      • (a) The 52 in 526 means virtual page number 52. An entry for this is found in the TLB where it is discovered that the corresponding frame (physical page) is 9. So the physical address sought is 96. Note that this agrees with finding the address via the two-tiered page table (check this).
      • (b) The TLB can hold look up information for a maximum of four pages. But there are five pages (apart from tables) currectly loaded in memory. So, assuming that we're looking for something in one of these five pages (``no page faults") then there is a 4 out of 5 chance that we'll find the virtual page to physical frame conversion in the TLB. That is, under the given assumption (no page faults), the hit ratio will be 4/5 = 0.8. So the miss ratio is 0.2. So the average look-up time will be 10 nsec + 0.2 * 100 nsec = 30 nsec = 30 times 10-9 sec. Multiply this by 100,000 = 105 to get 30 times 10-4 = 3 times 10-3 seconds = 3 msec.

    1. I recall everyone getting this, so I'll skip it. Let me know if you have a question.

    2. I recall everyone getting this, so I'll skip it. Let me know if you have a question.

    3. This one amounts to writing what's in the book in your own words.

    4. I recall everyone getting this, so I'll skip it. Let me know if you have a question.

  8. Here is the solution to the Final Exam.






Some Notes

  1. Note on Minix keyboard input



Links to related web material




Newsworthy messages (check regularly)