thttpd/mmc.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!" );
}