Tag Archives: PHP

PHP/Composer是如何加载一个类的

PHP/composer开发中,我们只需要require ‘vendor/autoload.php’,然后就可以直接使用各种类了。那么这些类是如何加载的呢?其中有没有什么可以优化的点呢?

概览

PHP/composer下,类的加载主要到如下部分(还没有包括各个部分的初始化逻辑):

PHP中zend_lookup_class_ex
    |-> EG(class_table)
    |-> spl_autoload_call
        |-> Composer\Autoload\ClassLoader::loadClass
            |-> findFile
                |-> class map lookup
                |-> PSR-4 lookup
                |-> PSR-0 lookup

PHP的类加载

首先,PHP在运行的时候,需要一个类,是通过zend_lookup_class_ex来找到这个类的相关信息的。

zend_lookup_class_ex查找类的主要逻辑如下(假设类名字放到变量lc_name中):

ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, const zval *key, int use_autoload) /* {{{ */
{
  // 1. 类名字转化为小写
    if (ZSTR_VAL(name)[0] == '\\') {
      lc_name = zend_string_alloc(ZSTR_LEN(name) - 1, 0);
      zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1);
    } else {
      lc_name = zend_string_tolower(name);
    }

  // 2. 直接在class_table中查找
  ce = zend_hash_find_ptr(EG(class_table), lc_name);
  if (ce) {
    if (!key) {
      zend_string_release(lc_name);
    }
    return ce;
  }
  // 3. 如果没有autoload_func,则注册默认的__autoload
  if (!EG(autoload_func)) {
    zend_function *func = zend_hash_str_find_ptr(EG(function_table), ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1);
    if (func) {
      EG(autoload_func) = func;
    } else {
      if (!key) {
        zend_string_release(lc_name);
      }
      return NULL;
    }

  }

  // 4. 加载ACLASS的过程中,又加载ACLASS,递归加载,直接找不到类
  if (zend_hash_add_empty_element(EG(in_autoload), lc_name) == NULL) {
    if (!key) {
      zend_string_release(lc_name);
    }
    return NULL;
  }

  // 5. 调用autoload_func
  ZVAL_STR_COPY(&fcall_info.function_name, EG(autoload_func)->common.function_name);
  fcall_info.symbol_table = NULL;

  zend_exception_save();
  if ((zend_call_function(&fcall_info, &fcall_cache) == SUCCESS) && !EG(exception)) {
    ce = zend_hash_find_ptr(EG(class_table), lc_name);
  }
  zend_exception_restore();

  if (!key) {
    zend_string_release(lc_name);
  }
  return ce;
}
  1. lc_name转化成小写(这说明PHP中类名字不区分大小写)
  2. 然后在EG(class_table)找,如果找到,直接返回(我们自己注册的类,扩展注册的类都是这样找到的)
  3. 然后查看EG(autoload_func) ,如果没有则将__autoload注册上(值得注意的是,如果注册了EG(autoload_func),则不会走__autoload)
  4. 通过EG(in_autoload)判断是否递归加载了(EG(in_autoload)是一个栈,记载了那些类正在被autoload加载)
  5. 然后调用EG(autoload_func),并返回类信息

SPL扩展注册

刚刚可以看到,PHP只会调用EG(autoload_func),根本没有什么SPL的事情,那么SPL是如何让PHP调用自己的类加机制的呢?

首先,我去找SPL扩展的MINIT过程,结果发现其中并没有相关的逻辑。

出乎我的意料,这个注册过程在spl_autoload_register中完成:

