CS 134

Other Tricks

  • Goat speaking

    Meh. It feels like half this stuff was invented in the 1970s.

  • PinkRobot speaking

    As is often the case in computer science, many of the fundamental ideas were developed early in the history of the field, but there's plenty of room for innovation and improvement. Let's look at a few more recent ideas.

Memory-Mapped Files

So far, we've mostly talked about demand-paging the code for a program and paging out unused data to swap space, but we can generalize this idea to include files as well, using memory-mapped files.

Here's an example program, main.c, that maps its own source code into memory and then counts the number of lines in the file:

#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int
main() {
        int fd = open("main.c", O_RDONLY);
        struct stat sb;
        fstat(fd, &sb);
        char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
        close(fd);
        printf("Mapped %zd bytes of main.c into memory at %p\n", sb.st_size, addr);

        int lines = 0;
        for (int i = 0; i < sb.st_size; i++) {
                if (addr[i] == '\n') {
                        lines++;
                }
        }

        printf("There are %d lines in main.c\n", lines);
        munmap(addr, sb.st_size);
        return 0;
}
  • Dog speaking

    Okay, that's cool. You can treat a file like memory! And you'll only read the parts you need.

  • Cat speaking

    But what if you need to write to the file?

  • PinkRobot speaking

    So long as you're not trying to make the file longer, you can change it in memory and the changes will be written back to disk when you munmap the file. You just need to change the PROT_READ to PROT_READ | PROT_WRITE when you call mmap and O_RDONLY to O_RDWR when you call open.

  • Duck speaking

    What if two processes map the same file?

  • PinkRobot speaking

    Then they'll share the same memory.

Shared Memory

We've already mentioned that if code is read only, it can be shared by all the processes running that code. But the virtual-memory system can also be used to share read-write data between processes with shared memory. Mapping the same writable file into two processes is one way to share memory between them.

  • Rabbit speaking

    POSIX also provides shm_open as a way to create shared-memory regions without needing to map a file. That's useful for sharing data between processes that don't need to persist the data to disk.

Shared Libraries

Besides the executable code, most programs also need to use various libraries, such as the C library that provides functions like strlen and printf, the C++ library that provides classes like std::string and std::iostream, and other useful libraries like libpng for reading and writing PNG files.

In the past, when you linked your program, the linker would work out which parts of the library you needed and copy them into your program (which is one reason why they were called libraries, because you just got the parts you needed). But this approach is wasteful if you're running many programs that all use the same functions from the same library.

Today we tend to map the entire library into memory and share it between all the programs that need it. These are known as shared libraries. We rely on demand paging to only load the parts of the library that are actually in use.

Shared Libraries on Linux and Windows

On Linux and Windows, although the physical frames are shared between processes, each process has its own copy of the page-table entries that map the shared library into memory, which allows each process to choose where in its address space it maps each library it wants. It can even map the library to different addresses in different runs of the same program. This approach does mean that each library needs a complete set of page-table entries for each process that uses it. Windows adds an optimization: when possible, it tries to load shared libraries at the same address in different processes, allowing the page-table entries to be shared.

Shared Libraries on macOS

On macOS, the system uses a single large shared region (also known as a submap) for all libraries that are part of the operating system. This region is mapped into every process's address space, and the page-table entries for the shared region are shared between all processes. The system only needs to keep one set of page-table entries for each library, no matter how many processes are using it. But it also means that each process has to map the library at the same address in its address space, and may have libraries in its address space that it never uses.

  • Horse speaking

    Hay! Going back to the idea of just mapping a file, if I read it in order, first byte to last byte, I'm not sure it'll be faster than just reading the file normally, because we'll keep getting page faults and needing to pause to read in the next page. I could have just done one big read and then counted the lines.

  • PinkRobot speaking

    Let's think about a fix…

When we have a large memory-mapped file and we start reading it sequentially from the first byte to the last, we'll get lots of page faults and read the data more slowly than if we'd just read the file in chunks. What could the operating system do to try to help with this problem?

Speculative Page Loading

When a program is reading a memory-mapped file sequentially, it's a good bet that the program plans to read in the entire file, or at least a large chunk of it. With most kinds of disks, it's almost the same cost to make a big read as a small one, so the OS can read in more than it needs and hope that the program will ask for the speculatively read data soon.

