跳转至

2017

有趣的 Java 日期格式化问题

今天在群里看到有人说,Java 的日期格式化有问题,如果用 YYYY-MM-dd ,今天的日期就会显示 2018-12-31 。我立马在本地用 Java REPL (aka Groovy) 跑了一下,果然如此:

$ date = new Date()
===> Sun Dec 31 10:51:26 CST 2017
$ import java.text.SimpleDateFormat
===> java.text.SimpleDateFormat
$ new SimpleDateFormat("YYYY-MM-dd").format(date)
===> 2018-12-31

解决方案是,把格式换为 yyyy-MM-dd ,确实就可以了。于是我就去研究了一下文档: Class SimpleDateFormat ,发现了问题:

y 代表 year ,而 Y 代表 week year 。根据 week year ,因为今年最后的一个星期在明年的部分更多,于是这个星期被归在了明年,所以这一周属于 2018,这就可以解释之前的那个输出问题了。

在 macOS 上试用 Gentoo/Prefix

前几天参加了许朋程主讲的 Tunight,对 Gentoo 有了一定的了解,不过看到如此复杂的安装过程和长久的编译时间,又看看我的 CPU,只能望而却步了。不过,有 Gentoo/Prefix 这个工具,使得我们可以在其它的操作系统(如 macOS,Solaris 等)上在一个 $EPREFIX 下跑 Portage,也就是把 Portage 运行在别的操作系统,当作一个包管理器,并且可以和别的操作系统并存。

首先还是祭出官网:Project:Prefix

首先设定好环境变量 $EPREFIX ,之后所有的东西都会安装到这个目录下,把 bootstrap-prefix.sh 下载到 $EPREFIX ,然后 ./bootstrap-prefix.sh ,会进行一系列的问题,一一回答即可。建议在运行前设置好 GENTOO_MIRRORS=http://mirrors.tuna.tsinghua.edu.cn/gentoo 由于 TUNA 没有对 gentoo_prefix 做镜像,只能把 distfiles 切换到 TUNA 的镜像上。

然后。。。

stage1...

stage2..

stage3.

emerge -e @world BOOM

经过 n 次跑挂以后,终于搞完了 stage3,然后 SHELL=bash ./bootstrap-prefix.sh $EPREFIX startscript 生成 startprefix ,在外面的 SHELL 中向切进来的时候运行这个即可。

然后就可以使用Gentoo/Prefix了。注意!此时的 $PATH 仅限于 $EPREFIX 下几个目录和 /usr/bin /bin 所以很多东西都会出问题(Emacs, Vim, Fish etc)。小心不要把自己的目录什么的搞挂了。

然后就可以假装试用 Gentoo 了!

哈哈哈哈哈哈哈

死于编译 libgcrypt 和 llvm。

LSP 和 C++

之前时间,巨硬发布了 LSP(Language Server Protocol),目的是解决目前 IDE 和各语言的 m+n 问题。想法很好,不过直到最近,终于有我觉得可以用的工具出来了,并且已经代替了我在使用的其它的插件。

由于我最近主要就是做做程设作业,做做 OJ 这些,主要就是和 C++ 打交道。所以我当然就开始找一些比较成熟的 C++的 LSP server。有一个 Sourcegraph 维护的 langserver.org ,上面有着目前的各个语言和编辑器/IDE 的支持情况,我刚才提到的 cquery 也会加入到这个列表里去。从这个列表里可以看到,我用的比较多的 Python 和 Haskell 都已经有不错的的 LSP server,我已经开始在本地体验 pyls 和 hie 了,感觉做得挺不错的。

回到 C++,我的主力编辑器是 Emacs,其次是 CLion,而 Emacs 上的LSP 支持 lsp-mode也在快速发展,与之配合的lsp-ui 也出现了很多很棒的功能。

下面开始编译并配置cquery

git clone https://github.com/jacobdufault/cquery --recursive
cd cquery
./waf configure # to use system clang, append --use-system-clang
./waf build

然后配置 Emacs:

