安装docker

当前仅展示apt方式安装,离线安装方式不展示

清理历史残留

如果系统有预装docker或者先前安装过,先卸载干净在安装

1
2
3
4
5
6
## 第一次安装时输出为空
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt remove $pkg; done

## 如果以前有安装过docker,相关数据可以删除掉
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd

apt安装

设置代理

由于docker被gfw屏蔽,访问时需要使用vpn,这里使用先前搭建的ss服务

新建apt.conf

1
sudo vi /etc/apt/apt.conf

添加proxy配置,默认源地址配置直连,如此后续配置docker源后,使用apt update便可直接访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Acquire::http::Proxy "http://127.0.0.1:8118";
Acquire::http::Proxy {
archive.ubuntu.com DIRECT;
security.ubuntu.com DIRECT;
cn.archive.ubuntu.com DIRECT;
mirrors.tuna.tsinghua.edu.cn DIRECT;
}
Acquire::https::Proxy "http://127.0.0.1:8118";
Acquire::https::Proxy {
archive.ubuntu.com DIRECT;
security.ubuntu.com DIRECT;
cn.archive.ubuntu.com DIRECT;
mirrors.tuna.tsinghua.edu.cn DIRECT;
}

配置源并安装

按照docker官方教材安装docker,其中,下载docker.asc的命令做了一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
# 配置proxy
export http_proxy=http://127.0.0.1:8118;export https_proxy=http://127.0.0.1:8118;
# 下载后在使用sudo搬到指定目录
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o docker.asc && sudo mv docker.asc /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update

# 如果是k8s部署,只需要containerd就够了
sudo apt install containerd.io docker-ce docker-ce-cli docker-buildx-plugin docker-compose-plugin

后续

将当前用户加入docker用户组

1
2
sudo groupadd docker
sudo usermod -aG docker $USER

部署registry

配置代理

因为gfw的原因,docker hub的访问会时不时抽风,在使用docker pull前可以先配置代理,当前步骤可跳过

创建配置文件

1
2
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo vi /etc/systemd/system/docker.service.d/http-proxy.conf

配置proxy

1
2
3
4
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:8118"
Environment="HTTPS_PROXY=http://127.0.0.1:8118"
Environment="NO_PROXY=localhost,127.0.0.1,registry.noname.io"

重启并验证docker环境变量

1
2
3
4
5
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker
# 输出如下
# Environment=HTTP_PROXY=http://127.0.0.1:8118 HTTPS_PROXY=http://127.0.0.1:8118 NO_PROXY=localhost,127.0.0.1,registry.noname.io

生成ssl证书

可以直接使用http,但后续k8s镜像的下载必须使用https,没有那么多耐心再去折腾,在这里先解决了,一劳永逸

/data/docker/是我用于专门存放与registry相关的数据,包括certs以及后续docker push上来的镜像数据

下面命令关键的域名registry.noname.io要填好

1
2
3
4
openssl req \
-newkey rsa:4096 -nodes -sha256 -keyout /data/docker/certs/noname.io.key \
-addext "subjectAltName = DNS:registry.noname.io" \
-x509 -days 365 -out /data/docker/certs/noname.io.crt

由此,https所需要的证书也就准备好了

创建registry服务

注意

  1. /data/docker/certs挂载到container的/data/certs目录
  2. /data/docker/registry挂载到container的/var/lib/registry目录,后续删除重建container时就保留下了image数据

方法一

如果只是创建一个简单的可运行registry,直接运行docker run

1
2
3
4
5
6
docker run -d -p 5000:5000 --restart always --name registry \
-v /data/docker/registry:/var/lib/registry \
-v /data/docker/certs:/data/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/data/certs/noname.io.crt \
-e REGISTRY_HTTP_TLS_KEY=/data/certs/noname.io.key \
registry:2.8.3

方法二

如果涉及比较复杂的配置,像权限管理、接入s3文件系统、缓存配置、中继配置等,则使用配置文件方式比较好

创建registry配置文件,参考配置文件:example YAML file,修改后如下

1
2
3
4
5
6
7
8
9
version: 0.1
storage:
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
tls:
certificate: /data/certs/noname.io.crt
key: /data/certs/noname.io.key

运行docker run

1
2
3
4
5
docker run -d -p 5000:5000 --restart always --name registry \
-v /data/docker/registry:/var/lib/registry \
-v /data/docker/certs:/data/certs \
-v /data/docker/config/config.yml:/etc/docker/registry/config.yml \
registry:2.8.3

修改系统配置

修改hosts

1
sudo vi /etc/hosts

添加私有registry的域名

1
127.0.0.1 registry.noname.io

修改docker配置

1
2
sudo mkdir /etc/docker
sudo vi /etc/docker/daemon.json

insecure-registries需要添加registry的域名端口

1
2
3
4
{
"exec-opts": ["native.cgroupdriver=systemd"],
"insecure-registries" : ["registry.noname.io:5000"]
}

重启服务

1
sudo systemctl restart docker

如果要开放局域网内的访问,配置防火墙

1
2
sudo ufw allow 5000/tcp
sudo ufw reload

测试验证

访问registry接口,此时因为registry还没有任何image数据,输出为空

1
curl -k https://registry.noname.io:5000/v2/_catalog | jq .

推送镜像测试

1
2
docker tag alpine:3.20.1 registry.noname.io:5000/alpine:3.20.1
docker push registry.noname.io:5000/alpine:3.20.1

参考文档

Install Docker Engine on Ubuntu
Distribution Registry
Private Docker registry with HTTPS and a Nginx reverse proxy using Docker Compose

前言

一般情况下,日常开发我只使用vagrant跟virtualbox,但后面想搭建一个k8s环境学习,需要一个免费、高性能、便于管理的虚拟机管理软件

综合考虑了下现在市面上的虚拟机管理软件

  1. virtualbox 的磁盘性能太差,nat网络所有虚拟机都是同一个IP:10.0.2.15,其他方面也不考虑了
  2. parallel desktop 太贵太黑心
  3. vmware 价格也不便宜还很吃电脑资源
  4. hyper-v 管理界面太难用,同时无法固定虚拟机IP

最终我选择组装一台台式机,安装了ubuntu系统,并在上面安装使用kvm虚拟机

cpu是amd 7700,8核心16线程;32GB内存;暂时不需要用到显卡;PC在路由器做了MAC跟IP的绑定,方便使用ssh

由于virt-manager太难用,最后选择cockpit作为kvm虚拟机管理软件

kvm安装

前置工作

确认cpu开启虚拟化功能

1
2
# 一般是cpu的核心数量
egrep -c '(vmx|svm)' /proc/cpuinfo

