分类 折腾 下的文章

神奇的Unsafe类: Java实例内存布局初探


前情提要

Java相比C/C++最大的一个特点就是没有提供指针, 或者说没有提供对内存的直接访问操作, 导致很多比较底层的操作根本无法进行.
一方面给Java带来了内存上的安全性, 不像C/C++稍微一处理不好就会Segment Fault, 更可怕的是内存访问越界了程序却没有退出, 这会导致程序发生一些莫名其妙的事情, 比如说由于错误修改了循环的迭代变量导致循环无法退出(被坑过的我流下了眼泪)等.
但另一方面降低了Java的灵活性, 也让人无法弄清楚Java的运行时内存布局, 以及进行一些神奇的Hack操作.

但是在Hotspot虚拟机中, 提供了UnSafe类可以给我们进行很多非常底层的虚拟机操作, 包括:

  • 根据对象和偏移读写内存的getInt(Object o, long offset)/putInt(Object o, long offset, int x)
  • 根据地址直接读写内存的getAddress(long address)/putAddress(long address, long x)
  • 在直接内存区(Direct Memory)中分配内存的allocateMemory(long bytes)/freeMemory(long address), 这部分内存主要是被java.nio中的类所使用
  • 内存直接复制copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes) 类似于memcpy
  • 属性偏移计算方法objectFieldOffset(Field f)
  • 一些底层内存信息addressSize()/pageSize()
  • 从字节码直接生成一个类defineClass(String name, byte[] b, int off, int len)
  • 加载实例也不是一个问题allocateInstance(Class cls)
  • 手动模拟Synchronized的加锁解锁操作monitorEnter(Object o)/monitorExit(Object o)
  • 抛出一个异常throwException(Throwable ee)
  • 喜闻乐见的CAS操作compareAndSwapObject(Object o, long offset, Object expected, Object x)
  • 以volatile模式操作某个属性getIntVolatile(Object o, long offset)
  • 读值并直接相加/设置的新原语getAndAddInt(Object var1, long var2, int var4)/getAndSetInt(Object var1, long var2, int var4)
  • 栅栏操作storeFence()/loadFence()

等等

不过, Oracle已经在JDK11里正式移除Unsafe类, 毕竟这些底层操作从某种角度来说破坏了Java构建的安全性, 虽然似乎其实有大量的开源框架都在使用该类, 比如Spring/Netty/Hadoop等等, 在新的JDK11下似乎也有替换的解决方案.
不过这些都不是重点, 本文的重点是如何通过Unsafe类提供的getAddress(long address)观察对象实例的内存布局.

正篇开始

Java的对象实例内存布局其实是一个老生常谈的问题了, 无论是著名的深入理解Java虚拟机还是网上都有很多文章说过这个问题, 但是我几乎没有看到结合实验来说明实例布局的实际情况的文章, 所以本文从实验的角度来说明这个问题.

对象实例内存布局总的来说, 可以分为三块:

  • 对象头
  • 实例数据
  • 对齐填充

下图展示了32位虚拟机的情况:
20190314172833568_30071.png

事实上, 对于64位虚拟机来说对象头其实有128Bit, 也就是16Byte. 在没有开启对象指针压缩的前提下, 对象头的布局才是如下所示:

|------------------------------------------------------------------------------------------------------------|--------------------|
|                                            Object Header (128 bits)                                        |        State       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                                  Mark Word (64 bits)                         |    Klass Word (64 bits)     |                    |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Normal       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Biased       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 |    OOP to metadata object   | Lightweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 |    OOP to metadata object   | Heavyweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                                                                     | lock:2 |    OOP to metadata object   |    Marked for GC   |
|------------------------------------------------------------------------------|-----------------------------|--------------------|

不过, 从JDK6开始, JVM就会默认开启对象指针压缩, 如果要让内存的结构如上图所示, 必须要手动关掉它-XX:-UseCompressedOops才行.

Talk is cheap. Show me the code.

public class MemoryAccess{
    private final static Unsafe unsafe = getUnsafe();
    public int a = 12345678;
    public int b = 87654321;
    public long c = -1;
    public char d = 66;

    public static void main(String[] args){
        MemoryAccess o = new MemoryAccess();
        long base = getBaseAddress(o), size = sizeOf(o);
        System.out.printf("Base: %x, Size: %d\n", base, size);

        // Read Memory First
        readMemory(base, size);

        // Fetch HashCode
        System.out.printf("HashCode: %x\n", o.hashCode());

        // Read a first
        System.out.println("Old a Val: " + o.a);
        // Modify a via memory
        int offset = 24, newAVal = 23333333;
        long newMemoryVal = (unsafe.getAddress(base + offset) & 0xffffffff00000000L) + newAVal;
        unsafe.putAddress(base + offset, newMemoryVal);
        // Read a again
        System.out.println("New a Val: " + o.a);

        // Read Memory Again
        readMemory(base, size);
    }

