Category Archives: Develop

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了。

Docker for Mac with Kubernetes初次尝试

首先,2018-01-09日Docker公司宣布了Docker for Mac支持Kubernetes

后来陆续尝试了几次,今天终于成功了,所以记录下。

安装Docker for Mac with Kubernetes

首先,安装Docker for Mac Edge版本:

brew cask install docker-edge

设置代理(我用的是https://github.com/netheril96/MEOW):

开启Kubernetes:

 

等待安装:

OK了:

kubectl version也能看到客户端版本和服务端版本了:

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.2", GitCommit:"5fa2db2bd46ac79e5e00a4e6ed24191080aa463b", GitTreeState:"clean", BuildDate:"2018-01-18T10:09:24Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.2", GitCommit:"5fa2db2bd46ac79e5e00a4e6ed24191080aa463b", GitTreeState:"clean", BuildDate:"2018-01-18T09:42:01Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"linux/amd64"}

创建kubernetes-dashboard 服务

$ kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml
secret "kubernetes-dashboard-certs" created
serviceaccount "kubernetes-dashboard" created
role "kubernetes-dashboard-minimal" created
rolebinding "kubernetes-dashboard-minimal" created
deployment "kubernetes-dashboard" created
service "kubernetes-dashboard" created

等一会就可以看到创建好了:

$ kubectl get pods --namespace kube-system
NAME READY STATUS RESTARTS AGE
etcd-docker-for-desktop 1/1 Running 0 6m
kube-apiserver-docker-for-desktop 1/1 Running 2 6m
kube-controller-manager-docker-for-desktop 1/1 Running 0 6m
kube-dns-6f4fd4bdf-zl9dh 3/3 Running 0 7m
kube-proxy-xsx8n 1/1 Running 0 7m
kube-scheduler-docker-for-desktop 1/1 Running 0 5m
kubernetes-dashboard-5bd6f767c7-6szsl 1/1 Running 0 1m
$ kubectl get deployments --namespace kube-system
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kube-dns 1 1 1 1 7m
kubernetes-dashboard 1 1 1 1 1m

先运行kubectl proxy:

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

然后访问:

http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/

即可看到dashboard:

来一个Wordpress的例子吧

clone仓库:

git clone https://github.com/kubernetes/examples.git
cd examples/mysql-wordpress-pd/

然后基本上按照官方教程来就好了

创建MySQL密码

创建密码

$ kubectl create secret generic mysql-pass --from-literal=password=YOUR_PASSWORD
secret "mysql-pass" created

使用kubectl get secrets 可以看到有一个名字为mysql-pass 的secret

$ kubectl get secrets
NAME TYPE DATA AGE
default-token-z7lmq kubernetes.io/service-account-token 3 15m
mysql-pass Opaque 1 3s

部署MySQL

开始部署MySQL服务:

$ kubectl create -f mysql-deployment.yaml
service "wordpress-mysql" created
persistentvolumeclaim "mysql-pv-claim" created
deployment "wordpress-mysql" created

看下pvc创建好了没有:

$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-pv-claim Bound pvc-32bcbb1a-1143-11e8-a423-025000000001 20Gi RWO hostpath 1m

看下MySQL的pod创建好了没有:

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
wordpress-mysql-58cf8dc9f9-5grjs 1/1 Running 0 1m

部署Wordpress

$ kubectl create -f wordpress-deployment.yaml
service "wordpress" created
persistentvolumeclaim "wp-pv-claim" created
deployment "wordpress" created

检查下pvc和service的状态:

$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-pv-claim Bound pvc-32bcbb1a-1143-11e8-a423-025000000001 20Gi RWO hostpath 3m
wp-pv-claim Bound pvc-a5f97603-1143-11e8-a423-025000000001 20Gi RWO hostpath 11s
$ kubectl get services wordpress
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
wordpress LoadBalancer 10.96.2.58 <pending> 80:30734/TCP 32s

此处EXTERNAL-IP为<pending>,这个我也不清楚为何。

等一会之后,访问http://127.0.0.1/ ,就可以看到熟悉的WordPress安装界面了:

