buckets/allocator.c (322 lines of code) (raw):

/* ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== */ #include <stdlib.h> #define APR_WANT_MEMFUNC #include <apr_want.h> #include <apr_pools.h> #include <apr_hash.h> #include <apr_strings.h> #include "serf.h" #include "serf_bucket_util.h" #include "serf_private.h" /* Define SERF__DEBUG_UNFREED_MEMORY if you're interested in tracking * unfreed blocks on pool cleanup. */ /* #define SERF__DEBUG_UNFREED_MEMORY */ typedef struct node_header_t { apr_size_t size; union { struct node_header_t *next; /* if size == 0 (freed/inactive) */ /* no data if size == STANDARD_NODE_SIZE */ apr_memnode_t *memnode; /* if size > STANDARD_NODE_SIZE */ } u; } node_header_t; /* The size of a node_header_t, properly aligned. Note that (normally) * this macro will round the size to a multiple of 8 bytes. Keep this in * mind when altering the node_header_t structure. Also, keep in mind that * node_header_t is an overhead for every allocation performed through * the serf_bucket_mem_alloc() function. */ #define SIZEOF_NODE_HEADER_T APR_ALIGN_DEFAULT(sizeof(node_header_t)) /* STANDARD_NODE_SIZE is manually set to an allocation size that will * capture most allocators performed via this API. It must be "large * enough" to avoid lots of spillage to allocating directly from the * apr_allocator associated with the bucket allocator. The apr_allocator * has a minimum size of 8k, which can be expensive if you missed the * STANDARD_NODE_SIZE by just a few bytes. */ /* ### we should define some rules or ways to determine how to derive * ### a "good" value for this. probably log some stats on allocs, then * ### analyze them for size "misses". then find the balance point between * ### wasted space due to min-size allocator, and wasted-space due to * ### size-spill to the 8k minimum. */ #define STANDARD_NODE_SIZE 128 /* When allocating a block of memory from the allocator, we should go for * an 8k block, minus the overhead that the allocator needs. */ #define ALLOC_AMT (8192 - APR_MEMNODE_T_SIZE) /* Define DEBUG_DOUBLE_FREE if you're interested in debugging double-free * calls to serf_bucket_mem_free() and access bucket allocator after it * destroyed. */ #define DEBUG_DOUBLE_FREE /* If we have to create our own allocator, keep this amount of free ram in the allocator before returning memory back to the OS */ #define PRIVATE_ALLOCATOR_MAX_FREE (4 * 1024 * 1024) typedef struct read_status_t { const serf_bucket_t *bucket; apr_status_t last; } read_status_t; #define TRACK_BUCKET_COUNT 100 /* track N buckets' status */ typedef struct track_state_t { int next_index; /* info[] is a ring. next bucket goes at this idx. */ int num_used; read_status_t info[TRACK_BUCKET_COUNT]; } track_state_t; struct serf_bucket_alloc_t { apr_pool_t *pool; apr_allocator_t *allocator; int own_allocator; serf_unfreed_func_t unfreed; void *unfreed_baton; apr_uint32_t num_alloc; node_header_t *freelist; /* free STANDARD_NODE_SIZE blocks */ apr_memnode_t *blocks; /* blocks we allocated for subdividing */ track_state_t *track; #ifdef SERF__DEBUG_UNFREED_MEMORY apr_hash_t *unfreed_blocks; #endif }; /* ==================================================================== */ static apr_status_t allocator_cleanup(void *data) { serf_bucket_alloc_t *allocator = data; #ifdef SERF__DEBUG_UNFREED_MEMORY apr_hash_index_t *hi; for (hi = apr_hash_first(NULL, allocator->unfreed_blocks); hi; hi = apr_hash_next(hi)) { void **block_p = (void**) apr_hash_this_key(hi); apr_size_t size = (apr_size_t) apr_hash_this_val(hi); if (allocator->unfreed) { allocator->unfreed(allocator->unfreed_baton, *block_p); } else { fprintf(stderr, "Unfreed memory: %p (%d bytes)\n", *block_p, size); } } #endif /* If we allocated anything, give it back. */ if (allocator->blocks) { apr_allocator_free(allocator->allocator, allocator->blocks); } /* If we allocated our own allocator (?!), destroy it here. */ if (allocator->own_allocator) { apr_allocator_destroy(allocator->allocator); } #ifdef DEBUG_DOUBLE_FREE /* Set POOL to NULL to detect allocator usage after destroy. */ allocator->pool = NULL; #endif return APR_SUCCESS; } serf_bucket_alloc_t *serf_bucket_allocator_create( apr_pool_t *pool, serf_unfreed_func_t unfreed, void *unfreed_baton) { serf_bucket_alloc_t *allocator = apr_pcalloc(pool, sizeof(*allocator)); allocator->pool = pool; allocator->allocator = apr_pool_allocator_get(pool); if (allocator->allocator == NULL) { /* This most likely means pools are running in debug mode, create our * own allocator to deal with memory ourselves */ apr_allocator_create(&allocator->allocator); apr_allocator_max_free_set(allocator->allocator, PRIVATE_ALLOCATOR_MAX_FREE); allocator->own_allocator = 1; } allocator->unfreed = unfreed; allocator->unfreed_baton = unfreed_baton; #ifdef SERF__DEBUG_UNFREED_MEMORY allocator->unfreed_blocks = apr_hash_make(pool); #endif #ifdef SERF_DEBUG_BUCKET_USE { track_state_t *track; track = allocator->track = apr_palloc(pool, sizeof(*allocator->track)); track->next_index = 0; track->num_used = 0; } #endif /* NOTE: On a fork/exec, the child won't bother cleaning up memory. This is just fine... the memory will go away at exec. NOTE: If the child will NOT perform an exec, then the parent or the child will need to decide who to clean up any outstanding connection/buckets (as appropriate). */ apr_pool_cleanup_register(pool, allocator, allocator_cleanup, apr_pool_cleanup_null); return allocator; } apr_pool_t *serf_bucket_allocator_get_pool( const serf_bucket_alloc_t *allocator) { return allocator->pool; } void *serf_bucket_mem_alloc( serf_bucket_alloc_t *allocator, apr_size_t size) { node_header_t *node; void *block; #ifdef DEBUG_DOUBLE_FREE if (allocator->pool == NULL) { /* Attempt to use bucket allocator after it destroyed by * pool cleanup. */ abort(); } #endif ++allocator->num_alloc; size += SIZEOF_NODE_HEADER_T; if (size <= STANDARD_NODE_SIZE) { if (allocator->freelist) { /* just pull a node off our freelist */ node = allocator->freelist; allocator->freelist = node->u.next; #ifdef DEBUG_DOUBLE_FREE /* When we free an item, we set its size to zero. Thus, when * we return it to the caller, we must ensure the size is set * properly. */ node->size = STANDARD_NODE_SIZE; #endif } else { apr_memnode_t *active = allocator->blocks; if (active == NULL || active->first_avail + STANDARD_NODE_SIZE >= active->endp) { apr_memnode_t *head = allocator->blocks; /* ran out of room. grab another block. */ active = apr_allocator_alloc(allocator->allocator, ALLOC_AMT); /* System couldn't provide us with memory. */ if (active == NULL) return NULL; /* link the block into our tracking list */ allocator->blocks = active; active->next = head; } node = (node_header_t *)active->first_avail; node->size = STANDARD_NODE_SIZE; active->first_avail += STANDARD_NODE_SIZE; } } else { apr_memnode_t *memnode = apr_allocator_alloc(allocator->allocator, size); if (memnode == NULL) return NULL; node = (node_header_t *)memnode->first_avail; node->u.memnode = memnode; node->size = size; } block = ((char *)node) + SIZEOF_NODE_HEADER_T; #ifdef SERF__DEBUG_UNFREED_MEMORY apr_hash_set(allocator->unfreed_blocks, apr_pmemdup(allocator->pool, &block, sizeof(block)), sizeof(block), (const void*) size); #endif return block; } void *serf_bucket_mem_calloc( serf_bucket_alloc_t *allocator, apr_size_t size) { void *mem; mem = serf_bucket_mem_alloc(allocator, size); if (mem == NULL) return NULL; memset(mem, 0, size); return mem; } void serf_bucket_mem_free( serf_bucket_alloc_t *allocator, void *block) { node_header_t *node; #ifdef DEBUG_DOUBLE_FREE if (allocator->pool == NULL) { /* Attempt to use bucket allocator after it destroyed by * pool cleanup. */ abort(); } #endif --allocator->num_alloc; node = (node_header_t *)((char *)block - SIZEOF_NODE_HEADER_T); if (node->size == STANDARD_NODE_SIZE) { /* put the node onto our free list */ node->u.next = allocator->freelist; allocator->freelist = node; #ifdef DEBUG_DOUBLE_FREE /* note that this thing was freed. */ node->size = 0; } else if (node->size == 0) { /* damn thing was freed already. */ abort(); #endif } else { #ifdef DEBUG_DOUBLE_FREE /* note that this thing was freed. */ node->size = 0; #endif /* now free it */ apr_allocator_free(allocator->allocator, node->u.memnode); } #ifdef SERF__DEBUG_UNFREED_MEMORY /* Remove block from unfreed blocks hashtable. */ apr_hash_set(allocator->unfreed_blocks, &block, sizeof(block), NULL); #endif } /* ==================================================================== */ #ifdef SERF_DEBUG_BUCKET_USE static read_status_t *find_read_status( track_state_t *track, const serf_bucket_t *bucket, int create_rs) { read_status_t *rs; if (track->num_used) { int count = track->num_used; int idx = track->next_index; /* Search backwards. In all likelihood, the bucket which just got * read was read very recently. */ while (count-- > 0) { if (!idx--) { /* assert: track->num_used == TRACK_BUCKET_COUNT */ idx = track->num_used - 1; } if ((rs = &track->info[idx])->bucket == bucket) { return rs; } } } /* Only create a new read_status_t when asked. */ if (!create_rs) return NULL; if (track->num_used < TRACK_BUCKET_COUNT) { /* We're still filling up the ring. */ ++track->num_used; } rs = &track->info[track->next_index]; rs->bucket = bucket; rs->last = APR_SUCCESS; /* ### the right initial value? */ if (++track->next_index == TRACK_BUCKET_COUNT) track->next_index = 0; return rs; } #endif /* SERF_DEBUG_BUCKET_USE */ apr_status_t serf_debug__record_read( const serf_bucket_t *bucket, apr_status_t status) { #ifndef SERF_DEBUG_BUCKET_USE return status; #else track_state_t *track = bucket->allocator->track; read_status_t *rs = find_read_status(track, bucket, 1); /* Validate that the previous status value allowed for another read. */ if (SERF_BUCKET_READ_ERROR(rs->last)) { /* Somebody read when they weren't supposed to. Bail. */ /*abort(); */ fprintf(stderr, "Should not read from %p again, last_status=%d, status=%d\n", bucket, rs->last, status); } /* Save the current status for later. */ rs->last = status; return status; #endif } void serf_debug__entered_loop(serf_bucket_alloc_t *allocator) { #ifdef SERF_DEBUG_BUCKET_USE track_state_t *track = allocator->track; read_status_t *rs = &track->info[0]; for ( ; track->num_used; --track->num_used, ++rs ) { if (rs->last == APR_SUCCESS) { /* Somebody should have read this bucket again. */ abort(); } /* ### other status values? */ } /* num_used was reset. also need to reset the next index. */ track->next_index = 0; #endif } void serf_debug__closed_conn(serf_bucket_alloc_t *allocator) { #ifdef SERF_DEBUG_BUCKET_USE /* Just reset the number used so that we don't examine the info[] */ allocator->track->num_used = 0; allocator->track->next_index = 0; #endif } void serf_debug__bucket_destroy(const serf_bucket_t *bucket) { #ifdef SERF_DEBUG_BUCKET_USE track_state_t *track = bucket->allocator->track; read_status_t *rs = find_read_status(track, bucket, 0); if (rs != NULL && !APR_STATUS_IS_EOF(rs->last) && !SERF_BUCKET_READ_ERROR(rs->last)) { /* The bucket was destroyed before it was read to completion. */ serf_bucket_t *bkt; /* Special exception for socket buckets. If a connection remains * open, they are not read to completion. */ if (SERF_BUCKET_IS_SOCKET(bucket)) return; /* Ditto for SSL Decrypt buckets. */ if (SERF_BUCKET_IS_SSL_DECRYPT(bucket)) return; /* Ditto for SSL Encrypt buckets. */ if (SERF_BUCKET_IS_SSL_ENCRYPT(bucket)) return; /* Ditto for barrier buckets. */ if (SERF_BUCKET_IS_BARRIER(bucket)) return; if (SERF_BUCKET_IS_AGGREGATE(bucket)) { apr_status_t status; const char *data; apr_size_t len; serf_bucket_aggregate_hold_open(bucket, NULL, NULL); status = serf_bucket_read(bucket, SERF_READ_ALL_AVAIL, &data, &len); if (APR_STATUS_IS_EOF(status) && !len) return; } bkt = serf_bucket_read_bucket((serf_bucket_t*)bucket, &serf_bucket_type_ssl_encrypt); if (bkt != NULL) { serf_bucket_destroy(bkt); return; } abort(); } #endif } void serf_debug__bucket_alloc_check( serf_bucket_alloc_t *allocator) { #ifdef SERF_DEBUG_BUCKET_USE if (allocator->num_alloc != 0) { abort(); } #endif }