    static Unsafe getUnsafe(){
        try{
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe)f.get(null);
        }catch(NoSuchFieldException | IllegalAccessException e){
            e.printStackTrace();
        }
        return null;
    }

    public static long sizeOf(Object object){
        long metaAddr = unsafe.getInt(object, 8L);
        return unsafe.getAddress(metaAddr + 4L) >>> 32;
    }

    static long getBaseAddress(Object obj){
        Object[] array = new Object[]{obj};
        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        return unsafe.getLong(array, baseOffset);
    }

    static void readMemory(long base, long length){
        System.out.printf("| %3s | %79s | %17s | %23s |\n", "Offset", "Binary", "Hex", "Dec");
        for(int i = 0; i < length; i += 8){
            long val = unsafe.getAddress(base + i);
            int leftVal = (int)(val >> 32);
            int rightVal = (int)(val);
            System.out.printf("| %6d | %s | %8x %8x | %11d %11d |\n", i, getLongBinaryStr(val), leftVal, rightVal, leftVal, rightVal);
        }
    }

    static String getLongBinaryStr(long val){
        return getLongBinaryStr(val, true);
    }

    static String getLongBinaryStr(long val, boolean padding){
        StringBuilder builder = new StringBuilder();
        for(int i = 0; i < 64; i++){
            if(padding && i % 4 == 0 && i != 0){
                builder.insert(0, " ");
            }
            builder.insert(0, val & 0x1);
            val = val >>> 1;
        }
        return builder.toString();
    }
}

简单解释一下:

  • getUnsafe用于获取Unsafe类的实例, 因为Unsafe类本来的设计是限定在rt.jar的内部使用, 直接使用getUnsafe()方法的话, 它会在内部验证Caller的类加载器是否为空, 即BootstrapClassLoader.
public static Unsafe getUnsafe() {
    Class cc = sun.reflect.Reflection.getCallerClass(2);
    if (cc.getClassLoader() != null)
        throw new SecurityException("Unsafe");
    return theUnsafe;
}
  • sizeOf用于获取对象实例的大小, 首先是从对象头中取得类在方法区的指针, 然后通过取方法区中的数据获得类的实例大小.
  • getBaseAddress用于获取一个对象实例的基地址, Unsafe类中也没有提供获得对象基地址的方法, 这里用了一个非常巧妙的方法, 即先把对象放到一个数组中, 然后读取数组的值即可知道对象指针的值, 也就是对象实例的基地址了.
  • readMemory用于从基地址开始打印指定长度的内存到控制台, 由于getAddress只能以64bit读取数据, 这里将这64bit分成左右两个部分分别显示十六进制和十进制值
  • getLongBinaryStr用于将long长度的数用带前缀0的二进制打印出来, Long.toBinaryString()是没有前缀0的

输出的结果如下所示:

Base: d4dc74e0, Size: 40
| Offset |                                                                          Binary |               Hex |                     Dec |
|      0 | 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 |        0        1 |           0           1 |
|      8 | 0000 0000 0000 0000 0000 0000 0000 0000 0001 0111 1110 0011 0100 0010 0100 0000 |        0 17e34240 |           0   400769600 |
|     16 | 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 | ffffffff ffffffff |          -1          -1 |
|     24 | 0000 0101 0011 1001 0111 1111 1011 0001 0000 0000 1011 1100 0110 0001 0100 1110 |  5397fb1   bc614e |    87654321    12345678 |
|     32 | 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0100 0010 |        0       42 |           0          66 |
HashCode: 6e0be858
Old a Val: 12345678
New a Val: 23333333
| Offset |                                                                          Binary |               Hex |                     Dec |
|      0 | 0000 0000 0000 0000 0000 0000 0110 1110 0000 1011 1110 1000 0101 1000 0000 0001 |       6e  be85801 |         110   199776257 |
|      8 | 0000 0000 0000 0000 0000 0000 0000 0000 0001 0111 1110 0011 0100 0010 0100 0000 |        0 17e34240 |           0   400769600 |
|     16 | 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 | ffffffff ffffffff |          -1          -1 |
|     24 | 0000 0101 0011 1001 0111 1111 1011 0001 0000 0001 0110 0100 0000 1001 1101 0101 |  5397fb1  16409d5 |    87654321    23333333 |
|     32 | 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0100 0010 |        0       42 |           0          66 |