清理Wordpress

kubectl delete secret mysql-pass
kubectl delete deployment -l app=wordpress
kubectl delete service -l app=wordpress
kubectl delete pvc -l app=wordpress

 

如何在macOS上运行GTK 3(hello, world)

首先,安装 Homebrew

然后,brew install gtk+3

随后,查阅GTK的官方文档,写Hello, world:

// hello.c
#include <gtk/gtk.h>

static void
activate (GtkApplication* app,
          gpointer        user_data)
{
  GtkWidget *window;

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Window");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);
  gtk_widget_show_all (window);
}

int
main (int    argc,
      char **argv)
{
  GtkApplication *app;
  int status;

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

然后编译:

gcc `pkg-config --cflags gtk+-3.0` -o hello hello.c `pkg-config --libs gtk+-3.0`

当前目录下就有了hello二进制可执行文件,执行即可:

[评论]systemd @ Facebook — a year later[All Systems Go! 2017]

今天看了All Systems Go! 2017上,systemd @ Facebook — a year later这个talk。记录下自己的感想。

首先,FaceBook软件更新还是比较及时的,CentOS 7 和 systemd 都上了:

可怜我司还在Ubuntu 14.04上

另外,很多基础组件都是和上游有良好的沟通的:

长远来看,紧跟上游对公司的技术实力、技术影响力都有好处。当然,短期来看,我的系统工作的很好,不跟进上游没毛病啊。国内,大部分是后者吧……

当然,大公司都有自己的repo:

FaceBook居然都已经接入了Meson,很让我吃惊。

资源管理

资源管理用的是cgroup2:

结合systemd中的slice:

在docker的潮流之下,FaceBook还是比较 old school style 的。不过,slice确实很够解决这个问题,后面可以看看

服务监控

systemd也提供了监控接口,通过dbus暴露出来:

一个daemon收集数据,上报:

使用Cython包装的sd-bus来获取数据,IPython写原型:

案例学习

一般都是直接关闭rpm里面重启的脚本,FaceBook还做的一个工具。

journalctl可以看单机日志,不知道分布式日志有没有用journald。

嗯,一个小坑

systemd的这个转义很反人类,但是他们并没有打算改!

FaceBook的建议

文档要贴近用户,积极参与上游,技术分享!

评论

Reddit上,很多人都不喜欢systemd这种经常改API,不考虑兼容性的做法。

当然,也有人讨论视频、文字交流的效率……


实际上,之前有一个系统需要部署。

我考察过很多daemonize的方式,从最经典的init.d,到中间的supervisord、upstrat,再到激进的systemd。

综合来看,systemd比较灵活,而且支持user-service,非常适合初期服务的部署。

但是,由于我司用的是Ubuntu 14.04,故只能切回到supervisord。服务的reload,对于supervisord是一个大问题。

个人而言,还是很期待systemd普及的。

另外,关于最后的建议。国内公司倒是经常tech talks,但是很多时候,并不会将自己的东西放开,也不会推向上游,这对长期的技术发展很不利。

如何调试iptables [CentOS 7]

首先,来一张非常有用的图:

基础知识

iptables可以jump到TRACE目标,开启包追踪(packet tracing)选项。

iptables有一个raw表,基本上是iptables最先处理包的地方。

 开始调试

在最入口的位置给符合条件的包开启包追踪,比如对所有进入的icmp包:

iptables -t raw -I PREROUTING -p icmp -j TRACE

有的时候,还需要加载需要的模块(CentOS 7下):

modprobe nf_log_ipv4

然后你就可以在/var/log/messages看到包追踪信息了。

关闭

删除掉TRACE规则即可:

iptables -t raw -D PREROUTING 1

这是一次对机器ping产生的trace信息:

 可以看到,firewalld强行加戏:)

