分类: 技术分享

  • 无聊工程: 使用境外流量卡创建一个家用局域网代理服务

    题外话

    如果不是某CuniqHK的卡签了合约,可能不会有这篇文章。因为在使用过程中我发现把卡插在手机里直接用会有很多不方便的地方,比如健康码、丰巢、共享单车之类的本地服务在使用外卡加载的时候会有各种奇奇怪怪的卡顿或者报错。

    另外需要说明这个纯粹时用来花境外流量的无聊工程,因为现在有很多成熟且高性价比的替代方案可以选择。

    软硬件需求清单

    • 一张境外流量卡
    • 一个随身WiFi设备:这个设备插在主机上,需要能被电脑识别成网卡。
    • 一台运行Ubuntu的主机设备(虚拟机其实也可以,用Host模式接入本地网络),我用的廉价NUC
    • 家中要有现成的无线路由器

    能搞出啥呢?

    蓝色虚线标注了经由代理访问的路由。当不使用代理时,经由主路由器走境内宽带。

    在使用iOS的手机上可以使用”无线局域网”设置界面内的“HTTP”代理功能,连上Ubuntu主机开启的代理服务,走USB 4G网卡的线路上(外)网。

    当然了,如果配合Clash之类的允许配置路由规则的App,费些功夫就能给不同的流量设置规则,使得流量卡线路和国内线路能够并用。本文只是想做PoC,所以没管这些。

    配置步骤

    首先,Ubuntu主机通过网线与主路由器相连。一般情形下,路由器设置一个“静态分配”的路由给这个Ubuntu主机,避免分配的局域网IP地址因为DHCP发生变动。

    随后把USB 4G网卡也插上Ubuntu主机,正常情况下主机会识别到这个USB网卡,在终端输入ifconfig能看到它:

    上图所示192.168.8.152是USB 4G网卡分配到的IP地址,10.36.1.30是主路由器给分配的地址,也是即将搭建的Proxy Server的服务地址。

    由于接入了两张网卡,ubuntu的默认路由里这两张网卡会有不同的优先级。通常来说后连入的设备优先级更低,这样在路由表走默认路由“兜底”的时候会走有线线路出去访问互联网。

    这并不是我们想要的,所以需要通过命令来把路由表改一下,让enx0c5b8f279a64这个设备优先。

    sudo route del default gw 10.36.1.1 enp3s0
    sudo route add default gw 10.36.1.1 dev enp3s0 metric 110 # 这边把优先级改成了110(更低)
    sudo route del default gw 192.168.8.1 enx0c5b8f279a64
    sudo route add default gw 192.168.8.1 dev enx0c5b8f279a64 metric 100 # 提升4G设备的优先级到100
    sudo ip route flush cache

    可以再输入一下route命令,看看是否Metric上的值发生了变化。

    接着是配置一个代理服务器,又一个开源i项目叫gost(https://v2.gost.run/)的,很方便。如果要保持持续运行的话,最简单的就是用nohup或者tmux启动它。

    sudo snap install core
    sudo snap install gost # 安装
    
    #随后通过命令就能启动了
    sudo gost -L 10.36.1.30:8080
    

    代理服务器启动后,就可以在手机上设置了,最简单的http代理就能work。需要注意一点,如果使用的是socks类代理,要注意DNS解析不能使用客户端机器上的,不然会有dns污染的现象。HTTP代理似乎不受影响(参考此处)。

    免责声明

    本文仅做技术讨论。至于如何获取境外流量卡合法上网,那就各显神通吧。

  • 找到一个不错的Typecho hCaptcha插件

    最近苦于机器人的垃圾评论,找了半天发现hCaptcha这个东西很好用。本以为小众的typecho需要自己手写,后来搜了一下发现已经有大佬帮忙写好了插件,地址是https://github.com/plok5202008/Typecho-HCaptcha

    使用起来也挺方便,

    1. 控制台-插件里启用HCaptcha,然后点击插件的设置,填写自己的site key和secret。没有这两个东西的话去https://www.hcaptcha.com/创建一个新用户即可,免费。
    2. 控制台-外观页面点入编辑当前外观,然后右侧选择comments.php文件。找到提交评论的按钮,一般是<button type="submit" class="submit">,在上面加一行
    <?php $this->hCaptcha(); ?>

    大功告成。

  • (使用VirtualBox)构建Ubuntu下的clash旁路由服务

    效果图一张

    截屏2020-11-16 下午10.55.42.png

    几个月前写了一篇文章,讲怎么在Windows下面利用HyperV安装OpenWRT软路由。最近发现了几篇旁路由的文章,另外发现其实clash完全可以在ubuntu下面跑,不用再费那么多事情去研究OpenWRT了,于是乎就有了本文。

    目标

    本文的目标是利用一台Windows主机(或直接使用Linux主机)搭建一个旁路由服务器,这个服务器与我的光纤猫LAN口连接,提供网关服务,并且能够通过网页ui对clash服务进行配置修改。
    需要使用旁路由服务的终端(或无线AP),可以通过手工设定网关地址与DNS地址来接驳到旁路由上;同时,需要默认路由服务的终端,只需使用默认DHCP服务即可。
    施工完成后,家里的网络拓扑大致如下:

    光纤猫 192.168.1.1
    |- 192.168.1.21 旁路由服务器
    |- 192.168.1.20 无线路由器(接驳旁路由网关192.168.1.21)
    |                  |- 192.168.50.xxx 手机、iPad、笔电等终端
    |- 192.168.1.19 电视机(无需旁路由服务,使用DHCP的网关192.168.1.1)
    |- ...

    硬件需求

    任意主机,直接运行Ubuntu系统或使用Windows系统。服务器需要有一个网口。本人用来跑旁路由服务的主机是一台Intel J1900,4GB内存的主机。

    VirtualBox配置

    首先需要安装一个Ubuntu,我选择的是20.04LTS Server版本,所以没有图形化界面。

    在系统安装好之后,关闭虚拟机,需要在VirtualBox里配置网络接口类型。默认的网络接口类型应该是NAT,这边找到主机连接光猫的那个网络接口,改为桥接。桥接的意思是,把虚拟主机看做一个独立的主机接入光纤猫的LAN网络,于是乎他就有了一个独立于物理机器的一个“平等的”IP地址了。这个IP地址也就是旁路由的网关地址。
    VirtualBox-Bridge-Network.jpg

    如果你的光猫可以配置静态IP地址分配的白名单,建议把这个旁路由的MAC地址对应的IP分配成静态的,避免因为光纤猫DHCP变动导致旁路由的IP地址不是一个静态值。

    Ubuntu配置

    允许ip转发

    sudo vim /etc/sysctl.conf

    找到 net.ipv4.ip_forward=1 这一行,取消掉注释。如果没有找到的话直接新增一行也可以的。随后执行

    sudo sysctl -p

    使修改生效。

    配置iptables

    依次执行下面的指令,注意注释的部分,修改成自己的网络配置

    iptables -t nat -N clash
    iptables -t nat -N clash_dns
    
    # 这个是fake-ip对应的dns地址,一般不用动
    iptables -t nat -A PREROUTING -p tcp --dport 53 -d 198.19.0.0/24 -j clash_dns
    iptables -t nat -A PREROUTING -p udp --dport 53 -d 198.19.0.0/24 -j clash_dns
    iptables -t nat -A PREROUTING -p tcp -j clash
    
    # 这里需要注意的是,下面两行最后的 192.168.1.21 是当前旁路由的 IP 地址,请根据你自己的实际情况修改
    # 如果你自己的旁路由 IP 跟下面的 IP 地址不对的话会造成无法翻墙
    iptables -t nat -A clash_dns -p udp --dport 53 -d 198.19.0.0/24 -j DNAT --to-destination 192.168.1.21:53
    iptables -t nat -A clash_dns -p tcp --dport 53 -d 198.19.0.0/24 -j DNAT --to-destination 192.168.1.21:53
    
    # 绕过一些内网地址
    iptables -t nat -A clash -d 0.0.0.0/8 -j RETURN
    iptables -t nat -A clash -d 10.0.0.0/8 -j RETURN
    iptables -t nat -A clash -d 127.0.0.0/8 -j RETURN
    iptables -t nat -A clash -d 169.254.0.0/16 -j RETURN
    iptables -t nat -A clash -d 172.16.0.0/12 -j RETURN
    iptables -t nat -A clash -d 192.168.0.0/16 -j RETURN
    iptables -t nat -A clash -d 224.0.0.0/4 -j RETURN
    iptables -t nat -A clash -d 240.0.0.0/4 -j RETURN
    
    # 注意, 这边的7892对应后续clash配置里的redir-port
    iptables -t nat -A clash -p tcp -j REDIRECT --to-ports 7892

    执行完上面的 iptables 命令之后,就完成了旁路由的路由功能了,但是此时 iptables 并没有永久保存,下次开机上面的配置就会丢失。为了使得重启之后 iptables 命令仍然存在,我们需要安装软件来实现:

    sudo apt install iptables-persistent

    安装的过程中会提示你是否需要保存 iptables 配置,直接选是就行。这时候即使电脑重启了也会应用这些路由规则。

    如果后面你有需要重新修改 iptables 的配置,那么只需要在执行完 iptables 之后再执行:

    sudo iptables-save > /etc/iptables/rules.v4

    即可将最新的 iptables 规则保存下来。

    下载Clash

    # 下载最新版 clash,注意根据自己的系统下载对应的版本,我的是 64 位的,所以下载的是 linux-amd64 这个版本
    wget https://github.com/Dreamacro/clash/releases/download/v1.3.0/clash-linux-amd64-v1.3.0.gz
    # 解压并且把二进制文件放到 /usr/bin ,并且加上可执行权限
    gzip -d clash-linux-amd64-v1.3.0.gz
    sudo mv clash-linux-amd64-v1.3.0 /usr/bin/clash
    sudo chmod +x /usr/bin/clash
    # 为 clash 添加绑定低位端口的权限,这样运行 clash 的时候无需 root 权限
    sudo setcap cap_net_bind_service=+ep /usr/bin/clash

    DlerCloud用户需要注意,这边要下载Premium tag的版本以支持rule-set,否则启动会报配置错误。下载地址在https://github.com/Dreamacro/clash/releases/tag/premium。有兴趣了解此中区别的可以参考这篇Issue.

    配置clash服务

    运行 clash 之前需要先创建配置文件,否则 clash 无法启动:

    # 创建文件夹
    mkdir -p ~/.config/clash
    cd ~/.config/clash
    # 创建配置文件
    touch config.yaml
    vim config.yaml

    在 config.yaml 文件里面填入下面的配置:

    # 以下部分不要修改!
    port: 7890
    socks-port: 7891
    redir-port: 7892
    allow-lan: true
    
    mode: Rule
    
    log-level: info
    # external-controller 主要是用于 web 端管理页面,必须监听在 0.0.0.0
    external-controller: 0.0.0.0:9090
    
    # secret 是进入管理面板所需要的密码,可填可不填,建议填上
    secret: "secret-password"
    
    # external-ui 表示管理面板的路径
    external-ui: dashboard
    
    dns:
      enable: true
      ipv6: false
      listen: 0.0.0.0:53
      enhanced-mode: fake-ip
      fake-ip-range: 198.18.0.1/16
      nameserver:
        - '192.168.1.1'
    
    # 下面部分则是代理的设置跟规则的设置,这里忽略不写。
    Proxy:
    #...

    实际操作中,如果遇到报错说53端口已经被占用了,可以把0.0.0.0这个通配的IP地址修改为主机所在的内网IP,对应我本人的情况就是192.168.1.21:53.
    [更正] 这边最好不要改0.0.0.0,不然可能会遇到本机DNS出错的问题,一个办法是改用root用户启动服务.

    如果需要校验配置文件是否合规,可以运行clash -t, 成功的话程序会输出”successful”字样。

    (选修)DlerCloud配置文件自动下载及覆盖

    一些使用DlerCloud的朋友可以拿到官方配置好的clash yaml,这边有个小的python脚本能够帮你自动下载原始的yaml然后覆盖一些配置,使得它能在服务器上的clash客户端使用。

    import os
    import subprocess
    import shutil
    import sys
    import yaml
    
    g_dler_config_url = '这边填入能够下载到yaml文件的网址'
    
    g_items_to_update = {
        'port': 7890,
        'socks-port': 7891,
        'redir-port': 7892,
        'external-controller': '192.168.1.21:9090',
        'external-ui': 'dashboard',
        'secret': 'THE_HOLY_PASSWORD',  # 填写你的clash网页端管理密码
        'dns': {
            'enable': True,
            'ipv6': False,
            'listen': '192.168.1.21:53',
            'enhanced-mode': 'fake-ip',
            'fake-ip-range': '198.18.0.1/16',
            'nameserver': [
                '192.168.1.1',
            ],
        },
        'log-level': 'warning',
    }
    
    g_keys_to_remove = [
        'mixed-port',
    ]
    
    def is_root():
        if sys.platform == 'linux':
            return os.geteuid() == 0
        else:
            raise ValueError('not linux!')
    
    def update_yaml_and_save(input_path, output_path):
        with open(input_path, encoding='utf-8') as f:
            y = yaml.load(f, Loader=yaml.FullLoader)
        y.update(g_items_to_update)
        for key in g_keys_to_remove:
            del y[key]
        with open(output_path, 'w', encoding='utf-8') as f:
            yaml.dump(y, f, allow_unicode=True)
    
    if __name__ == '__main__':
        # NOTE: 这边的文件路径按需修改
        input_path = '/home/ubuntu/Download/dler_orig.yaml'
        subprocess.run(['wget', g_dler_config_url, '-O', input_path])
        output_path = '/home/ubuntu/.config/clash/config.yaml'
        shutil.copy(output_path, output_path + '.bak')
        update_yaml_and_save(input_path, output_path)
        print('saved to %s, restarting service...'%output_path)
        subprocess.run(['sudo', 'systemctl', 'restart', 'clash.service'])
        print('done')
    

    配置clash网页前端

    这边选用第三方开发的yacd面板,源码在https://github.com/haishanh/yacd
    具体安装方式就是把源码拷贝到上面配置文件中写好的位置:

    # 先进入到配置文件的目录
    cd ~/.config/clash
    # 下载前端代码压缩包,如果要使用官方的管理面板则把链接替换成 https://github.com/Dreamacro/clash-dashboard/archive/gh-pages.zip
    wget https://github.com/haishanh/yacd/archive/gh-pages.zip
    # 解压缩并且把目录名改成 dashboard
    unzip gh-pages.zip
    mv yacd-gh-pages/ dashboard/

    配置clash服务

    sudo touch /etc/systemd/system/clash.service
    sudo vim /etc/systemd/system/clash.service

    然后填入下面的内容。注意把2处YOUR USER NAME的部分换成你自己的用户名
    [更新] Ubuntu 18 实测如果用非root用户启动,会有DNS绑定低端口53失败的问题,最后我直接把下面的User改成User=root然后用sudo启了。不知道有没有更好的解法,希望大佬看到了能留个正解。

    [Unit]
    Description=clash daemon
    
    [Service]
    Type=simple
    User=YOUR USER NAME
    ExecStart=/usr/bin/clash -d /home/YOUR USER NAME/.config/clash/
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target

    保存退出。然后启动clash服务并设置为开机启动:

    # 启动 clash
    sudo systemctl start clash.service
    # 启动开机自启
    sudo systemctl enable clash.service

    如无意外的话这时候 clash 已经运行起来了,通过浏览器访问 http://192.168.1.80:9090/ui 就能看到 clash 的管理面板了。

    运行Clash的机器本身无法联网?

    这是因为使用了fake-ip模式,任何DNS请求都会响应成我们的fake-ip.

    参考Ubuntu的代理配置,走的是bash分用户的配置文件。所以分别在自己用户和root下配置一个简单的function。打开~/.bashrc然后在最后加入以下内容:

    # proxyon
    proxyon() {
        export https_proxy=http://127.0.0.1:7890
        export http_proxy=http://127.0.0.1:7890
        export all_proxy=socks5://127.0.0.1:7891
        echo "HTTP/HTTPS Proxy on"
    }
    
    # proxyoff
    proxyoff() {
        unset http_proxy
        unset https_proxy
        unset all_proxy
        echo "HTTP/HTTPS Proxy off"
    }
    

    保存后source ~/.bashrc让配置文件生效。 当需要使用代理时直接执行proxyon即可。

    修改客户端的旁路由配置

    不论是什么系统,若要使用旁路由作为网关,只要修改以下项目:

    • 手动配置IP地址(关闭DHCP),主机的IP地址可以是照抄DHCP分配给你的那个,或者在同一网段里自选一个不冲突的,按文中例子是192.168.1.X其中X!=1 && X!=21;
    • 子网掩码255.255.255.0
    • 网关地址192.168.1.21,也就是虚拟机旁路由的IP地址;
    • DNS配置198.19.0.1,如果要填次要DNS,可以填198.19.0.2

    我把华为路由器接在了光猫的另一个LAN口(和旁路由平级),这边给一个华为路由器上的参考配置:
    router-manual-ip.jpg

    测速

    家里的宽带是300Mbps,无线路由器使用旁路由作为上层网关后,亲测性能上是没问题的。Speedtest为证:
    speedtest.jpg

    参考文献

  • [C++] 在linux或windows上使用direct io

    首先

    Direct IO是一种不用内核缓存的IO, 它可以做到直接将用户空间的内存直接写入磁盘或者将磁盘数据直接读到用户空间的缓冲区,这种策略就是不用内核的缓存而使用用户自己设计的缓存. 需要注意的是,使用DirectIO会完全绕过系统的预取(prefetch)以及页缓存机制,如果不是必须,那么我认为还是优先考虑普通的read或者直接mmap吧

    Linux

    几个方面注意一下就可以了

    • 在调用[open]1时,把O_DIRECT加上。比如int fd = open("/path/to/file", O_DIRECT, O_RDONLY);
    • 用于文件读写的buffer,必须和磁盘的块大小对齐(保守起见一般可以设为4KB)。有两种方法能拿到地址对齐的内存块:

      • 直接使用posix_memalign;
      • 直接new一段内存,然后根据返回的内存地址,往后找到第一个满足对齐要求的地址就可以。这种方法会浪费前面一段空间,不过其实posix_memalign在系统操作的时候”浪费”了;
      • 借用mmap申请MAP_ANONYMOUS匿名映射,addr参数填NULL的话mmap出来的地址是页对齐的(至少是4K对齐),所以可以直接拿来用;
    • 如果需要lseek之类的操作,注意seek的文件位置偏移量必须是磁盘块大小的整数倍;
    • 2.4内核下, 每次文件读写的长度必须是块大小的整数倍(e.g. N * 4KB). Linux 2.6.0+无此要求;

    Windows下

    参考这篇文章,可以知道Windows下也可以启用类似的机制,对应的打开文件flag是FILE_FLAG_NO_BUFFERING.
    用法类似,与Linux不同的地方在于:

    • CreateFileA(...)调用的dwFlagsAndAttributes参数里把FILE_FLAG_NO_BUFFERING填上;
    • 用于文件读写的buffer也是需要对齐的

      • 也可以使用类似的方法_aligned_malloc申请对齐的内存,但是必须注意要使用_aligned_free释放内存,否则runtime error;
  • Boost Python的C++对象, Pickle支持及其原理

    默认用boost python包裹的C++对象是不支持pickle的,如果要用pickle.dumps(obj)的话那会提示错误

    Pickling of "xxx" instances is not enabled.

    这边吐槽一下在最新的代码里,给的reference链接其实还是不可用的。真正正确的是https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/reference/topics/pickle_support.html

    让你的class支持Pickle协议

    若要让你的C++ Class支持Pickle协议,比较“正统”的方法是利用boost提供的boost::python::pickle_suite. 拿代码说话:

    struct world_t {
        world_t(const string& country) { ... }
    };
    
    struct world_pickle_suite : boost::python::pickle_suite
      {
        static
        boost::python::tuple
        getinitargs(const world_t& w)
        {
          // [可选实现] 返回一个boost::python::tuple元组,其值被用来构造
          // 如果一个类的构造函数**不需要参数**的话,可以不用重载这个方法。
          return boost::python::make_tuple(w.country());
        }
    
        static
        boost::python::tuple
        getstate(const world_t& w)
        {
          // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
          // 那么要用此函数返回其状态值
        }
    
        static
        void
        setstate(world_t& w, boost::python::tuple state)
        {
          // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
          // 那么要用此函数把state里的状态恢复到w
        }
      };
    
    boost::python::class_<world_t>("world", args<const std::string&>())
      // 定义world_t的boost python包装类
      .def_pickle(world_pickle_suite());
      // ...
    

    需要注意的是,如果在suite里定义了getstate/setstate并且这个类的__dict__属性非空,boost是会报错的。一种可能的情况是你包装的类是一个子类,这时候还需要自己实现__getstate_manages_dict__属性。这边不赘述,可参考这里

    原理简释

    总所周知,Python3里若要在自定义的类里实现可Pickle,至少需要:

    • 它的__dict__属性是可pickle的,或;
    • 调用__getstate__()拿到的返回值是可pickle的,随后会调用__setstate__()方法在unpickle的时候恢复状态.
      但在这些函数的背后,再稍微底层一点其实是通过__reduce__()方法实现的(更严格一点,它会先去寻找__reduce_ex__()是否可用)。这个方法简单来说就是返回一个tuple, 这个tuple有从构建对象到恢复状态所需的各种元素.

    In fact, these methods are part of the copy protocol which implements the __reduce__() special method.
    https://docs.python.org/3/library/pickle.html

    所以boost::python其实自己实现了一个对象的__reduce__()方法,在src/object/pickle_support.cpp里(源文件在这)。主要做了几件事

    1. 拿到当前对象的__class__属性(也就是class object);
    2. 检查对象是否有__safe_for_unpickling__属性。如果没有的话,就是本文最早提到的报错了;
    3. 检查对象是否有__getinitargs__()方法,若有则取值。没有的话,unpickle的时候按照无参数构造来处理;
    4. 检查对象是否有__getstate__()方法,若有则取值;
    5. 检查对象是否有__dict__属性,若有则会检查是否有__getstate_manages_dict__属性并获取这两个属性的值。
    6. 返回tuple(),内容依次为1-5里拿到的值。

    可以看到,这个__reduce__()方法是遵循了Python里的object.__reduce__()协定的。当然了,如果某些使用者觉得继承一个suite类都觉得麻烦或者达不到自己的需求,也可以通过其他手段完成,只要记得自己整一个__reduce__()方法,只需满足Python的协定即可.

    class_obj.attr("__reduce__") = boost::python::object(/*YOUR REDUCE FUNCTION*/);

    再深一点~

    如果你跟我一样无聊,那就来看看cpython自己想要__reduce__()的时候是怎么用到__getstate__()的吧.

    cpython/Objects/typeobject.c这边有个函数叫

    static PyObject* _common_reduce(PyObject *self, int proto)

    ,在pickle protocol>2时会调用`static PyObject *
    reduce_newobj(PyObject *obj)`方法,这个方法是Python里大多数默认对象的默认__reduce__逻辑。其中有一句

    state = _PyObject_GetState(obj,
                    !hasargs && !PyList_Check(obj) && !PyDict_Check(obj));

    _PyObject_GetState里大致是从__dict__或者__slots__属性去获取对象的状态。

    这边再往上一些还能看到有调用_PyObject_GetNewArguments()的,里面的逻辑就会去拿__getnewargs_ex__或是__getnewargs__属性的值作为__new__一个新对象时候的传入参数。这两个函数是Pickle协议文档里介绍的四个推荐覆盖的“高阶”函数之一。与之对应,Python并不希望一般人去自己写一个__reduce__()出来.

    看到这里,再回想一下boost::python,pickle_suite里面的getinitargs()getstate()就分别对应到__getnewargs__()__getstate__()

  • Apache2配置微信公众号的反向代理

    题外话

    迫于妹子生日要到了,今年手头又比较紧,所以打算做个微信公众号的小东西骗骗她😜
    目前跑网站的服务器虽然配置不咋地,但是服务器上也就一个typecho的php项目要跑,平时访问量也门可罗雀,所以打算利用原有的服务器搭个反向代理到微信公众号的服务端。

    目标

    主域名example.com:

    • 80/443端口直接到原有的网站

    子域名wechat.example.com

    • 80端口反向代理到本地服务http://localhost:8766/

    实作

    其实很简单,新建一个VirtualHost即可。
    创建一个/etc/apache2/sites-available/wechat.conf内容如下:

    <VirtualHost *:80>
            ServerName wechat.example.com
    
            ServerAdmin webmaster@localhost
    
            ProxyPass / http://localhost:8766/
            ProxyPassReverse / http://localhost:8766/
        
    
            ErrorLog ${APACHE_LOG_DIR}/error.log
            CustomLog ${APACHE_LOG_DIR}/access.log combined
    </VirtualHost>
    

    其中的关键是ProxyPass以及ProxyPassReverse.

    完成之后,用a2ensite wechat启用这个VirtualHost, 再systemctl reload apache2便万事大吉。

  • 记录一下找了半天的huge page坑——fork越来越慢的原因

    背景

    之前发现Jupyter Notebook下面,如果数据占用多的话,开多进程池会特别的慢。一开始以为是Python的锅,但是把multiprocessing.pool改成直接用os.fork()调用以后,问题依旧。照理来说unix下面使用fork开进程,会启用copy-on-write机制,内存增长并不是特别明显,但是实际在htop下面看内存仍然会在fork之后增长,并且和进程数量是线性相关的。

    原因

    随后想了老半天,想到了可能和页表有关系。查了一下,跑的服务器上huge page确实被禁用了(不知为何…).

    fork的机制简单地说,是在创建新进程的时候把老的进程控制块(Process Control Block)里内存页表拷贝给了新的PCB——这边具体内存的信息是不拷贝的。由于当时Notebook跑的数据处理任务,里面已经用了不少内存(100GB+),所以拷贝的时候如果用默认的4KB内存页,将会有100 * 1024 * 1024 / 4 = 104,857,600个页表! 按典型一个页表项(Page Table Entry)大小4Bytes计算,一个进程开出来光页表会耗400MB内存.

  • 使用互信息(Mutual Information)来评价特征

    特征(feature, X)与响应(outcome, y)之间的互信息(mutual information, MI)是一种衡量两个变量之间相关性的方法。该方法将相关性这一定义拓展到非线性的关系上。具体而言,它衡量了一个随机变量经由另一随机变量能得到的信息量。

    MI的概念与信息熵(entropy)的概念密不可分。信息熵度量了一个随机变量携带的信息量。形式上,两个随机变量(X,Y)的互信息I(X,Y)定义如下:
    连续形式
    continuous mutual information

    离散形式
    discrete mutual information

    sklern.feature_selection.mutual_info_regression方法实现了计算所有特征与一个连续输出之间互信息值的函数,可用于挑选最可能携带预测信息的特征。它也提供一个分类器版本。

    本文全文翻译自Stefan Jansen’s Hands-On Machine Learning for Algorithmic Trading*

  • Support parallel XZ decompression for unix (7zip LZMA SDK based, C/C++)

    为unix平台增加XZ多线程解压缩支持(基于7zip LZMA SDK, C/C++)

    Note

    This post has nothing to do with the pixz project. I am talking about decompressing the original xz archive using 7-zip’s LZMA SDK under unix environment.

    Background

    Originally the 7zip’s LZMA SDK (version 19.00) only covers parallel xz decompression for Windows systems. This post shows the C code that adds support for lib pthread, i.e. unix systems.
    Compiling with C++ using the C library should also work, I have tested it on my own box.

    Little Details

    Actually the original writer has completed all the necessary abstraction for the multi-threading pipeline. All I have done is adding some macros and pthread equivalents to Windows threading model.

    Usage

    Replace lzma/C/... with below files. The new code should work on both Windows and Unix systems.

    Git Repo

    See https://github.com/idailylife/lzma-xz-parallel

    Source Code

    Threads.h

    /* Threads.h -- multithreading library
    2017-06-18 : Igor Pavlov : Public domain */
    
    #ifndef __7Z_THREADS_H
    #define __7Z_THREADS_H
    
    #ifdef _WIN32
    #include <windows.h>
    #else
    #include <pthread.h>
    #endif
    
    #include "7zTypes.h"
    
    EXTERN_C_BEGIN
    
    WRes HandlePtr_Close(HANDLE *h);
    WRes Handle_WaitObject(HANDLE h);
    
    #ifdef _WIN32
    typedef HANDLE CThread;
    #define Thread_Construct(p) *(p) = NULL
    #define Thread_WasCreated(p) (*(p) != NULL)
    #define Thread_Close(p) HandlePtr_Close(p)
    #define Thread_Wait(p) Handle_WaitObject(*(p))
    #else
    typedef void* LPVOID;
    typedef pthread_t* CThread;
    #define Thread_Construct(p) *(p) = NULL
    #define Thread_WasCreated(p) (*(p) != NULL)
    #define Thread_Close(p) HandleThread_Close(*(p))
    #define Thread_Wait(p) HandleThread_Join(*(p))
    WRes HandleThread_Close(pthread_t* th);
    WRes HandleThread_Join(pthread_t* th);
    
    #endif
    
    
    typedef
    #ifdef UNDER_CE
      DWORD
    #else
      unsigned
    #endif
      THREAD_FUNC_RET_TYPE;
    
    #define THREAD_FUNC_CALL_TYPE MY_STD_CALL
    #define THREAD_FUNC_DECL THREAD_FUNC_RET_TYPE THREAD_FUNC_CALL_TYPE
    typedef THREAD_FUNC_RET_TYPE (THREAD_FUNC_CALL_TYPE * THREAD_FUNC_TYPE)(void *);
    WRes Thread_Create(CThread *p, THREAD_FUNC_TYPE func, LPVOID param);
    
    #ifdef _WIN32
    
    typedef HANDLE CEvent;
    typedef CEvent CAutoResetEvent;
    typedef CEvent CManualResetEvent;
    #define Event_Construct(p) *(p) = NULL
    #define Event_IsCreated(p) (*(p) != NULL)
    #define Event_Close(p) HandlePtr_Close(p)
    #define Event_Wait(p) Handle_WaitObject(*(p))
    WRes Event_Set(CEvent *p);
    WRes Event_Reset(CEvent *p);
    WRes ManualResetEvent_Create(CManualResetEvent *p, int signaled); // not used
    WRes ManualResetEvent_CreateNotSignaled(CManualResetEvent *p); // not used
    WRes AutoResetEvent_Create(CAutoResetEvent *p, int signaled);
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent *p);
    
    #else
    typedef struct {
      bool state;
      pthread_mutex_t mutex;
      pthread_cond_t cond;
    } event_t;
    
    typedef event_t* CEvent;
    typedef CEvent CAutoResetEvent;
    #define Event_Construct(p) *(p) = NULL
    #define Event_IsCreated(p) (*(p) != NULL)
    
    WRes Event_Close(CEvent* p);
    WRes Event_Set(CEvent *p);
    WRes Event_Reset(CEvent *p);
    WRes Event_Wait(CEvent* p);
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent* p);
    
    #endif
    
    // CSemaphore is not used for decoding
    #ifdef _WIN32
    typedef HANDLE CSemaphore;
    #define Semaphore_Construct(p) *(p) = NULL
    #define Semaphore_IsCreated(p) (*(p) != NULL)
    #define Semaphore_Close(p) HandlePtr_Close(p)
    #define Semaphore_Wait(p) Handle_WaitObject(*(p))
    WRes Semaphore_Create(CSemaphore *p, UInt32 initCount, UInt32 maxCount);
    WRes Semaphore_ReleaseN(CSemaphore *p, UInt32 num);
    WRes Semaphore_Release1(CSemaphore *p);
    #endif
    
    #ifdef _WIN32
    
    typedef CRITICAL_SECTION CCriticalSection;
    WRes CriticalSection_Init(CCriticalSection *p);
    #define CriticalSection_Delete(p) DeleteCriticalSection(p)
    #define CriticalSection_Enter(p) EnterCriticalSection(p)
    #define CriticalSection_Leave(p) LeaveCriticalSection(p)
    
    #else
    /// use mutex instead
    typedef pthread_mutex_t* CCriticalSection
    WRes CriticalSection_Init(CCriticalSection *p);
    WRes CriticalSection_Delete(CCriticalSection *p);
    WRes CriticalSection_Enter(CCriticalSection *p);
    WRes CriticalSection_Leave(CCriticalSection *p);
    
    #endif
    EXTERN_C_END
    
    #endif
    

    Threads.c

    /* Threads.c -- multithreading library
    2017-06-26 : Igor Pavlov : Public domain */
    
    #include "Precomp.h"
    
    #ifdef _WIN32
      #ifndef UNDER_CE
      #include <process.h>
      #endif
    #endif
    
    #include "Threads.h"
    
    #ifdef _WIN32
    static WRes GetError()
    {
      DWORD res = GetLastError();
      return res ? (WRes)res : 1;
    }
    
    static WRes HandleToWRes(HANDLE h) { return (h != NULL) ? 0 : GetError(); }
    static WRes BOOLToWRes(BOOL v) { return v ? 0 : GetError(); }
    
    WRes HandlePtr_Close(HANDLE *p)
    {
      if (*p != NULL)
      {
        if (!CloseHandle(*p))
          return GetError();
        *p = NULL;
      }
      return 0;
    }
    
    WRes Handle_WaitObject(HANDLE h) { return (WRes)WaitForSingleObject(h, INFINITE); }
    
    #else
    /// unix specific functions
    
    WRes HandleThread_Close(pthread_t* th) {
      free(th);
      th = NULL;
      return 0;
    }
    
    WRes HandleThread_Join(pthread_t* th) {
      return pthread_join(*th, NULL);
    }
    
    #endif
    
    
    #ifdef _WIN32
    WRes Thread_Create(CThread *p, THREAD_FUNC_TYPE func, LPVOID param)
    {
      /* Windows Me/98/95: threadId parameter may not be NULL in _beginthreadex/CreateThread functions */
      
      #ifdef UNDER_CE
      
      DWORD threadId;
      *p = CreateThread(0, 0, func, param, 0, &threadId);
    
      #else
    
      unsigned threadId;
      *p = (HANDLE)_beginthreadex(NULL, 0, func, param, 0, &threadId);
       
      #endif
    
      /* maybe we must use errno here, but probably GetLastError() is also OK. */
      return HandleToWRes(*p);
    }
    #else
    pthread_attr_t g_th_attrs[64]; //NOTE: maximum of 64 threads
    size_t g_th_index = 0;
    
    WRes Thread_Create(CThread *p, THREAD_FUNC_TYPE func, LPVOID param)
    {
      *p = malloc(sizeof(pthread_t));
      pthread_t* th = *p;
      int ret = pthread_attr_init(&(g_th_attrs[g_th_index]));
      assert(ret==0);
      ret = pthread_create(th, &(g_th_attrs[g_th_index]), func, param);
      g_th_index++;
      return ret;
    }
    #endif
    
    
    #ifdef _WIN32
    static WRes Event_Create(CEvent *p, BOOL manualReset, int signaled)
    {
      *p = CreateEvent(NULL, manualReset, (signaled ? TRUE : FALSE), NULL);
      return HandleToWRes(*p);
    }
    
    WRes Event_Set(CEvent *p) { return BOOLToWRes(SetEvent(*p)); }
    WRes Event_Reset(CEvent *p) { return BOOLToWRes(ResetEvent(*p)); }
    
    WRes ManualResetEvent_Create(CManualResetEvent *p, int signaled) { return Event_Create(p, TRUE, signaled); }
    WRes AutoResetEvent_Create(CAutoResetEvent *p, int signaled) { return Event_Create(p, FALSE, signaled); }
    WRes ManualResetEvent_CreateNotSignaled(CManualResetEvent *p) { return ManualResetEvent_Create(p, 0); }
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent *p) { return AutoResetEvent_Create(p, 0); }
    
    #else 
    ///unix
    
    WRes Event_Close(CEvent* p) {
      if (!p || !(*p))
        return 0;
      event_t* evt = *p;
      pthread_cond_destroy(&evt->cond);
      pthread_mutex_destroy(&evt->mutex);
      free(evt);
      *p = NULL;
    }
    
    
    WRes Event_Set(CEvent *p) {
      event_t* evt = *p;
      if (pthread_mutex_lock(&evt->mutex) != 0) {
        return 1;
      }
      evt->state = true;
    
      if (evt->manual_reset) {
        if (pthread_cond_broadcast(&evt->cond)) {
          return 1;
        }
      } else {
        if (pthread_cond_signal(&evn->cond)) {
          return 1;
        }
      }
    
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
    
      return 0;
    }
    
    WRes Event_Reset(CEvent* p) {
      event_t* evt = *p;
      if (pthread_mutex_lock(&evt->mutex)) {
        return 1;
      }
    
      evt->state = false;
    
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
    
      return 0;
    }
    
    WRes Event_Wait(CEvent* p) {
      event_t* evt = *p;
      if (pthread_mutex_lock(&evt->mutex)) {
        return 1;
      }
    
      while (!evt->state) {
        if (pthread_cond_wait(&evt->cond, &evt->mutex)) {
          pthread_mutex_unlock(&evt->mutex);
          return 1;
        }
      }
    
      evt->state = false;
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
      return 0;
    }
    
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent *p) {
      *p = malloc(sizeof(event_t));
      memset(evt, 0, sizeof(event_t));
      evt->state = false;
      evt->manual_reset = false;
      if (pthread_mutex_init(&evt->mutex, NULL)) {
        return 1;
      }
      if (pthread_cond_init(&evt->cond, NULL)) {
        return 1;
      }
      return 0;
    }
    
    #endif
    
    
    #ifdef _WIN32
    
    WRes Semaphore_Create(CSemaphore *p, UInt32 initCount, UInt32 maxCount)
    {
      *p = CreateSemaphore(NULL, (LONG)initCount, (LONG)maxCount, NULL);
      return HandleToWRes(*p);
    }
    
    static WRes Semaphore_Release(CSemaphore *p, LONG releaseCount, LONG *previousCount)
      { return BOOLToWRes(ReleaseSemaphore(*p, releaseCount, previousCount)); }
    WRes Semaphore_ReleaseN(CSemaphore *p, UInt32 num)
      { return Semaphore_Release(p, (LONG)num, NULL); }
    WRes Semaphore_Release1(CSemaphore *p) { return Semaphore_ReleaseN(p, 1); }
    
    #endif
    
    
    #ifdef _WIN32
    WRes CriticalSection_Init(CCriticalSection *p)
    {
      /* InitializeCriticalSection can raise only STATUS_NO_MEMORY exception */
      #ifdef _MSC_VER
      __try
      #endif
      {
        InitializeCriticalSection(p);
        /* InitializeCriticalSectionAndSpinCount(p, 0); */
      }
      #ifdef _MSC_VER
      __except (EXCEPTION_EXECUTE_HANDLER) { return 1; }
      #endif
      return 0;
    }
    
    #else
    WRes CriticalSection_Init(CCriticalSection *p) {
      *p = malloc(sizeof(pthread_mutex_t));
      if (pthread_mutex_init(*p, NULL)) {
        return 1;
      }
      return 0;
    }
    
    WRes CriticalSection_Delete(CCriticalSection *p) {
      pthread_mutex_t* mtx = *p;
      return pthread_mutex_destroy(mtx);
    }
    
    WRes CriticalSection_Enter(CCriticalSection *p) {
      if (pthread_mutex_lock(&evt->mutex)) {
        return 1;
      }
      return 0;
    }
    
    WRes CriticalSection_Leave(CCriticalSection *p) {
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
      return 0;
    }
    
    #endif
    

    XzDec.c

    add the following macro

    #ifndef _WIN32
    #define S_OK 0x00000000
    #define E_FAIL 0x80004005
    #endif

    MtDec.c

    replace function ThreadFUnc1 with:

    static THREAD_FUNC_RET_TYPE THREAD_FUNC_CALL_TYPE ThreadFunc1(void* pp)
    {
      static int g_ok_stat = 0x0;
      static int g_err_stat = 0x80004005;
      WRes res;
      CMtDecThread *t = (CMtDecThread*)pp;
      CMtDec *p;
    
      res = ThreadFunc2(t);
      p = t->mtDec;
      if (res == 0) {
    #ifdef _WIN32
        return p->exitThreadWRes;
    #else
        if (p->exitThreadWRes) { return &g_err_stat; }
        else { return &g_ok_stat; }
    #endif
      }
      {
        // it's unexpected situation for some threading function error
        if (p->exitThreadWRes == 0)
          p->exitThreadWRes = res;
        PRF(printf("\nthread exit error = %d\n", res));
        p->exitThread = True;
        Event_Set(&p->threads[0].canRead);
        Event_Set(&p->threads[0].canWrite);
        MtProgress_SetError(&p->mtProgress, MY_SRes_HRESULT_FROM_WRes(res));
      }
    #ifdef _WIN32
        return res;
    #else
        return &g_err_stat;
    #endif
    }
  • Visual Studio Code 如何在离线环境下安装远程调试(vscode-server)以及安装各种插件

    公司的生产环境里的服务器是禁止了绝大多数互联网连接的,但是有些环境又只有生产环境里有。为了方便,以前总是直接ssh进去用vim写代码,但是总觉得效率还是不够高。(这是我的问题😄)。另一个方法是在本地机器上写好代码,然后rsync过去一把梭。但是写一些稍大的工程的时候,一把梭很难成功。今天决定趁周末解决这种“非技术问题”。

    远程搬砖码字调试的意思如下图:
    vscode-remote-dbg-arch

    画个重点就是:

    • 本地机器上安装好VS Code;
    • 远程机器上安装好调试工具(vscode-server);
    • 代码留在远程机器上,本地机器仅作为”文本编辑器”这样的前端界面;

    本地机器安装远程调试插件(Remote Development Extension)

    这个步骤没什么难度,因为本地机器是可以连上网的。在VS Code的Extension页面搜索”Remote Development”安装便是。
    这边提一句,笔者的环境是“墙中墙”,运行VS Code的也是一个“虚拟机”,里面也没网。像这样的情况,需要根据远程连接的类型(SSH, WSL, Docker…)选择具体的插件,比如SSH的话去 https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh 这个页面,点击右侧的”Download Extension”下载一个vsix后缀的文件传到墙里面。然后选择下图”…”里面的”Install from VSIX”离线安装

    install_from_vsix.PNG

    离线安装vscode-server调试服务器

    正常安装完远程调试插件以后,左侧边栏会多出一个远程连接的tab. 创建一个连接以后,右键那个连接选择”connect to host using new window”会新建一个窗口连接远程服务器。右下角会弹出一个泡泡提示正在安装什么的,这时候要点击”details”看一下详情。这个步骤因为没有网络,它是永远无法完成的…除非我们人为帮他下载好了放好

    install_vs_debug_server.PNG
    留意上图红框中的commit hash,我们需要从微软官网手工把这个东西下载下来。

    找个有网的机器,去下载https://update.code.visualstudio.com/commit:${commit_id}/server-linux-x64/stable,地址里面的${commit_id}用刚才红框里的代替。下载下来应该是个tar文件,然后把它传到你的远程机器上一个临时位置,这边存放的文件夹用/tmp代替.
    随后操作:

    1. 创建vscode-server的文件夹
    2. 解压缩
    3. touch一个0文件,表示加载已经全部完成
      具体bash如下:
    mkdir -p ~/.vscode-server/bin/${commit_id}
    # assume that you upload vscode-server-linux-x64.tar.gz to /tmp dir
    tar zxvf /tmp/vscode-server-linux-x64.tar.gz -C ~/.vscode-server/bin/${commit_id} --strip 1
    touch ~/.vscode-server/bin/${commit_id}/0

    搞定之后,重新执行刚才的连接远程服务器的操作。这时候如果去看details会发现VS Code认为服务器文件已经下载好了,于是他就会开始各种部署。

    离线安装扩展插件到远程服务器

    成功连接后你会发现一些本地装好的插件在远程是没有的。比如下图中的C++插件就是处于本地有但是远程没有的状态。这种插件一般都是需要获取编译运行环境的,由于本地无法模拟远程环境,所以必须要在远程装对应插件。

    remote_ext_vscode.PNG

    离线安装远程插件其实不难。将上文提到的VSIX离线文件下载了传到远程服务器上后(注意vsix有些是区分平台的,要和远程服务器平台一致,而非本地),可以直接安装。