我们可以看到对象的基地址是0xd4dc74e0, 对象的长度是40Byte.

前128Bit就是之前提到的对象头, 后面的数据分别是64bit长度的属性c=-1, 由于Intel x86是小端序, 所以接下来是32bit长的属性a=12345678和32bit长的属性b=87654321, 最后是8bit长的属性d=66, 以及补齐到64bit对齐的填充0.
和书中说的一样, 实例数据部分会优先分配比较长的属性, 并尽量将相同宽度的属性放在一起.

对于对象头, 参考之前的对象头表, 0x17e34240是指向metadata的指针, 对象长度也是从这个指针所指向的内存中获取的, 而MarkWord部分, 却只知道对象现在是无锁状态(01), 为什么理应放hashCode的部分是0呢?

因为我们还没有计算过hashCode, 只有计算过hashCode, 那么在对象头部分才会将hashCode的值填入.
我们读取过一遍hashCode以后, 再次读取对象头部分的时候, 就会发现hashCode部分存入了之前计算出的hash值, 即0x6e0be858.
从此可见, 对象头中的hashCode相当于是一层简单的缓存, 避免对同一个对象hashCode的重复计算.

后面的代码演示了, 通过直接对内存进行修改来修改对象属性的值. 我们首先读取了属性a的值为12345678, 通过直接对内存进行修改, 我们就其的值改为了23333333.
这里的offset可以通过对对象实例内存进行观察得到, 也可以通过之前提到的objectFieldOffset(Field f)方法获得, 我猜大概也可以直接从方法区中类的metadata中读到.

另外就是这里没有演示出来的一点, 对象的metadata和类的class对象在内存里没有任何关系, 类的class对象也是一个位于堆区的普通对象, 是Class类的一个实例.

总结与展望

通过对Unsafe的操作, 我们能够对对象实例部分的内存进行任意读写, 经过测试, 整个堆区的内存都是可以任意读写的.
这从某种角度来说提供了一个高效的修改对象的方式, 相比反射过程冗长的检查过程, 这种方式简单直接当然也很容易出错, 而且非常依赖于具体的实现.

通过对对象实例内存的读取, 可以对对象实例内存的布局和数据变化有了更加深入的认识和了解, 实例内存布局不再是一个简单的图表或者一个虚拟机实现的头文件, 而是实实在在的数据变化.

考虑到, 我们可以通过对象实例获取到对象方法区的指针, 那么可以通过方法区内存的读取, 从而得到类在方法区内存的布局.
更进一步说, 如果我们能够找到对象的方法表, 而且这块内存可写, 那么我们其实可以做到方法级的Hook操作.
目前已知的一些Hook, 无论是自定义类加载器/动态代理或者是Instrument机制, 我们都只能整体的替换一个类, 而不能做到精准地只替换掉一个方法.
也许, 通过这种操作, 能够实现单独对一个方法的替换.

当然, 这样的hack操作肯定不会在生产环境中使用, 但是从对JVM深入理解以及好玩的角度来说, 我觉得继续探究一下还是挺有意思的.

Ref1: http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
Ref2: https://github.com/keerath/openjdk-8-source/blob/master/jdk/src/share/classes/sun/misc/Unsafe.java


博客又又又搬家了&VPS折腾记录


搬迁到新的VPS

现在的博客已经从之前的Banwagon搬迁到了UltraVPS上, 虽然年付用从$19变成了$40, 但是依然感觉很值:
一方面是线路很棒, LA的QN机房, IPV6线路到达LA之后只要四跳总跳数是14跳, IPV4线路也是差不多的, 但是因为我这边要先去北京赛尔的机房所以总跳数是17跳.
另外一方面是配置感人, 双核2G+(50G+10G)SSD, 这个配置只要$3.33每月真的感觉很难找到第二家了, 何况还是KVM, 线路质量也好, 也支持IPV6, 简直完美.

