APC简介

APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”。它为我们提供了缓存和优化PHP的中间代码的框架。 APC的缓存分两部分:系统缓存和用户数据缓存。

  • 系统缓存 它是指APC把PHP文件源码的编译结果缓存起来,然后在每次调用时先对比时间标记。如果未过期,则使用缓存的中间代码运行。默认缓存 3600s(一小时)。但是这样仍会浪费大量CPU时间。因此可以在php.ini中设置system缓存为永不过期(apc.ttl=0)。不过如果这样设置,改运php代码后需要重启WEB服务器。目前使用较多的是指此类缓存。
  • 用户数据缓存 缓存由用户在编写PHP代码时用apc_store和apc_fetch函数操作读取、写入的。如果数据量不大的话,可以一试。如果数据量大,使用类似memcache此类的更加专著的内存缓存方案会更好。

在APC中我们也可以享受APC带来的缓存大文件上传进度的特性,需要在php.ini中将apc.rfc1867设为1,并且在表单中加一个隐藏域 APC_UPLOAD_PROGRESS,这个域的值可以随机生成一个hash,以确保唯一。之前的一篇文章PHP文件上传进度的实现原理中有对此更为细致的说明。

APC与PHP内核的交互

APC是作为一个扩展添加到PHP体系中的。因此,按照PHP的扩展规范,它会有PHP_MINIT_FUNCTION、PHP_MSHUTDOWN_FUNCTION、PHP_RINIT_FUNCTION、PHP_RSHUTDOWN_FUNCTION等宏定义的函数。在PHP_MINIT_FUNCTION(apc)中有调用apc_module_init中,并且在此函数中通过重新给zend_compile_file赋值以替换系统自带的编译文件过程,从而将APC自带的功能和相关数据结构插入到整个PHP的体系中。

这里会有一个问题,如果出现多个zend_compile_file的替换操作呢?在实际使用过程,这种情况会经常出现,比如当我们使用xdebug扩展时,又使用了apc,此时PHP是怎么处理的呢?不管是哪个扩展,在使用zend_compile_file替换时,都会有一个自己的compile_file函数(替换用),还有一个作用域在当前扩展的,一个旧的编译函数:old_compile_file。相当于每个扩展当中都保留了一个对于前一个编译函数的引用,形成一个单向链表。并且,所有最终的op_array都是在新的zend_compile_file中通过old_compile_file生成,即都会沿着这条单向链表,将编译的最终过程传递到PHP的zend_compile_file实现。在传递过程中,每经过一个节点,这些节点都会增加一些属于自己的数据结构,以实现特定的需求。

APC内部存储结构

在APC内部,对于系统缓存和用户缓存分别是以两个全局变量存储,从代码逻辑层面就隔离了两种缓存,当然,这两种存储的实现过程和数据结构是一样的,它们都是apc_cache_t类型,如下:

 
    /* {{{ struct definition: apc_cache_t */
    struct apc_cache_t {
        void* shmaddr;                共享缓存的本地进程地址
        cache_header_t* header;       缓存头,存储在共享内存中
        slot_t** slots;               缓存的槽数组,存储在共享内存中
        int num_slots;                存储在缓存中的槽个数
        int gc_ttl;                   GC列表中槽的最大生存时间
        int ttl;                      如果对槽的访问时间大于这个TTL,需要则移除这个槽
        apc_expunge_cb_t expunge_cb;  /* cache specific expunge callback to free up sma memory */
        uint has_lock;                为可能存在的造成同一进程递归锁而存在的标记 /* flag for possible recursive locks within the same process */
    };
    /* }}} */

