Memory Mapped Files

There comes a time when you want to read and write to and from files so that the information is shared between processes. Think of it this way: two processes both open the same file and both read and write from it, thus sharing the information. The problem is, sometimes it's a pain to do all those fseek()s and stuff to get around. Wouldn't it be easier if you could just map a section of the file to memory, and get a pointer to it? Then you could simply use pointer arithmetic to get (and set) data in the file.

Well, this is exactly what a memory mapped file is. And it's really easy to use, too. A few simple calls, mixed with a few simple rules, and you're mapping like a mad-person.

Mapmaker

Before mapping a file to memory, you need to get a file descriptor for it by using the open() system call:

    int fd;

    fd = open("mapdemofile", O_RDWR);

In this example, we've opened the file for read/write access. You can open it in whatever mode you want, but it has to match the mode specified in the prot parameter to the mmap() call, below.

To memory map a file, you use the mmap() system call, which is defined as follows:

    void *mmap(void *addr, size_t len, int prot, int flags,
               int fildes, off_t off);

What a slew of parameters! Here they are, one at a time:

addr
This is the address we want the file mapped into. The best way to use this is to set it to (caddr_t)0 and let the OS choose it for you. If you tell it to use an address the OS doesn't like (for instance, if it's not a multiple of the virtual memory page size), it'll give you an error.

len
This parameter is the length of the data we want to map into memory. This can be any length you want. (Aside: if len not a multiple of the virtual memory page size, you will get a blocksize that is rounded up to that size. The extra bytes will be 0, and any changes you make to them will not modify the file.)

prot
The "protection" argument allows you to specify what kind of access this process has to the memory mapped region. This can be a bitwise-ORd mixture of the following values: PROT_READ, PROT_WRITE, and PROT_EXEC, for read, write, and execute permissions, respectively. The value specified here must be equivalent to the mode specified in the open() system call that is used to get the file descriptor.

flags
There are just miscellaneous flags that can be set for the system call. You'll want to set it to MAP_SHARED if you're planning to share your changes to the file with other processes, or MAP_PRIVATE otherwise. If you set it to the latter, your process will get a copy of the mapped region, so any changes you make to it will not be reflected in the original file--thus, other processes will not be able to see them. We won't talk about MAP_PRIVATE here at all, since it doesn't have much to do with IPC.

fildes
This is where you put that file descriptor you opened earlier.

off
This is the offset in the file that you want to start mapping from. A restriction: this must be a multiple of the virtual memory page size. This page size can be obtained with a call to getpagesize().

As for return values, as you might have guessed, mmap() returns -1 on error, and sets errno. Otherwise, it returns a pointer to the start of the mapped data.

Anyway, without any further ado, we'll do a short demo that maps the second "page" of a file into memory. First we'll open() it to get the file descriptor, then we'll use getpagesize() to get the size of a virtual memory page and use this value for both the len and the off. In this way, we'll start mapping at the second page, and map for one page's length. (On my Linux box, the page size is 4K.)

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/mman.h>

    int fd, pagesize;
    char *data;

    fd = fopen("foo", O_RDONLY);
    pagesize = getpagesize();
    data = mmap((caddr_t)0, pagesize, PROT_READ, MAP_SHARED, fd, pagesize);

Once this code stretch has run, you can access the first byte of the mapped section of file using data[0]. Notice there's a lot of type conversion going on here. For instance, mmap() returns caddr_t, but we treat it as a char*. Well, the fact is that caddr_t usually is defined to be a char*, so everything's fine.

Also notice that we've mapped the file PROT_READ so we have read-only access. Any attempt to write to the data (data[0] = 'B', for example) will cause a segmentation violation. Open the file O_RDWR with prot set to PROT_READ|PROT_WRITE if you want read-write access to the data.

Unmapping the file

There is, of course, a munmap() function to un-memory map a file:

    int munmap(caddr_t addr, size_t len);

This simply unmaps the region pointed to by addr (returned from mmap()) with length len (same as the len passed to mmap()). munmap() returns -1 on error and sets the errno variable.

Once you've unmapped a file, any attempts to access the data through the old pointer will result in a segmentation fault. You have been warned!

A final note: the file will automatically unmap if your program exits, of course.

Concurrency, again?!

If you have multiple processes manipulating the data in the same file concurrently, you could be in for troubles. You might have to lock the file or use semaphores to regulate access to the file while a process messes with it. Look at the Shared Memory document for a (very little bit) more concurrency information.

A simple sample

Well, it's code time again. I've got here a demo program that maps its own source to memory and prints the byte that's found at whatever offset you specify on the command line.

The program restricts the offsets you can specify to the range 0 through the file length. The file length is obtained through a call to stat() which you might not have seen before. It returns a structure full of file info, one field of which is the size in bytes. Easy enough.

Here is the source for mmapdemo.c:

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

    int main(int argc, char *argv[])
    {
        int fd, offset;
        char *data;
        struct stat sbuf;

        if (argc != 2) {
            fprintf(stderr, "usage: mmapdemo offset\n");
            exit(1);
        }

        if ((fd = open("mmapdemo.c", O_RDONLY)) == -1) {
            perror("open");
            exit(1);
        }

        if (stat("mmapdemo.c", &sbuf) == -1) {
            perror("stat");
            exit(1);
        }

        offset = atoi(argv[1]);
        if (offset < 0 || offset > sbuf.st_size-1) {
            fprintf(stderr, "mmapdemo: offset must be in the range 0-%d\n", \
                                                               sbuf.st_size-1);
            exit(1);
        }
        
        if ((data = mmap((caddr_t)0, sbuf.st_size, PROT_READ, MAP_SHARED, \
                                                    fd, 0)) == (caddr_t)(-1)) {
            perror("mmap");
            exit(1);
        }

        printf("byte at offset %d is '%c'\n", offset, data[offset]);

        return 0;
    }

That's all there is to it. Compile that sucker up and run it with some command line like:

    $ mmapdemo 30
    byte at offset 30 is 'e'

I'll leave it up to you to write some really cool programs using this system call.

Conclusions

Memory mapped files can be very useful, especially on systems that don't support shared memory segments. In fact, the two are very similar in most respects. (Memory mapped files are committed to disk, too, so this could even be an advantage, yes?) With file locking or semaphores, data in a memory mapped file can easily be shared between multiple processes.

HPUX man pages

If you don't run HPUX, be sure to check your local man pages!

Copyright © 1997 by Brian "Beej" Hall. This guide may be reprinted in any medium provided that its content is not altered, it is presented in its entirety, and this copyright notice remains intact. Contact beej@ecst.csuchico.edu for more information.