PHP_FUNCTION(spl_autoload_register)
{
    // 已经将SPL注册到PHP了,且当前用户要注册到spl的autoload函数已经注册,则跳过
    if (SPL_G(autoload_functions) && zend_hash_exists(SPL_G(autoload_functions), lc_name)) {
      if (!Z_ISUNDEF(alfi.closure)) {
        Z_DELREF_P(&alfi.closure);
      }
      goto skip;
    }

    // 如果必要的话,初始化SPL_G(autoload_functions)
    if (!SPL_G(autoload_functions)) {
      ALLOC_HASHTABLE(SPL_G(autoload_functions));
      zend_hash_init(SPL_G(autoload_functions), 1, NULL, autoload_func_info_dtor, 0);
    }

    // 如果之前已经注册了spl_autoload,那就将spl_autoload转移到autoload_functions中
    spl_func_ptr = zend_hash_str_find_ptr(EG(function_table), "spl_autoload", sizeof("spl_autoload") - 1);
    if (EG(autoload_func) == spl_func_ptr) { /* registered already, so we insert that first */
      autoload_func_info spl_alfi;

      spl_alfi.func_ptr = spl_func_ptr;
      ZVAL_UNDEF(&spl_alfi.obj);
      ZVAL_UNDEF(&spl_alfi.closure);
      spl_alfi.ce = NULL;
      zend_hash_str_add_mem(SPL_G(autoload_functions), "spl_autoload", sizeof("spl_autoload") - 1,
          &spl_alfi, sizeof(autoload_func_info));
      if (prepend && SPL_G(autoload_functions)->nNumOfElements > 1) {
        /* Move the newly created element to the head of the hashtable */
        HT_MOVE_TAIL_TO_HEAD(SPL_G(autoload_functions));
      }
    }

    // 将用户要注册的函数,即lc_name,放到autoload_functions中
    if (zend_hash_add_mem(SPL_G(autoload_functions), lc_name, &alfi, sizeof(autoload_func_info)) == NULL) {
      if (obj_ptr && !(alfi.func_ptr->common.fn_flags & ZEND_ACC_STATIC)) {
        Z_DELREF(alfi.obj);
      }
      if (!Z_ISUNDEF(alfi.closure)) {
        Z_DELREF(alfi.closure);
      }
      if (UNEXPECTED(alfi.func_ptr->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) {
        zend_string_release(alfi.func_ptr->common.function_name);
        zend_free_trampoline(alfi.func_ptr);
      }
    }
    if (prepend && SPL_G(autoload_functions)->nNumOfElements > 1) {
      /* Move the newly created element to the head of the hashtable */
      HT_MOVE_TAIL_TO_HEAD(SPL_G(autoload_functions));
    }
skip:
    zend_string_release(lc_name);
  }

  // 根据autoload_functions的值,决定向PHP注册spl_autoload_call还是spl_autoload
  if (SPL_G(autoload_functions)) {
    EG(autoload_func) = zend_hash_str_find_ptr(EG(function_table), "spl_autoload_call", sizeof("spl_autoload_call") - 1);
  } else {
    EG(autoload_func) =	zend_hash_str_find_ptr(EG(function_table), "spl_autoload", sizeof("spl_autoload") - 1);
  }

  RETURN_TRUE;
}

在composer环境下,这个函数的功能就是,将用户的autoload函数放到SPL_G(autoload_functions)中,且将spl_autoload_call注册到PHP中。

这样,PHP在找一个类的时候,就会调用spl_autoload_call了。

spl_autoload_call逻辑

spl_autoload_call的逻辑很简单:

PHP_FUNCTION(spl_autoload_call)
{
  if (SPL_G(autoload_functions)) {
    HashPosition pos;
    zend_ulong num_idx;
    int l_autoload_running = SPL_G(autoload_running);
    SPL_G(autoload_running) = 1;
    lc_name = zend_string_alloc(Z_STRLEN_P(class_name), 0);
    zend_str_tolower_copy(ZSTR_VAL(lc_name), Z_STRVAL_P(class_name), Z_STRLEN_P(class_name));
    zend_hash_internal_pointer_reset_ex(SPL_G(autoload_functions), &pos);
    // 遍历之前注册的autoload_functions
    while (zend_hash_get_current_key_ex(SPL_G(autoload_functions), &func_name, &num_idx, &pos) == HASH_KEY_IS_STRING) {
      alfi = zend_hash_get_current_data_ptr_ex(SPL_G(autoload_functions), &pos);
      if (UNEXPECTED(alfi->func_ptr->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) {
        zend_function *copy = emalloc(sizeof(zend_op_array));

        memcpy(copy, alfi->func_ptr, sizeof(zend_op_array));
        copy->op_array.function_name = zend_string_copy(alfi->func_ptr->op_array.function_name);
        // 调用autoload_function
        zend_call_method(Z_ISUNDEF(alfi->obj)? NULL : &alfi->obj, alfi->ce, &copy, ZSTR_VAL(func_name), ZSTR_LEN(func_name), retval, 1, class_name, NULL);
      } else {
        zend_call_method(Z_ISUNDEF(alfi->obj)? NULL : &alfi->obj, alfi->ce, &alfi->func_ptr, ZSTR_VAL(func_name), ZSTR_LEN(func_name), retval, 1, class_name, NULL);
      }
      zend_exception_save();
      if (retval) {
        zval_ptr_dtor(retval);
        retval = NULL;
      }
      // 如果调用结束之后,能在class_table找到类,则返回
      if (zend_hash_exists(EG(class_table), lc_name)) {
        break;
      }
      zend_hash_move_forward_ex(SPL_G(autoload_functions), &pos);
    }
    zend_exception_restore();
    zend_string_free(lc_name);
    SPL_G(autoload_running) = l_autoload_running;
  } else {
    /* do not use or overwrite &EG(autoload_func) here */
    zend_call_method_with_1_params(NULL, NULL, NULL, "spl_autoload", NULL, class_name);
  }
}
  1. 判断SPL_G(autoload_functions)存在
  2. 依次调用autoload_functions
  3. 如果调用完成后,这个类存在了,那就返回

至此,SPL的部分已经讲完了。我们来看看composer做了什么。

composer注册autoload

composer的autoload注册在 ‘vendor/autoload.php’ 中完成,这个文件完成了两件事:

  1. include vendor/composer/autoload_real.php
  2. 调用ComposerAutoloaderInit<rand_id>::getLoader()

vendor/composer/autoload_real.php仅仅定义了ComposerAutoloaderInit<rand_id>类和composerRequire<rand_id>函数。

<rand_id>是类似id一样的东西,确保要加载多个composer的autoload的时候不会冲突。composerRequire<rand_id>则是为了避免ComposerAutoloader require文件的时候,文件修改了ComposerAutoloader的东西。

接下来我们关注下ComposerAutoloaderInit<rand_id>::getLoader()做了哪些事情。

这个类的loader只会初始化一次,第二次是直接返回已经存在的loader了:

if (null !== self::$loader) {
    return self::$loader;
}

如果是第一次调用,先注册['ComposerAutoloaderInit<rand_id>', 'loadClassLoader'],然后new一个\Composer\Autoload\ClassLoader 作为$loader,然后立马取消注册loadClassLoader

也就是说['ComposerAutoloaderInit<rand_id>', 'loadClassLoader']的唯一作用就是加载\Composer\Autoload\ClassLoader

接下来就是在ComposerAutoloaderInit<rand_id>::getLoader()初始刚刚拿到的$loader了:

// autoload_namespaces.php里面放的是PSR-0
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}
// autoload_psr4.php里面放的是PSR-4注册的
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
    $loader->setPsr4($namespace, $path);
}
// autoload_classmap.php放的是classmap注册的
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
    $loader->addClassMap($classMap);
}
// ……
// 将[$loader, 'loadClass']注册到spl中
$loader->register(true);
// ……
// autoload_files.php是file声明的autoload
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire32715bcfade9cdfcb6edf37194a34c36($fileIdentifier, $file);
}
return $loader;
  1. autoload_namespaces.php返回的是各个包里面声明的PSR-0加载规则,是一个数组。key为namespace,有可能为空字符串;value为路径的数组。
  2. $loader->set,如果$namespace/$prefix为空,直接放到$loader->fallbackDirsPsr0数组中。如果不为空,则放到$loader->prefixesPsr0[$prefix[0]][$prefix]中(这可能是为了减少PHP内部的hash表冲突,加快查找速度)。
  3. autoload_psr4.php返回的是各个包里面声明的PSR-4加载规则,是一个数组。key为namespace,有可能为空字符串;value为路径的数组。
  4. $loader->setPsr4,如果$namespace/$prefix为空,直接放到$loader->fallbackDirsPsr4数组中。如果不为空,则将$namespace/$prefix的长度放到$loader->prefixLengthsPsr4[$prefix[0]][$prefix]中,将路径放到$loader->prefixDirsPsr4[$prefix]中。
  5. autoload_classmap.php返回的是各个包里面声明的classmap加载规则,是一个数组。key为class全名,value为文件路径。(这个信息是composer扫描全部文件得到的)
  6. $loader->addClassMap,则将这些信息array_merge到$loader->classMap中。
  7. autoload_files.php返回的是各个包里面声明的file加载规则,是一个数组。key为每个文件的id/hash,value是每个文件的路径。
  8. 注意,autoload_files.php里面的文件,在getLoader中就已经被include了。

