Linux Application

在窗口中显示X桌面

最近在鼓捣 awesome 的时候看到这么一个脚本,可以在 gnome (或其它任何桌面) 的窗口中显示 X 桌面,这样以来测试 awesome 的配置就很方便了!Xephyr 真是个好东西啊!

[bash]

!/bin/sh

#

test.sh

Login :

Started on Thu Sep 3 15:29:14 2009 Cedric GESTES

$Id$

#

Author(s):

- Cedric GESTES

#

This program 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 program is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

GNU General Public License for more details.

#

You should have received a copy of the GNU General Public License

along with this program; if not, write to the Free Software

Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

#

Xephyr -ac -br -noreset -screen 800x600 :1 &
sleep 1
DISPLAY=:1.0 awesome -c ~/.config/awesome/rc.lua
[/bash]

/ 和 //

看下面这个的例子:

[wangcong@cr0 ~]$ cd /

[wangcong@cr0 /]$ pwd

/

[wangcong@cr0 /]$ cd //

[wangcong@cr0 //]$ pwd

//

[wangcong@cr0 //]$ cd ///

[wangcong@cr0 /]$ pwd

/

[wangcong@cr0 /]$ cd ////

[wangcong@cr0 /]$ pwd

/

其实 POSIX 规范里有讲到,Pathname Resolution

A pathname consisting of a single slash shall resolve to the root directory of the process. A null pathname shall not be successfully resolved. A pathname that begins with two successive slashes may be interpreted in an implementation-defined manner, although more than two leading slashes shall be treated as a single slash.
大多数实现,包括 bash,都是把 // 当 / 来处理,但是仍然显示 //。但 zsh 不是,zsh 把 // 当 / 处理并显示。

ping 的 mdev 值

在运行 ping 命令的时候,里面有一项输出叫 mdev,如下所示:

~% ping www.google.pt

PING www.l.google.com (72.14.203.103) 56(84) bytes of data.

64 bytes from tx-in-f103.1e100.net (72.14.203.103): icmp_req=1 ttl=50 time=84.1 ms

64 bytes from tx-in-f103.1e100.net (72.14.203.103): icmp_req=2 ttl=50 time=83.0 ms

64 bytes from tx-in-f103.1e100.net (72.14.203.103): icmp_req=3 ttl=50 time=85.5 ms

^C

—- www.l.google.com ping statistics —-

3 packets transmitted, 3 received, 0% packet loss, time 2001ms

rtt min/avg/max/mdev = 83.019/84.227/85.506/1.016 ms

它是什么意思呢? ping 的手册中并没有提到。我们不妨看一下 ping 的源代码,见 ping_common.c:

tsum += triptime;
tsum2 += (long long)triptime * (long long)triptime

以及

tsum /= nreceived + nrepeats;
tsum2 /= nreceived + nrepeats;
tmdev = llsqrt(tsum2 - tsum * tsum);

所以我们可以得出:

mdev = SQRT(SUM(RTT*RTT) / N - (SUM(RTT)/N)^2)

也就是这个平均偏差的公式:

所以 mdev 就是 Mean Deviation 的缩写,它表示这些 ICMP 包的 RTT 偏离平均值的程度,这个值越大说明你的网速越不稳定。

关于 bash -c

我们知道 -c 的意思是 command,所以 bash -c 后面应该跟一个 command。连 bash 的 man page 中都这么说:

-c string If the -c option is present, then commands are read from string.
If there are arguments after the string, they are assigned to the positional parameters, starting with $0.
而实际上不对。我们看下面的例子:

% bash -c “echo a b c”
a b c
% bash -c echo a b c

% bash -cx “echo a”

  • echo a
    a

如果 -c 后面跟一个参数 string的话,那么第三个例子中的 -cx 应该会报错,找不到x这个命令,而实际上没有报错,也就是说,-c 和 -x 一样,后面不跟任何参数。

再看上面第二个例子中,为什么只有换行输出?手册中说,如果后面还有参数的话,那么它们被赋值给$0,$1,$2等等,也就是说,bash -c echo a b c,实际上只执行了 bash -c echo,所以只输出了换行!我们可以看更好的例子:

% bash -c ‘echo “$0 is $0, $1 is $1, $2 is $2”‘ foo bar biz
$0 is foo, $1 is bar, $2 is biz

% echo ‘echo “$0 is $0, $1 is $1, $2 is $2”‘ > /tmp/args
% chmod +x /tmp/args
% bash -c /tmp/args foo bar biz
$0 is /tmp/args, $1 is , $2 is

% bash -c ‘/tmp/args; echo $0 is $0, $1 is $1, $2 is $2’ foo bar biz
$0 is /tmp/args, $1 is , $2 is
$0 is foo, $1 is bar, $2 is biz

关于配置 vlan 设备名字

以前只知道 vlan 设备的名字都是像 bond0.77 这种格式,前两天拿到一台机器,上面的 vlan 是 vlan77,而且 ifcfg-vlan77 多了一个 PHYSDEV=bond0。这才明白原来 vlan 还可以这么配置。

可是我仍然想测一下之前的 bond0.77 这个格式的配置,因为 kdump 要解释这两个不同的格式。于是我把 ifcfg-vlan77 重命名为 ifcfg-bond0.77,并且把 DEVICE=vlan77 改为 DEVICE=bond0.77,结果重启之后出错。看了半天的 ifup 的代码也没找到问题出在哪里。忽然注意到 /proc/net/vlan 下面仍然是 vlan77,于是开始扒内核的源代码,终于在 net/8021q/vlanproc.c 里发现了问题的所在。

我忘记修改 VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 这一项!看内核源代码很了然,VLAN_PLUS_VID_NO_PAD 就是说名字格式是 “vlan” + VLANID,而且没有填充0,也就是 “vlan77” 这种格式。VLAN_NAME_TYPE=DEV_PLUS_VID_NO_PAD 才是我想要的设备名+VLANID,比如 bond0.77。如果不加“NO_PAD”的话,VLAN_NAME_TYPE=DEV_PLUS_VID 就会把设备命名成 bond0.0077了。

用 gmail 发送补丁

如果你使用 gmai l帐户,想通过 git send-email 发送补丁的话,需要加几个额外的参数,如下所示,

git send-email —smtp-server smtp.gmail.com —from USER@gmail.com —smtp-encryption=tls —smtp-user=USER@gmail.com

或者用git config进行相应的设置,

git config —global sendemail.smtpserver smtp.gmail.com
git config —global sendemail.smtpencryption tls
git config —global sendemail.smtpuser USER@gmail.com

什么是 veth

Linux container 中用到一个叫做veth的东西,这是一种新的设备,专门为 container 所建。

veth 从名字上来看是 Virtual ETHernet 的缩写,它的作用很简单,就是要把从一个 network namespace 发出的数据包转发到另一个 namespace。veth 设备是成对的,一个是 container 之中,另一个在 container 之外,即在真实机器上能看到的。

建立 veth 类型的设备可以用下面的命令:

ip link add name n1.0.1 type veth peer name n1.0

这里建立了一对veth设备,名字分别是 n1.0.1 和 n1.0。注意,这两个设备是完全对称的,也就是说,除了名字不一样,它们的作用是完全相同的,从其中一个发出就会从另一个收到。从代码(drivers/net/veth.c::veth_newlink())我们也可以看得出来:

[c]
priv = netdev_priv(dev);
priv->peer = peer;

    priv = netdev_priv(peer);
    priv->peer = dev;

[/c]

然后我们可以用下面的命令设置它们的 namespace:

ip link set n1.0 netns pidof lxc

通常 veth 会和 bridge 搭配使用(我们在lxc的配置文件中也可以看出),这样一来,从外面进来的包(比如 eth0)就可以转发到 container 之中了。

各种 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() 了。