+APC Quick-Start Braindump
+
+This is a rapidly written braindump of how APC currently works in the
+form of a quick-start guide to start hacking on APC.
+
+1. Install and use APC a bit so you know what it does from the end-user's
+ perspective.
+ user-space functions are all explained here:
+
+2. Grab the current APC code from CVS:
+
+ cvs -d:pserver:cvsread@cvs.php.net:/repository login
+ Password: phpfi
+ cvs -d:pserver:cvsread@cvs.php.net:/repository co pecl/apc
+
+ apc/php_apc.c has most of the code for the user-visible stuff. It is
+ also a regular PHP extension in the sense that there are MINIT, MINFO,
+ MSHUTDOWN, RSHUTDOWN, etc. functions.
+
+3. Build it.
+
+ cd pecl/apc
+ phpize
+ ./configure --enable-apc --enable-mmap
+ make
+ cp modules/apc.so /usr/local/lib/php
+ apachectl restart
+
+4. Debugging Hints
+
+ apachectl stop
+ gdb /usr/bin/httpd
+ break ??
+ run -X
+
+ Grab the .gdbinit from the PHP source tree and have a look at the macros.
+
+5. Look through apc/apc_sma.c
+ It is a pretty standard memory allocator.
+
+ apc_sma_malloc, apc_sma_realloc, apc_sma_strdup and apc_sma_free behave to the
+ caller just like malloc, realloc, strdup and free
+
+ On server startup the MINIT hook in php_apc.c calls apc_module_init() in
+ apc_main.c which in turn calls apc_sma_init(). apc_sma_init calls into
+ apc_mmap.c to mmap the specified sized segment (I tend to just use a single
+ segment). apc_mmap.c should be self-explanatory. It mmaps a temp file and
+ then unlinks that file right after the mmap to provide automatic shared memory
+ cleanup in case the process dies.
+
+ Once the region has been initialized we stick a header_t at the beginning
+ of the region. It contains the total size in header->segsize and the number
+ of bytes available in header->avail.
+
+ After the header comes a bit of a hack. A zero-sized block is inserted just
+ to make things easier later on. And then a huge block that is basically
+ the size of the entire segment minus the two (for the 0-sized block, and this one)
+ block headers.
+
+ The code for this is:
+
+ header = (header_t*) shmaddr;
+ header->segsize = sma_segsize;
+ header->avail = sma_segsize - sizeof(header_t) - sizeof(block_t) - alignword(sizeof(int));
+ memset(&header->lock,0,sizeof(header->lock));
+ sma_lock = &header->lock;
+ block = BLOCKAT(sizeof(header_t));
+ block->size = 0;
+ block->next = sizeof(header_t) + sizeof(block_t);
+ block = BLOCKAT(block->next);
+ block->size = header->avail;
+ block->next = 0;
+
+ So the shared memory looks like this:
+
+ +--------+-------+---------------------------------+
+ | header | block | block |
+ +--------+-------+---------------------------------+
+
+ sma_shmaddrs[0] gives you the address of header
+
+ The blocks are just a simple offset-based linked list (so no pointers):
+
+ typedef struct block_t block_t;
+ struct block_t {
+ size_t size; /* size of this block */
+ size_t next; /* offset in segment of next free block */
+ size_t canary; /* canary to check for memory overwrites */
+#ifdef __APC_SMA_DEBUG__
+ int id; /* identifier for the memory block */
+#endif
+ };
+
+ The BLOCKAT macro turns an offset into an actual address for you:
+
+ #define BLOCKAT(offset) ((block_t*)((char *)shmaddr + offset))
+
+ where shmaddr = sma_shaddrs[0]
+
+ And the OFFSET macro goes the other way:
+
+ #define OFFSET(block) ((int)(((char*)block) - (char*)shmaddr))
+
+ Allocating a block with a call to apc_sma_allocate() walks through the
+ linked list of blocks until it finds one that is >= to the requested size.
+ The first call to apc_sma_allocate() will hit the second block. We then
+ chop up that block so it looks like this:
+
+ +--------+-------+-------+-------------------------+
+ | header | block | block | block |
+ +--------+-------+-------+-------------------------+
+
+ Then we unlink that block from the linked list so it won't show up
+ as an available block on the next allocate. So we actually have:
+
+ +--------+-------+ +-------------------------+
+ | header | block |------>| block |
+ +--------+-------+ +-------------------------+
+
+ And header->avail along with block->size of the remaining large
+ block are updated accordingly. The arrow there representing the
+ link which now points to a block with an offset further along in
+ the segment.
+
+ When the block is freed using apc_sma_deallocate() the steps are
+ basically just reversed. The block is put back and then the deallocate
+ code looks at the block before and after to see if the block immediately
+ before and after are free and if so the blocks are combined. So you never
+ have 2 free blocks next to each other, apart from at the front with that
+ 0-sized dummy block. This mostly prevents fragmentation. I have been
+ toying with the idea of always allocating block at 2^n boundaries to make
+ it more likely that they will be re-used to cut down on fragmentation further.
+ That's what the POWER_OF_TWO_BLOCKSIZE you see in apc_sma.c is all about.
+
+ Of course, anytime we fiddle with our shared memory segment we lock using
+ the locking macros, LOCK() and UNLOCK().
+
+ That should mostly take care of the low-level shared memory handling.
+
+6. Next up is apc_main.c and apc_cache.c which implement the meat of the
+ cache logic.
+
+ The apc_main.c file mostly calls functions in apc_sma.c to allocate memory
+ and apc_cache.c for actual cache manipulation.
+
+ After the shared memory segment is created and the caches are initialized,
+ apc_module_init() installs the my_compile_file() function overriding Zend's
+ version. I'll talk about my_compile_file() and the rest of apc_compile.c
+ in the next section. For now I will stick with apc_main.c and apc_cache.c
+ and talk about the actual caches. A cache consists of a block of shared
+ memory returned by apc_sma_allocate() via apc_sma_malloc(). You will
+ notice references to apc_emalloc(). apc_emalloc() is just a thin wrapper
+ around PHP's own emalloc() function which allocates per-process memory from
+ PHP's pool-based memory allocator. Don't confuse apc_emalloc() and
+ apc_sma_malloc() as the first is per-process and the second is shared memory.
+
+ The cache is stored in/described by this struct allocated locally using
+ emalloc():
+
+ struct apc_cache_t {
+ void* shmaddr; /* process (local) address of shared cache */
+ header_t* header; /* cache header (stored in SHM) */
+ slot_t** slots; /* array of cache slots (stored in SHM) */
+ int num_slots; /* number of slots in cache */
+ int gc_ttl; /* maximum time on GC list for a slot */
+ int ttl; /* if slot is needed and entry's access time is older than this ttl, remove it */
+ };
+
+ Whenever you see functions that take a 'cache' argument, this is what they
+ take. And apc_cache_create() returns a pointer to this populated struct.
+
+ At the beginning of the cache we have a header. Remember, we are down a level now
+ from the sma stuff. The sma stuff is the low-level shared-memory allocator which
+ has its own header which is completely separate and invisible to apc_cache.c.
+ As far as apc_cache.c is concerned the block of memory it is working with could
+ have come from a call to malloc().
+
+ The header looks like this:
+
+ typedef struct header_t header_t;
+ struct header_t {
+ int num_hits; /* total successful hits in cache */
+ int num_misses; /* total unsuccessful hits in cache */
+ slot_t* deleted_list; /* linked list of to-be-deleted slots */
+ };
+
+ Since this is at the start of the shared memory segment, these values are accessible
+ across all the yapache processes and hence access to them has to be locked.
+
+ After the header we have an array of slots. The number of slots is user-defined
+ through the apc.num_slots ini hint. Each slot is described by:
+
+ typedef struct slot_t slot_t;
+ struct slot_t {
+ apc_cache_key_t key; /* slot key */
+ apc_cache_entry_t* value; /* slot value */
+ slot_t* next; /* next slot in linked list */
+ int num_hits; /* number of hits to this bucket */
+ time_t creation_time; /* time slot was initialized */
+ time_t deletion_time; /* time slot was removed from cache */
+ time_t access_time; /* time slot was last accessed */
+ };
+
+ The slot_t *next there is a linked list to other slots that happened to hash to the
+ same array position.
+
+ apc_cache_insert() shows what happens on a new cache insert.
+
+ slot = &cache->slots[hash(key) % cache->num_slots];
+
+ cache->slots is our array of slots in the segment. hash() is simply:
+
+ static unsigned int hash(apc_cache_key_t key)
+ {
+ return key.data.file.device + key.data.file.inode;
+ }
+
+ That is, we use the file's device and inode to uniquely identify it. Initially
+ we had used the file's full path, but getting that requires a realpath() call which
+ is amazingly expensive since it has to stat each component of the path to resolve
+ symlinks and get rid of relative path components. By using the device+inode we
+ can uniquely identify a file with a single stat.
+
+ So, on an insert we find the array position in the slots array by hasing the device+inode.
+ If there are currently no other slots there, we just create the slot and stick it into
+ the array:
+
+ *slot = make_slot(key, value, *slot, t)
+
+ If there are other slots already at this position we walk the link list to get to
+ the end. Here is the loop:
+
+ while (*slot) {
+ if (key_equals((*slot)->key.data.file, key.data.file)) {
+ /* If existing slot for the same device+inode is different, remove it and insert the new version */
+ if ((*slot)->key.mtime != key.mtime) {
+ remove_slot(cache, slot);
+ break;
+ }
+ UNLOCK(cache);
+ return 0;
+ } else if(cache->ttl && (*slot)->access_time < (t - cache->ttl)) {
+ remove_slot(cache, slot);
+ continue;
+ }
+ slot = &(*slot)->next;
+ }
+
+ That first key_equals() check sees if we have an exact match meaning the file
+ is already in the cache. Since we try to find the file in the cache before doing
+ an insert, this will generally only happen if another process managed to beat us
+ to inserting it. If we have a newer version of the file at this point we remove
+ it an insert the new version. If our version is not newer we just return without
+ doing anything.
+
+ While walking the linked list we also check to see if the cache has a TTL defined.
+ If while walking the linked list we see a slot that has expired, we remove it
+ since we are right there looking at it. This is the only place we remove stale
+ entries unless the shared memory segment fills up and we force a full expunge via
+ apc_cache_expunge(). apc_cache_expunge() walks the entire slots array and walks
+ down every linked list removing stale slots to free up room. This is obviously
+ slow and thus only happens when we have run out of room.
+
+ apc_cache_find() simply hashes and returns the entry if it is there. If it is there
+ but older than the mtime in the entry we are looking for, we delete the one that is
+ there and return indicating we didn't find it.
+
+ Next we need to understand what an actual cache entry looks like. Have a look at
+ apc_cache.h for the structs. I sort of glossed over the key part earlier saying
+ that we just used the device+inode to find a hash slot. It is actually a bit more
+ complex than that because we have two kinds of caches. We have the standard file
+ cache containing opcode arrays, but we also have a user-controlled cache that the
+ user can insert whatever they want into via apc_store(). For the user cache we
+ obviously don't have a device+inode. The actual identifier is provided by the user
+ as a char *. So the key is actually a union that looks like this:
+
+ typedef union _apc_cache_key_data_t {
+ struct {
+ int device; /* the filesystem device */
+ int inode; /* the filesystem inode */
+ } file;
+ struct {
+ char *identifier;
+ } user;
+ } apc_cache_key_data_t;
+
+ struct apc_cache_key_t {
+ apc_cache_key_data_t data;
+ int mtime; /* the mtime of this cached entry */
+ };
+
+ And we have two sets of functions to do inserts and finds. apc_cache_user_find()
+ and apc_cache_user_insert() operate on the user cache.
+
+ Ok, on to the actual cache entry. Again, because we have two kinds of caches, we
+ also have the corresponding two kinds of cache entries described by this union:
+
+ typedef union _apc_cache_entry_value_t {
+ struct {
+ char *filename; /* absolute path to source file */
+ zend_op_array* op_array; /* op_array allocated in shared memory */
+ apc_function_t* functions; /* array of apc_function_t's */
+ apc_class_t* classes; /* array of apc_class_t's */
+ } file;
+ struct {
+ char *info;
+ zval *val;
+ unsigned int ttl;
+ } user;
+ } apc_cache_entry_value_t;
+
+ And then the actual cache entry:
+
+ struct apc_cache_entry_t {
+ apc_cache_entry_value_t data;
+ unsigned char type;
+ int ref_count;
+ };
+
+ The user entry is pretty simple and not all that important for now. I will
+ concentrate on the file entries since that is what holds the actual compiled
+ opcode arrays along with the functions and classes required by the executor.
+
+ apc_cache_make_file_entry() in apc_cache.c shows how an entry is constructed.
+ The main thing to understand here is that we need more than just the opcode
+ array, we also need the functions and classes created by the compiler when it
+ created the opcode array. As far as the executor is concerned, it doesn't know
+ that it isn't operating in normal mode being called right after the parse/compile
+ phase, so we need to recreate everything so it looks exactly like it would at
+ that point.
+
+7. my_compile_file() and apc_compile.c
+
+ my_compile_file() in apc_main.c controls where we get the opcodes from. If
+ the user-specified filters exclude the file from being cached, then we just
+ call the original compile function and return. Otherwise we fetch the request
+ time from Apache to avoid an extra syscall, create the key so we can look up
+ the file in the cache. If we find it we stick it on a local stack which we
+ use at cleanup time to make sure we return everything back to normal after a
+ request and call cached_compile() which installs the functions and classes
+ associated with the op_array in this entry and then copy the op_array down
+ into our memory space for execution.
+
+ If we didn't find the file in the cache, we need to compile it and insert it.
+ To compile it we simply call the original compile function:
+
+ op_array = old_compile_file(h, type TSRMLS_CC);
+
+ To do the insert we need to copy the functions, classes and the opcode array
+ the compile phase created into shared memory. This all happens in apc_compile.c
+ in the apc_copy_op_array(), apc_copy_new_functions() and apc_copy_new_classes()
+ functions. Then we make the file entry and do the insert. Both of these
+ operations were described in the previous section.
+
+8. The Optimizer
+
+ The optimizer has been deprecated.
+
+If you made it to the end of this, you should have a pretty good idea of where things are in
+the code. I skimmed over a lot of things, so plan on spending some time reading through the code.
+