Oct 21 15:34:50 vultr kernel: TRACE: raw:PREROUTING:policy:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING_ZONES_SOURCE:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING_ZONES:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public_log:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public_deny:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public_allow:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:return:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:policy:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING_ZONES_SOURCE:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING_ZONES:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public_log:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public_deny:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public_allow:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:return:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:DOCKER:return:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:policy:5 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:INPUT:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:INPUT_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:INPUT:policy:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT:rule:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT_ZONES_SOURCE:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT:rule:5 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT_ZONES:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public_log:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public_deny:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public_allow:return:13 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: security:INPUT:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: security:INPUT_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: security:INPUT:policy:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:INPUT:policy:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0

参考资料

Hello Wayland — Wayland教程

翻译自https://hdante.wordpress.com/2014/07/08/the-hello-wayland-tutorial/

TLDR

我写了一个wayland下的hello world,源码在 https://github.com/hdante/hello_wayland

介绍

从最终用户的角度,很容易理解wayland是什么:它是一个新的窗口系统,它将显示服务器和窗口管理器合并了[1]。从技术角度来看,wayland是为了摆脱传统,使用现代设计来实现一个高效的窗口系统,解决X窗口系统中长期存在的效率问题和一些极端情况[2]。这个教程展示如何实现一个作为wayland客户端的hello world程序、解释基础的wayland概念、创建一个GUI程序的必要流程。hello world程序不需要任何GUI工具包,它直接使用底层的wayland协议,以便解释wayland的基础概念。本教程是我自己研究wayland协议的结果。教程分为两部分。这是第一篇教程,解释所有的概念和程序的高级部分。

再问一次,什么是wayland?

wayland窗口系统的的完整设计分为好几层。如果你下载了wayland library的代码[3],或者你看了下wayland的API[4],你会注意到两层:

  1. 最基础的一层是进程间通讯功能的实现,以及一些实用工具。比如主循环调度器和一些数据类型。大部分这些代码都出现在wayland library中(所有在src文件夹[5]中的内容),并且和窗口系统无关。
  2. 第二层是窗口系统协议。它的描述在protocol/wayland.xml[6]文件中,这个文件应该算是一种接口定义语言。IDL文件可以用wayland-scanner 工具处理,并在 wayland-client-protocol.h 和 wayland-server-protocol.h中生成代理方法。协议定义了客户端程序和显示服务器的基础功能。比如访问输入设备、注册共享缓存以便显示在屏幕上。wayland library并不实现这些协议。这些协议的实现被分割到一个第三方层。服务端的参考实现是weston的一部分[7],它在客户端和服务端都定义了一些附加层。以实现wayland协议。在hello world程序中,我们并不需要了解任何关于weston的东西。我们仅仅需要IDL文件。

从上面关于wayland library的描述中,我们发现wayland的三个定义。它是一个(额听用途的)IPC库,不像D-Bus库,它仅仅用于显示服务器,并且它没有定义wayland到底是什么、wayland协议的定义以及如何找到协议定义。我相信即使人们在阅读官方文档后,大部分人也不明白wayland到底是什么。我想,这三个定义澄清了“什么是wayland”这个问题,每一个定义都能够用在不同的上下文中。

Hello, World!

介绍的部分已经够长了,我们来看看hello world的代码:

#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wayland-client.h>

#include "helpers.h"

static const unsigned WIDTH = 320;
static const unsigned HEIGHT = 200;
static const unsigned CURSOR_WIDTH = 100;
static const unsigned CURSOR_HEIGHT = 59;
static const int32_t CURSOR_HOT_SPOT_X = 10;
static const int32_t CURSOR_HOT_SPOT_Y = 35;

static bool done = false;

void on_button(uint32_t button)
{
    done = true;
}