(use-package lsp-mode
  :ensure t
  :diminish
  lsp-mode
  :commands
  (lsp-mode)
  :config
  (lsp-define-stdio-client
   lsp-pyls
   "python"
   #'get-project-root
   '("/usr/local/bin/pyls")))

(use-package lsp-ui
  :commands
  lsp-ui-mode
  :init
  (add-hook 'lsp-mode-hook 'lsp-ui-mode))

(use-package cquery
  :load-path
  "path_to_cquery/emacs"
  :config
  (setq
     cquery-executable "path_to_cquery/build/app"
     cquery-resource-dir "path_to_cquery/clang_resource_dir"))

接下来,需要配置 基于 Clang 的 工具都需要的 Compilation Database。Sacrasm 对这个有一个非常完整的总结 ,可以查看里面的方法。我这里推荐在 CMake 项目中用 CMake 自带的,加上nickdiego/compiledb-generator 应付基于Makefile/Autotools的项目。如果都不适用,就按照cquery的README写一个简单的.cquery文件即可,不需要Bear那种必须关闭SIP的方案。

然后就可以享受很多功能了!还是挺好用的。

Nginx 的内存池实现

今晚参加了 Tunight,会长给我们讲了 Nginx 的一些内部运作的机制和原理。中间的时候,会长展示的代码中用到了线程池方面的一些函数,但是大多地方只有调用 ngx_pcalloc 而没有看到相应的对象释放的过程,于是在演示的最后,会长应大家要求对 Nginx 魔幻的内存池实现做了现场代码分析。

在分析的中途遇到了很多坑,最后才终于理清了内存池的工作原理。这里直接解释结论吧。以下代码均摘自 Nginx 1.13.7,代码都可以在官方仓库找到。

首先分析一下创建一个内存池的函数:

ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }

    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;

    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

现在开始分段分析这个函数:在这里,一个内存池用一个 ngx_pool_t (aka struct ngx_pool_s) 类型的数据进行包装,所有的关于内存池的操作都基于相应的内存池对象。 ngx_log_t 表示输出信息的对象,与内存池无关,后面也不会讨论它。

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }

    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;

这里通过调用 ngx_memalign 分配一段(能对齐就对齐,不能对齐就放弃的)以 size 为大小的内存,做为这个内存池第一个块的内存,这个块的头是完整的,其中 p->d.lastp->d.end 分别表示可用于分配对象的内存段的开始和结束,在用 p->d.next 连接起来的链表中,每个链表实际上只有 d 是存储了数据,后面的各个域都不再使用。这里的 p->d.failed 涉及到链表的优化,在以后会接触到。

    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;

这里的 size 计算出实际用于对象分配的内存大小, p->max 存储了当前这个块最大能容纳的对象的大小, p->current 会和上面的 p->d.failed 合在一起对链表进行优化。 p->chain 与其他功能关系较密切,不会在本文中展开,而 p->cleanup 允许外部注册一些清理函数,实现起来并不难。

接下来,由于 ngx_pnallocngx_pcalloc 都和 ngx_palloc 相近,这里只对 ngx_palloc 进行分析:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);
    }
#endif

    return ngx_palloc_large(pool, size);
}

这里分了两种情况,如果要分配的内存大于一个块的最大值,那么这段内存必须要单独分配单独维护,所以调用了 ngx_palloc_large ,下面对其分析:

static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;

    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;

    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

        if (n++ > 3) {
            break;
        }
    }

    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

这里的 ngx_alloc 就是对 malloc 的简单封装,直接分配一段内存,然后向 pool->large 中以 ngx_pool_large_t 组成的链表中插入。这里有一个小优化:因为 ngx_pool_large_t 本身也要占用内存,为了复用已经被释放的 ngx_pool_large_t ,尝试链表的前几项,如果几项中都没有空的位置,因为 ngx_pool_large_t 本身是一个很小的对象,自然可以复用自己在内存池中分配对象的方法 ngx_palloc_small ,然后把它加入到 pool->large 的链表的第一向前。如果很大的内存都在分配后很快释放,这种方法可以复用很多的 ngx_pool_large_t

接下来分析 ngx_palloc_small

static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
    u_char      *m;
    ngx_pool_t  *p;

    p = pool->current;

    do {
        m = p->d.last;

        if (align) {
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }

        if ((size_t) (p->d.end - m) >= size) {
            p->d.last = m + size;

            return m;
        }

        p = p->d.next;

    } while (p);

    return ngx_palloc_block(pool, size);
}