安装kvm

安装kvm以及依赖软件

1
2
3
4
5
6
7
8
9
10
11
12
sudo apt install qemu-system-x86 libvirt-daemon-system virtinst \
virt-manager virt-viewer ovmf swtpm qemu-utils guestfs-tools \
libosinfo-bin tuned

# 开机启动
sudo systemctl enable libvirtd

# 验证安装
sudo virt-host-validate qemu

# 如果有使用window的需要,可以下载virtio
wget https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.240-1/virtio-win-0.1.240.iso

安装后工作

将用户添加到kvm跟libvirt组

1
2
sudo usermod -aG kvm $USER
sudo usermod -aG libvirt $USER

cockpit安装

1
sudo apt install cockpit cockpit-machines

防火墙配置

1
2
sudo ufw allow 9090/tcp
sudo ufw reload

浏览器打开访问web管理页面 https://192.168.0.105:9090

kvm with cockpit preview

参考文档

How to Install QEMU/KVM on Ubuntu to Create Virtual Machines
Managing KVM Virtual Machines with Cockpit Web Console in Linux
gpu-passthrough-tutorial

前言

由于国内恶劣的网络环境,作为一名频繁访问外网的Web开发者,不得不自建一个VPN服务,以下简单介绍Shadowsocks的搭建过程。

单纯的ss流量会迅速被gfw识别到并屏蔽,因此使用ss时需要增加obfs用以混淆欺骗gfw,可以极大程度减小被屏蔽的风险。当然,如果你用的是比较出名的服务器供应商,如bandwagon的服务器,那么被屏蔽的机会也很大。

服务端使用的一键安装脚本twist由于开发者已经放弃维护长时间,目前只有参考作用,如果你有耐心,也可以自己修改后再使用,也就跟下面的服务端搭建教程一样。搞完是真的累,太渣了,太需要提高自己的编程水平,写一个一体化的软件包才行

服务端

twist脚本

服务端使用的是twist脚本,里面集成了Shadowsocks+simple-obfs+fail2ban以及相关依赖包,使用shell编写,感兴趣的可以看看源代码,下载地址如下

1
https://github.com/forestbat/Twist

下载后,脚本添加执行权限

1
chmod +x twist

切换到root账户,twist执行需要

1
su - root

修改twist

twist文件里面有一些软件需要删掉/更换,有些则需要手动安装,先手动执行一遍twist脚本./twist install,确认有哪些错误,然后选择执行下面的命令

  1. 修改dependenciesinstall函数
1
2
3
4
5
6
7
# 不同的系统不一样,不要全改
# 1. 新增依赖包,如libcork-dev
# 2. 名称更换,如python-pip改为python3-pip
apt install python3-dev python3-pip python3-setuptools python3-m2crypto libcork-dev

# 将pip改为pip3,可能需要使用pip安装m2crypto
pip3 install qrcode
  1. 修改sslibevinstall函数
1
2
# github的包命名改了,解压后变成了libsodium-stable
tar zxf libsodium-${libsodiumver}.tar.gz && mv libsodium-stable libsodium-${libsodiumver}

或者删掉libsodium这段,直接手动编译安装,如下

1
2
3
4
5
wget https://github.com/jedisct1/libsodium/releases/download/1.0.19-RELEASE/libsodium-1.0.19.tar.gz
tar zxf libsodium-1.0.19.tar.gz
cd libsodium-1.0.19
./configure
make && make install
  1. 修改sslibevinstall函数
1
2
3
4
5
6
7
# 在 tar zxf ${ssobfsver}.tar.gz 这一行后面修改添加代码,因为obfs也有很长时间没有更新,导致在新的编译器下报错
if [ "$ssobfstag" == "v0.0.5" ]; then
# patch
sed -i -e "249s/$/ else {result = OBFS_ERROR;}/" /tmp/${ssobfsver}/src/obfs_http.c
sed -i -e '246d' /tmp/${ssobfsver}/src/obfs_http.c
# patch end
fi
  1. 修改sslibevinstallerrsslibevinstall函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# sslibevinstallerr函数如下
# 删掉mbedtls相关包,与libsodium类似,因为命名问题我直接绑定版本了
rm -rf libsodium-${libsodiumver}.tar.gz libsodium-${libsodiumver} ${mbedtlsver}.tar.gz ${mbedtlsver} ${sslibevver}.tar.gz $sslibevver ${ssobfsver}.tar.gz $ssobfsver

# sslibevinstall函数如下
# 搜索与mbedtlstag有关的代码段,修改绑定版本v2.28.1
# ldconfig
[ -z "$mbedtlstag" ] && mbedtlstag="v2.28.1"
mbedtlsver="mbedtls-$(echo ${mbedtlstag} | sed -e 's/^[a-zA-Z]//g')"
wget -t 3 -T 30 -nv -O ${mbedtlsver}.tar.gz https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/${mbedtlstag}.tar.gz
[ "$?" != "0" ] && sslibevinstallerr "mbedtls-$(echo ${mbedtlstag} | sed -e 's/^[a-zA-Z]//g')"
[ -d ${mbedtlsver} ] && rm -rf "${mbedtlsver}"
tar zxf ${mbedtlsver}.tar.gz
pushd $mbedtlsver
make SHARED=1 CFLAGS=-fPIC "-j$((MAKECORES+1))" && make DESTDIR=/usr install || sslibevinstallerr "mbedtls-${mbedtlstag} | sed -e 's/^[a-zA-Z]//g'" err
popd
ldconfig
rm -rf libsodium-${libsodiumver}.tar.gz libsodium-${libsodiumver} mbedtls-${mbedtlsver}.tgz mbedtls-${mbedtlsver}

安装完毕

执行修改后的twist脚本

1
./twist install

如果twist安装成功,此时命令行下将会展示ss服务的二维码以及url,手机端可以用小火箭扫描,桌面端可以复制url导入

客户端

安装软件

1
2
# obfs混淆ss流量;privoxy将http流量转换为socks5流量
sudo apt install shadowsocks-libev simple-obfs privoxy

ss配置

修改ss配置,配置文件可以是/etc/shadowsocks-libev/config.json,也可以是/etc/shadowsocks-libev/config-obfs.json

1
sudo vi /etc/shadowsocks-libev/config.json

按下方模板配置:

1
2
3
4
5
6
7
8
9
10
11
{
"server":["server_ip"],
"mode":"tcp_and_udp",
"server_port":443,
"local_port":1080,
"password":"server_password",
"timeout":86400,
"method":"xchacha20-ietf-poly1305",
"plugin":"obfs-local",
"plugin_opts":"obfs=tls;obfs-host=mzstatic.com;obfs-uri=/"
}