int main(void)
{
    struct wl_buffer *buffer;
    struct wl_shm_pool *pool;
    struct wl_shell_surface *surface;
    int image;

    hello_setup_wayland();

    image = open("images.bin", O_RDWR);

    if (image < 0) {
        perror("Error opening surface image");
        return EXIT_FAILURE;
    }

    pool = hello_create_memory_pool(image);
    surface = hello_create_surface();
    buffer = hello_create_buffer(pool, WIDTH, HEIGHT);
    hello_bind_buffer(buffer, surface);
    hello_set_cursor_from_pool(pool, CURSOR_WIDTH,
        CURSOR_HEIGHT, CURSOR_HOT_SPOT_X, CURSOR_HOT_SPOT_Y);
    hello_set_button_callback(surface, on_button);

    while (!done) {
        if (wl_display_dispatch(display) < 0) {
            perror("Main loop error");
            done = true;
        }
    }

    fprintf(stderr, "Exiting sample wayland client...\n");

    hello_free_cursor();
    hello_free_buffer(buffer);
    hello_free_surface(surface);
    hello_free_memory_pool(pool);
    close(image);
    hello_cleanup_wayland();

    return EXIT_SUCCESS;
}

wayland协议太啰嗦,所以我将代码分成两部分,包含了协议细节的部分以及其他部分。主模块,如上所述,包含了高级层面的hello world实现。main函数代表与显示服务器进行通信以显示hello world窗口并接受指针设备输入所需的完整步骤,在单击时关闭应用程序。代码的相关部分的描述如下:

#include "helpers.h"

helper模块包含了hello_*函数和wayland中的一大堆全局对象。全局根对象是一个名字为display的对象,代表了一个与显示服务器的连接,并且被用于发送请求、接受事件。在运行主循环的时候,也会用到display对象。在本教程的下一部分中将详细介绍helper模块。

static const unsigned WIDTH = 320;
static const unsigned HEIGHT = 200;
static const unsigned CURSOR_WIDTH = 100;
static const unsigned CURSOR_HEIGHT = 59;
static const int32_t CURSOR_HOT_SPOT_X = 10;
static const int32_t CURSOR_HOT_SPOT_Y = 35;
在本教程中,我们将一个图像显示为主窗口,另一个用于显示光标。 它们的位置是硬编码的。 然而,在一般应用中,值将被动态计算。
void on_button(uint32_t button)
{
    done = true;
}

这是按钮回调。当一个按钮被点击的时候,我们设置done为true,可以让我们离开main中的主循环。

hello_setup_wayland();

程序开始就会调用setup函数,连接到显示服务器,从服务器请求一系列的全局对象,填充到代理变量中去。

image = open("images.bin", O_RDWR);

然后,我们打开图片文件。这个图片文件包含了硬编码的图片,已经是显示用的数据格式了:它是显示用的像素值。

pool = hello_create_memory_pool(image);

wayland的主要设计哲学就是处理图像的时候更加高效。wayland使用在客户端和服务器端共享内存的方式达到这个目标,所以数据不会在服务器和客户端之间复制。客户端和服务器端共享的基本元素就是共享内存池,就是一段内存被mmap到客户端和服务器端。在内存池中,一系列图片能够被当作缓存对象添加到其中,这些图片也都是共享的。

在hello world程序中,我们mmap了我们的硬编码图像文件。在一般程序中,会创建一个空的内存池,比如用shm_open()创建一个共享内存对象,然后用展示widget的图片,动态构造图片缓存。在写hello world程序的时候,我必须决定我应不应该创建空的内存池、分配内存,以及哪种方式更容易理解。或者我使用一个不太直观的例子来创建一个预建内存池。我决定用这个直观的例子,有一个重要的原因:如果你阅读整个hello world的源代码,你会发现在任何地方都没有内存复制操作。图像文件打开一次,并显示一次。不需要额外的副本。这样做是为了明确指出,如果认真实施,wayalnd应用程序可以实现效率最大化。

surface = hello_create_surface();

代表了可见元素的对象变成了平面。平面是一个长方形区域,有自己的位置和大小。平面被缓冲对象填充。在平面的生命周期中,将附加几个缓冲区作为平面内容,并且会求服务器重绘平面。在hello world例子中,平面对象的类型是wl_shell_surface,被用来创建顶级窗口。

buffer = hello_create_buffer(pool, WIDTH, HEIGHT);

缓冲对象里面有平面要显示的内容。缓冲在内存池中被创建(它们是内存池切片),以便它们由客户端和服务器共享。在我们的例子中,我们不会创建一个空的缓冲对象,我们用一个已经填充好数据的的缓冲对象。