首先,从 pool->current 遍历(这样做的原因下面会提到)已有的各个块,寻找有没有哪个块能容纳下现在需要的大小,如果能就可以调整 p->d.last 返回,否则就分配一个新的块到内存池中,再从新的块中分配需要的大小的内存。需要一提的是,在设计中,小的对象是随着整个内存池的销毁而被一起释放的,不会在中途被释放,而大的对象尽量要用完即释放。接下来分析 ngx_palloc_block

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    psize = (size_t) (pool->d.end - (u_char *) pool);

    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;

    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;

    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }

    p->d.next = new;

    return m;
}

为了节省内存,结构体中并没有记录实际分配的内存块的大小,于是根据第一个块的大小分配当前的块,虽然这里用的也是一个类型为 ngx_pool_t 结构体,实际上只用到了 new->d 中的内容维护块组成的链表和块内的分配情况。然后从 pool->current 开始找块的链表的结尾,找到节尾后把当前的块加到结尾的后面,然后把刚才需要分配的小对象的地址返回。与此同时,由于调用这个函数的时候,一定是当前的对象在已有的从 pool->current 开始的块中都放不下了,我们给这些块的 p->d.failed 进行自增,意思是说这个块在分配新的对象的时候又一次放不下了,如果放不下的次数比较多,我们可以认为这个块已经装得比较满了,那么,我们把 pool->current 设为它的后继,以后在分配新的对象的时候就会自动跳过这些比较满的块,从而提高了效率。

ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *l;

    for (l = pool->large; l; l = l->next) {
        if (p == l->alloc) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            ngx_free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

ngx_pfree 的实现可以看出,只有大的对象才会要求尽快释放,小的对象和没有被手动释放的大的对象都会随着内存池生命周期的结束而一起释放。如 ngx_destroy_pool 中的实现:

void
ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;

    for (c = pool->cleanup; c; c = c->next) {
        if (c->handler) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);
        }
    }

#if (NGX_DEBUG)

    /*
     * we could allocate the pool->log from this pool
     * so we cannot use this log while free()ing the pool
     */

    for (l = pool->large; l; l = l->next) {
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
    }

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                       "free: %p, unused: %uz", p, p->d.end - p->d.last);

        if (n == NULL) {
            break;
        }
    }

#endif

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_free(p);

        if (n == NULL) {
            break;
        }
    }
}

这个函数首先调用了一系列用户定义的 pool->cleanup 链表中的函数,允许自定义回收一些特定的资源。然后对每一个 pool->large 链表中的内容分别释放,最后再把各个块中所有的内存整块释放。注意 ngx_large_block_t 也是存在块中的,所以顺序不能反了。

在 Jupyter Notebook 中运行 C++ 代码

刚刚在 HN 上看到了这么一个文章:Interactive Workflows for C++ with Jupyter HN ,终于可以在 Jupyter Notebook 里跑 C++代码了,很开心,于是开始自己研究了起来怎么本地跑。

首先当然是更新一波 jupyter,安装一波 cling:

pip3 install -U jupyter
brew install cling

然后根据官方教程里的要求执行:

cd /usr/local/share/cling/Jupyter/kernel
pip3 install -e .
jupyter kernelspec install cling-cpp11
jupyter kernelspec install cling-cpp14
jupyter kernelspec install cling-cpp17
jupyter kernelspec install cling-cpp1z

结果发现找不到jupyter-kernelspec,遂重装了一下jupyter-client这个包,果然就可以了。打开一个 notebook 测试:

jupyter notebook

然后创建一个 C++14 的 Notebook,结果发现一直 Kernel rebooting,错误信息是说找不到../Cellar/cling/0.5/lib/libclingJupyter.dylib。这一看就是路径处理的问题,当前目录肯定不是/usr/local,肯定出现了什么问题,然后研究发现cling-kernel.py中对cling判断是否是个连接,如果是连接则按照连接去找cling的安装目录,但是!没有考虑到这个连接是个相对路径的问题(Homebrew 你背锅吗)。于是我愉快地改了代码并提交了PR。修复了以后就可以用了。

以下是一个小小的例子:

>> jupyter console --kernel cling-cpp14
Jupyter console 5.2.0