对于一个缓存,apc_cache_t类型的变量是其入口,它包含了这个缓存的一些全局信息。每个缓存都会有多个缓存槽,包含在slots字段中,slots的个数包含在num_slots字段,槽的过程时间控制在于ttl字段。对于用户缓存和系统缓存,默认情况下系统缓存数量为1000,实际上APC创建了1031个,也就是说默认情况下APC最少可以缓存1031个文件的中间代码。当然这个值还需要考虑内存大小,计算slot的key后的分布等等。更多的关于缓存的统计信息存储在header字段中,header字段结构为cache_header_t,如下:

 
struct cache_header_t {
        apc_lck_t lock;             读写锁,独占阻塞缓存锁
        apc_lck_t wrlock;           写锁,为防止缓存爆满
        unsigned long num_hits;     缓存命中数
        unsigned long num_misses;   缓存未命中数
        unsigned long num_inserts;  插入缓存总次数
        unsigned long expunges;     清除的总次数
        slot_t* deleted_list;       指向被清除的槽的链表
        time_t start_time;          以上计数器被重置的时间
        zend_bool busy;             当apc在忙于清除缓存时告诉客户端此时状态的标记
        int num_entries;            统计的实体数
        size_t mem_size;            统计的被用于缓存的内存大小
        apc_keyid_t lastkey;        用户缓存最后一写入的key
    };

一个缓存包含多个slots,每个slot都是一个slot结构体的变量,其结构如下:

 
    struct slot_t {
        apc_cache_key_t key;        槽的key
        apc_cache_entry_t* value;   槽的值
        slot_t* next;               链表中的下一个槽
        unsigned long num_hits;     这个bucket的命中数/* number of hits to this bucket */
        time_t creation_time;       槽的初始化时间
        time_t deletion_time;       槽从缓存被移除的时间 /* time slot was removed from cache */
        time_t access_time;         槽的最后一次被访问的时间
    };

每个槽包含一个key,以apc_cache_key_t结构体存储;包含一个值,以apc_cache_entry_t结构体存储。如下:

 
    typedef struct apc_cache_key_t apc_cache_key_t;
    struct apc_cache_key_t {
        apc_cache_key_data_t data;
        unsigned long h;              /* pre-computed hash value */
        time_t mtime;                 /* the mtime of this cached entry */
        unsigned char type;
        unsigned char md5[16];        /* md5 hash of the source file */
    };

结构说明如下:

  • data字段 apc_cache_key_data_t类型,一个联合体,存储key的关联信息,比如对于系统缓存,其可能会存储文件的路径或OS的文件device/inode;对于用户缓存可能会存储用户给定的标识或标识长度。
  • h字段 文件完整路径或用户给定的标识的hash值,使用的hash算法为PHP自带的time33算法;或者文件所在device和inode的和
  • mtime字段 缓存实体的修改时间
  • type字段 APC_CACHE_KEY_USER:用户缓存; APC_CACHE_KEY_FPFILE:系统缓存(有完整路径); APC_CACHE_KEY_FILE: 系统缓存(需要查找文件)
  • md5字段 文件内容的MD5值,这个字段与前面四个字段不同,它是可选项,可以通过配置文件的apc.file_md5启用或禁用。并且这个值是在初始化实体时创建的。看到这里源文件的md5值,想起之前做过一个关于MySQL数据表中访问路径查询的优化,开始时通过直接查询路径字段,在数据量达到一定级别时,出现了就算走索引还是会很慢的情况,各种方案测试后,采用了以新增一个关于访问路径的md5值查询解决。

除了入口,APC在最终的数据存储上对于系统缓存和用户缓存也做了区分,在_apc_cache_entry_value_t分别对应file和user。

 
    typedef union _apc_cache_entry_value_t {
        struct {            
            char *filename; /* absolute path to source file */
            zend_op_array* op_array;     存储中间代码的op_array
            apc_function_t* functions; /* array of apc_function_t's */
            apc_class_t* classes; /* array of apc_class_t's */
            long halt_offset; /* value of __COMPILER_HALT_OFFSET__ for the file */
        } file;                         file结构体 系统缓存所用空间,包括文件名,,
        struct {
            char *info;
            int info_len;
            zval *val;
            unsigned int ttl;           过期时间
        } user;                         ser结构体 用户缓存所用空间
    } apc_cache_entry_value_t;

如图所示:

APC缓存存储结构

APC缓存存储结构

初始化