到这儿,我们的$loader已经初始化好了,而且也已经注册到SPL中了

composer加载类

我们之前是将[$loader, ‘loadClass’]注册到了SPL中,那就看看它的逻辑吧:

public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);
        // 根据我们刚刚的分析,此处返回值是根本没有用
        return true;
    }
}

所以看下来,重点在findFile函数里面:

public function findFile($class)
{
    // 通过classmap找这个类
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }
    // 这里涉及到一个composer的性能优化:
    // https://getcomposer.org/doc/articles/autoloader-optimization.md#optimization-level-2-a-authoritative-class-maps
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }
    // 这里同样也涉及到性能优化:
    // https://getcomposer.org/doc/articles/autoloader-optimization.md#optimization-level-2-b-apcu-cache
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }
    // 这个函数处理了PSR-0和PSR-4的加载规则
    $file = $this->findFileWithExtension($class, '.php');

    // ……
    return $file;
}

如果是classmap的加载规则,那就会在这儿加载成功。如果是PSR-0或者PSR-4,则需要看看findFileWithExtension的逻辑了:

private function findFileWithExtension($class, $ext)
{
    // PSR-4 lookup
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

    // $prefix不为空的PSR-4加载规则
    $first = $class[0];
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath.'\\';
            if (isset($this->prefixDirsPsr4[$search])) {
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                    if (file_exists($file = $dir . $pathEnd)) {
                        return $file;
                    }
                }
            }
        }
    }

    // $prefix为空的PSR-4加载规则
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    // PSR-0 lookup
    if (false !== $pos = strrpos($class, '\\')) {
        // namespaced class name
        $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
        // PEAR-like class name
        $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }

    // $prefix不为空的PSR-0加载规则
    if (isset($this->prefixesPsr0[$first])) {
        foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
            if (0 === strpos($class, $prefix)) {
                foreach ($dirs as $dir) {
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                        return $file;
                    }
                }
            }
        }
    }

    // $prefix为空的PSR-0加载规则
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // 从include path中找文件
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    return false;
}
  1. $prefix不为空的PSR-4加载规则:
    1. 比如类A\B\C,先找A\B\对应目录下面的C.php;再找A\对应目录下面的B\C.php;以此类推
  2. $prefix为空的PSR-4加载规则
    1. 如果找不到,那就在fallbackDirsPsr4下找A\B\C.php文件
  3. $prefix不为空的PSR-0加载规则
    1. PSR-0支持namespace和下划线分隔的类(PEAR-like class name);这点对一些需要向namespace迁移的旧仓库很有用
    2. 对于类A\B\C或者A_B_C,先找A\B\对应目录下面的C.php;再找A\对应目录下面的B\C.php;以此类推
  4. $prefix为空的PSR-0加载规则
    1. 如果找不到,直接在prefixesPsr0中找A\B\C.php文件
  5. 如果还没有找到,在条件允许的状态下,可以到include path中找A\B\C.php文件

