532 lines
12 KiB
C
532 lines
12 KiB
C
/* mmc.c - mmap cache
|
|
**
|
|
** Copyright © 1998,2001,2014 by Jef Poskanzer <jef@mail.acme.com>.
|
|
** 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 <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
#include <fcntl.h>
|
|
#include <syslog.h>
|
|
#include <errno.h>
|
|
|
|
#ifdef HAVE_MMAP
|
|
#include <sys/mman.h>
|
|
#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!" );
|
|
}
|