在APC扩展的模块初始化函数(PHP_MINIT_FUNCTION(apc))中,APC会调用apc_module_init函数初始化缓存所需要的全局变量,如系统缓存则调用apc_cache_create创建缓存全局变量apce_cache,默认情况下会分配1031个slot所需要的内存空间,用户缓存也会调用同样的方法创建缓存,存储在另一个全局变量apc_user_cache,默认情况下会分配4099个内存空间。这里分配的空间的个数都是素数,在APC的代码中有一个针对不同数量的素数表primes(在apc_cache.c文件)。素数的计算是直接遍历素数表,找到表中第一个比需要分配的个数大的素数。

缓存key生成规则

APC的缓存中的每个slot都会有一个key,key是 apc_cache_key_t结构体类型,除了key相关的属性,关键是h字段的生成。 h字段决定了此元素落于slots数组的哪一个位置。对于用户缓存和系统缓存,其生成规则不同。

  • 用户缓存通过apc_cache_make_user_key函数生成key。通过用户传递进来的key字符串,依赖PHP内核中的hash函数(PHP的hashtable所使用的hash函数:zend_inline_hash_func),生成h值。
  • 系统缓存通过apc_cache_make_file_key函数生成key。通过APC的配置项apc.stat的开关来区别对待不同的方案。在打开的情况下,即 apc.stat= On 时,如果被更新则自动重新编译和缓存编译后的内容。此时的h值是文件的device和inode相加所得的值。在关闭的情况下,即apc.stat=off时,当文件被修改后,如果要使更新的内容生效,则必须重启Web服务器。此时h值是根据文件的路径地址生成,并且这里的路径是绝对路径。即使你是使用的相对路径,也会查找PG(include_path)定位文件,以取得绝对路径,所以使用绝对路径会跳过检查,可以提高代码的效率。

添加缓存过程

以用户缓存为例,apc_add函数用于给APC缓存中添加内容。如果key参数为字符串中,APC会根据此字符串生成key,如果key参数为数组,APC会遍历整个数组,生成key。根据这些key,APC会调用_apc_store将值存储到缓存中。由于这是用户缓存,当前使用的缓存为apc_user_cache。执行写入操作的是apc_cache_make_user_entry函数,其最终调用apc_cache_user_insert执行遍历查询和写入操作。与此对应,系统缓存使用apc_cache_insert执行写入操作,其最终都会调用_apc_cache_insert。

不管是用户缓存还是系统缓存,大体的执行过程类似,步骤如下:

  1. 通过求余操作,定位当前key的在slots数组中的位置: cache->slots[key.h % cache->num_slots];
  2. 在定位到slots数组中的位置后,遍历当前key对应的slot链表,如果存在slot的key和要写入的key匹配或slot过期,清除当前slot。
  3. 在最后一个slot的后面插入新的slot。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows APC(Asynchronous Procedure Call)是一种异步过程调用机制,它允许一个线程在另一个线程上异步执行指定的函数。APC 机制是 Windows 操作系统中非常重要的一部分,它被广泛用于实现各种系统功能,例如异步 I/O、线程池等。 在 Windows 中,每个线程都有一个 APC 队列,该队列中存储了需要在该线程上异步执行的函数。当一个线程进入 Alertable 状态时(例如调用 Sleep、WaitForSingleObject 等函数),它会检查自己的 APC 队列中是否有待处理的 APC,如果有,则会立即执行 APC 中指定的函数。 APC 机制的具体原理如下: 1. 创建 APC 对象 首先,创建一个 APC 对象,该对象包含要在目标线程上执行的函数和参数。 2. 将 APC 对象插入到目标线程的 APC 队列 使用 QueueUserAPC 函数将 APC 对象插入到目标线程的 APC 队列中。当目标线程进入 Alertable 状态时,它会检查自己的 APC 队列中是否有待处理的 APC,如果有,则会立即执行 APC 中指定的函数。 3. 触发目标线程进入 Alertable 状态 为了让目标线程进入 Alertable 状态,可以使用 Sleep、WaitForSingleObject 等函数来实现。当目标线程进入 Alertable 状态时,它会检查自己的 APC 队列中是否有待处理的 APC,如果有,则会立即执行 APC 中指定的函数。 总之,APC 机制是 Windows 操作系统中非常重要的一部分,它提供了一种有效的异步过程调用机制,可以在不阻塞目标线程的情况下异步执行指定的函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值