这样,composer就找到了这个类对应的文件,并且include了。

php-fpm的reload过程

背景

谈谈PHP的Reload操作中提到reload会让sleep提前结束,所以就探究了下fpm的reload操作如何实现。

本文在php7.0 fpm下分析,process_control_timeout设置不为0。


重启信号

首先,我们从PHP源码可以知道,fpm的reload操作实际上就是对fpm进程发送了USR2信号。

程序在处理信号的时候,主进程的逻辑相当于“暂停”了,如果在这儿执行一些操作的话,第一,有些局部变量拿不到;第二,可能会打断主进程的逻辑。所以,信号处理函数仅仅是通知主进程,用户发送了这个信号。

信号处理函数的注册

fpm的master进程中,fpm_signals_init_main函数通过sigaction注册了信号处理函数sig_handler

int fpm_signals_init_main() /* {{{ */
{
	struct sigaction act;

	// 。。。。。。

	memset(&act, 0, sizeof(act));
	act.sa_handler = sig_handler;
	sigfillset(&act.sa_mask);

	if (0 > sigaction(SIGTERM,  &act, 0) ||
	    0 > sigaction(SIGINT,   &act, 0) ||
	    0 > sigaction(SIGUSR1,  &act, 0) ||
	    0 > sigaction(SIGUSR2,  &act, 0) ||
	    0 > sigaction(SIGCHLD,  &act, 0) ||
	    0 > sigaction(SIGQUIT,  &act, 0)) {

		zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");
		return -1;
	}
	return 0;
}
/* }}} */

简而言之,通过sigfillset设置为block掉所有的信号,然后通过sigaction设置对应的信号处理函数。

信号处理函数

刚刚我们提到,当我们reload fpm时,systemctl向fpm的master进程发送USR2信号。这个时候,之前注册的信号处理函数sig_handler函数被执行:

static void sig_handler(int signo) /* {{{ */
{
	static const char sig_chars[NSIG + 1] = {
		[SIGTERM] = 'T',
		[SIGINT]  = 'I',
		[SIGUSR1] = '1',
		[SIGUSR2] = '2',
		[SIGQUIT] = 'Q',
		[SIGCHLD] = 'C'
	};
	char s;
        // ***
	s = sig_chars[signo];
	zend_quiet_write(sp[1], &s, sizeof(s));
	errno = saved_errno;
}
/* }}} */

关键点在zend_quiet_write,它就是write函数。sig_handler函数就是向sp[1]中写入了一个字符串2。

此处需要注意的是,sp[0]和sp[1]是通过socketpair创建的本地套接字,用于信号处理函数和主进程的通信。


master开始重启

之前的信号处理函数,仅仅是通过管道通知主进程,但是程序的主逻辑仍然不会被打乱,那fpm master进程怎么reload呢?

答案就在fpm_event_loop中,这是master进程的事件循环。

在循环之前,我们需要用sp[0]创建一个struct fpm_event_s,添加到监听的fd中:

int fpm_event_set(struct fpm_event_s *ev, int fd, int flags, void (*callback)(struct fpm_event_s *, short, void *), void *arg) /* {{{ */
{
	if (!ev || !callback || fd < -1) {
		return -1;
	}
	memset(ev, 0, sizeof(struct fpm_event_s));
	ev->fd = fd;
	ev->callback = callback;
	ev->arg = arg;
	ev->flags = flags;
	return 0;
}
/* }}} */

然后将这个struct fpm_event_s,也就是代码中的ev,添加到监听的fd中。

