//****************************************************************************//
//***************** Memory Management - June 19th, 2018 *********************//
//**************************************************************************//

- 10:03 - A grand total of 10 people in the room with 2 minutes to start...is that normal for this "late" in the semester?
    - A couple more trickled in at the last second, but still, only ~30% of the class is actually here...
- So, we've talked about datapaths, pipelining, and getting our basic hardware ducks in a row - now, we're starting to move up the abstraction tree to "processes"
----------------------------------------------------------

- First off, last lecture was a little messy, so let's clarify a few things:
    - We WILL be using the book definitions for the exams, but to be thorough...
    1) FCFS behaves differently from a normal queue; it actually uses a PRIORITY QUEUE, or "heap"
        - Now, for a normal queue, the time to add is O(1); for a priority queue/heap, it's O(log n)
    2) Response time/turnaround time are explained as the same in the book; in other textbooks, they are NOT
        - In other literature, the response time is "how long does the process take to start running, after I started it"
        - This is why RR is so good; it's not a whole lot better for turnaround time, but it's significantly better by this definition of response time!
    3) The "quantum size" of the Round Robin algorithm can have a significant impact
        - If we have a shorter "time quantum," we can SIGNIFICANTLY improve the response time, but we have to switch the processes more often - and every time we switch processes, the dispatcher/scheduler have to run too! Because of that, we actually end up increasing the overall turnaround time as the time quantum goes down
            - As we'll learn later, this is called "thrashing"
        - ALSO, FCFS can be considered a special case of "Round Robin" with an infinite time quantum

- "From a teaching perspective, I try to teach 2 main things about these managers: MECHANICS (the details of how things actually work) and POLICIES (the different ways of doing things and their trade-offs)"

- So, in the good ol' days before the internet, Professor Mark would be coding away on his Apple II, adding features left and right, and suddenly things would start going into CHAOS - nothing worked anymore!
    - ...why? Well, because the computer had run out of memory!
        - "Even today, computers struggle with memory management - it's an old problem, but still a current one!"
    - So, if we have a looooooong program, just throwing the whole thing into physical memory might not be the greatest idea ever - but how did people get around that?
        - One old technique is to break the program up into its individual functions/procedures - to start off, we just load the "main" procedure from the hard drive, then load/unload in the code for the other procedures as they're needed
            - This was known as OVERLAYING, and you had to define which procedures were needed, and when to load/unload them, for EVERY procedure
                - ...as you can imagine, this was error-prone and annoyed a LOT of programmers

