/* mmc.c - mmap cache ** ** Copyright © 1998,2001,2014 by Jef Poskanzer . ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ** ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ** OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ** SUCH DAMAGE. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_MMAP #include #endif /* HAVE_MMAP */ #include "mmc.h" #include "libhttpd.h" #ifndef HAVE_INT64T typedef long long int64_t; #endif /* Defines. */ #ifndef DEFAULT_EXPIRE_AGE #define DEFAULT_EXPIRE_AGE 600 #endif #ifndef DESIRED_FREE_COUNT #define DESIRED_FREE_COUNT 100 #endif #ifndef DESIRED_MAX_MAPPED_FILES #define DESIRED_MAX_MAPPED_FILES 2000 #endif #ifndef DESIRED_MAX_MAPPED_BYTES #define DESIRED_MAX_MAPPED_BYTES 1000000000 #endif #ifndef INITIAL_HASH_SIZE #define INITIAL_HASH_SIZE (1 << 10) #endif #ifndef MAX #define MAX(a,b) ((a)>(b)?(a):(b)) #endif #ifndef MIN #define MIN(a,b) ((a)<(b)?(a):(b)) #endif /* The Map struct. */ typedef struct MapStruct { ino_t ino; dev_t dev; off_t size; time_t ct; int refcount; time_t reftime; void* addr; unsigned int hash; int hash_idx; struct MapStruct* next; } Map; /* Globals. */ static Map* maps = (Map*) 0; static Map* free_maps = (Map*) 0; static int alloc_count = 0, map_count = 0, free_count = 0; static Map** hash_table = (Map**) 0; static int hash_size; static unsigned int hash_mask; static time_t expire_age = DEFAULT_EXPIRE_AGE; static off_t mapped_bytes = 0; /* Forwards. */ static void panic( void ); static void really_unmap( Map** mm ); static int check_hash_size( void ); static int add_hash( Map* m ); static Map* find_hash( ino_t ino, dev_t dev, off_t size, time_t ct ); static unsigned int hash( ino_t ino, dev_t dev, off_t size, time_t ct ); void* mmc_map( char* filename, struct stat* sbP, struct timeval* nowP ) { time_t now; struct stat sb; Map* m; int fd; /* Stat the file, if necessary. */ if ( sbP != (struct stat*) 0 ) sb = *sbP; else { if ( stat( filename, &sb ) != 0 ) { syslog( LOG_ERR, "stat - %m" ); return (void*) 0; } } /* Get the current time, if necessary. */ if ( nowP != (struct timeval*) 0 ) now = nowP->tv_sec; else now = time( (time_t*) 0 ); /* See if we have it mapped already, via the hash table. */ if ( check_hash_size() < 0 ) { syslog( LOG_ERR, "check_hash_size() failure" ); return (void*) 0; } m = find_hash( sb.st_ino, sb.st_dev, sb.st_size, sb.st_ctime ); if ( m != (Map*) 0 ) { /* Yep. Just return the existing map */ ++m->refcount; m->reftime = now; return m->addr; } /* Open the file. */ fd = open( filename, O_RDONLY ); if ( fd < 0 ) { syslog( LOG_ERR, "open - %m" ); return (void*) 0; } /* Find a free Map entry or make a new one. */ if ( free_maps != (Map*) 0 ) { m = free_maps; free_maps = m->next; --free_count; } else { m = (Map*) malloc( sizeof(Map) ); if ( m == (Map*) 0 ) { (void) close( fd ); syslog( LOG_ERR, "out of memory allocating a Map" ); return (void*) 0; } ++alloc_count; } /* Fill in the Map entry. */ m->ino = sb.st_ino; m->dev = sb.st_dev; m->size = sb.st_size; m->ct = sb.st_ctime; m->refcount = 1; m->reftime = now; /* Avoid doing anything for zero-length files; some systems don't like ** to mmap them, other systems dislike mallocing zero bytes. */ if ( m->size == 0 ) m->addr = (void*) 1; /* arbitrary non-NULL address */ else { size_t size_size = (size_t) m->size; /* loses on files >2GB */ #ifdef HAVE_MMAP /* Map the file into memory. */ m->addr = mmap( 0, size_size, PROT_READ, MAP_PRIVATE, fd, 0 ); if ( m->addr == (void*) -1 && errno == ENOMEM ) { /* Ooo, out of address space. Free all unreferenced maps ** and try again. */ panic(); m->addr = mmap( 0, size_size, PROT_READ, MAP_PRIVATE, fd, 0 ); } if ( m->addr == (void*) -1 ) { syslog( LOG_ERR, "mmap - %m" ); (void) close( fd ); free( (void*) m ); --alloc_count; return (void*) 0; } #else /* HAVE_MMAP */ /* Read the file into memory. */ m->addr = (void*) malloc( size_size ); if ( m->addr == (void*) 0 ) { /* Ooo, out of memory. Free all unreferenced maps ** and try again. */ panic(); m->addr = (void*) malloc( size_size ); } if ( m->addr == (void*) 0 ) { syslog( LOG_ERR, "out of memory storing a file" ); (void) close( fd ); free( (void*) m ); --alloc_count; return (void*) 0; } if ( httpd_read_fully( fd, m->addr, size_size ) != size_size ) { syslog( LOG_ERR, "read - %m" ); (void) close( fd ); free( (void*) m ); --alloc_count; return (void*) 0; } #endif /* HAVE_MMAP */ } (void) close( fd ); /* Put the Map into the hash table. */ if ( add_hash( m ) < 0 ) { syslog( LOG_ERR, "add_hash() failure" ); free( (void*) m ); --alloc_count; return (void*) 0; } /* Put the Map on the active list. */ m->next = maps; maps = m; ++map_count; /* Update the total byte count. */ mapped_bytes += m->size; /* And return the address. */ return m->addr; } void mmc_unmap( void* addr, struct stat* sbP, struct timeval* nowP ) { Map* m = (Map*) 0; /* Find the Map entry for this address. First try a hash. */ if ( sbP != (struct stat*) 0 ) { m = find_hash( sbP->st_ino, sbP->st_dev, sbP->st_size, sbP->st_ctime ); if ( m != (Map*) 0 && m->addr != addr ) m = (Map*) 0; } /* If that didn't work, try a full search. */ if ( m == (Map*) 0 ) for ( m = maps; m != (Map*) 0; m = m->next ) if ( m->addr == addr ) break; if ( m == (Map*) 0 ) syslog( LOG_ERR, "mmc_unmap failed to find entry!" ); else if ( m->refcount <= 0 ) syslog( LOG_ERR, "mmc_unmap found zero or negative refcount!" ); else { --m->refcount; if ( nowP != (struct timeval*) 0 ) m->reftime = nowP->tv_sec; else m->reftime = time( (time_t*) 0 ); } } void mmc_cleanup( struct timeval* nowP ) { time_t now; Map** mm; Map* m; /* Get the current time, if necessary. */ if ( nowP != (struct timeval*) 0 ) now = nowP->tv_sec; else now = time( (time_t*) 0 ); /* Really unmap any unreferenced entries older than the age limit. */ for ( mm = &maps; *mm != (Map*) 0; ) { m = *mm; if ( m->refcount == 0 && now - m->reftime >= expire_age ) really_unmap( mm ); else mm = &(*mm)->next; } /* Adjust the age limit if there are too many bytes mapped, or ** too many or too few files mapped. */ if ( mapped_bytes > DESIRED_MAX_MAPPED_BYTES ) expire_age = MAX( ( expire_age * 2 ) / 3, DEFAULT_EXPIRE_AGE / 10 ); else if ( map_count > DESIRED_MAX_MAPPED_FILES ) expire_age = MAX( ( expire_age * 2 ) / 3, DEFAULT_EXPIRE_AGE / 10 ); else if ( map_count < DESIRED_MAX_MAPPED_FILES / 2 ) expire_age = MIN( ( expire_age * 5 ) / 4, DEFAULT_EXPIRE_AGE * 3 ); /* Really free excess blocks on the free list. */ while ( free_count > DESIRED_FREE_COUNT ) { m = free_maps; free_maps = m->next; --free_count; free( (void*) m ); --alloc_count; } } static void panic( void ) { Map** mm; Map* m; syslog( LOG_ERR, "mmc panic - freeing all unreferenced maps" ); /* Really unmap all unreferenced entries. */ for ( mm = &maps; *mm != (Map*) 0; ) { m = *mm; if ( m->refcount == 0 ) really_unmap( mm ); else mm = &(*mm)->next; } } static void really_unmap( Map** mm ) { Map* m; m = *mm; if ( m->size != 0 ) { #ifdef HAVE_MMAP if ( munmap( m->addr, m->size ) < 0 ) syslog( LOG_ERR, "munmap - %m" ); #else /* HAVE_MMAP */ free( (void*) m->addr ); #endif /* HAVE_MMAP */ } /* Update the total byte count. */ mapped_bytes -= m->size; /* And move the Map to the free list. */ *mm = m->next; --map_count; m->next = free_maps; free_maps = m; ++free_count; /* This will sometimes break hash chains, but that's harmless; the ** unmapping code that searches the hash table knows to keep searching. */ hash_table[m->hash_idx] = (Map*) 0; } void mmc_term( void ) { Map* m; while ( maps != (Map*) 0 ) really_unmap( &maps ); while ( free_maps != (Map*) 0 ) { m = free_maps; free_maps = m->next; --free_count; free( (void*) m ); --alloc_count; } } /* Make sure the hash table is big enough. */ static int check_hash_size( void ) { int i; Map* m; /* Are we just starting out? */ if ( hash_table == (Map**) 0 ) { hash_size = INITIAL_HASH_SIZE; hash_mask = hash_size - 1; } /* Is it at least three times bigger than the number of entries? */ else if ( hash_size >= map_count * 3 ) return 0; else { /* No, got to expand. */ free( (void*) hash_table ); /* Double the hash size until it's big enough. */ do { hash_size = hash_size << 1; } while ( hash_size < map_count * 6 ); hash_mask = hash_size - 1; } /* Make the new table. */ hash_table = (Map**) malloc( hash_size * sizeof(Map*) ); if ( hash_table == (Map**) 0 ) return -1; /* Clear it. */ for ( i = 0; i < hash_size; ++i ) hash_table[i] = (Map*) 0; /* And rehash all entries. */ for ( m = maps; m != (Map*) 0; m = m->next ) if ( add_hash( m ) < 0 ) return -1; return 0; } static int add_hash( Map* m ) { unsigned int h, he, i; h = hash( m->ino, m->dev, m->size, m->ct ); he = ( h + hash_size - 1 ) & hash_mask; for ( i = h; ; i = ( i + 1 ) & hash_mask ) { if ( hash_table[i] == (Map*) 0 ) { hash_table[i] = m; m->hash = h; m->hash_idx = i; return 0; } if ( i == he ) break; } return -1; } static Map* find_hash( ino_t ino, dev_t dev, off_t size, time_t ct ) { unsigned int h, he, i; Map* m; h = hash( ino, dev, size, ct ); he = ( h + hash_size - 1 ) & hash_mask; for ( i = h; ; i = ( i + 1 ) & hash_mask ) { m = hash_table[i]; if ( m == (Map*) 0 ) break; if ( m->hash == h && m->ino == ino && m->dev == dev && m->size == size && m->ct == ct ) return m; if ( i == he ) break; } return (Map*) 0; } static unsigned int hash( ino_t ino, dev_t dev, off_t size, time_t ct ) { unsigned int h = 177573; h ^= ino; h += h << 5; h ^= dev; h += h << 5; h ^= size; h += h << 5; h ^= ct; return h & hash_mask; } /* Generate debugging statistics syslog message. */ void mmc_logstats( long secs ) { syslog( LOG_NOTICE, " map cache - %d allocated, %d active (%lld bytes), %d free; hash size: %d; expire age: %lld", alloc_count, map_count, (long long) mapped_bytes, free_count, hash_size, (long long) expire_age ); if ( map_count + free_count != alloc_count ) syslog( LOG_ERR, "map counts don't add up!" ); }