实际上,这个添加过程也和fpm不同的异步模型有关(都是由对应fpm_event_module_s的add方法实现的),比如epoll的实现就是将ev参数整体放到epoll_event的data.ptr中的。(poll的add可以参考源码

当所有的fd都添加了之后(当然不仅仅是signal相关的fd咯),我们就可以使用wait方法等待事件来临了。(epoll和poll也都各自实现了wait方法)

好,回到sig_handler给sp[1]写了个字符串2。wait方法接到了信号,拿到对应的ev,调用 fpm_event_fire ,实际上就是调用了ev的callback方法,就是fpm_got_signal方法

static void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
{
	char c;
	int res, ret;
	int fd = ev->fd;

	do {
		res = read(fd, &c, 1);

		switch (c) {
			// 。。。。。。
			case '2' :                  /* SIGUSR2 */
				zlog(ZLOG_DEBUG, "received SIGUSR2");
				zlog(ZLOG_NOTICE, "Reloading in progress ...");
				fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);
				break;
		}

		if (fpm_globals.is_child) {
			break;
		}
	} while (1);
	return;
}
/* }}} */

如果接收到了字符串2,则执行

fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET)

实际上就这么几行

void fpm_pctl(int new_state, int action) /* {{{ */
{
	switch (action) {
		case FPM_PCTL_ACTION_SET :
			//。。。。。。
			fpm_signal_sent = 0;
			fpm_state = new_state;

			zlog(ZLOG_DEBUG, "switching to '%s' state", fpm_state_names[fpm_state]);
			/* fall down */

		case FPM_PCTL_ACTION_TIMEOUT :
			fpm_pctl_action_next();
			break;
		//。。。。。
	}
}
/* }}} */

即,将fpm_state设置为FPM_PCTL_STATE_RELOADING后,没有break,继续执行fpm_pctl_action_next

static void fpm_pctl_action_next() /* {{{ */
{
	int sig, timeout;

	if (!fpm_globals.running_children) {
		fpm_pctl_action_last();
	}

	if (fpm_signal_sent == 0) {
		if (fpm_state == FPM_PCTL_STATE_TERMINATING) {
			sig = SIGTERM;
		} else {
			sig = SIGQUIT;
		}
		timeout = fpm_global_config.process_control_timeout;
	} else {
		if (fpm_signal_sent == SIGQUIT) {
			sig = SIGTERM;
		} else {
			sig = SIGKILL;
		}
		timeout = 1;
	}

	fpm_pctl_kill_all(sig);
	fpm_signal_sent = sig;
	fpm_pctl_timeout_set(timeout);
}
/* }}} */

即,给所有子进程发送SIGQUIT信号。

这边还有一个fpm_pctl_timeout_set,这个等会讨论。


子进程处理信号

父进程发送完信号了,就该子进程处理啦。

子进程只有SIGQUIT交给sig_soft_quit处理。子进程初始化完成后,收到了SIGQUIT信号,由sig_soft_quit处理,最终调用fcgi_terminate处理:

void fcgi_terminate(void)
{
	in_shutdown = 1;
}

就是将in_shutdown设置为1。


子进程退出

子进程的循环主体在fcgi_accept_request中,其中多处判断in_shutdown,若为1则直接退出:


超时处理

前面提到的超时处理的回调函数fpm_pctl_timeout_set。执行了如下操作:

fpm_pctl(FPM_PCTL_STATE_UNSPECIFIED, FPM_PCTL_ACTION_TIMEOUT);

在这种条件下,发送的信号变成了SIGTERM

在子进程的信号处理函数(fpm_signals_init_child)设置中,SIGTERM的处理行为是SIG_DFL,也就是直接退出子进程。


为何sleep会被打断?

我们可以看到,sleep的实现就是系统调用sleep(php_sleep是sleep的一个宏):

/* {{{ proto void sleep(int seconds)
   Delay for a given number of seconds */
PHP_FUNCTION(sleep)
{
	zend_long num;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &num) == FAILURE) {
		RETURN_FALSE;
	}
	php_sleep((unsigned int)num);

}
/* }}} */

sleep函数执行时,此时进程的状态是S:

interruptible sleep

此时一旦有信号触发,立马处理信号,比如我们刚刚说过的SIGQUIT,结束了之后发现,sleep执行完了。

因为sleep的说明写了啊:

sleep() makes the calling thread sleep until seconds seconds have
       elapsed or a signal arrives which is not ignored.