cling-X


In [1]: #include <stdio.h>
Out[1]:

In [2]: char *s = "Hello, world!";
input_line_4:2:12: warning: ISO C++11 does not allow conversion from string literal to 'char *' [-Wwritable-strings]
 char *s = "Hello, world!";
           ^
Out[2]:

In [3]: printf("%s",s);
Hello, world!Out[3]:
(int) 13

Okay,大功告成!

用 CPUID 获取评测机器的 CPU

用 CPUID 检测各大 OJ 测评机所用的 CPU(以及日常黑 BZOJ)的启发,我决定去测试一下徐老师自己写的 OJ(名为 Tyche)所跑的机器是什么 CPU。于是我改造一下代码,用以下代码测评:

#include <stdint.h>
#include <iostream>
#include <time.h>
#include <cpuid.h>
#include <sys/time.h>
static void cpuid(uint32_t func, uint32_t sub, uint32_t data[4]) {
    __cpuid_count(func, sub, data[0], data[1], data[2], data[3]);
}
int main() {
    uint32_t data[4];
    char str[48];
    for(int i = 0; i < 3; ++i) {
        cpuid(0x80000002 + i, 0, data);
        for(int j = 0; j < 4; ++j)
            reinterpret_cast<uint32_t*>(str)[i * 4 + j] = data[j];
    }

    struct timeval stop, start;
    gettimeofday(&start, NULL);
    while(1) {
        gettimeofday(&stop, NULL);
        if(stop.tv_usec - start.tv_usec > (str[##EDITME##] - 32) * 10000)
            break;
    }
}

经过测试,usleep()clock()都被封杀,但是gettimeofday()存活了下来。然后我就不断地C-a上面的###EDITME###,根据评测出来的时间推算出字符串,然后得到以下结果:

0 ~ 7 : PADDING
8 73 I
9 110 n
10 116 t
11 101 e
12 108 l
13 40 (
14 82 R
15 41 )
16 32 SPC
17 67 C
18 111 o
19 114 r
20 101 e
21 40 (
22 84 T
23 77 M
24 41 )
25 32 SPC
26 105 i
27 51 3
28 45 -
29 50 2
30 49 1
31 50 2
32 48 0
33 32 SPC
34 67 C
35 80 P
36 85 U
37 32 SPC
38 64 @
39 32 SPC
40 51 3
41 46 .
42 51 3
43 48 0
44 71 G
45 72 H
46 122 z

连起来就是这个 CPU

Intel(R) Core(TM) i3-2120 CPU @ 3.30GHz

相比之下,还是比 BZOJ 好哈哈哈(又黑 BZOJ)。后来有大神在群里建议,可以用字符串比较的方式,对了就让题目 AC,不对就 WA。这个方法更加适合手里已经知道了一些常见 CPUID 的返回字符串,这里就是这样。

一个代替 Pulse Secure 客户端的工具

清华的校外 VPN 服务使用的是 Pulse Secure,所以在外网我们需要在客户端上安装 Pulse Secure 才能使用内网的 info 和网络学堂等网站。但是 Pulse Secure 一是非自由软件二界面难看,所以我找到了一个代替它的工具:OpenConnect.

安装后,输入以下命令:

sudo openconnect --user 你的学号 sslvpn.tsinghua.edu.cn --juniper --reconnect-timeout 60 --servercert sha256:398c6bccf414f7d71b6dc8d59b8e3b16f6d410f305aed7e30ce911c3a4064b31

然后输入你的 info 密码即可。

分析一个我第一次见的素数测试函数

今天逛到这个连接,发现其中的第四种素数判定方法很有意思:

#include<stdio.h>
#include<math.h>
int p[8]={4,2,4,2,4,6,2,6};
int prime(int n)
{
    int i=7,j,q;
    if(n==1)return 0;
    if(n==2||n==5||n==3)return 1;
    if(n%2==0||n%3==0||n%5==0)return 0;
    q=(int)sqrt(n);
    for(;i<=q;){
        for(j=0;j<8;j++){
            if(n%i==0)return 0;
            i+=p[j];
        }
        if(n%i==0)return 0;
    }
    return 1;
}
void main()
{
    int n;
    scanf("%d",&n);
    if(prime(n))puts("Yes");
    else puts("No");
}

仔细研究发现,这里利用的是这样的原理:

  1. 判断是不是 1, 2, 3, 5 及其倍数
  2. 从 7 开始,不断考虑其是否是素数,那么,这个 p 是什么回事呢?

首先把 p 的各个元素加起来,和为 30,然后就可以发现一个规律: 7 为质数,7+2=9 不是质数,7+4=11 为质数,11+2=13 为质数,13+2=15 为合数,15+2=17 为质数,17+2=19 为质数,19+2=21 为合数,21+2=23 为质数,23+2=25 为合数,25+2=27 为合数,27+2=29 为质数,29+1=31 为质数,31+2=33 为合数,33+2=35 为合数,35+2=37 为质数。 观察以上所有的合数,都含有 2 或者 3 或者 5 的因子,而 30 又是 2,3,5 的公倍数,也就是说,后面的素数模 30 的余数不可能是上面这些合数,而剩下的素数才可能是真正的素数,于是跳过了很多素数的判断。

至于这个函数的性能如何,还需要进一步测试来进行判断。

关于 scanf 和 scanf_s 的问题

最近作为程设基础的小教员,收到很多同学的求助,关于scanfscanf_s的问题已经遇到了两次,特此写一篇博文来叙述一下这个问题。

一开始,有同学问我,

char a;
scanf("%c",&a);
为什么会报错?我说,vs 默认强制要求使用 scanf_s 函数,于是我建议这位同学把这个错误信息关掉了。嗯。经过百度,这位同学的问题解决了。

后来,又有一位同学问我,

char a;
scanf_s("%c",&a);
程序为什么会崩溃?我想了想,如果 scanf_s 和 scanf 是一样的行为,这段代码是没问题的。但 scanf_s 既然安全,必然是在字符串方面做了处理。这里的 char*勉强也算一个?网上一查,果然,应该写成scanf_s("%c",&a,1);,字符串则要写成scanf_s("%s",str,sizeof(str)),来保证缓冲区不会溢出。

但是,这样解决这个问题又面临着不同的选择:

  1. 学习scanf_sscanf的不同,把所有scanf换成scanf_s并做相应的修改。 这样当然符合了语言进化的潮流,也会让 vs 闭嘴。但是,scanf_s 只有在 C11 标准中有,而且,根据cpprefrence.com 上关于 scanf 的描述,只有在__STDC_LIB_EXT1__被定义且在#include<stdio.h>之前#define __STDC_WANT_LIB_EXT1__才能确保使用scanf_s能使用,当然在 vs 较新版本中是默认可以使用的。但是,程设基础的作业是要丢到 oj 上的,而 oj 上的编译器不一定支持这些,所以这个选项不行。
  2. 坚持用scanf,自己按照题目要求保证缓冲区不溢出,同时让 vs 闭嘴。 网上已有教程,已经讲的很全面了,大家可以根据这个教程把 vs 教训一顿。为了能在 oj 里跑,建议用里面的方法五到八。(个人最推荐在文件头添加#define _CRT_SECURE_NO_WARNINGS

以后再遇到这个问题,我就丢这个连接上来就好了咯。yeah!

一个搞笑的伸展树的 Wiki

光哲同学在群里发了这个链接,特别搞笑,特此分享: 伸展树 - 百度百科

伸展树(Spaly Tree,事实上在国内 IO 界常常被称作 Tajarn 发明的 Spaly Tree,与此同理的还有 Terap),也叫分裂树,是一种二叉排序树,它能在 O(n log n) 内完成插入、查找和删除操作。它由 Daniel Sleator 和 Robert Tajarn 发现,后者对其进行了改造。它的优势可以不断伸展枝干(一个月 2~3 次),从而使树冠散开,提高光合作用效率。木材坚硬,是重要的经济类乔木。与其他植物不同的是,伸展树可以进行出芽生殖,繁殖速度极快。

回顾昨天的酒井知识竞赛

昨天晚上,我作为蒟蒻组的一员在三教 2102 参加了酒井知识竞赛,并因此鸽掉了 TUNA 和 Lab mU 的迎新会 hhh,不过运气好拿到了二等奖的好成绩,获得 Paperang 便携打印机一台。中间遇到了好一些网络方面的知识,这对于没有记忆 OSI 模型的我无疑有巨大的难度。下面是几道比较有印象的题目:

  1. 以下哪个不是编程语言? A. J B. L C. R D. K 这题不难,R 肯定对,J 见过,K 略微有印象,选 B
  2. IPv6 链路层地址解析的协议是? A. ARP B. Neighbour Solicitation C. Neighbour Advertisement D. Neighbour Discovery 对于一个没研究过 IPv6 的人来说这只好蒙了。。。ARP 是 IPv4 时代的,ND(Neighbour Discovery 则是 IPv6 时代的新产物,把 ARP 和 ICMP 等协议的功能都包含了进来,并且有新的功能。之前样题里还出现过问 IPv6 中去掉了 Unicast,Anycast,Multicast,Broadcast 中的哪种,答案是 Broadcast。
  3. 第一个把程序错误称做 bug 的是? 选项太多忘了,答案是 Grace Hopper,因为当时一只飞蛾意外飞入了机器导致了故障,后来慢慢就流传下来了。
  4. 以下不是网络操作系统的是? A. Windows NT B. OS/2 warp C. DOS D. Netware 当时我没见过 D,于是就选了。。。然后就挂了,Netware 是 Novell 开发的系统,OS/2 warp 当然是历史悠久的系统啦,而 DOS=Disk Operating System 所以没有“网络”二字。。。晕倒
  5. 以下是用作局域网的协议是? A. TCP/IP B. IPX/SPX C. NetBEUI D. RS-232-C TCP/IP 当然不仅限于局域网,RS-232-C 是接口,当时蒙了 B 结果就对了,白白拿了 50 分哈哈哈。IPX/SPX 是 Novell 设计用在 Netware 系统上的局域网协议,NetBEUI 则是 NetBIOS 的一个历史遗留的一个“别称”。
  6. 姚期智的夫人给谁取了中文名? 当然是 Donald Ervin Knuth 啦!高德纳万岁!

华为随行 WiFi 2 mini 开箱

前段时间,我办了 4G 升级,移动送了一张副卡,有不少免费的流量,由于我的手机是 iPhone 不支持双卡,老爸就借了我他的 GlocalMe 当成 MiFi 来用,不过呢 GlocalMe 放在这里当然是大材小用了,所以我就网购了华为随行 WiFi 2 mini,把我的副卡装上一个壳放进去就可以了!把这个 MiFi 插入电脑,会弹出一个目录,里面有 Win/Mac 的驱动安装文件,打开后在网络设置里就有 HUAWEI_MOBILE 的连接了,并自动打开网络配置界面。设置一下 SSID 和密码,就能正常使用了,手机连上也很正常,手机上可以下载 HUAWEI HiLink 来配置 MiFi,挺爽的。随赠的有联通的上网卡,不过我准备在北京买个上网卡放 MiFi 里面用。

In macOS Sierra, Karabiner-Elements finally support complex modifications

In favor of this commit, Karabiner-Elements now supports the much welcomed yet long-lost feature, namely complex modifications that enable users to trigger complex keypress.

Now I can achieve this:

If I press <Enter>, then:
1. If <Enter> is pressed alone, then send <Enter>.
2. If <Enter> is pressed along with other keys, then send <Control> + Other.

By adding this code to ~/.config/karabiner/karabiner.json :

"complex_modifications": {
    "rules": [
        {
            "manipulators": [
                {
                    "description": "Change return_or_enter to left_control. (Post return_or_enter if pressed alone)",
                    "from": {
                        "key_code": "return_or_enter",
                        "modifiers": {
                            "optional": [
                                "any"
                            ]
                        }
                    },
                    "to": [
                        {
                            "key_code": "left_control"
                        }
                    ],
                    "to_if_alone": [
                        {
                            "key_code": "return_or_enter"
                        }
                    ],
                    "type": "basic"
                }
            ]
        }
    ]
},
in one of profiles.

Note: the snippet above is adopted from this example. You can explore more examples since the GUI is not updated accordingly yet.

Important: Until NOW (2017-06-15), this feature is only implemented in beta versions of Karabiner-Elements (at least 0.91.1).