Archives

四表妹二三事

说起四表妹,恐怕知道她的人无不感叹:她是一朵奇女子,女人中的奇葩。

认识四表妹是在酒桌上,她一出场就把我镇住了。话说我认识的女人也不少,可像她那样首次出场就不把我这外人放眼里的还真不多……当时一起吃饭的是我们三个男人和四表妹,按理说有女士在场我们怎么也得收敛点儿啊,讲黄段子我们还没好意思开口她先开始了,这反倒显得我们几个男人很羞涩了。吃完还不够,她表示酒还没喝够,于是我们跑到大排档接着喝,结果可想而知,她是喝得最多的。

后来和她混熟了,发现她简直是个活宝,走到哪里都是焦点。她在男人圈和女人圈都混得开,人送外号四表妹。四表妹长着一张萝莉脸,正儿八经卖起萌来也能捕获不少男人的心,可是,又不知有多少人看到这张脸然后听到她开口第一句话之后铩羽而归,因为她的发言通常以“你妹啊……”、“我草!”等口头禅开始。有时我甚至怀疑她是不是在黑道上混过,天天以“老娘”、“爷”自居。总之,一副女流氓的德行。

不过,那只是表象,四表妹其实挺有才的。上能搞计算机编程写代码,下能搞文艺摄影织毛衣!女人会的洗衣做饭打扫卫生她都会,男人会的换灯泡修马桶修电脑她也行。用她自己的话说是,“思想上的女流氓,生活上的好姑娘”。我说,我草,这不简直就是二十一世纪新女性的典范嘛!

四表妹的故事有很多,三天三夜都讲不完,何况她每天还在孜孜不倦地制造着新的传说呢。我在这里只选取三个印象深刻的讲一下:

1. 跟四表妹表白过的男生可不少,其中不知哪个倒霉蛋跟她表白时说,“我喜欢你。”她特淡定地回了一句,“其实我也挺喜欢我自己的……”然后就没有然后了!

2. 某理工男在一MM面前炫耀,拿出一张电路图比比划划,MM一脸崇拜地表情,娇嫩地说 :”你好厉害,我好崇拜你!” 恰好四表妹经过,瞄了一眼,然后淡淡地说,“你这个地方画短路了。” 理工男顿时就凌乱了……

3. 四表妹那天去昌平,出了地铁要打黑车才能到。一个开黑车的男人看到她,拿个烤串儿签子站路边儿威胁她:“你不坐车我扎你了!”这要是换了别的姑娘八成就吓蒙了,可惜他运气太差,这次遇到的对手是四表妹。四表妹打小儿是被吓大的,她马上换了一张哀伤的脸,说了一句:“来吧,我有艾滋!”然后他扭头就走开了……

这就是四表妹,萝莉的脸,纯爷们儿的心,看似女流氓,实则好姑娘!

无论怎么说,可她毕竟还是个小女人,再坚强的外表也掩饰不住苦逼的内心。别人不懂我懂。她所谓的“人生苦短”我总算明白是“人生苦逼而且短暂”的意思了!所以那天我跟她说,别人都觉得我们乐观坚强,可是他们不知道,其实哪一个乐观的人背后不都藏着一段苦逼的故事!

各种 tunnel

网络中有各种各样的 tunnel,让人眼花缭乱。这里简单分析一下各种 tunnel 的作用。

概括地讲,所谓 tunnel 就是把下一层(比如IPv4层)的包封装到上一层(比如 SSH,HTTP)或者同一层(比如IPv6层)的协议中进行传输,从而实现网络之间的穿透。很明显,这种实现有个前提,那就是,发送端和接收端必须各有一个解析这种包的程序或者内核模块才能实现正常通信。

看最简单的例子,SSH Tunnel,在贵国局域网中的大家都懂的。在本地你需要做端口映射,在中转的ssh 服务器上你需要做port forward。当然,把 HTTP 做到 SSH 之上,算是TCP over TCP了。不过还有更狠的,叫 PingTunnel,基于ICMP的。

这是应用层上的 tunnel,下面看重点,内核中的 tunnel。内核中的那几个 tunnel 可以通过 ip tunnel 命令看到:

% ip tunnel help
Usage: ip tunnel { add | change | del | show | prl | 6rd } [ NAME ]
          [ mode { ipip | gre | sit | isatap } ] [ remote ADDR ] [ local ADDR ]
          [ [i|o]seq ] [ [i|o]key KEY ] [ [i|o]csum ]
          [ prl-default ADDR ] [ prl-nodefault ADDR ] [ prl-delete ADDR ]
          [ 6rd-prefix ADDR ] [ 6rd-relay_prefix ADDR ] [ 6rd-reset ]
          [ ttl TTL ] [ tos TOS ] [ [no]pmtudisc ] [ dev PHYS_DEV ]
...

有四个:ipip、gre、sit、isatap。我们一个一个地看。

ipip 是把 IP 层封装到 IP 层的一个 tunnel,看起来似乎是浪费,实则不然。它的作用其实基本上就相当于一个基于IP层的网桥!我们知道,普通的网桥是基于mac层的,根本不需 IP,而这个 ipip 则是通过两端的路由做一个 tunnel,把两个本来不通的网络通过点对点连接起来。ipip 的源代码在内核 net/ipv4/ipip.c 中可以找到。

gre 和它类似,但它功能还要更强大一些,还支持广播,它可以取代 ipip。它的源代码在 net/ipv4/gre.c。更多介绍看这篇文章

sit 和 isatap 都是 IPv6 over IPv4 的 tunnel,它们的源代码在 net/ipv6/sit.c 中。它们之间也不同,和IPv6的地址有关,写在这里恐怕放不下了,故省略。

别慌,反过来的也有,叫 ip6_tunnel,IPv4 over IPv6。见源代码 net/ipv6/ip6_tunnel.c。

我在前面一篇文章中介绍的 tun 设备,从名字你也看得出来,它不就是 tunnel 的前三个字母嘛!vpnc 使用的就是这个 tunnel,建立一个点对点的通讯,在本地机器上有守候进程vpnc,在远端有vpn服务器。

IPSec 还用到一个叫 L2TP 的 tunnel,在内核源代码 net/l2tp 中,PPTP 是另外一个,在 drivers/net/pptp.c 中实现。这两个比较复杂,我也不熟悉。更多介绍请看内核文档 Documentation/networking/l2tp.txt

关于 tun/tap 设备

长期以来对tun和tap这对兄弟分不太清,今天下定决心研究了一下代码,总算是搞明白了。

首先它们都是从/dev/net/tun里ioctl出来的虚拟设备,一个是通过IFF_TUN,另一个是 IFF_TAP。最好的例子莫过于vpnc里面的代码了。