- There HAD to be a better way to organize stuff in memory then just letting the programmer manually define everything - but first, let's REALLY think about what a program is
    - There are 3 times we care about dealing with the program's memory and addresses:
        - Compile Time (have to know where the program resides; moving the program's instructions would require a recompile)
        - Load Time (compiler has to generate code that can be moved around in memory; just reload the code if the starting address has changed)
        - Execution Time ()
    - So, to handle this stuff, we need our computer to support a few different things:
        - POSITION INDEPENDENT CODE, so that we aren't depending on the program being loaded at a particular memory address (e.g. base+offset addressing instead of hardcoding memory locations)
        - DYNAMIC LOADING, where we only load a routine when it's needed
            - If you've ever seen .dll files in your computer, that's what these are - the program fetches instructions from the .dll files as it's needed
        - DYNAMIC LINKING, 
    - The main problem behind ALL of this, though, is to let our programs use memory more flexibly - to try and find a way to let the programmer/CPU think they're in one place in memory, when in reality, behind the scene, our processor can be putting it somewhere else that's more convenient 

- To do this, we'll have to talk about a few things:
    - Logical/Virtual vs Physical Addresses
    - Paging
    - Segmentation
    - Virtual Memory

- First up, let's talk about addresses themselves
    - In the computer, there's the idea of "LOGICAL ADDRESSES" (aka "Virtual Addresses"), which are generated by the CPU
        - ALL of the program manipulation in our code - pointers, etc., - are done with logical addresses (e.g. if we have a 32-bit architecture, then we have 2^32 logical memory addresses)
        - ...the PROBLEM, though, is that we might have less physical memory than the addressing space (e.g. 32-bit architecture but only 512mb of RAM)!
            - We might also have multiple processes trying to use the same memory location, etc.
        - There's a few different schemes we can try to use to manage this memory: by putting a "memory manager" component that will map between logical addresses and the actual physical address
            - One of the basic ones is to separate the KERNEL memory and "User" memory
                - In this scheme, the "kernel" memory for the OS is like the teacher's desk - you don't touch it!
                - Practically, if an address is given to us that tries to access the OS's code, we call a trap - otherwise, we let it write to memory as normal
                - This is an improvement, but a bit simplistic, and doesn't solve the problem of processes "fighting" over each other's memory
            - Another one is "STATIC RELOCATION" - each process is given a "lower bound" and an "upper bound" address, and they can only run in that partition that they're given
                - The catch with this one, though, is that once a process is given a partition, that's it - it's given in terms of absolute addresses, so it's fixed and dependent on never mvoing
            - So, a better version of this is to support DYNAMIC RELOCATION - instead of giving a process two fixed bounds, we instead just give it a base and a maximum offset
    - So, we're giving each process it's own fixed-size partition - great!
        - The ISSUE with these fixed-size partition, though, is that we can end up wasting space via INTERNAL FRAGMENTATION - if we give each process 8kb partitions, but a process is only using 2kb of memory, then it has 6kb of memory just sitting around that NO other processes can use!
    - To solve this, we can try to have DYNAMIC partitions, where we can have partitions that grow or shrink as needed by the program
        - Now, in 2110, we had the malloc() assignment, where we tried to "compact" the free memory segments into 1 segment - actually doing this in the hardware, though, and trying to rearrange the processes to be contiguous with free memory, is TOUGH
        - So, what we often end up with here is EXTERNAL FRAGMENTATION - we have free blocks of memory that're only a couple kilobytes long, too short for anyone to use!

- So, we want to keep memory contiguous, but re-arranging all the processes is tough from a hardware standpoint...but let's be smart about this: we don't need the PHYSICAL memory to be contiguous - we just need the user to THINK the memory partions are contiguous! We can try doing this with "Paging"! (recheck this ENTIRE section)
    - The idea behind PAGING is to conceptually break up logical/"virtual" memory and physical memory into equal-sized blocks
        - So, we're taking the "logical" memory that the process thinks it has and breaking it up into multiple "pages" all of the same size
        - The physical memory, then, is broken into a bunch of equal-sized "frames," and we put our virtural memory into these frames to run as they become available
    - To keep track of these different pages, we have a PAGE TABLE in memory that maps a given logical address into the page # it belongs to (e.g., all addresses between 0 and 99 maps to "page #0"), and then maps THAT into an available "frame" in physical memory (e.g. Virtual Page #1 might be in Physical Frame/Page #12, Virtual Page #2 might be in Physical Frame/Page #35, etc.)
        - By doing this, the user *thinks* the memory is contiguous, but the physical location of the memory can be wherever we need!
    - So, how do we actually set this addressing scheme up in memory?
        - Basically, instead of having a raw address #, we split the page table's addresses into a "virtual page number," and an offset from the starting address of that page #
        - Then, when converting this to a physical address, we can just decode this into "VPN * (size of page) + offset"
- Now, this scheme assumes that all of the program's code is in memory - but, usually, not all of the program's code is running at the same time, and often much of it (error-checking, etc.) doesn't have to be run at all
    - This brings us into Chapter 8, with DEMAND PAGING - the first time we ask for a page, it isn't loaded into memory yet, so we go get the data from the disk, find a free frame, put the data in that frame, and then mark that page in the data as being loaded (to do this, we add an extra "valid/invalid" bit to the page table)
        - This is a "lazy algorithm" in the good sense - we're not doing more work than we have to!