hello_bind_buffer(buffer, surface);

为了使缓冲区显示到屏幕,我们需要将缓冲区数据绑定到平面,也就是将平面内容设置为缓冲区数据。绑定操作也会讲平面提交到服务器。在wayland中,有一个平面所有权的概念:要不就是客户端拥有平面:客户端可以在平面上绘制(服务端复制了一份旧的平面);要不就是服务端拥有平面:客户端无法修改平面,因为服务器正在屏幕上绘制它。为了将所有权转移到服务器,我们给服务器提交请求;为了将所有权返回给客户端,服务器发送一个release事件。在一般应用程序中,表面将来回转移所有权,但在hello应用程序中,只需执行一次即可,作为绑定操作的一部分。

hello_set_cursor_from_pool(pool, CURSOR_WIDTH,
    CURSOR_HEIGHT, CURSOR_HOT_SPOT_X, CURSOR_HOT_SPOT_Y);

设置了主窗口了之后,我们来配置鼠标指针。这是必须的步骤:客户端必须设置鼠标指针。在这儿,我们将鼠标指针设置为内存池中的内容。helper模块为鼠标指针创建一个平面和缓冲对象。我必须决定是将指针配置放在helper里面,还是将它放到外面的高层逻辑里面来。我决定用后者,来展示wayland中的角色划分:客户端能够控制绘制什么、如何绘制。

hello_set_button_callback(surface, on_button);

设置的最后一步是设置回调:将界面点击和on_callback回调关联起来。

while (!done) {
    if (wl_display_dispatch(display) < 0) {
        perror("Main loop error");
        done = true;
    }
}

这段代码调用主循环,将全局变量display作为参数。当done为true的时候,主循环退出——或者因为出错了,或者因为按钮被点击了。

hello_free_cursor();
hello_free_buffer(buffer);
hello_free_surface(surface);
hello_free_memory_pool(pool);
close(image);
hello_cleanup_wayland();

程序结束了,清理所有我们申请的资源。

总结

这是wayland的hello world教程的第一部分。主要讨论什么是wayland,如何描述操作,在一个高级的例子中。在下一部分,我将会详细介绍helper函数。

截图

原作者截图:

我的截图:

参考

[1] http://wayland.freedesktop.org/architecture.html
[2] http://mirror.linux.org.au/linux.conf.au/2013/ogv/The_real_story_behind_Wayland_and_X.ogv
[3] http://cgit.freedesktop.org/wayland/wayland/tree/
[4] http://wayland.freedesktop.org/docs/html/
[5] http://cgit.freedesktop.org/wayland/wayland/tree/src
[6] http://cgit.freedesktop.org/wayland/wayland/tree/protocol/wayland.xml
[7] http://cgit.freedesktop.org/wayland/weston/tree/

如何在Linux/Fedora下编译安装为知笔记

虽然为知收费了,但是目前只有为知笔记的Linux客户端做的不错,也只能用它了。

首先,安装编译期间的依赖:

# git拉代码,cmake编译
sudo dnf install -y git cmake
# 编译器
sudo dnf install -y gcc gcc-c++
# qt5相关的包
sudo dnf install -y qt5-qtbase-devel qt5-linguist qt5-qtwebengine-devel qt5-qtwebsockets-devel

sudo dnf install -y zlib-devel

拉代码(此处以v2.5.0分支为例):

git clone https://github.com/WizTeam/WizQTClient.git
cd /path/to/WizQTClient
git checkout v2.5.0

编译:

# 安装到~/.local/目录
cmake . -DCMAKE_INSTALL_PREFIX=$HOME/.local/
# -j 参数加速编译
make -j $(nproc)
# 安装
make install

安装完成后,为知笔记位于$HOME/.local/bin/WizNote,也可以在应用列表中找到。


  • 编译的时候cmake检测模块不通过,会提示如下错误:
CMake Error at cmake/QtChooser.cmake:1 (find_package):
  By not providing "FindQt5.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "Qt5", but
  CMake did not find one.
  Could not find a package configuration file provided by "Qt5" with any of
  the following names:
    Qt5Config.cmake
    qt5-config.cmake