[c]
int tun_open(char *dev, enum if_mode_enum mode)
{
struct ifreq ifr;
int fd, err;

    if ((fd = open("/dev/net/tun", O_RDWR)) < 0) {
            error(0, errno,
                    "can't open /dev/net/tun, check that it is either device char 10 200 or (with DevFS) a symlink to ../misc/net/tun (not misc/net/tun)");
            return -1;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = ((mode == IF_MODE_TUN) ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
    if (*dev)
            strncpy(ifr.ifr_name, dev, IFNAMSIZ);

    if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
            close(fd);
            return err;
    }
    strcpy(dev, ifr.ifr_name);
    return fd;

}
[/c]

用的ioctl的命令都是同一个TUNSETIFF。

虽然是出自一个娘,但它们仍然有大的不同。tun是点对点的设备,而tap是一个普通的以太网卡设备。也就是说,tun设备其实完全不需要有物理地址的!它收到和发出的包不需要arp,也不需要有数据链路层的头!而tap设备则是有完整的物理地址和完整的以太网帧。

用一个实际的例子来验证一下:

tap0      Link encap:Ethernet  HWaddr 0E:78:39:78:E7:A7
          inet addr:192.168.1.109  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::c78:39ff:fe78:e7a7/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:21 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          inet addr:X.X.X.X  P-t-P:X.X.X.X  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1412  Metric:1
          RX packets:6 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:690 (690.0 b)  TX bytes:402 (402.0 b)

% ethtool -i tun0
driver: tun
version: 1.6
firmware-version: N/A
bus-info: tun
% ethtool -i tap0
driver: tun
version: 1.6
firmware-version: N/A
bus-info: tap

继续回来看代码。还是vpnc的代码 tunip.c,看它发送的时候做了什么处理:

[c]
static int tun_send_ip(struct sa_block s)
{
int sent, len;
uint8_t
start;

    start = s->ipsec.rx.buf;
    len   = s->ipsec.rx.buflen;

    if (opt_if_mode == IF_MODE_TAP) {

ifndef sun

            /*
             * Add ethernet header before s->ipsec.rx.buf where
             * at least ETH_HLEN bytes should be available.
             */
            struct ether_header *eth_hdr = (struct ether_header *) (s->ipsec.rx.buf - ETH_HLEN);

            memcpy(eth_hdr->ether_dhost, s->tun_hwaddr, ETH_ALEN);
            memcpy(eth_hdr->ether_shost, s->tun_hwaddr, ETH_ALEN);

            /* Use a different MAC as source */
            eth_hdr->ether_shost[0] ^= 0x80; /* toggle some visible bit */
            eth_hdr->ether_type = htons(ETHERTYPE_IP);

            start = (uint8_t *) eth_hdr;
            len += ETH_HLEN;

endif

    }

    sent = tun_write(s->tun_fd, start, len);
    if (sent != len)
            syslog(LOG_ERR, "truncated in: %d -> %dn", len, sent);
    hex_dump("Tx pkt", start, len, NULL);
    return 1;

}
[/c]

从上面的代码我们很容易看出:

1. 所谓发送就是对/dev/net/tun进行写操作。对称的,所谓接收就是读操作。
2. 如果是tap设备,发送时还要多加一个以太网的头。

我们再看内核中对应的代码是怎么处理的,在drivers/net/tun.c 中的 tun_get_user():

[c]
switch (tun->flags & TUN_TYPE_MASK) {
case TUN_TUN_DEV:
if (tun->flags & TUN_NO_PI) {
//…
}

            skb_reset_mac_header(skb);
            skb->protocol = pi.proto;
            skb->dev = tun->dev;
            break;
    case TUN_TAP_DEV:
            skb->protocol = eth_type_trans(skb, tun->dev);
            break;

[/c]

内核直接忽略了 tun 设备的以太网帧。现在,整个流程我们就已经很清楚了。

可是,上面只是用vpnc的例子。我们知道,实际中像kvm虚拟机才是tap的使用大户,我们很有必要看一下kvm是怎么使用tap设备的。为了方便起见,我们不看 qemu-kvm,因为它的代码过于复杂,我们看一个简单的kvm tools的实现。

这部分的主要代码在 virtio/net.c里面,virtio_net__tap_init()是在启动虚拟机时初始化tap设备的,然后启动两个线程分别监控tap设备的收发,代码是virtio_net_rx_thread()和virtio_net_tx_thread(),它们负责把进来的IO操作转换成对/dev/net/tun的读写。可是,IO操作是怎么进来的呢?这是关键。

顺着代码里的“针”一个个找下去,我们不难发现,IO操作是由kvm模拟出来的。首先它会把CPU指令中对应的IO操作进行转化,这部分在内核中,arch/x86/kvm/emulate.c::x86_emulate_insn():

[c]
do_io_in:
c->dst.bytes = min(c->dst.bytes, 4u);
if (!emulator_io_permited(ctxt, ops, c->src.val, c->dst.bytes)) {
emulate_gp(ctxt, 0);
goto done;
}
if (!pio_in_emulated(ctxt, ops, c->dst.bytes, c->src.val,
&c->dst.val))
goto done; / IO is needed /
break;

[/c]
pio_in_emulated() 调用的 emulator_pio_in_emulated() 会进一步触发KVM_EXIT_IO:

[c]
static int emulator_pio_in_emulated(int size, unsigned short port, void val,
unsigned int count, struct kvm_vcpu
vcpu)
{
if (vcpu->arch.pio.count)
goto data_avail;

    trace_kvm_pio(0, port, size, 1);

    vcpu->arch.pio.port = port;
    vcpu->arch.pio.in = 1;
    vcpu->arch.pio.count  = count;
    vcpu->arch.pio.size = size;

    if (!kernel_pio(vcpu, vcpu->arch.pio_data)) {
    data_avail:
            memcpy(val, vcpu->arch.pio_data, size * count);
            vcpu->arch.pio.count = 0;
            return 1;
    }

    vcpu->run->exit_reason = KVM_EXIT_IO;
    vcpu->run->io.direction = KVM_EXIT_IO_IN;
    vcpu->run->io.size = size;
    vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE;
    vcpu->run->io.count = count;
    vcpu->run->io.port = port;

    return 0;

}
[/c]

内核部分结束,转到用户空间,用户空间的 vcpu 会捕捉到这个事件,在 kvm-cpu.c::kvm_cpu__start() 中:

[c]
case KVM_EXIT_IO: {
bool ret;

                    ret = kvm__emulate_io(cpu->kvm,
                                    cpu->kvm_run->io.port,
                                    (u8 *)cpu->kvm_run +
                                    cpu->kvm_run->io.data_offset,
                                    cpu->kvm_run->io.direction,
                                    cpu->kvm_run->io.size,
                                    cpu->kvm_run->io.count);

                    if (!ret)
                            goto panic_kvm;
                    break;
            }

[/c]
kvm__emulate_io() 就会调用在 virtio/net.c 注册的 virtio_net_pci_io_in(),数据就这样流向了 tap 网卡了。

关于 loop device

我们平时挂载一个img文件一般是通过mount -o loop来挂载,而它实际上等价于下面两步:

losetup /dev/loop0 example.img
mount /dev/loop0 /home/you/dir

我们可以看 util-linux-ng 源代码中的 mount/mount.c 文件,在 loop_check() 里有这么一段代码:

[c]

    if (!*loopdev || !**loopdev)
      *loopdev = find_unused_loop_device();
    if (!*loopdev)
      return EX_SYSERR;     /* no more loop devices */
    if (verbose)
      printf(_("mount: going to use the loop device %sn"), *loopdev);

    if ((res = set_loop(*loopdev, *loopfile, offset, sizelimit,
                        opt_encryption, pfd, &loop_opts))) {

[/c]

第一步是把文件和某个空闲的loop设备相关联起来,这里是 /dev/loop0。用的是系统调用ioctl(LOOP_SET_FD),这样以来对 /dev/loop0 的读写就会转化成对 example.img 的读写了。

第二步就容易理解了,和挂载普通块设备没什么区别了。mount之所以把这两步合为一步是想让你省去手工搜索空闲的loop设备。

现在看看它是怎么工作的:调用 LOOP_SET_FD 的时候内核会把 img 对应的 struct file 关联到设备对应的 lo->lo_backing_file 上去。同时,内核启动一个内核线程来监控 /dev/loopX 的读写请求(loop_thread()),对于每一个 bio,它都会进行相应的转换,对应到对 lo->lo_backing_file 上的读写。以写为例,我们可以看do_lo_send_write():

[c]
static int do_lo_send_write(struct loop_device lo, struct bio_vec bvec,
loff_t pos, struct page *page)
{
int ret = lo_do_transfer(lo, WRITE, page, 0, bvec->bv_page,
bvec->bv_offset, bvec->bv_len, pos >> 9);
if (likely(!ret))
return __do_lo_send_write(lo->lo_backing_file,
page_address(page), bvec->bv_len,
pos);
printk(KERN_ERR “loop: Transfer error at byte offset %llu, “
“length %i.n”, (unsigned long long)pos, bvec->bv_len);
if (ret > 0)
ret = -EIO;
return ret;
}

[/c]

而__do_lo_send_write() 直接就调用 file->f_op->write() 了。

Sleep sort

4chan BBS 上一个排序的程序火了,它叫休眠排序,很有意思。

[bash]

!/bin/bash

function f() {
sleep “$1”
echo “$1”
}
while [ -n “$1” ]
do
f “$1” &
shift
done
wait

[/bash]

其实它的原理很简单,就是,要对N个整数进行排序的话,启动N个进程(线程),每个进程休眠对应的整数指定的秒数,然后再打印该数,最后你在终端上看到的肯定是排序之后的结果了……看了之后你会不会也觉得这太坑爹了?!可是,它就是能工作,而且占用CPU很少!

值得一提的是底下回复中给出的OpenMP版本(如果要尝试的话需要安装openmpi)和Perl版本。

[c]
/*

  • @file sleepsort.c
  • @brief sorts numbers
    *
  • @compile gcc sleepsort.c -fopenmp -o sleepsort
    *
  • @author Gerald Jay Sussman (Massachvsetts Institvte of Technology)
    */

include

include

include

int main(int argc, char **argv) {
int i;

omp_set_num_threads(argc);

pragma omp parallel for

for (i = 0; i < argc - 1; i++) {
long int this = atol(argv[i+1]);

sleep(this);

printf(&quot;%ldn&quot;, this);
fflush(stdout);

}

return 0;
}

[/c]
[perl]
fork and sleep $_, say, last for @ARGV; 1 while 1 -wait
[/perl]

除了不能对浮点数和负数进行排序,它还有一个缺点,那就是其中某个进程需要睡眠最大的那个数指定的时间,然后才能得出最后结果。下面有人提出了改进,我试了试,没有一个完美的。理想的情况下它应该能够对正负整数、浮点数进行排序,而且最坏也不要花太多时间,感兴趣的同学可以自己改进一下。

推荐一部科幻片

众所周知,明天要上映一部宏伟的科幻片,我郑重推荐大家拉着自己的妹子或者小伙儿去电影院看一下。主要理由有三点:

1. 该片用生动的例子向我们讲述了如何激情地组建一个派对;

2. 该片用隐喻的手法向我们展示了那时候多么好:学生不用上课,工人不用上班,搞革命还可以搞一搞革命女青年……

3. 该片励志,片中有那么多持有贵国绿卡的明星出镜,这告诉我们移民要趁早。

有不少人对该片拿下八亿票房表示质疑,我看主要是因为嫉妒,导演定八亿这个目标实在是太谦虚了!全国有七千多万高危人群,一人买一张50元的电影票都够35亿了!更何况还有那么多像我这样不明真相的群众。所以,我觉得该片拿下八亿美元不成问题!

关于网桥的IP地址

网桥的IP地址应该设置在网桥上面,而非网桥下面的从属设备。虽然内核完全允许你那么做,但那是不推荐的。

原因有两个:

一、最主要的原因是网桥本身是基于第二层,数据链路层的,网桥的作用是把从一个从属设备上接受来的数据包转发到合适的目标设备上,通过学习接受来的数据包中的目的物理地址。因此,如果网桥本身不需要和外界通信的话,任何IP地址都不需要!它只是一个透明的桥(不考虑STP),连接起了两个网络。

二、试想如果网桥和它下面从属的设备都有IP地址的话,那么路由就可能会改变发送的包的出口,干扰了网桥本身学习到的“知识”,如果正好到达一个错误的出口设备上,包就会无法到达目的地址。类似地,bonding在一起的设备也一样,IP地址应该设置在master上面。

写给未来的恋人

也许在将来的某一天,我会爱上一个姑娘,去和她认认真真地谈一场恋爱。我希望那时的她能明白我的想法,和一些我对恋爱的看法。

1. 你不必有惊人的美貌,也不一定要有过人的才华,但你一定要是一个善良而简单的姑娘,最好还要有乐观的性格和坚强的意志。

2. 不是我不愿意养你,只要是我喜欢你就肯定愿意养你,但是,你要能自己养活自己,因为这才能保证你的自由!我不希望哪一天我不在了你不能自己养活自己了,所以这很重要。

3. 如果我说我喜欢你了,那便是真喜欢了。我这个人爱憎分明,喜欢的人可以捧上天,讨厌的人可以骂得口无遮掩。我不是虚伪的人,我对那种虚伪的人深恶痛绝!

4. 既然答应要谈恋爱,那么我希望我们彼此都是认真的,真心的。希望我们都能珍惜在一起的每一天。不是我不期待什么天长地久,而是我觉得那不是一个实际的目标,能认真过好在一起的每一天就足够了!

5. 虽然说是要认真谈恋爱,可是,请不要感到有什么压力,提出分手仍然是你的自由,也是你的权利,不可以被剥夺的权利!就算我有一万个不愿意,我也坚决捍卫你的这个权利!

6. 如果你喜欢上了别人,请尽管去找,我绝对不会拦你。不是我不喜欢你,不是我舍得让你离开我,恰恰相反,正是因为我喜欢你,我才希望你幸福,既然你能找到了更好的人我有何理由去阻止你?毕竟,幸福不幸福只有你自己心里知道,我不能替你做出判断。

7. 不要仅仅因为在乎我的感受,或者道德上的压力才勉强自己和我在一起。这种施舍来的爱情我也不会接受!追求你自己的幸福对于你来说比什么都重要!我可以理解。

8. 两个人在一起要坚信彼此能给对方幸福。既然我答应和你在一起了,那么我也能保证会尽量给你幸福,不敢说有什么荣华富贵,可至少敢说,只要还有我一口吃的就一定不会饿着你!

9. 或许我不能给你什么房子和车子,但我可以给你一个温暖的家。幸福与房子车子没任何关系。这与挣钱多少无关,与愚蠢不愚蠢有关。如果房价是合理的,你想要一个房子是非常可以理解的,可现在的房价用“不合理”来形容显然都不够了。你应该是一个聪明的姑娘!

10. 既然你也认为爱情那么美好,请不要利用我对你的爱,也不要滥用我对你的信任。

11. 两个人在一起没有不吵架的,这是一个正常的磨合的过程。所以如果我们吵架了,请不要斤斤计较。

12. 最后,希望你记住,我爱你!你敢天长,我就敢地久!

亲爱的凡高

这本书读了好长时间了,都是抽旅途中的空闲时间一点一点读的,有时候是在飞机上,有时候是在火车上。

我读过关于凡高的书有几本了,觉得还是这一本最好,因为理解一个人莫过于他的日记或书信。这本书汇集了凡高写给他弟弟的信,真实记录了当时的凡高处于一个怎样的境地,把一个活生生的凡高还原给我们!

世间的幸福他没得到多少,却尝遍了人生的各种痛苦,贫穷潦倒、失恋、背叛、不被理解……可他还是那么执着地画着他的画!有时候读得潸然泪下,为他那种坚强和执着所感动……亲爱的凡高,你何必如此执着!

从中摘录一些我喜欢的句子,与你分享。

当我画一个太阳,我希望人们感觉它在以惊人的速度旋转,正在发出骇人的光热巨浪。
当我画一片麦田,我希望人们感觉到原子正朝着它们最后的成熟和绽放努力。
当我画一棵苹果树,我希望人们能感觉到苹果里面的果汁正把苹果皮撑开,果核中的种子正在为结出果实奋进。
当我画一个男人,我就要画出他滔滔的一生。
如果生活中不再有某种无限的、深刻的、真实的东西,我将不再眷恋人间……
改变了的是我的生活中困难少了一些,我的前途似乎有了一线光明,至于我的内心,我观察事物的方法,与我思考问题的方法,并没有改变。要是说有什么改变的话,那就是我现在的思考、信赖与爱,比过去的更加严肃了。
在我本应该得到友谊、强烈的与诚挚的好意的时候,却反而使我感到空虚;我感到一种可怕的灰心丧气地侵蚀着我的精神;命运似乎给好意的天性设下障碍,一股嫌恶的情绪使我透不过气来。人们喊着:“天啊,日子真不好过!”
对我说来,生活已经变得十分珍贵,我恋爱了,我很高兴。我的生活与爱情是一回事。我以为那“不,永远永远不”好像是一块冰,我把它按到我的心上,让它化掉。谁会赢得胜利呢?是寒冷的冰块,还是我温暖的心?
提奥,我想娶这个女人,我爱她,她爱我。我要尝一尝家庭生活的苦乐,以便按照我自己的经验去画它。
亲爱的弟弟,我还是很想你来这里。我时常抑制自己画油画的欲望;多亏你的赠予,我的前途已经出现了新的希望;由于你已经为我清除了那么多的障碍,我以为我是高于千百个其他人而享有特权的。许多画家由于没有钱而不能继续画画;我无法用言语向你表达,我是如何地感谢你。我学画比别人开始得晚一些,为了要弥补那一段损失的时间,我必须加倍努力工作;虽然我很热心,但如果不是由于你,我就会干不下去。我认为全年能够无忧无虑地画画,是最开心的事。
良心是一个人的指南针,虽然指针有的时候会出现偏差,虽然人们经常发觉用指南针来指导他的方向的时候会出现不正常的现象,但是人们仍然努力按照指南针所指的方向前进。