执行下面的命令,确认软件可正常运行

1
2
3
4
5
6
7
8
# 查看输出确认无异常报错
sudo ss-local -c /etc/shadowsocks-libev/config.json

# 1080端口
sudo netstat -tnlp | grep 1080

# 如果发现ss服务未能正常启动,查看status信息
sudo systemctl status shadowsocks-libev.service

修改systemd配置文件

1
sudo vi /usr/lib/systemd/system/shadowsocks-libev.service

ss-server改为ss-local,因为我们修改的是shadowsocks-libev.service,配置文件就是config.json,这里就不需要再修改什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  This file is part of shadowsocks-libev.
#
# Shadowsocks-libev is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This file is default for Debian packaging. See also
# /etc/default/shadowsocks-libev for environment variables.

[Unit]
Description=Shadowsocks-libev Default Client Service
Documentation=man:shadowsocks-libev(8)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
DynamicUser=true
EnvironmentFile=/etc/default/shadowsocks-libev
LimitNOFILE=32768
ExecStart=/usr/bin/ss-local -c $CONFFILE $DAEMON_ARGS

[Install]
WantedBy=multi-user.target

重启ss服务

1
2
sudo systemctl daemon-reload
sudo systemctl restart shadowsocks-libev

privoxy配置

修改配置文件/etc/privoxy/config

1
sudo vi /etc/privoxy/config

修改listen-address,因为有时我会将地址共享出来给局域网其他设备使用,所以我一般会将127.0.0.1改为0.0.0.0。在末尾添加forward-socks5指令,将所有http流量转发到1080端口

1
2
listen-address  0.0.0.0:8118
forward-socks5 / 127.0.0.1:1080 .

重启privoxy服务

1
sudo systemctl restart privoxy

防火墙开放8118端口访问(可选,如果没有打算给局域网内的其他机器使用就不要打开)

1
2
sudo ufw allow 8118/tcp
sudo ufw reload

最后验证

配置环境变量http_proxy以及https_proxy,访问外网地址,只要有html数据流输出即可

1
2
3
export http_proxy=http://127.0.0.1:8118;export https_proxy=http://127.0.0.1:8118;

curl -i https://www.google.com/

参考文档

Twist脚本github地址
Debian11下编译安装Shadowsocks-libev
Linux中使用ShadowSocks+Privoxy代理

以下简单介绍如何在C语言层面使用PHP类

对象

创建一个PHP对象,对象类似关联数组,对象之上可关联任意多个函数

1
2
3
4
5
6
7
PHP_FUNCTION(makeObject) {
object_init(return_value);

// 添加属性
zend_update_property_string(NULL, return_value, "prop1", strlen("prop1"), "val1");
zend_update_property_long(NULL, return_value, "prop2", strlen("prop2"), 123);
}

调用函数并打印结果var_dump(makeObject());,输出如下

1
2
3
4
5
6
object(stdClass)#1 (2) {
["prop1"]=>
string(3) "val1"
["prop2"]=>
int(123)
}

创建一个类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// class定义存储于zend_class_entry
zend_class_entry *test_ce_myclass;

static const zend_function_entry test_methods[] = {
// 类方法使用宏指令PHP_ME
// public function hello()
PHP_ME(MyClass, hello, NULL, ZEND_ACC_PUBLIC)
// 与函数定义相同
PHP_FE_END
};

// 主菜,注册并加载类
static void test_init_myclass()
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "MyClass", test_methods);
// 注册MyClass类
test_ce_myclass = zend_register_internal_class(&ce);
// 添加属性/常量等
// public $success = true;
zend_declare_property_bool(test_ce_myclass, "success", sizeof("success")-1, 1, ZEND_ACC_PUBLIC);
}

// 类方法hello
PHP_METHOD(MyClass, hello)
{
RETURN_STRING("hello");
}

其中,函数test_init_myclass的最后一个参数ZEND_ACC_PUBLIC为访问控制标记之一公共访问,常用的访问控制标记还有以下几个

1
2
3
4
5
6
7
ZEND_ACC_STATIC
ZEND_ACC_PUBLIC
ZEND_ACC_PROTECTED
ZEND_ACC_PRIVATE
ZEND_ACC_CTOR
ZEND_ACC_DTOR
ZEND_ACC_DEPRECATED

一个class定义注册相关逻辑已经完成,要在PHP中使用类MyClass还需要在模块初始化MINIT中添加运行test_init_myclass以加载类MyClass

1
2
3
4
5
PHP_MINIT_FUNCTION(test)
{
test_init_myclass();
return SUCCESS;
}

编译test模块并开启后,运行var_dump(new MyClass());,将得到以下类似输出

1
2
3
4
object(MyClass)#1 (1) {
["success"]=>
bool(true)
}

我们也可以直接在C层面初始化并生成一个实例化类对象,增加一个工厂方法factory