需要注意的是,php的sleep没有return系统调用sleep的返回值,所以即使信号打断了sleep,也仅仅是跳过sleep继续执行而已。而PHP代码无法通过返回值知道sleep是被打断了,还是真的sleep了指定的时间。

composer的自动加载机制解读

按照composer文档的说法,如果是composer项目,只需要在开始的时候require 'vendor/autoload.php'即可享受类的自动加载特性。可是这是如何实现的呢?

vendor/autoload.php

以Laravel 5.1项目为例,vendor/autoload.php文件只做了两件事情:

  1. include vendor/composer/autoload_real.php
  2. 调用ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()

vendor/composer/autoload_real.php仅仅定义了ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b类和composerRequireb6d254015e39cf5090fb84fdb1ed664b函数。(b6d254015e39cf5090fb84fdb1ed664b应该是类似id一样的东西,确保每次不同)

接下来我们关注下ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()做了哪些事情。

ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()

首先,这个类的loader只会初始化一次,第二次是直接返回已经存在的loader了:

if (null !== self::$loader) {
    return self::$loader;
}

如果是第一次调用,先注册['ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b', 'loadClassLoader'],然后new一个\Composer\Autoload\ClassLoader 作为loader,然后立马取消注册loadClassLoader

接下来就一步一步处理各种autoload了。

autoload_namespaces.php

直接从vendor/composer/autoload_namespaces.php中返回的就是$namespace$path的一个数组。通过\Composer\Autoload\ClassLoader::set方法来记录这些信息:

if (!$prefix) {
    // 如果namespace/prefix为空,则在其他PSR-0规则中查不到,才会到这个规则中
    $this->fallbackDirsPsr0 = (array) $paths;
} else {
    // 以后根据对应的namespace/prefix查找类的时候,第一个字母用来加速
    // 使用第一个字母作为一级key,可以减少二级key的冲突
    $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}

autoload_psr4.php

直接从该文件的返回值是$namespace$path的数组。通过\Composer\Autoload\ClassLoader::setPsr4来记录此信息:

if (!$prefix) {
    // 如果namespace/prefix为空,则其他PSR-4规则查找不到的时候,会到此规则中查找
    $this->fallbackDirsPsr4 = (array) $paths;
} else {
    // TODO
    $length = strlen($prefix);
    // 略去错误检查

    // TODO
    $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
    // 用来根据类前缀找path
    $this->prefixDirsPsr4[$prefix] = (array) $paths;
}

autoload_classmap.php

直接从该文件的返回值是$classname$path的数组。通过\Composer\Autoload\ClassLoader::addClassMap来记录此信息:

if ($this->classMap) {
    $this->classMap = array_merge($this->classMap, $classMap);
} else {
    $this->classMap = $classMap;
}

这些都处理完之后,\Composer\Autoload\ClassLoader::register[$this, 'loadClass']注册为autoload函数(通过spl_autoload_register)。

autoload_files.php

直接从该文件的返回值是$fileIdentifier到文件路径的映射,通过之前定义的composerRequireb6d254015e39cf5090fb84fdb1ed664b函数来require每个文件。

这个函数在require文件的时候同时也设置了$GLOBALS['__composer_autoload_files'][$fileIdentifier]的值为true。

\Composer\Autoload\ClassLoader::loadClass

这个函数的作用是装载一个类。

if ($file = $this->findFile($class)) {
    includeFile($file);

    return true;
}

\Composer\Autoload\ClassLoader::findFile

这个函数是composer装载类的重点。

  1. 首先是从classMap里面找$class对应的文件,如果有,直接返回。
  2. 然后从prefixDirsPsr4找到前缀符合的文件,如果找到,直接返回。(那个prefixLengthsPsr4就是用来判断需要从$class去掉的前缀长度)
  3. 接下来,直接从fallbackDirsPsr4对应的目录中查找文件。
  4. PSR-0加载
    • namespaced class name变换规则:\A_namespace\a_class=>/A_namespace/a/class.php 
    • PEAR-like class name变换规则:\A_namespace\a_class=>/A/namespace/a/class.php 

    prefixesPsr0中查找(和prefixDirsPsr4类似),直接找到对应的文件,返回。

  5. 如果没有,直接从fallbackDirsPsr0中尝试加载。