你只需要通过dnf provides */Qt5Config.cmake 命令来找找哪个包提供了,比如qt5-qtbase-devel

  • make的时候会报:
lib/quazip/zip.h:56:18: fatal error: zlib.h: No such file or directory
 #include "zlib.h"

也是一样的处理方式,使用dnf provides */zlib.h 找到包,比如zlib-devel

  • 为什么使用 $HOME/.local/ 作为安装目录:

首先,/usr/local 作为安装目录,需要root权限才能安装,删除的时候也不好删除,因为/usr/local/ 下可能也有别的软件。

其次,/opt/wiznote 作为安装目录,也需要root权限,而且也会导致wiznote.desktop中申明的图标名称找不到。

$HOME/.local/ 作为安装目录,生成的wiznote.desktop开箱即用,也不需要root权限,比较方便。

[译]输入系统如何工作——键盘输入

本文是 How input works – Keyboard input 的翻译。

上一篇博文中,我[作者]解释了KWin中,输入设备是如何打开和处理的。在这篇博文中,我们将仔细关注下键盘设备和键盘事件。

键盘并不总是键盘

在Linux中,键盘是一个非常奇特的设备。你不会只有一个键盘,你肯定会有很多个。很多设备宣称自己是键盘,但是只支持一个键。比如,电源按钮或者带静音按钮、音量加减按钮的外置耳机。从输入系统的角度来看,这些设备也是键盘。

对于KWin,发现真正支持的键盘是很重要的。如果没有一个“真的”键盘连接(或者说开启),我们的虚拟键盘应该自动激活。比如,如果你从平板PC二合一设备上拔了键盘,它应该转换为平板模式,即有虚拟键盘的模式。如果连接上了键盘,虚拟键盘就不是主要的输入设备了。libinput提供了函数来检测键盘支持哪些键。我们使用这个功能来区分不同的键盘类型。

键盘事件

在这里,键盘是最简单的输入设备。libinput仅仅触发一个LIBINPUT_EVENT_KEYBOARD_KEY事件,并包含了被按下或者释放的键。KWin有一个专用线程,用来从libinput读取事件。所以这些事件现在仅仅是放在处理队列中,主线程同时也得到新事件通知。一旦主线程处理事件,事件就被转换成我们的输入重定向类。不管事件是通过哪种源头抵达,所有输入事件都会通过输入重定向。KWin不仅仅支持来自libinput的事件,并且支持嵌套会话(即KWin运行在X11之上,或者在另一个Wayland服务器之上),还有在集成测试中使用的模拟事件。这意味着一旦事件到达输入重定向,我们基本上就丢失了事件来自哪个设备这些信息。最近,我们扩展了内部API,可以在事件处理函数中包含设备源,这是可选的。可以在调试终端中使用,来显示哪些设备产生了哪些事件。

xkbcommon

现在,一个键的按下、释放事件到达了中心分发方法KeyboardInputRedirection::processKey。第一件也是重要的一件事就是在xbkcommon中更新键盘状态。xkbcommon被用将物理键为事件,转换过程根据键盘布局、键盘状态(比如激活的修饰符)来进行。比如:如果我按下了“y”键(键码是21)并且“Shift”键是按下的状态,在德语键盘布局下,就会创建“Z”按下的事件;但是在英语键盘布局下,它会是“Y”。

在KWin中,我们将所有需要的xkbcommon功能都封装到Xkb类中。这个类追踪活跃的键盘布局、执行键盘布局切换(当布局变换的时候展示OSD)。它知道上一次按键的符号,当前活跃的修饰符和快捷键相关的修饰符。

当xkb的状态更新的时候,我们也检查很多键盘状态变更。用户是不是激活了数字锁定?如果是这样,我们需要让键盘上的LED做对应的变化,所以我们的libinput代码能够更新物理键盘上LED的状态。修饰副是不是变掉了?如果是这样,我们要将新的修饰符集合通知给Wayland窗口。尽管实际上从key到符号的翻译发生在Wayland客户端,但刚刚讲的翻译过程也在Wayland服务端进行了。为什么KWin也要做这一层翻译呢?这是因为KWin也需要在很多地方使用键盘符号,比如在当前窗口中过滤按键,来实现全局快捷键。