1
2
3
4
5
6
7
8
9
10
static const zend_function_entry test_methods[] = {
PHP_ME(MyClass, hello, NULL, ZEND_ACC_PUBLIC)
PHP_ME(MyClass, factory, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_FE_END
};

PHP_METHOD(MyClass, factory)
{
object_init_ex(return_value, test_ce_myclass);
}

此时,我们可以使用MyClass::factory()获取一个新的MyClass对象

如果需要对MyClass进行一些操作,像在PHP使用构造方法,在test_methods里添加__construct,如此,PHP在new MyClass()将自动调用构造方法__construct

1
2
3
4
5
6
7
8
9
10
11
12
static const zend_function_entry test_methods[] = {
// ZEND_ACC_CTOR
PHP_ME(MyClass, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
PHP_ME(MyClass, hello, NULL, ZEND_ACC_PUBLIC)
PHP_ME(MyClass, factory, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_FE_END
};

PHP_METHOD(MyClass, __construct)
{

}

调用类方法

上面,虽然对象在new时会自动调用__construct函数进行初始化,但factory不会自动调用构造函数,仅返回包含默认值的新对象,为此,我们需要在factory内调用构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "zend_interfaces.h"

PHP_METHOD(MyClass, factory)
{
zval *myzval;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_ZVAL(myzval)
ZEND_PARSE_PARAMETERS_END();

object_init_ex(return_value, test_ce_myclass);

// zend_call_method,其他的还有
// zend_call_method_with_0_params
// zend_call_method_with_1_params
// zend_call_method_with_2_params
// ZEND_API zval* zend_call_method(zval *object_pp, zend_class_entry *obj_ce, zend_function **fn_proxy, const char *function_name, size_t function_name_len, zval *retval, int param_count, zval* arg1, zval* arg2);
zend_call_method(return_value, test_ce_myclass, NULL, "__construct", sizeof("__construct")-1, NULL, 1, myzval, NULL);
}

this

先前我们已经了解函数返回值return_value的使用,现在,我们来看如何在方法内访问$this,PHP提供getThis()函数

1
2
3
4
5
6
7
8
9
10
11
PHP_METHOD(MyClass, __construct)
{
char *msg;
size_t msg_len;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_STRING(msg, msg_len)
ZEND_PARSE_PARAMETERS_END();

zend_update_property_string(test_ce_myclass, getThis(), "msg", sizeof("msg")-1, msg);
}

关联结构体

我们构建一个结构体,这个结构体在PHP是无法访问的,但可以在扩展内访问对该结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 还不是特别了解
typedef struct _test_struct {
zend_object std;
int unknown_id;
char *unknown_str;
} test_struct;

static zend_object *create_test_struct(zend_class_entry *class_type) {
test_struct *intern;

intern = ecalloc(1, sizeof(test_struct) + zend_object_properties_size(class_type));

zend_object_std_init(&intern->std, class_type);
object_properties_init(&intern->std, class_type);

intern->std.handlers = zend_get_std_object_handlers();

return &intern->std;
}

static void free_test_struct(void *object) {
test_struct *secrets = (test_struct*)object;
if (secrets->unknown_str) {
efree(secrets->unknown_str);
}
efree(secrets);
}

访问结构体test_struct

1
2
3
4
5
6
7
8
PHP_METHOD(MyClass, attachStruct) {
test_struct *secrets;

//
secrets = (test_struct*)getThis();

RETURN_LONG(secrets->unknown_id);
}

异常

PHP的异常继承自Exception,所以下面除了新增一个异常类,还涉及了类的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <zend_exceptions.h>

zend_class_entry *test_ce_exception;

static void test_init_exception() {
zend_class_entry ce;
// 设置第三个参数为NULL,继承Exception所有方法,无自定义行为
INIT_CLASS_ENTRY(ce, "MyException", NULL);
//
test_ce_exception = zend_register_internal_class_ex(&ce, (zend_class_entry*)zend_exception_get_default());
}

PHP_MINIT_FUNCTION(test)
{
test_init_myclass();
//
test_init_exception();
return SUCCESS;
}

在方法/函数中抛出异常

1
2
3
PHP_METHOD(MyClass, throwExcept) {
zend_throw_exception(test_ce_exception, "custom exception throw", 1024);
}

完整代码

php_ext_tutorial

参考文档

PHP Extensions Made Eldrich: Classes

从19年开始,ss流量已经能被gfw识别并屏蔽,原本打算切换到v2ray,但配置混淆需要域名反而会暴露出更多信息,此外,网上一篇文章指出v2ray开发者失联的同时一项v2ray流量识别的专利被注册,因此决定放弃v2ray并转向另一个被linus盛赞的wireguard。

wireguard是非常简单、现代化、快速的vpn,使用最新的加密技术,udp传输,支持ip漫游等。wireguard没有服务端、客户端的区分,每一台设备都是一个peer

安装

添加wireguard的PPA源

1
2
3
sudo add-apt-repository ppa:wireguard/wireguard
sudo apt update
sudo apt install wireguard

wireguard作为linux内核模块方式加载,内核更新wireguard也会自动重新编译,重启系统或使用modprobe开启模块

1
sudo modprobe wireguard

检查wireguard模块是否成功加载,成功输出与下面类似

1
2
3
4
vagrant@bionic:~$ lsmod | grep wireguard
wireguard 221184 0
ip6_udp_tunnel 16384 1 wireguard
udp_tunnel 16384 1 wireguard

生成keys

wireguard提供了两个命令行工具wg以及wg-quick,使用wg生成公钥和私钥

1
2
umask 077
wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey

类似ssh,需要为每台设备生成一对publickey/privatekey

配置

我们需要生成一个虚拟网卡,名称可以按个人喜好选择,这里选择用wg0,IP网段使用10.10.0.1/24,确保该网段没有被你的家庭或公司网络使用,编辑文件/etc/wireguard/wg0.conf

1
2
3
4
5
6
7
8
[Interface]
PrivateKey = <your server private key here>
Address = 10.10.0.1/24
Address = fd86:ea04:1111::1/64
SaveConfig = true
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp0s3 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o enp0s3 -j MASQUERADE
ListenPort = 51820

PrivateKey填上一步生成的私钥,填入你选择的Address,这里配置了两个IP4/6的地址,enp0s3是你服务器主要的网络设备,使用ifconfigip a查看你的主设备名,ListenPort默认为51820,建议修改

要注意的是SaveConfig设置,wireguard配置后不需要restartreload,使用wg命令添加peer即可立即生效。但是,这些操作都是直接写入到内存中而不是磁盘,因此如果你重启系统或服务,那么你就会失去添加的配置,所以这里设置为true,自动保存配置。但这个配置有时候也会让人非常困扰,你可能添加配置后重启服务,然后你发现添加的配置不见了,需要先停止服务再添加配置最后重启。

添加网络配置,使得内核可以正确路由我们的NAT流量

1
2
3
4
5
sudo bash -c 'cat << EOF >> /etc/sysctl.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF'
sudo sysctl -p

运行命令,开启wg0设备

1
sudo wg-quick up wg0

如果一切顺利,那么运行下面命令你将看到类似的输出

1
2
3
4
5
vagrant@bionic:~$ sudo wg
interface: wg0
public key: Td4IBZg2s2bMSjewD0h4oBhlbaJteygxjT83xeM7uTY=
private key: (hidden)
listening port: 51820

客户端配置

客户端配置与服务端类似

1
2
3
4
5
6
7
8
9
10
11
[Interface]
Address = 10.10.0.2/32
Address = fd86:ea04:1111::2/128
SaveConfig = true
PrivateKey = <your client private key here>
DNS = 8.8.8.8

[Peer]
PublicKey = <your server public key here>
Endpoint = <your server public ip>:51820
AllowedIPs = 0.0.0.0/0, ::/0

填写服务器的publickey、ip、端口,配置0.0.0.0/0, ::/0将所有流量传输到服务端

在服务端添加客户端配置

1
sudo wg set wg0 peer <client-public-key> allowed-ips 10.10.0.2/32,fd86:ea04:1111::2/128

以上的操作针对的是linux系统,官方推出了可用的iOS/MacOS/Android/Windows图形界面软件,其中有一个选项要注意的是On-Demand,有EthernetWi-Fi选项,如果勾选,那么会在选中网络中会自动开启vpn

设置开机启动

如果你的系统使用systemd,如ubuntu,设置wireguard开机启动命令如下

1
systemctl enable wg-quick@wg0

开放防火墙

UFW

1
sudo ufw allow 51820/udp

参考文档

WireGuard: fast, modern, secure VPN tunnel
How to setup your own VPN server using WireGuard on Ubuntu
Setting Up PiHole, Wireguard VPN server and Client (Ubuntu Server)

本文将简单介绍资源类型、资源的创建、访问、销毁操作。不再建议使用资源类型,使用类更为合适

资源类型

zval可以表示大部分的PHP数据类型,但有一样不能很好的表示其结构:指针。由于不透明的结构、无法使用传统运算符进行操作等,使得指针在PHP的表示变得困难。因此PHP用一个特殊的标记表示指针:资源,为了使资源标记具有意义,必须先注册到zend engine才能使用。

在头文件定义结构体php_test_person以及资源名称,放置在#define语句后,PHP_MINIT_FUNCTION(test);之前

1
2
3
4
5
6
typedef struct {
zend_string *name;
zend_long age;
} php_test_person;

#define PHP_TEST_PERSON_RES_NAME "Person Data"

源文件定义le_*全局变量,MINIT阶段注册,用于获取资源类型、字面意义名称、析构函数

1
2
3
4
5
6
int le_test_person;

PHP_MINIT_FUNCTION(test)
{
le_test_person = zend_register_list_destructors_ex(NULL, NULL, PHP_TEST_PERSON_RES_NAME, module_number);
}

初始化资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PHP_FUNCTION(test_person_new)
{
php_test_person * person;
zend_string * name;
zend_long age;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_STR(name);
Z_PARAM_LONG(age);
ZEND_PARSE_PARAMETERS_END();

if (age < 0 || age > 255) {
php_error_docref(NULL, E_WARNING, "Nonsense age (%ld) given, person resource not created.", age);
RETURN_FALSE;
}

person = emalloc(sizeof(php_test_person));
person->name = zend_string_copy(name); // estrndup + zend_string => zend_string_copy
person->age = age;

RETURN_RES(zend_register_resource(person, le_test_person));
}

函数接收参数name以及age,参数进行校验通过后,申请一段内存空间并写入数据,return_value返回该资源。PHP不需要知道资源的具体内部表示,只需要获取该资源存储的指针以及资源类型

函数接收资源参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PHP_FUNCTION(test_person_greet)
{
php_test_person *person;
zval *zperson;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_RESOURCE(zperson);
ZEND_PARSE_PARAMETERS_END();

person = (php_test_person *) zend_fetch_resource_ex(zperson, PHP_TEST_PERSON_RES_NAME, le_test_person);

php_printf("Hello ");
PHPWRITE(ZSTR_VAL(person->name), ZSTR_LEN(person->name));
php_printf("!\nAccording to my records, you are %ld years old.\n", person->age);

RETURN_TRUE;
}

ZEND_FETCH_RESOURCE在PHP7中已被删除,目前获取资源的函数是zend_fetch_resourcezend_fetch_resource_exzend_fetch_resource_ex函数需要一个zval、字面意义名称、资源类型,返回一个指针。函数内部切记不要free该指针

销毁资源

在PHP中使用fopen打开文件并获得一个资源句柄$fp,接下来unset($fp)时文件被关闭,即使没有使用fclose函数。其中的奥秘在zend_register_list_destructors_ex,该函数第一个参数为常规资源的析构函数,第二个为持久化资源的析构函数,当离开资源变量所在作用域时,自动调用清理/析构函数,释放内存、关闭连接或执行其他清理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
le_test_person = zend_register_list_destructors_ex(php_test_person_dtor, NULL, PHP_TEST_PERSON_RES_NAME, module_number);

static void php_test_person_dtor(zend_resource *res)
{
php_test_person *person = (php_test_person *) res->ptr;

if (person) {
if (person->name) {
zend_string_release(person->name); // efree zend_string => zend_string_release
}
efree(person);
}
}

强制销毁资源

使用zend_list_delete销毁资源,该函数可销毁任何资源类型变量

1
2
3
4
5
6
7
8
9
10
11
12
PHP_FUNCTION(test_person_delete)
{
zval * zperson;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_RESOURCE(zperson);
ZEND_PARSE_PARAMETERS_END();

zend_list_delete(Z_RES_P(zperson));

RETURN_TRUE;
}

持久化资源

持久化资源与常规资源不同的地方在析构函数声明注册的位置,数据内存申请使用pemalloc代替emalloc

1
2
3
4
5
6
7
int le_test_person_persist;

PHP_MINIT_FUNCTION(test)
{
le_test_person = zend_register_list_destructors_ex(php_test_person_dtor, NULL, PHP_TEST_PERSON_RES_NAME, module_number);
le_test_person_persist = zend_register_list_destructors_ex(NULL, php_test_person_persist_dtor, PHP_TEST_PERSON_RES_NAME, module_number);
}

通常情况下,php_test_person_dtor会在请求结束后调用,php_test_person_persist_dtor在扩展shutdown阶段调用

1
2
3
4
5
6
7
8
9
10
11
static void php_test_person_persist_dtor(zend_resource *res)
{
php_test_person *person = (php_test_person *) res->ptr;

if (person) {
if (person->name) {
zend_string_release(person->name);
}
pefree(person, 1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PHP_FUNCTION(test_person_pnew)
{
php_test_person * person;
zend_string * name;
zend_long age;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_STR(name);
Z_PARAM_LONG(age);
ZEND_PARSE_PARAMETERS_END();

if (age < 0 || age > 255) {
php_error_docref(NULL, E_WARNING, "Nonsense age (%ld) given, person resource not created.", age);
RETURN_FALSE;
}

person = pemalloc(sizeof(php_test_person), 1);
person->name = zend_string_dup(name, 1);
person->age = age;

RETURN_RES(zend_register_resource(person, le_test_person_persist));
}

test_person_pnewtest_person_new仅在数据初始化、资源类型方面有差异

查找已存在的持久化资源

为了可以重用持久化资源,需要将其保存在一个安全的地方,zend engine提供了一个executor global通过EG(persistent_list)访问,该变量类型为HashTable

重新修改test_person_pnew

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
PHP_FUNCTION(test_person_pnew2)
{
php_test_person * person;
char * key_raw;
size_t key_len;
zend_string * name, * key;
zend_long age;
zval * zperson = NULL;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_STR(name);
Z_PARAM_LONG(age);
ZEND_PARSE_PARAMETERS_END();

if (age < 0 || age > 255) {
php_error_docref(NULL, E_WARNING, "Nonsense age (%ld) given, person resource not created.", age);
RETURN_FALSE;
}

key_len = spprintf(&key_raw, 0, "test_person_%s_%ld\n", ZSTR_VAL(name), age);
key = zend_string_init(key_raw, key_len, 1);
efree(key_raw);

if ((zperson = zend_hash_find(&EG(persistent_list), key)) != NULL) {
person = (php_test_person *) zend_fetch_resource_ex(zperson, PHP_TEST_PERSON_RES_NAME, le_test_person_persist);

ZVAL_RES(return_value, zend_register_persistent_resource_ex(key, person, le_test_person_persist));
zend_string_release(key);
return ;
}

person = pemalloc(sizeof(php_test_person), 1);
person->name = zend_string_copy(name);
person->age = age;

ZVAL_RES(return_value, zend_register_persistent_resource_ex(key, person, le_test_person_persist));
zend_string_release(key);
}

test_person_pnew2先确定EG(persistent_list)是否已经存在,已存在则直接使用,不存在则申请内存初始化资源

参考文档

Extension Writing Part III: Resources
Extension Writing Part III: Resources
Upgrading PHP extensions from PHP5 to NG
PHP Internals Book
References about Maintaining and Extending PHP

PHP扩展开发第二章,主要介绍函数参数获取、zval数据类型、数组及数组遍历以及$GLOBALS全局变量访问

函数参数

扩展自定义函数不需要声明形参,Zend Engine会给每个函数传递一个参数列表zend_execute_data *execute_data,函数参数通过宏块ZEND_PARSE_PARAMETERS_STARTZEND_PARSE_PARAMETERS_END获取,当然,旧的方法zend_parse_parameters也可以使用,只是会更麻烦点

下面示例函数将接收参数name以及打印的次数t_param并打印name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
PHP_FUNCTION(test)
{
// 参数1
zend_string *name;
// 参数2
zend_long t_param = 0;
int times = 1;

// 最低可接收1个参数,最多2个
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STR(name)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(t_param)
ZEND_PARSE_PARAMETERS_END();

// 判断是否有传递默认值
if (ZEND_NUM_ARGS() == 2) {
times = (int) (t_param < 1 ? 1 : t_param);
}

// 打印
for (int i = 0; i < times; i++) {
php_printf("Hello %s", ZSTR_VAL(name));
}
RETURN_TRUE;
}

常用的有数据类型以及参数获取方法有下

Variable Type Macro
zend_bool Z_PARAM_BOOL
zend_long Z_PARAM_LONG
double Z_PARAM_DOUBLE
zend_string * Z_PARAM_STR
char * Z_PARAM_STRING
zval * Z_PARAM_RESOURCE
zval * Z_PARAM_ARRAY
zval * Z_PARAM_OBJECT
zval * Z_PARAM_ZVAL

ZVAL结构

PHP7的zval相对PHP5做了比较大的改动,zval、zend_string、zend_array重构,bool类型分成了true、false两种类型直接存储在(zval).u1.type_info等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// php7 zval变量结构
typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;

struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};

zval类型判断,一般情况下,如果参数与Z_PARAM_*的类型不一致,Zend Engine会进行转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 接收任意类型参数,最后输出其类型
PHP_FUNCTION(test_type)
{
zval *uservar;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ZVAL(uservar);
ZEND_PARSE_PARAMETERS_END();

switch (Z_TYPE_P(uservar)) {
case IS_NULL:
php_printf("NULL");
break;
case IS_TRUE:
php_printf("Boolean: TRUE");
break;
case IS_FALSE:
php_printf("Boolean: FALSE");
break;
case IS_LONG:
php_printf("Long: %ld", Z_LVAL_P(uservar));
break;
case IS_DOUBLE:
php_printf("Double: %f", Z_DVAL_P(uservar));
break;
case IS_STRING:
php_printf("String: ");
PHPWRITE(Z_STRVAL_P(uservar), Z_STRLEN_P (uservar));
break;
case IS_RESOURCE:
php_printf("Resource");
break;
case IS_ARRAY:
php_printf("Array");
break;
case IS_OBJECT:
php_printf("Object");
break;
default:
php_printf("Unknown");
}
}

打印数组内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PHP_FUNCTION(test_again)
{
zval *zname;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ZVAL(zname);
ZEND_PARSE_PARAMETERS_END();

// 将改变原有参数类型
convert_to_string(zname);
php_printf("Hello ");
PHPWRITE(Z_STRVAL_P(zname), Z_STRLEN_P(zname));
RETURN_TRUE;
}

convert_to_*会将参数转换成指定的类型,即改变原始参数类型、数据,在php代码中,原本以传参的形式传递的变量值会被修改。要解决这个问题,可以使用临时变量,也可以使用convert_to_*_ex,该类函数在转换类型前会先调用SEPARATE_ZVAL_IF_NOT_REF,避免修改原始变量

数组实现

PHP的数组用途非常广泛,类对象属性的存储也依赖数组,数组的底层实现结构为HashTable

下面函数创建一个数据并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PHP_FUNCTION(test_array)
{
// $arr = [];
array_init(return_value);
// $arr[3] = 123;
add_index_long(return_value, 3, 123);
// $arr[] = "example";
add_next_index_string(return_value, "example");

char *mystr = estrdup("five");
add_next_index_string(return_value, mystr);
efree(mystr);

// $arr["pi"] = 3.1415926;
add_assoc_double(return_value, "pi", 3.1415926);

zval subarr;
array_init(&subarr);
add_next_index_string(&subarr, "hello");
add_assoc_zval(return_value, "subarr", &subarr);
}
  • add_next_index_*添加元素进数组,由系统分配一个递增的数字key
  • add_index_*添加元素进数组,由用户指定一个数字类型的key
  • add_assoc_*添加元素进数组,由用户指定的字符串类型key

注意:add_assoc_*非二进制安全

扩展函数返回值不使用return语句,配置zval *return_value即可,该变量由宏PHP_FUNCTION提供,返回值RETURN_TRUE也是通过设置return_value完成返回值赋值,默认设置为IS_NULL类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 遍历并打印数组字符串值
PHP_FUNCTION(test_array_strings)
{
zval *arr, *data;
HashTable *arr_hash;
HashPosition pointer;
int array_count;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_ARRAY(arr);
ZEND_PARSE_PARAMETERS_END();

//
arr_hash = Z_ARRVAL_P(arr);
array_count = zend_hash_num_elements(arr_hash);

php_printf("The array passed contains %d elements\n", array_count);

/*
* 可使用宏ZEND_HASH_FOREACH_VAL、ZEND_HASH_FOREACH_END
*/
for (
zend_hash_internal_pointer_reset_ex(arr_hash, &pointer);
data = zend_hash_get_current_data_ex(arr_hash, &pointer);
zend_hash_move_forward_ex(arr_hash, &pointer)
) {
// example 1
// 如果是字符串就打印
// if (Z_TYPE_P(data) == IS_STRING) {
// PHPWRITE(Z_STRVAL_P(data), Z_STRLEN_P(data));
// php_printf("\n");
// }

// example 2
// 将参数转为字符串打印
// zval tmp;
// tmp = *data;
// zval_copy_ctor(&tmp);
// convert_to_string(&tmp);
// PHPWRITE(Z_STRVAL(tmp), Z_STRLEN(tmp));
// php_printf("\n");
// zval_dtor(&tmp);

// example 3
// 打印数组key以及value
int keytype;
zend_string * str_index;
zend_ulong num_index;

keytype = zend_hash_get_current_key_ex(arr_hash, &str_index, &num_index, &pointer);
if (HASH_KEY_IS_STRING == keytype) {
// 字符串类型key
PHPWRITE(ZSTR_VAL(str_index), ZSTR_LEN(str_index));
} else if (HASH_KEY_IS_LONG == keytype) {
// 数字key
php_printf("%ld", num_index);
}

php_printf(" => ");

zval tmp;
tmp = *data;
zval_copy_ctor(&tmp);
convert_to_string(&tmp);
PHPWRITE(Z_STRVAL(tmp), Z_STRLEN(tmp));
php_printf("\n");
zval_dtor(&tmp);
}

RETURN_TRUE;
}

zend_hash_get_current_key_ex返回的类型有

  • HASH_KEY_IS_LONG:整型key
  • HASH_KEY_IS_STRING:字符串型key
  • HASH_KEY_NON_EXISTENT:遍历完整个数组,没有更多元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 打印数组val
static int test_array_walk(zval *pDest)
{
zval tmp;

tmp = *pDest;
zval_copy_ctor(&tmp);
convert_to_string(&tmp);
PHPWRITE(Z_STRVAL(tmp), Z_STRLEN(tmp));
php_printf("\n");
zval_dtor(&tmp);

return ZEND_HASH_APPLY_KEEP;
}

// 打印prefix以及数组val
static int test_array_walk_arg(zval *pDest, void *argument)
{
php_printf("%s", (char *)argument);
test_array_walk(pDest);

return ZEND_HASH_APPLY_KEEP;
}

// 打印prefix、数组val以及suffix
static int test_array_walk_args(zval *pDest, int num_args, va_list args, zend_hash_key *hash_key)
{
php_printf("%s", va_arg(args, char *));
test_array_walk(pDest);
php_printf("%s\n", va_arg(args, char *));

return ZEND_HASH_APPLY_KEEP;
}

// 遍历数组
PHP_FUNCTION(test_array_walk)
{
zval *arr;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_ARRAY(arr);
ZEND_PARSE_PARAMETERS_END();

// 简单的遍历数组处理
zend_hash_apply(Z_ARRVAL_P(arr), (apply_func_t) test_array_walk);
// 遍历数组,带一个参数
zend_hash_apply_with_argument(Z_ARRVAL_P(arr), (apply_func_arg_t) test_array_walk_arg, "Hello");
// 遍历数组,带多个参数
zend_hash_apply_with_arguments(Z_ARRVAL_P(arr), (apply_func_args_t) test_array_walk_args, 2, "Hello ", "Welcome to my extension!");
}

上面test_array_walk类似array_maptest_array_walk简单遍历数组,test_array_walk_arg遍历数组,可接收一个任意类型的额外参数,下面代码中用作数组元素的前缀,test_array_walk_args遍历数组,可接收任意多个参数,下面代码中用作数组元素的前缀、后缀

返回值类型:

  • ZEND_HASH_APPLY_KEEP:维持原有元素,继续遍历数组剩余元素
  • ZEND_HASH_APPLY_REMOVE:删除原有元素,继续遍历数组剩余元素
  • ZEND_HASH_APPLY_STOP:维持原有元素,停止遍历数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 返回数组指定key的value
PHP_FUNCTION(test_array_value)
{
zval *arr, *offset, *val;
char * tmp;
zend_string *str_index = NULL;
zend_ulong num_index;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_ARRAY(arr);
Z_PARAM_ZVAL(offset);
ZEND_PARSE_PARAMETERS_END();

// 对非integer/string的key进行转换
switch (Z_TYPE_P(offset)) {
case IS_NULL:
case IS_FALSE:
num_index = 0;
break;
case IS_TRUE:
num_index = 1;
break;
case IS_DOUBLE:
num_index = (long) Z_DVAL_P(offset);
break;
case IS_LONG:
case IS_RESOURCE:
num_index = Z_LVAL_P(offset);
break;
case IS_STRING:
str_index = zval_get_string(offset);
break;
case IS_ARRAY:
tmp = "Array";
str_index = zend_string_init(tmp, sizeof(tmp) - 1, 0);
break;
case IS_OBJECT:
tmp = "Object";
str_index = zend_string_init(tmp, sizeof(tmp) - 1, 0);
break;
default:
tmp = "Unknown";
str_index = zend_string_init(tmp, sizeof(tmp) - 1, 0);
}

if (str_index && (val = zend_hash_find(Z_ARRVAL_P(arr), str_index)) == NULL) {
RETURN_NULL();
} else if (!str_index && (val = zend_hash_index_find(Z_ARRVAL_P(arr), num_index)) == NULL) {
RETURN_NULL();
}

*return_value = *val;
zval_copy_ctor(return_value);
}

PHP7中zval在栈分配,函数执行完清理,需要深入复制一份给return_value

Symbol Tables

全局变量$GLOBALS的底层存储结构也是HashTable,存储在一个全局结构体Executor Globals(_zend_executor_globals)里,通过EG(symbol_table)访问相关数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 访问全局变量$GLOBALS
PHP_FUNCTION(test_get_global_var)
{
zval *val;
zend_string *varname;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_STR(varname);
ZEND_PARSE_PARAMETERS_END();

// 全局变量符号表:EG(symbol_table)
if ((val = zend_hash_find(&EG(symbol_table), varname)) == NULL) {
php_error_docref(NULL, E_NOTICE, "Undefined variable: %s", ZSTR_VAL(varname));
RETURN_NULL();
}

*return_value = *val;
zval_copy_ctor(return_value);
}

完整代码

php_ext_tutorial

参考文档

Extension Writing Part II: Parameters, Arrays, and ZVALs (Unable To Access)
Extension Writing Part II: Parameters, Arrays, and ZVALs [continued] (Unable To Access)
Extension Writing Part II: Parameters, Arrays, and ZVALs
Upgrading PHP extensions from PHP5 to NG
PHP Internals Book
References about Maintaining and Extending PHP
Internal value representation in PHP 7 - Part 1
Internal value representation in PHP 7 - Part 2
PHP’s new hashtable implementation

flamegraph是一款可视化工具,用户分析程序堆栈调用,x轴堆栈轨迹,y轴堆栈深度,x轴长度越长,占用更多cpu时间。下面简单介绍如何使用xdebug以及flamegraph生成火焰图

准备工作

配置xdebug参数

1
2
3
4
5
xdebug.trace_output_name = xdebug.trace.%t.%s
xdebug.trace_enable_trigger = 1
xdebug.trace_output_dir = /tmp
xdebug.trace_enable_trigger_value = "<secret key>"
xdebug.trace_format=1

下载flamegraph,git clone https://github.com/brendangregg/FlameGraph.git,放置在任意位置

flamegraph

获取堆栈数据

请求页面/接口url,在url后面增加?XDEBUG_TRACE=<secret key>,获取php堆栈数据 curl http://youdomain.com?XDEBUG_TRACE=<secret key>,如果是命令行命令,则在php命令后使用参数php -d xdebug.auto_trace=1

转换堆栈数据

1
./stackcollapse-xdebug.php /tmp/xdebug.trace.1579703170._data_www_yii2_web_index_php.xt > out.folded

生成火焰图

1
./flamegraph.pl out.folded > out.svg

示例

yii2 flamegraph example

参考

How to generate PHP Flamegraphs
Flame Graphs visualize profiled code

很长的一段时间,被xubuntu下的一个monospace字体显示问题困扰着,在网上找了很多方法也没有解决问题,在一次误打误撞的情况下解决了,简单把自己问题的过程记录下来,希望给一些小伙伴们帮助

问题描述

xubuntu下monospace在浏览器以及vscode下显示异常,具体如下图所示

浏览器chrome/firefox下的monospace

monofont display incorrect in browser

vscode下的monospace

monofont display incorrect in vscode

网上搜索结果大部分指示,是由于libfreetype6 2.8.1-2ubuntu2的一个bug导致,需要升级到最新的libfreetype2.9或者降级到libfreetype2.8.0,按照搜索的文档进行libfreetype的升级/降级后,显示效果依旧,问题没有得到解决

解决过程

在我放弃治疗后过了一段时间,突然想起,一直以来都忽略了验证linux的字体信息。首先,获取显示异常的字体font-family,如下图

browser get font family

使用fc-match验证字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ fc-match monospace
DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

$ fc-match consolas
DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

$ fc-match Menlo
DejaVuSans.ttf: "DejaVu Sans" "Book"

$ fc-match "PingFang SC"
DejaVuSans.ttf: "DejaVu Sans" "Book"

$ fc-match "Microsoft YaHei"
NotoSansCJK-Regular.ttc: "Noto Sans CJK SC" "Regular"

可以看到Menlo、”PingFang SC”、”Microsoft YaHei”的实际字体跟monospace并不一致,抱着死马当活马医的心态,编辑字体配置文件~/.config/fontconfig/fonts.conf,内容如下,

1
2
3
4
5
6
7
8
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
<alias>
<family>Menlo</family>
<prefer><family>DejaVu Sans Mono</family></prefer>
</alias>
</fontconfig>

退出,重新登录,发现这个困扰了很久的问题终于解决,如下图

monofont the correct view

vscode可以通过更改font-family恢复正常的显示效果

注意

ubuntu/xubuntu、中文/英文的系统默认的字体会有些许差异,以上的操作可能不一定生效,可以新增~/.config/fontconfig/conf.d/00-menlo.conf

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias binding="same">
<family>Menlo</family>
<accept>
<family>DejaVu Sans Mono</family>
</accept>
</alias>
</fontconfig>

参考文档

[Ubuntu 18.04 LTS] Font not rendering properly
Ubuntu 18.04 LTS Font not rendering properly
Why doesn’t fc-match respect my match and edit rule for Courier when it does for Consolas?

工欲善其事必先利其器,xdebug是php开发的一个杀手级利器,vagrant是一款非常便捷的虚拟机管理工具,两者搭配有时会出现一些奇怪的问题,近期在重新配置开发环境时,出现xdebug连不上的异常情况,花了些时间才找到问题并解决,记录下解决问题的过程,希望能帮助到一些小伙伴

问题及解决过程

异常情况

xdebug配置后,客户端多次请求,vscode均无任何反应。检查服务端、客户端配置,确认没有差错;监测网络数据,只有初次请求数据,无结果返回

客户端/服务器配置信息

服务器是vagrant + ubuntu 18.04 + PHP 7.4,客户端是Windows 10,编辑器是vscode

初始xdebug配置如下

1
2
3
4
5
6
7
8
[xdebug]
xdebug.remote_enable = 1
xdebug.remote_autostart = 1
xdebug.remote_port = 9001
xdebug.remote_handler = "dbgp"
xdebug.remote_mode = req
xdebug.remote_connect_back = 1
xdebug.overload_var_dump=0

vscode配置launch.json如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9001,
"pathMappings": {
"/data/example": "D:\\data\\example"
},
"log": true
}
]
}

开启xdebug日志监测

1
xdebug.remote_log=/tmp/xdebug.log

再次请求,获取日志数据如下

1
2
3
4
5
6
7
[140114918627520] Log opened at 2019-09-04 11:49:00
[140114918627520] I: Checking remote connect back address.
[140114918627520] I: Checking header 'HTTP_X_FORWARDED_FOR'.
[140114918627520] I: Checking header 'REMOTE_ADDR'.
[140114918627520] I: Remote address found, connecting to 192.168.33.1:9001.
[140114918627520] E: Time-out connecting to client (Waited: 200 ms). :-(
[140114918627520] Log closed at 2019-09-04 11:49:00

日志显示服务端有正常向客户端9001端口发起连接,但客户端无响应导致超时,检查客户端防火墙无异常

最后,在谷歌上找到一个类似的问题,发现问题与vagrant有关,具体问题描述见参考文档链接

解决方法

1
2
3
; 移除 xdebug.remote_connect_back = 1
; 添加
xdebug.remote_host=10.0.2.2

重启php-fpm,再次测试,vscode可连接xdebug,问题解决

参考文档

How to configure Xdebug in PhpStorm through Vagrant
xdebug Time-out connecting to client

0%