为什么会是(50G+10G)呢...
这是因为我第一次见到有商家虽然说是50G, 但是其实另外给了你10G用来装系统, 然后50G是完全的数据分区的...
于是我用了btrfs, 将这两个虚拟硬盘做成了一个逻辑卷嗯[Btrfs大法好

详细的评测+安利可以看这篇博文: https://blog.cysi.me/2018/03/new-vps-ultravps-providerservice.html
我当初就是看了这个之后心动的_(:3」∠)_
50G的SSD虽然不多, 但是对于自用的Seafile完全足够, 而且2G RAM对于Seafile Pro的配置条件也完全满足, 嗯
所以现在这上面部署了我的博客和Seafile还有你懂的服务.

购买的话, 这家似乎支持PayPal, 然后反欺诈还蛮严格的, 请务必填上看起来没问题的联系信息, 不然订单不会通过审核的.

具体的使用情况的话, 可能是开会已经开完了, 所以SSH上去不会突然就断掉, 也没有SSH很卡的情况, 虽然没法和本地路由器比, 但是也还是可以接受的.
说起来, 这家的KVM居然可以通过cat /proc/cpuinfo看到CPU类型, 是E5-2670.
看起来似乎母鸡是多CPU的, 不过很迷啊, 为什么CPU是这么老旧的一代E5...这个CPU不是都上岸多年了吗...
不过反正便宜就不管那么多了, 而且我真的没有任何需要CPU性能的服务.
IO的话...大概读写240M/s的样子, emmm凑合吧, 毕竟便宜.
ping值从170ms-240ms不等, HTTP下载速度的话, 大概最快可以接近4M/s...
不过主要还是看脸的, 线路路由会随时间而变化= =
比如说一开始是从武汉移动从上海出国的, 然后现在就变成了走教育网从北京出国...
总而言之, 这么便宜, 又能满足各项需求, 很适合自己的情况, 还要什么自行车啊.

最近试用了一下Syncthing...不得不说线路质量真是棒啊_(:3」∠)_
在IPV4环境下的上下行带宽:
QQ截图20180410133805.png
上下行感觉很明显是被限制在20Mbps的样子...
看了一下路由感觉是走了学校的电信出口, 所以可能是在学校的出口被限速了吧应该
在IPV6环境下的上下行带宽:
QQ截图20180410133204.png
这个上行带宽真的是吓哭我了, 第一次在公网见到这么高速率好吗!
说起来这个上行带宽的上升曲线, 真是生动形象的TCP慢启动过程啊233
下行的带宽虽然不及上行带宽, 但是还是非常可以的.
明明服务器上用了BBR, 上下行的路由还是一样的, 为什么带宽差这么多呢_(:3」∠)_
不是很懂(有可能是网络环境的影响吧, 比如说被大量PT流量给拥塞了什么的

博客搬迁

博客系统还是Typecho, 这货在死了几年以后居然STM的诈尸了. 不过更新之后, 多了一个很棒的功能就是备份还原.
这样博客的搬迁工作就非常简单了, 只要在原博客上备份, 再来新博客上还原就好, 也不用倒腾数据库了, 而且似乎我也没啥文件的样子...

然后过了这么几年, 我也从服务器小白逐渐进阶, 原来用的是LNMP的一键安装包, 现在还是觉得手动配置Nginx和PHP比较好.
然后网站前几天也弄了全站HTTPS, 用的是Let's encrypt的证书啦, 不过因为用了cloudflare的CDN, 所以拿到的证书可能是cf的共享证书...
然后用了大佬的自动部署脚本: https://github.com/gileshuang/letsautoencrypt
这个站点还有多个备份域名, 比如说:
http://www.slkun.me / http://blog.slkun.me
http://www.doris.work / http://blog.doris.work
http://www.lovedoris.moe / http://blog.lovedoris.moe
= =嗯 那个lovedoris.moe是没有cf的CDN部署的
然后哦Nginx的部署其实还是折腾了一下的= =因为其实对Nginx并不熟悉, 配置如下需要的可以参考:

server {
    listen       localhost:port;
    server_name  domainName;

    access_log  /nginx/logs/typecho.access.log;
    error_log  /nginx/logs/typecho.error.log;

    root        webRoot;
    index       index.php index.html index.htm;

    location / {
        if (!-e $request_filename) {
            rewrite ^(.*)$ /index.php$1 last;
        }
    }

    location ~ '\.php(/|$)' {
        include        fastcgi.conf;
        fastcgi_pass   unix:/run/php-fpm/php-fpm.sock;
        fastcgi_split_path_info       ^(.+\.php)(.*)$;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
    }

    # redirect server error pages to the static page /50x.html
    #error_page  404              /404.html;
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

VPS折腾史

在16年入手了Bandwagon的256M VPS以后, 突然看到这家又推出了PHOENIX AZ的Special套餐, 年付$18就可以有1G RAM+20G SSD, 于是脑子一热就入手了.
因为位置问题, 所以ping值是会高一些, 然后路由一般般吧. 性能的话...emmm Xean D-1540? 我第一次听说这个系列的志强= = IO还是一如既往的优秀平均500+M/s

本来之前用这个256M的主要是因为这款的机房可切换, 万一要是ip翻车我还可以换个机房继续用, 可是用了一年多感觉非常稳, 完全没有任何问题.
但是以防万一, 所以这个256M的还是一直在用, 然后那个1G的因为硬盘大内存多, 于是我就在上面装了Seafile的服务, 用于文件同步和备份.
Seafile的话又是一个可以吹一波的好用软件, 这个的话以后再写好了.

后来, 正好之前那个256M的到期, 加上Banwagon推出了KVM的VPS, 然后还是联通和CN2直连的, 内存也是512M, 年付$28. 考虑到IPV6我可以用之前那个PHOENIX AZ, 而且PHOENIX AZ的那个在公网的使用体验不佳, 原来的256M一方面是OVZ而且内存太小, 于是就入手了这个.
其实也考虑过别的VPS商家, 比如说DigitalOcean, Vultr, Linode之类的, 但是最低$5/mo的价格实在是感人肺腑, 而且开了Vultr和DigitalOcean试用, 线路带宽实在堪忧, HTTP下载速度不超过100K/s, 这个就非常尴尬了. 所以就觉得Banwagon虽然便宜, 但是还慢靠谱的(对于我的网络环境来说)
这个CN2直连果然还是满靠谱的, 因为KVM所以并不清楚CPU的型号, 但是IO如上一样平均600+M/s吧. 线路的话, ping只有160ms左右吧, HTTP下载速度基本是不会低于500K/s的, 一般是1M/s左右, 好的话可以到2M/s.

随着照片什么的备份多了起来, 逐渐感觉20G SSD开始不够用了起来, 而且PHOENIX AZ的这个的速度突然变得非常慢, 又是HTTP下载速度不超过100K/s的节奏(确切的说是ipv6线路, ipv4线路好像还可以). 而且这个是OVZ的虚拟化, 没法换内核, 也就没法BBR, 很多L2TP之类的操作也没法弄, 于是就开始物色有没有存储更多价格差不多的VPS来替换.

正好看到小伙伴有推荐Virmach, 然后看了一下感觉这个价格真...
我本来以为$20年付已经非常便宜了...没想到还是$10年付的OVZ, 甚至还有$12.5年付的KVM...
我只感觉这家是不是分分钟要跑路啊, 再加上看到这个其实有点犹豫.
但是看到年付$35的512M RAM + 500G HDD瞬间就没了脾气, 决定先开一个月试试看, 结果没注意是备货状态= =
不过前几天也正式部署了, 虽然说是500G, 但其实只有465G, 感觉像是挂了一个500G的物理硬盘的感觉...不会是单盘吧= =
性能嘛...嗯 真TM感人...和当年我笔记本的那个超薄7mm硬盘差不多...IO读写不上百你敢信? 大概只有50-70M/s的样子...
网络情况...虽然是在NY, 虽然ping值很高, 但是HTTP下载还可以, 平均2M/s左右吧...所以还是有点可用性的
不过的话, 我已经有了50G SSD的那个了, 所以这个其实没啥需求了嗯

关于VPS的推荐和介绍, 可以看一下这篇博文, 写得挺全的: https://yorkchou.com/host-provider.html
然后后悔自己黑五的时候怎么没多关注一下_(:3」∠)_

所以现在手上有三个VPS, 到时候可能那两个Bandwagon就不续费了, 然后考虑弄一个日本来玩嗯(如果Vultr$2.5有货的话)


使用树莓派3构建校园网关路由器


为什么我要采用树莓派3作为路由器

我原来的用的无线路由是WR841N, 刷了Openwrt, 用于锐捷认证和提供ipv4网络.
前些日子折腾在路由器后提供ipv6网络, 可是学校网络不支持relay mode, 因此只能在路由上配置NAT6.
然而NAT6是非常消耗资源的一项操作. 当北邮人PT下载跑满百兆带宽的时候, 路由器的负载就会上到5以上.
担心长时间路由器会HOLD不住, 所以萌生了购买了一个新的路由器的想法.
本来已经看好了MT7610a的小米路由mini, 但是转念一想为什么不用树莓派搭建一个网关呢.
成本并没有增加多少, 而且5V的供电也可以在没电之后继续使用移动电源使用.
更何况树莓派3的四核A53比起MT7610a的性能不知道好了多少倍.
以及我并不需要千兆无线, 而且MT7610a只有百兆有线.

除了树莓派3的其他选择

在同等价位来考虑, 其实香蕉派M2+其实是一个更好的选择.
香蕉派M2+的劣势下在于四核A7@1.2g比起树莓派3要差.
但是, 优势同样非常明显.
树莓派3的最大缺点就在于IO带宽, 其物理拓扑结构是主芯片引出一个USB2.0接口到网卡同时作为HUB引出四个USB口.
也就是说, 板载的网卡LAN9514和四个USB接口同时共用一个USB2.0的480Mbps带宽.
当需要进行大IO操作时, 劣势就明显的体现出来了.
香蕉派M2+的网卡以及两个USB口都是直连芯片的, 只受到AMBA带宽的限制.
根据我看到的实测数据大概可以达到60+MByte/s, 相比之下, 树莓派的实测数据只能到10+MByte/s.
如果需要更大的IO带宽, 那么最好还是右转上J1900这样的凌动x86, 大概也只要500左右.
除了IO带宽以外, 香蕉派M2+还板载8G emmc. 相比TF卡, 稳定性大大提升, 同时也省下买TF卡的钱.
不过我倒是有一大把闲置的TF卡, 其实倒不是什么问题.
以及, 在香蕉派上有官方提供的Android系统, 以后也可以改造成电视盒子使用.

至于, 为什么最后还是选择了树莓派...原因还是更好友好的社区支持嗯.
换而言之就是用的人多, 踩坑的概率小= =

硬件准备

树莓派3一只. USB有线网卡一只, TF卡8G一只, 5V2A电源一只(5V1A也可以)

开工

在系统的选择上, 我采用的是Openwrt的衍生系统LEDE.
根据官方Wiki的叙述, 大概就是Openwrt的一帮开发者看Openwrt的低效不爽, 自己另外开个新坑的感觉.
不过, 确实LEDE的内核版本会比Openwrt要新, 所以就用它了.

LEDE的下载地址: https://downloads.lede-project.org/snapshots/targets/brcm2708/bcm2710/
下载那个: lede-brcm2708-bcm2710-rpi-3-ext4-sdcard.img.gz 就好了√

安装请参考Openwrt Wiki: https://wiki.openwrt.org/toh/raspberry_pi_foundation/raspberry_pi
简述的话就是在Linux内:

dd if=lede-brcm2708-bcm2710-rpi-3-ext4-sdcard.img of=/dev/sd* bs=2M conv=fsync

值得注意的是, 这会更改TF卡的分区表, 如果有什么数据的话, 请做好备份.
默认的话, 会分出一个20M的/boot分区和一个256M的/分区. 显然没有很好的利用好TF卡的空间.
所以, 我们还需要重新对TF卡分个区. 这可以使用GParted做到:

sudo apt-get install gparted
sudo umount /media/username/* #使用GParted前先取消挂载
sudo gparted

只要File-Device选择TF卡, 然后右键分区, 选择Resize就可以调整分区大小了√

Wiki中还提供了一些有用的信息:
比如说可以通过GPIO连接串口:
115200 8N1

Pin 6Pin 8Pin 10
GroundTXRX

以及可以通过修改/boot/config.txt文件修改一些配置.

修改软件源

很遗憾, 官方默认的软件源在国内的普通网络情况下似乎是连不上的.
而且, 官方的软件是滚动更新的. 一旦换了内核版本简直就是噩梦...
特别是opkg还没有提供upgrade的功能_(:3」∠)_
所以, 我特意把我安装时的软件源镜像了一份存在了服务器上. 大概一共才500M不到吧√
地址是: http://raspberrypi.slkun.me/mirror/LEDE/20161119/
速度什么的另说...至少以后官方滚内核的时候, 可以保证还有旧的软件可以用.
服务会在我还在用这一版的前提下会一直提供的嗯.

新版的17.01和18.06已出, 该镜像不再维护

软件源的配置文件在: /etc/opkg/distfeeds.conf

src/gz reboot_core http://raspberrypi.slkun.me/mirror/LEDE/20161119/core/
src/gz reboot_base http://raspberrypi.slkun.me/mirror/LEDE/20161119/base
src/gz reboot_telephony http://raspberrypi.slkun.me/mirror/LEDE/20161119/telephony
src/gz reboot_packages http://raspberrypi.slkun.me/mirror/LEDE/20161119/packages
src/gz reboot_routing http://raspberrypi.slkun.me/mirror/LEDE/20161119/routing
src/gz reboot_luci http://raspberrypi.slkun.me/mirror/LEDE/20161119/luci
#src/gz reboot_usr http://raspberrypi.slkun.me/mirror/LEDE/20161119/usr

usr目录是打算放我自己编译的一些软件, 现阶段就是mentohust咯.
请手动下载安装, 目前还没有配置Packages信息.

网络配置

默认配置的树莓派的网口为lan口, 是一个静态的192.168.1.1的ip, 没有dhcp服务器也没有dhcp客户端.
而且默认配置的LEDE也没有安装luci, 只能通过SSH登录.
因此, 需要修改/etc/config/network文件将其修改成为dhcp客户端模式以能够连上网.

config interface 'loopback'
        option ifname 'lo'
        option proto 'static'
        option ipaddr '127.0.0.1'
        option netmask '255.0.0.0'

config globals 'globals'
        option ula_prefix 'fd8f:4e62:2b25::/48'

config interface 'lan'
        option ifname 'eth0'
        option proto 'dhcp'

这样只要把树莓派的lan口连上可以上网的路由器就可以了.

LEDE内部配置

系统镜像准备好之后, 把TF卡插进树莓派, 然后上电.
通过查看路由器的DHCP表来获得树莓派的ip, 当然也可以在网络配置时采用静态ip.
但是需要注意配置正确的网关, 不然内网可以访问到树莓派, 但是树莓派不能通过路由上网.

硬件驱动

我用的USB网卡是ASIX AX88772B, 虽然很廉价, 但是基本上也能够跑满100M的带宽.
确认路由器可以连上网以后, 安装驱动:

opkg update
opkg install kmod-usb-net kmod-usb-asix

这样使用ifconfig就可以看到eth1的第二张网卡了.

安装Luci

opkg install luci luci-i18n-base-zh-cn luci-i18n-firewall-zh-cn luci-theme-material

安装一些实用工具

opkg install ipset vim-full git git-http zsh curl libustream-openssl
opkg install wpad

安装Mentohust

ipk包可以从镜像下的usr分类下下载.

opkg install mentohust luci-luci-app-mentohust

注意不要直接使用mentohust的交互式命令行输入用户名和密码.
该编译版本与shell不太兼容, 我也懒得去找为什么.
直接使用/etc/mentohust文件配置, 或者通过luci就好.

安装ipv6 NAT支持

opkg install kmod-ipt-nat6

添加启动时脚本到/etc/rc.local

#!/bin/sh /etc/rc.common

# For enable mentohust for sure at startup
MAX_TRIES=60
COUNT=1
PING_TEST=$(ping -w 3 www.baidu.com | grep "3 packets transmitted, 3 packets received")

# detecting internet is ok
while [ ! -n "$PING_TEST" ]
        do
        if [ $COUNT -gt $MAX_TRIES ]
        then
                logger -t MENTOHUST "Cannot access the Internet (reached retry limit $MAX_TRIES times)" && exit 1
        fi
        mentohust -k
        sleep 10
        mentohust -d
        sleep 60
        logger -t MENTOHUST "Try to connect the Internet... ($COUNT time)"
        COUNT=$((COUNT+1))
        PING_TEST=$(ping -w 3 www.baidu.com | grep "3 packets transmitted, 3 packets received")
done
logger -t MENTOHUST "Internet is accessible!"

# NAT6 init script for OpenWrt 
# Depends on package: kmod-ipt-nat6 ip6tables
# Ref:  https://wiki.openwrt.org/doc/howto/ipv6.nat6

MAX_TRIES=60
WAN6_NAME="wan6" 
WAN6_INTERFACE=$(uci get "network.$WAN6_NAME.ifname") 
LAN_ULA_PREFIX=$(uci get network.globals.ula_prefix)
PROBE=0
COUNT=1

# detecting ipv6
while [ $PROBE -eq 0 ]
        do
        if [ $COUNT -gt $MAX_TRIES ]
        then
                logger -t NAT6 "No IPv6 route found (reached retry limit $MAX_TRIES times)" && exit 1
        fi
        sleep 30
        logger -t NAT6 "detect IPv6 route... ($COUNT time)"
        COUNT=$((COUNT+1))
        PROBE=$(route -A inet6 | grep -c '::/0')
done

ip6tables -t nat -I POSTROUTING -s "$LAN_ULA_PREFIX" -o "$WAN6_INTERFACE" -j MASQUERADE
logger -t NAT6 "configure the IPv6 NAT table: $LAN_ULA_PREFIX at $WAN6_INTERFACE"

WAN6_GATEWAY=$(ifconfig "$WAN6_INTERFACE" | grep 'Global' | awk '{print $3}'| awk -F':' '{print $1":"$2":"$3":"$4"::1"}')
logger -t NAT6 "get the gateway of IPv6: $WAN6_GATEWAY"

route -A inet6 add default gw "$WAN6_GATEWAY" dev "$WAN6_INTERFACE"
logger -t NAT6 "set the gateway of IPv6: $WAN6_GATEWAY at $WAN6_INTERFACE"

logger -t NAT6 "IPV6 Configure done!"

exit 0

前一部分是刚开机时(早上来电时)如果发现认证不成功没有网, 就反复尝试认证直到成功为止.
第二部分是判断网络是否存在ipv6, 如果存在则建立ipv6 NAT转发.

配置网络

最后需要把网口做出合理的分配. /etc/config/network

config interface 'loopback'
    option ifname 'lo'
    option proto 'static'
    option ipaddr '127.0.0.1'
    option netmask '255.0.0.0'

config globals 'globals'
    option ula_prefix '1111:2222:3333:4444::/64'

config interface 'lan'
    option ifname 'eth1'
    option force_link '1'
    option type 'bridge'
    option proto 'static'
    option ipaddr '192.168.1.1'
    option netmask '255.255.255.0'
    option ip6assign '64'

config interface 'wan'
    option ifname 'eth0'
    option proto 'dhcp'
    option hostname 'Raspberry_Pi'
    option peerdns '0'
    option dns '223.5.5.5 208.67.222.220 202.114.0.242'

config interface 'wan6'
    option ifname 'eth0'
    option proto 'dhcpv6'

结束

完成配置之后重启, 将wan口接在树莓派本身的网口上, 下级交换机/路由器接在USB无线网卡上就可以正常工作. 其他关于Openwrt的配置就自己发挥想象咯.
在树莓派上, 性能和存储容量基本上不是问题, 唯一的限制点就是IO带宽了.
如果要挂移动硬盘之类的, 请注意供电和速度, 或者直接上香蕉派/凌动工控主板就好了.


博客迁移工作


原本博客是挂在AwardSpace下的, 最初用的是免费套餐, 去年这个时候Basic促销只要$3一年就顺手入了.
现在Basic到期了, 续费的价格就比较感人了, 一年得$60.

正好一直都想入手一个VPS玩, 最近又查了一下发现了Bandwagonhost这家服务提供商, 最低级的套餐一年只要$19.
虽然只是单核, 256M RAM, 10G SSD和500G月流量, 但是像我这种日PV不会上10, 月PV不会上100的个人站点就随意了.
至于为什么不用Hexo这样的静态博客的话, 是因为想用PHP写点自己用的小服务什么的.
而且上了VPS的话, 那么SS的钱就可以省下来了, 而且也可以搭个VPN, 怎么看都觉得很棒.

怎么买这种事情就不说了, 可以银联, 支付宝和信用卡付款, 长时间有着5%off的优惠, 自己找找优惠码就好了.
有一点值得注意的是, 除了上述的通用套餐外, 还有为中国特别优化过链路的和性能配置好很多的另外两个套餐.
但是, 他们不能换机房, 也就是不能换ip. 万一以后被GFW吃了, 就GG. 以防万一, 还是入手了这个.

性能的话, 据说这家VPS是有很大的超售的.
SSH登录上去的感觉就是卡卡卡卡卡卡卡卡. 但是毕竟这么便宜, 要求也就不要太高了.
虽然SSH上去卡, 但是性能什么的感觉其实还行. 编译各种乱七八糟的组件也还挺快.
CPU是E3v5, 最大睿频4G. SSD RAID10的读写能够有900M/s左右.
网络速度, 我这边ping差不多是160+ms. 下载速度, 会从900+K/s慢慢涨到2M/s.
总而言之, 就是凑合用还是能行的, 要求别太高了. 毕竟便宜.

我用的博客系统是Typecho, 相比WordPress还是快不少, 原生支持MarkDown, 虽然不是太完美.
迁移主要分为两个部分, 一是从数据库导出数据. 另外一部分是下载文件.
数据库导出数据直接使用PHPMyAdmin就好了. 下载文件用FileZilla下载.
下载文件的时候就能够感受出来, 原来的主机商比新的VPS慢= =
毕竟欧洲还是没有美国西海岸近, 从网络拓扑上来说.

需要注意的就是, 修改config.inc.php里的数据库配置文件.
我在VPS安装的服务器是LNMP-1.3, 提供了很方便的脚本.
Nginx据说比Apache省内存, 适合像我这种内存比较紧张的情况.
对于Typecho而言, 默认安装, 在进管理员界面的时候会404.
需要修改/usr/local/nginx/conf/vhost下的站点配置文件
将include enable-php.conf -> include enable-php-pathinfo.conf
然后重启lnmp就可以了.

我在迁移中遇到的另外一个问题是, 数据库导入option表以后, HTTP 500内部服务器错误.
如果全新安装的话就不会有, 是正常的.
因为自己的文章不多, 外带想删掉以前的一些东西.
就干脆不要原来的数据了, 直接全新配置一遍.
以前的文章数据直接从导出的数据库文件里复制就好了.
只要把rn变成换行, 那么就和你在后台输入的内容是一致的.[Markdown大法好

顺带还尝试配置了一下IPSec Over L2TP, 但是好像内核并不支持IPSec...暂时就不折腾了吧