Laravel Facade实现细节考

前两天有人讲Laravel中的Facade的时候,看到了__callStatic的实现,问为何如此,所以查了一番。

现有实现

switch实现

我们在调用Facede的方法的时候,绝大多数都会被__callStatic来处理,Larvel 5.1的__callStatic实现如下:

switch (count($args)) {
    case 0:
        return $instance->$method();
    case 1:
        return $instance->$method($args[0]);
    case 2:
        return $instance->$method($args[0], $args[1]);
    case 3:
        return $instance->$method($args[0], $args[1], $args[2]);
    case 4:
        return $instance->$method($args[0], $args[1], $args[2], $args[3]);
    default:
        return call_user_func_array([$instance, $method], $args);
}

这个实现是 Taylor Otwell 最初实现的版本

cufa实现

是switch实现的简化版:

return call_user_func_array([$instance, $method], $args);

unpack实现

注意,Argument Unpacking 特性在php 5.6才实现。

return $instance->$method(...$args);

这个是 Laravel 5.3 的实现。

实现对比

代码如下:

class A
{
    public function get()
    {
        return 'a';
    }
}

class Facade
{
    public static function __callStatic($method, $args)
    {
        // 待替换
    }

    private static function getFacadeRoot()
    {
        return new A();
    }
}

表中为在各个情况下调用 Facede::get 100000000次的秒数

参数数量 php70+switch php70+cufa php70+unpack php56+switch php56+cufa php56+unpack
0 33 39 34 74 107 65
1 38 46 37 84 117 75
2 41 48 38 91 118 80
3 44 52 38 108 125 89
4 50 49 39 118 137 94
5 58 50 41 189 146 99
6 61 53 42 194 155 107

结论

可以看到,在PHP 5.6下,switch实现比cufa实现,在参数个数比较少的时候,有很大的优势,估计这个也是作者在第一次实现的时候,选择switch实现,而不是更简单的cufa的原因吧。

另外,unpack在PHP 5.6下更快,但由于当时需要支持PHP 5.3,所以不能使用。在Laravel 5.3中,由于最低PHP版本为 5.6.4 ,所以可以大胆的使用unpack了。

此外,实际上, PHP 7.0下,unpack在没有参数时,比switch要慢,但是个人觉得差别不大。

最后,PHP 7.0 的速度提升还是很明显的。

参考资料

https://github.com/laravel/framework/pull/12120

https://gist.github.com/nikic/6390366

composer中指定依赖分支名的坑

之前只是看着别人写的composer.json,知道如果依赖一个项目的master分之,则在依赖的version中可以写dev-master。我就以为所有对分支的依赖,都是写成dev-<branch_name>。

后来发现,v1.x这种分支名,不能直接使用dev-v1.x 来声明依赖。

查了下,https://getcomposer.org/doc/02-libraries.md#branches

For every branch, a package development version will be created. If the branch name looks like a version, the version will be {branchname}-dev. For example, the branch 2.0 will get the 2.0.x-dev version (the .x is added for technical reasons, to make sure it is recognized as a branch). The 2.0.x branch would also be valid and be turned into 2.0.x-dev as well. If the branch does not look like a version, it will be dev-{branchname}. master results in a dev-master version.

翻译如下:

对于每个分支,对应的包都会创建一个开发版本。如果分支名看起来像一个版本号,则开发版本的名字是{branchname}-dev。

比如,分支2.0则会创建2.0.x-dev版本(后面的 .x 是为了技术原因添加,为了确保这个版本能够被识别为分支)。

2.0.x 分支也会被转换为 2.0.x-dev。如果分支看起来不像一个版本,版本号将会是dev-{branchname}。master分支的版本号是dev-master。


另外,当同时存在1.0和1.0.x分支时,1.0.x-dev指向的是1.0分支。