调试终端中的键盘状态

Xkb状态更新功能也能够处理哪些只需要修饰符键就能触发的快捷键。实际上,在这儿处理是不对的,但我们的输入过滤代码没法保证能捕获到所有的输入事件。对于快捷键中的修饰符,看多所有的事件是很基础的需求。所以,唯一的处理地方只能在Xkb中了。这不是最优雅的解决方案,但是能工作。这个功能也被X11使用,我在之前的博文中讲过。

KWin的过滤

现在KWin有足够的信息来处理键盘事件。对于此,它创建自定义的QKeyEvent并将其送到输入过滤链。KWin的输入处理使用了输入过滤器组成的链。每个过滤器都能够对事件做一系列操作,并决定这个事件是不是应该被继续处理,或结束处理。

比如,我们有锁屏过滤器。如果屏幕是锁屏状态,这个过滤器就会过滤事件处理,确保事件只会被送到屏幕锁定程序,不会被其他窗口处理。或者这儿有一个过滤器,确保ctrl+alt+f1在锁屏情况下仍然能够工作。另一个过滤器用来处理全局快捷键,一个将事件传递给特效系统,比如窗口轮转。

在调试终端中,一个输入事件过滤器显示被按下的键

最后一个过滤器是我们的转发过滤器。这个过滤器的任务是转发事件到窗口。它将事件从当前活动的Wayland平面,传递到KWayland::Server。

获得键盘输入焦点的Wayland表面

Wayland服务器需要获取了键盘焦点的平面。在获取了键盘焦点的情况下,KWin相比而言微不足道。KWin有一个叫活动窗口的概念。在转发事件之前,KWin会检查哪个是键盘焦点窗口。如果有一个活动窗口,则该窗口平面被标记为KWayland::Server中的键盘焦点平面。

我们的KWayland::Server库关注发送丢失键盘焦点和获取键盘焦点事件到窗口,所以KWin不需要关注这些。这是我们有一个KWayland::Server抽象层的优势——和混成器无关的一切都在库中被处理了。

Wayland中键盘事件的处理

之前提到的转发过滤器更新键盘平面,并且将当前键盘事件发送到Wayland客户端。对于所有在keysymbol中的处理,都不需要了。key code已经送到了客户端。

客户端从回调中得到键盘事件,现在也将其送到xkbcommon。在Wayland中,keymap从服务端送到客户端,以便服务端和客户端有同样的keymap。客户端现在将key code转换成key symbol,和之前KWin做的一样。

接下来的事件处理在客户端完成。比如在QT中,这一步会产生QKeyEvent,然后送到当前输入焦点的widget。

重复输入键

键盘输入也有一些特殊的模式:重复键。当一些键被按下,产生重复的键输入。KWin使用来自键盘模块的配置来决定什么时候、什么频率来产生重复的按键。一个重复的键不会被转发到KWayland客户端。而是KWin通过Wayland键盘协议告诉键重复的设定,而不是被客户端直接处理。

不幸的是,QT中这个是不能工作的,使用的是硬编码。所以当前在Plasma Wayland的会话中,根据使用的GUI库不同,对重复键的支持很破碎。如果运行在Wayland上的话,KWin支持是正常的,X11程序是对的,GTK程序是对的,QT程序是错的。


如果你想支持我们[作者]的工作,请考虑捐助

[译]输入系统如何工作——创建设备

本文是How input works – creating a Device的翻译。

最近,我[作者]在KWin/Wayland输入栈上做了一些工作,比如实现了鼠标手势和鼠标限制协议(pointer constraints protocol),并且想写一些系列博客来描述输入事件是如何从设备传递到应用的。在第一篇博文中,我将关注于创建和配置输入设备,以及与此相关的其他事情。

evdev