In macOS, these speculatively read pages go on their own page queue in the VM system, because keeping these pages around isn't essential. If they're not used soon after they were loaded, they probably won't be used at all.

Copy-on-Write (Helping Forks)

When a process forks, the child process gets a copy of the parent's address space. But if the child never writes to that memory, it's a waste of time and memory to actually duplicate the memory. Instead, the OS can use copy-on-write.

In copy-on-write, the OS marks the pages as read-only in both the parent and child processes. If either process tries to write to the memory, the OS makes a copy of the affected page and gives it to the writing process; pages that no one writes to don't have to be duplicated.

Copy-on-write saves memory and the cost of copying all the bytes of a process's address space when it forks. But it's not completely free. What costs are there to using copy-on-write?

Purgable Memory

When Apple launched the iPhone, they wanted to use the same core operating system as macOS, but they had to make some changes to make it work well on a phone. One issue with a phone is that the secondary storage is flash memory that can only be written to a limited number of times. So the operating system needs to be careful about how it uses memory, and shouldn't just swap out pages to disk whenever it feels like it—they decided the best approach was to have no swap space on the iPhone.

  • Dog speaking

    So we can only page out things that are read-only, like code and shared libraries? Because those are things we can throw away and reload from disk if we need them again.

  • Duck speaking

    Yeah, you can't just throw the data away!

  • PinkRobot speaking

    Can't always, sure. Can't ever, perhaps not.

Purgable memory is memory a program has that it technically doesn't need. An example is hidden tabs in your web browser—the browser can keep an image of what's on the page so that if you switch tabs it can show you the page right away, but if you never switch back to that tab, it's just wasting memory. And if it had to, your browser could redraw the page from scratch (or even reload it entirely from the Internet). So the program can mark that memory as purgable, meaning that it can be thrown away if the system needs the memory for something else.

Killing Processes to Free Memory

If there isn't enough free memory, and we don't reclaim enough by throwing out purgable memory, there is one more option—terminate some of the processes on the machine to free up memory. On iOS, the kernel first asks the program nicely to wind up its affairs and exit, but if it doesn't do so quickly, the kernel will kill the process. Programs for iOS are generally expected to be able to quit and restart fairly transparently, so this possibility isn't as big a deal as it might be on a desktop system.

Linux has a similar feature, called the OOM killer (Out-Of-Memory killer). When the system runs out of memory, the kernel will pick a process to kill based on a variety of factors, such as how much memory the process is using, how much CPU time it's using, and how long it's been since the process was started. The OOM killer will send a signal to the process, giving it a chance to clean up and exit gracefully, but if it doesn't, the kernel will kill the process.

Address-Space Layout Randomization

One of the ways that attackers can exploit a program is by knowing where things are in memory. For example, if the attacker knows where the stack is, they can overwrite the return address of a function and take control of the program. To make these kinds of attacks harder, the operating system can randomize the layout of the address space with address-space–layout randomization (ASLR).

ASLR works by placing all the pieces of the address space (the stack, the heap, the code, the shared libraries) at random locations in memory. Now the attacker doesn't know where the stack is—they have to find it before they can do anything else, which makes it much harder to exploit a program.

Today macOS, Linux, and Windows all use ASLR to protect against attacks. It doesn't prevent attacks, but it makes them harder to pull off.

On macOS, does the shared region approach for system shared libraries pose any challenges for ASLR?

  • Pig speaking

    What about that “wired memory” you were going to tell us MORE about?

Wired Memory

Wired memory is just memory that is “wired down”—it can't be paged out. The term is used by the macOS and BSD kernels, and refers to memory that the kernel has decided needs to stay in memory no matter what. For example, critical information about each process, or memory that is actively being used for I/O cannot be paged out.

  • Rabbit speaking

    Relatedly, POSIX provides the mlock system call that user programs can use to ask for a range of memory to be locked in place.

  • Hedgehog speaking

    Why would you do that?

  • PinkRobot speaking

    Timing-critical tasks (like playing back audio) would stutter if they suffered delays due to page faults.

  • Pig speaking

    So if I use mlock I can get MORE memory because it won't be paged out.

  • PinkRobot speaking

    Well, the operating system can always say "No" to a mlock request.

  • Goat speaking

    Meh. Can I say “no” to learning anything more?

  • PinkRobot speaking

    Well, I think this is a good enough place to wrap up this topic…

(When logged in, completion status appears here.)