Linux内核通过evdev API来产生输入事件。如果你对evdev如何工作感兴趣,我推荐你读一读Peter Hutterer写的优秀的博文。对于我们关注的层面来说,输入事件API太底层了,我们仅仅想用对它的一个抽象。

libinput 和设备文件

对输入事件API的抽象叫做libinput。它允许我们得到通知,不论何时添加、删除输入设备、不论何时产生输入事件。但是别急,我们首先需要打开输入设备,这是我们的第一个挑战。

正常情况下,设备文件不能被用户读取。这有好的一面,否则每个程序都能够读取所有的键盘事件。这种情况下,键盘记录器太容易实现了。

但是如果KWin以普通用户的方式运行,普通用户又无法读取设备文件,那KWin如何获取输入?为了让KWin获取输入,我们需要特殊的支持。libinput就是为这种情况准备的,它自己不会尝试打开文件,它会使用用户提供的open_restricted函数。KWin就是这么干的,并将打开文件的任务交给了logind。logind允许一个进程控制当前登录会话。并且这个登录会话的控制进程可以打开一些设备文件。所以KWin和logind的dbus API交互,成为登录会话的控制进程,通过logind的API打开设备文件,并将其传递回libinput。

这也是为什么对于整个Wayland会话,KWin在运行时依赖logind的DBus接口。请注意,这并不意味着你需要使用logind或者sysemd。这仅仅意味着需要一个进程来同logind的DBus接口通讯。

KWin中的设备

现在libinput已经准备好打开设备文件,并且为每个设备触发了LIBINPUT_EVENT_DEVICE_ADDED事件。KWin为每一种设备类型创建了一个小的门面类,来应用设备的配置。KWin支持读取Plasma的鼠标配置模块中的配置选项。并且KWin打开了LED灯,如果设备支持LED灯的话,比如数字锁定灯和大写锁定灯,这也是设备配置过程的一部分。

所有的输入设备都被KWin创建,并且可以在调试终端显示(打开KRunner,输入“KWin”)。KWin从libinput中读取了很多关于设备的信息,并展示在调试终端中。在输入事件选项卡中,每种事件都包含了设备产生该事件的一些信息。

所有的设备也通过DBus暴露出来,和KWin的调试终端信息一样。这意味着可以通过DBus来动态修改配置。不过,KWin在成功应用配置后,会将其保存,以确保你在重启系统、或者重新插拔外部设备后,你的设置能够被恢复。这是一个很重要的特性,用来支持触摸板配置模块。

如果你想支持作者的工作,考虑捐赠给作者,让世界变得更美好!——KDE 2016年年终募款

How to use mysql-connector-python 2.1.5 on Django 1.10?

MySQL Connector/Python currently not available from PyPI

When you want use mysql-connector-python as db backend in Django, the first thing you noticed is that the latest version of mysql-connector-python on PyPI is 2.0.4, but official version is 2.1.5.

The official developer explained this.(But why not host mysql-connector-python on PyPI?!).

so, you can bypass this problem. Add follow line into requirements.txt:

https://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-2.1.5.tar.gz#egg=mysql-connector-python

when you configured Connector/Python Django Backend, and run python manage.py migrate , you will meet error:

DatabaseError: Data truncated for column ‘applied’ at row 1

This is because mysql.connector.django cannot handle datetime with timezone.

You need patch 93c7b38.

when you fix this, you must drop all table, and re-run python manage.py migrate, you will meet another error:

  File "/path/to/venv/lib/python3.5/site-packages/mysql/connector/django/operations.py", line 223, in bulk_insert_sql
    return "VALUES " + ", ".join([items_sql] * num_values)
TypeError: can't multiply sequence by non-int of type 'tuple'

can’t multiply sequence by non-int of type ‘tuple’

This is because bulk_insert_sql ‘s third argument changed from int to list of placeholder rows, since Django 1.9.

You need another patch, patch 8dab00b. (Of cause, we doesn’t drop support of Django 1.6)


Now you can use mysql-connector-python 2.1.5 on Django 1.10, if you meet another problem, post the comment here!