momo zone

调核人的blog

Monthly Archives: 十二月 2012

关于现代linux动态库的搜索

最近才知道LD_LIBRARY_PATH这个传统的环境变量在引入ldconfig 后降级了,也就是说LD_LIBRARY_PATH指示的是额外的库路径,不能通过覆盖它而覆盖整个运行时的库路径。

现代的运行时so加载是这样的,运行时的/lib/ld-x.xx.so 会按照/etc/ld.so.conf中指示的路径去搜索库文件,然后才是LD_LIBRARY_PATH指示的路径。然后是/lib,最后是/usr/lib这些传统路径。

除了/etc/ld.so.conf 和传统的LD_LIBRARY_PATH能够新增额外的库路径还可以在编译时进行:

gcc test.c -g -o test.bin -Wl, -R /tmp/lib -Wl,–dynamic-linker=/lib/ld-2.11.2.so

gcc test.c -g -o test.bin -Wl, –rpath /tmp/lib -Wl,–dynamic-linker=/lib/ld-2.11.2.so

参数 -Wl,–dynamic-linker=/lib/ld-2.11.2.so 当然可以不用指定,编译好的elf文件可以用readelf -a查看:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00012 0x00012 R   0x1
      [Requesting program interpreter: /lib/ld-2.11.2.so]
  LOAD           0x000000 0x08048000 0x08048000 0x008bc 0x008bc R E 0x1000
  LOAD           0x000ef4 0x08049ef4 0x08049ef4 0x00124 0x00164 RW  0x1000
  DYNAMIC        0x000f08 0x08049f08 0x08049f08 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x0005c 0x0005c R   0x4
  GNU_EH_FRAME   0x000818 0x08048818 0x08048818 0x00024 0x00024 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x000ef4 0x08049ef4 0x08049ef4 0x0010c 0x0010c R   0x1

但这样指定之后运行时对动态库的搜索将变得复杂一些:

问题反应在elf文件里:

0xXXXXXXXX(RPATH) Library rpath: [<path>]
0xXXXXXXXX(RUNPATH) Library runpath: [<path>]

我在opensuse上测试如果指定-R或–rpath则这两个section都会添加。有其他的发行版会只设置RPATH,而只有在指定–enable-new-dtags才会加上另一个。但无论如何不会出现只有RUNPATH而没有RPATH的情况。

总结一下就是这样子:

ELF 中 RPATH ELF 中 RUNPATH LD_LIBRARY_PATH 变量 尝试加载目录的顺序
未设置 未设置 未设置 ldconfig=> /lib => /usr/lib
未设置 未设置 设置 ${LD_LIBRARY_PATH} => ldconfig=>/lib => /usr/lib
设置 未设置 未设置 ${RPATH} => ldconfig=>/lib => /usr/lib
设置 未设置 设置 ${RPATH} => ${LD_LIBRARY_PATH} =>ldconfig=> /lib => /usr/lib
设置 或 未设置 设置 设置 ${LD_LIBRARY_PATH} => ${RUN_PATH} =>  ldconfig=>/lib => /usr/lib
设置 或 未设置 设置 未设置 ${RUN_PATH} => ldconfig=> /lib => /usr/lib

注意,这里有个非常怪异的设计,如果设置了RUN_PATH则完全无视RPATH,而且也不先看自己RUN_PATH,而是去看LD_LIBRARY_PATH。

另外参考了一下Qt项目的文档,这里有更详细和完备的描述

Unless loading object has RUNPATH: #如果不含有RUNPATH
    RPATH of the loading object,
        then the RPATH of its loader (unless it has a RUNPATH), ...,  #按照调用链向上递归查找RPATH直到可执行程序
        until the end of the chain, which is either the executable     #遇到RUNPATH就退出循环
        or an object loaded by dlopen
    Unless executable has RUNPATH:
        RPATH of the executable
LD_LIBRARY_PATH
RUNPATH of the loading object
ld.so.cache
default dirs
Advertisements

[转]linux kernel 网络协议栈之GRO(Generic receive offload)

GRO(Generic receive offload)在内核2.6.29之后合并进去的,作者是一个华裔Herbert Xu ,GRO的简介可以看这里:

http://lwn.net/Articles/358910/

先来描述一下GRO的作用,GRO是针对网络接受包的处理的,并且只是针对NAPI类型的驱动,因此如果要支持GRO,不仅要内核支持,而且驱动也必须调用相应的借口,用ethtool -K gro on来设置,如果报错就说明网卡驱动本身就不支持GRO。

GRO类似tso,可是tso只支持发送数据包,这样你tcp层大的段会在网卡被切包,然后再传递给对端,而如果没有gro,则小的段会被一个个送到协议栈,有了gro之后,就会在接收端做一个反向的操作(想对于tso).也就是将tso切好的数据包组合成大包再传递给协议栈。

如果实现了GRO支持的驱动是这样子处理数据的,在NAPI的回调poll方法中读取数据包,然后调用GRO的接口napi_gro_receive或者napi_gro_frags来将数据包feed进协议栈。而具体GRO的工作就是在这两个函数中进行的,他们最终都会调用__napi_gro_receive。下面就是napi_gro_receive,它最终会调用napi_skb_finish以及__napi_gro_receive。

1
2
3
4
5
6
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
    skb_gro_reset_offset(skb);
    return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}

然后GRO什么时候会将数据feed进协议栈呢,这里会有两个退出点,一个是在napi_skb_finish里,他会通过判断__napi_gro_receive的返回值,来决定是需要将数据包立即feed进协议栈还是保存起来,还有一个点是当napi的循环执行完毕时,也就是执行napi_complete的时候,先来看napi_skb_finish,napi_complete我们后面会详细介绍。

在NAPI驱动中,直接调用netif_receive_skb会将数据feed 进协议栈,因此这里如果返回值是NORMAL,则直接调用netif_receive_skb来将数据送进协议栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
    switch (ret) {
    case GRO_NORMAL:
//将数据包送进协议栈
        if (netif_receive_skb(skb))
            ret = GRO_DROP;
        break;
//表示skb可以被free,因为gro已经将skb合并并保存起来。
    case GRO_DROP:
    case GRO_MERGED_FREE:
//free skb
        kfree_skb(skb);
        break;
//这个表示当前数据已经被gro保存起来,但是并没有进行合并,因此skb还需要保存。
    case GRO_HELD:
    case GRO_MERGED:
        break;
    }
    return ret;
}

GRO的主要思想就是,组合一些类似的数据包(基于一些数据域,后面会介绍到)为一个大的数据包(一个skb),然后feed给协议栈,这里主要是利用Scatter-gather IO,也就是skb的struct skb_shared_info域(我前面的blog讲述ip分片的时候有详细介绍这个域)来合并数据包。

在每个NAPI的实例都会包括一个域叫gro_list,保存了我们积攒的数据包(将要被merge的).然后每次进来的skb都会在这个链表里面进行查找,看是否需要merge。而gro_count表示当前的gro_list中的skb的个数。

1
2
3
4
5
6
7
8
9
struct napi_struct {
................................................
//个数
    unsigned int        gro_count;
......................................
//积攒的数据包
    struct sk_buff      *gro_list;
    struct sk_buff      *skb;
};

紧接着是gro最核心的一个数据结构napi_gro_cb,它是保存在skb的cb域中,它保存了gro要使用到的一些上下文,这里每个域kernel的注释都比较清楚。到后面我们会看到这些域的具体用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct napi_gro_cb {
    /* Virtual address of skb_shinfo(skb)->frags[0].page + offset. */
    void *frag0;
    /* Length of frag0. */
    unsigned int frag0_len;
    /* This indicates where we are processing relative to skb->data. */
    int data_offset;
    /* This is non-zero if the packet may be of the same flow. */
    int same_flow;
    /* This is non-zero if the packet cannot be merged with the new skb. */
    int flush;
    /* Number of segments aggregated. */
    int count;
    /* Free the skb? */
    int free;
};

每一层协议都实现了自己的gro回调函数,gro_receive和gro_complete,gro系统会根据协议来调用对应回调函数,其中gro_receive是将输入skb尽量合并到我们gro_list中。而gro_complete则是当我们需要提交gro合并的数据包到协议栈时被调用的。

下面就是ip层和tcp层对应的回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const struct net_protocol tcp_protocol = {
    .handler =  tcp_v4_rcv,
    .err_handler =  tcp_v4_err,
    .gso_send_check = tcp_v4_gso_send_check,
    .gso_segment =  tcp_tso_segment,
//gso回调
    .gro_receive =  tcp4_gro_receive,
    .gro_complete = tcp4_gro_complete,
    .no_policy =    1,
    .netns_ok = 1,
};
static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
    .gso_send_check = inet_gso_send_check,
    .gso_segment = inet_gso_segment,
//gso回调
    .gro_receive = inet_gro_receive,
    .gro_complete = inet_gro_complete,
};

gro的入口函数是napi_gro_receive,它的实现很简单,就是将skb包含的gro上下文reset,然后调用__napi_gro_receive,最终通过napi_skb_finis来判断是否需要讲数据包feed进协议栈。

1
2
3
4
5
6
7
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
//reset gro对应的域
    skb_gro_reset_offset(skb);
    return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}

napi_skb_finish一开始已经介绍过了,这个函数主要是通过判断传递进来的ret(__napi_gro_receive的返回值),来决定是否需要feed数据进协议栈。它的第二个参数是前面处理过的skb。

这里再来看下skb_gro_reset_offset,首先要知道一种情况,那就是skb本身不包含数据(包括头也没有),而所有的数据都保存在skb_shared_info中(支持S/G的网卡有可能会这么做).此时我们如果想要合并的话,就需要将包头这些信息取出来,也就是从skb_shared_info的frags[0]中去的,在 skb_gro_reset_offset中就有做这个事情,而这里就会把头的信息保存到napi_gro_cb 的frags0中。并且此时frags必然不会在high mem,要么是线性区,要么是dma(S/G io)。 来看skb_gro_reset_offset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void skb_gro_reset_offset(struct sk_buff *skb)
{
    NAPI_GRO_CB(skb)->data_offset = 0;
    NAPI_GRO_CB(skb)->frag0 = NULL;
    NAPI_GRO_CB(skb)->frag0_len = 0;
//如果mac_header和skb->tail相等并且地址不在高端内存,则说明包头保存在skb_shinfo中,所以我们需要从frags中取得对应的数据包
    if (skb->mac_header == skb->tail &&
        !PageHighMem(skb_shinfo(skb)->frags[0].page)) {
//可以看到frag0保存的就是对应的skb的frags的第一个元素的地址
        NAPI_GRO_CB(skb)->frag0 =
            page_address(skb_shinfo(skb)->frags[0].page) +
            skb_shinfo(skb)->frags[0].page_offset;
//然后保存对应的大小。
        NAPI_GRO_CB(skb)->frag0_len = skb_shinfo(skb)->frags[0].size;
    }
}

接下来就是__napi_gro_receive,它主要是遍历gro_list,然后给same_flow赋值,这里要注意,same_flow是一个标记,表示某个skb是否有可能会和当前要处理的skb是相同的流,而这里的相同会在每层都进行判断,也就是在设备层,ip层,tcp层都会判断,这里就是设备层的判断了。这里的判断很简单,有2个条件:
1 设备是否相同
2 mac的头必须相等

如果上面两个条件都满足,则说明两个skb有可能是相同的flow,所以设置same_flow,以便与我们后面合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static gro_result_t
__napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
    struct sk_buff *p;
    if (netpoll_rx_on(skb))
        return GRO_NORMAL;
//遍历gro_list,然后判断是否有可能两个skb 相似。
    for (p = napi->gro_list; p; p = p->next) {
//给same_flow赋值
        NAPI_GRO_CB(p)->same_flow =
            (p->dev == skb->dev) &&
            !compare_ether_header(skb_mac_header(p),
                          skb_gro_mac_header(skb));
        NAPI_GRO_CB(p)->flush = 0;
    }
//调用dev_gro_receiv
    return dev_gro_receive(napi, skb);
}

接下来来看dev_gro_receive,这个函数我们分做两部分来看,第一部分是正常处理部分,第二部份是处理frag0的部分。

来看如何判断是否支持GRO,这里每个设备的features会在驱动初始化的时候被初始化,然后如果支持GRO,则会包括NETIF_F_GRO。 还有要注意的就是,gro不支持切片的ip包,因为ip切片的组包在内核的ip会做一遍,因此这里gro如果合并的话,没有多大意义,而且还增加复杂度。

在dev_gro_receive中会遍历对应的ptype(也就是协议的类链表,以前的blog有详细介绍),然后调用对应的回调函数,一般来说这里会调用文章开始说的ip_packet_type,也就是 inet_gro_receive。

而 inet_gro_receive的返回值表示我们需要立刻feed 进协议栈的数据包,如果为空,则说明不需要feed数据包进协议栈。后面会分析到这里他的详细算法。

而如果当inet_gro_receive正确返回后,如果same_flow没有被设置,则说明gro list中不存在能和当前的skb合并的项,因此此时需要将skb插入到gro list中。这个时候的返回值就是HELD。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
    struct sk_buff **pp = NULL;
    struct packet_type *ptype;
    __be16 type = skb->protocol;
    struct list_head *head = &ptype_base[ntohs(type) & PTYPE_HASH_MASK];
    int same_flow;
    int mac_len;
    enum gro_result ret;
//判断是否支持gro
    if (!(skb->dev->features & NETIF_F_GRO))
        goto normal;
//判断是否为切片的ip包
    if (skb_is_gso(skb) || skb_has_frags(skb))
        goto normal;
    rcu_read_lock();
//开始遍历对应的协议表
    list_for_each_entry_rcu(ptype, head, list) {
        if (ptype->type != type || ptype->dev || !ptype->gro_receive)
            continue;
        skb_set_network_header(skb, skb_gro_offset(skb));
        mac_len = skb->network_header - skb->mac_header;
        skb->mac_len = mac_len;
        NAPI_GRO_CB(skb)->same_flow = 0;
        NAPI_GRO_CB(skb)->flush = 0;
        NAPI_GRO_CB(skb)->free = 0;
//调用对应的gro接收函数
        pp = ptype->gro_receive(&napi->gro_list, skb);
        break;
    }
    rcu_read_unlock();
//如果是没有实现gro的协议则也直接调到normal处理
    if (&ptype->list == head)
        goto normal;
//到达这里,则说明gro_receive已经调用过了,因此进行后续的处理
//得到same_flow
    same_flow = NAPI_GRO_CB(skb)->same_flow;
//看是否有需要free对应的skb
    ret = NAPI_GRO_CB(skb)->free ? GRO_MERGED_FREE : GRO_MERGED;
//如果返回值pp部位空,则说明pp需要马上被feed进协议栈
    if (pp) {
        struct sk_buff *nskb = *pp;
        *pp = nskb->next;
        nskb->next = NULL;
//调用napi_gro_complete 将pp刷进协议栈
        napi_gro_complete(nskb);
        napi->gro_count--;
    }
//如果same_flow有设置,则说明skb已经被正确的合并,因此直接返回。
    if (same_flow)
        goto ok;
//查看是否有设置flush和gro list的个数是否已经超过限制
    if (NAPI_GRO_CB(skb)->flush || napi->gro_count >= MAX_GRO_SKBS)
        goto normal;
//到达这里说明skb对应gro list来说是一个新的skb,也就是说当前的gro list并不存在可以和skb合并的数据包,因此此时将这个skb插入到gro_list的头。
    napi->gro_count++;
    NAPI_GRO_CB(skb)->count = 1;
    skb_shinfo(skb)->gso_size = skb_gro_len(skb);
//将skb插入到gro list的头
    skb->next = napi->gro_list;
    napi->gro_list = skb;
//设置返回值
    ret = GRO_HELD;

然后就是处理frag0的部分,以及不支持gro的处理。

这里要需要对skb_shinfo的结构比较了解,我在以前的blog对这个有很详细的介绍,可以去查阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pull:
//是否需要拷贝头
    if (skb_headlen(skb) < skb_gro_offset(skb)) {
//得到对应的头的大小
        int grow = skb_gro_offset(skb) - skb_headlen(skb);
        BUG_ON(skb->end - skb->tail < grow);
//开始拷贝
        memcpy(skb_tail_pointer(skb), NAPI_GRO_CB(skb)->frag0, grow);
        skb->tail += grow;
        skb->data_len -= grow;
//更新对应的frags[0]
        skb_shinfo(skb)->frags[0].page_offset += grow;
        skb_shinfo(skb)->frags[0].size -= grow;
//如果size为0了,则说明第一个页全部包含头,因此需要将后面的页全部移动到前面。
        if (unlikely(!skb_shinfo(skb)->frags[0].size)) {
            put_page(skb_shinfo(skb)->frags[0].page);
//开始移动。
            memmove(skb_shinfo(skb)->frags,
                skb_shinfo(skb)->frags + 1,
                --skb_shinfo(skb)->nr_frags * sizeof(skb_frag_t));
        }
    }
ok:
    return ret;
normal:
    ret = GRO_NORMAL;
    goto pull;
}

接下来就是inet_gro_receive,这个函数是ip层的gro receive回调函数,函数很简单,首先取得ip头,然后判断是否需要从frag复制数据,如果需要则复制数据

1
2
3
4
5
6
7
8
9
10
11
12
//得到偏移
        off = skb_gro_offset(skb);
//得到头的整个长度(mac+ip)
    hlen = off + sizeof(*iph);
//得到ip头
    iph = skb_gro_header_fast(skb, off);
//是否需要复制
    if (skb_gro_header_hard(skb, hlen)) {
        iph = skb_gro_header_slow(skb, hlen, off);
        if (unlikely(!iph))
            goto out;
    }

然后就是一些校验工作,比如协议是否支持gro_reveive,ip头是否合法等等

1
2
3
4
5
6
7
8
9
10
11
12
13
    proto = iph->protocol & (MAX_INET_PROTOS - 1);
    rcu_read_lock();
    ops = rcu_dereference(inet_protos[proto]);
//是否支持gro
    if (!ops || !ops->gro_receive)
        goto out_unlock;
//ip头是否合法
    if (*(u8 *)iph != 0x45)
        goto out_unlock;
//ip头教研
    if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
        goto out_unlock;

然后就是核心的处理部分,它会遍历整个gro_list,然后进行same_flow和是否需要flush的判断。

这里ip层设置same_flow是根据下面的规则的:
1 4层的协议必须相同
2 tos域必须相同
3 源,目的地址必须相同

如果3个条件一个不满足,则会设置same_flow为0。
这里还有一个就是判断是否需要flush 对应的skb到协议栈,这里的判断条件是这样子的。
1 ip包的ttl不一样
2 ip包的id顺序不对
3 如果是切片包

如果上面两个条件某一个满足,则说明skb需要被flush出gro。

不过这里要注意只有两个数据包是same flow的情况下,才会进行flush判断。原因很简单,都不是有可能进行merge的包,自然没必要进行flush了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//取出id
    id = ntohl(*(__be32 *)&iph->id);
//判断是否需要切片
    flush = (u16)((ntohl(*(__be32 *)iph) ^ skb_gro_len(skb)) | (id ^ IP_DF));
    id >>= 16;
//开始遍历gro list
    for (p = *head; p; p = p->next) {
        struct iphdr *iph2;
//如果上一层已经不可能same flow则直接继续下一个
        if (!NAPI_GRO_CB(p)->same_flow)
            continue;
//取出ip头
        iph2 = ip_hdr(p);
//开始same flow的判断
        if ((iph->protocol ^ iph2->protocol) |
            (iph->tos ^ iph2->tos) |
            ((__force u32)iph->saddr ^ (__force u32)iph2->saddr) |
            ((__force u32)iph->daddr ^ (__force u32)iph2->daddr)) {
            NAPI_GRO_CB(p)->same_flow = 0;
            continue;
        }
//开始flush的判断。这里注意如果不是same_flow的话,就没必要进行flush的判断。
        /* All fields must match except length and checksum. */
        NAPI_GRO_CB(p)->flush |=
            (iph->ttl ^ iph2->ttl) |
            ((u16)(ntohs(iph2->id) + NAPI_GRO_CB(p)->count) ^ id);
        NAPI_GRO_CB(p)->flush |= flush;
    }
    NAPI_GRO_CB(skb)->flush |= flush;
//pull ip头进gro,这里更新data_offset
    skb_gro_pull(skb, sizeof(*iph));
//设置传输层的头的位置
    skb_set_transport_header(skb, skb_gro_offset(skb));
//调用传输层的reveive方法。
    pp = ops->gro_receive(head, skb);
out_unlock:
    rcu_read_unlock();
out:
    NAPI_GRO_CB(skb)->flush |= flush;
}

然后就是tcp层的gro方法,它的主要实现函数是tcp_gro_receive,他的流程和inet_gro_receiv类似,就是取得tcp的头,然后对gro list进行遍历,最终会调用合并方法。

首先来看gro list遍历的部分,它对same flow的要求就是source必须相同,如果不同则设置same flow为0.如果相同则跳到found部分,进行合并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//遍历gro list
    for (; (p = *head); head = &p->next) {
//如果ip层已经不可能same flow则直接进行下一次匹配
        if (!NAPI_GRO_CB(p)->same_flow)
            continue;
        th2 = tcp_hdr(p);
//判断源地址
        if (*(u32 *)&th->source ^ *(u32 *)&th2->source) {
            NAPI_GRO_CB(p)->same_flow = 0;
            continue;
        }
        goto found;
    }

接下来就是当找到能够合并的skb的时候的处理,这里首先来看flush的设置,这里会有4个条件:
1 拥塞状态被设置(TCP_FLAG_CWR).
2 tcp的ack的序列号不匹配 (这是肯定的,因为它只是对tso或者说gso进行反向操作)
3 skb的flag和从gro list中查找到要合并skb的flag 如果他们中的不同位 不包括TCP_FLAG_CWR | TCP_FLAG_FIN | TCP_FLAG_PSH,这三个任意一个域。
4 tcp的option域不同

如果上面4个条件有一个满足,则会设置flush为1,也就是找到的这个skb(gro list中)必须被刷出到协议栈。

这里谈一下flags域的设置问题首先如果当前的skb设置了cwr,也就是发生了拥塞,那么自然前面被缓存的数据包需要马上被刷到协议栈,以便与tcp的拥塞控制马上进行。

而FIN和PSH这两个flag自然不需要一致,因为这两个和其他的不是互斥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
found:
    flush = NAPI_GRO_CB(p)->flush;
//如果设置拥塞,则肯定需要刷出skb到协议栈
    flush |= (__force int)(flags & TCP_FLAG_CWR);
//如果相差的域是除了这3个中的,就需要flush出skb
    flush |= (__force int)((flags ^ tcp_flag_word(th2)) &
          ~(TCP_FLAG_CWR | TCP_FLAG_FIN | TCP_FLAG_PSH));
//ack的序列号必须一致
    flush |= (__force int)(th->ack_seq ^ th2->ack_seq);
//tcp的option头必须一致
    for (i = sizeof(*th); i < thlen; i += 4)
        flush |= *(u32 *)((u8 *)th + i) ^
             *(u32 *)((u8 *)th2 + i);
    mss = skb_shinfo(p)->gso_size;
    flush |= (len - 1) >= mss;
    flush |= (ntohl(th2->seq) + skb_gro_len(p)) ^ ntohl(th->seq);
//如果flush有设置则不会调用 skb_gro_receive,也就是不需要进行合并,否则调用skb_gro_receive进行数据包合并
    if (flush || skb_gro_receive(head, skb)) {
        mss = 1;
        goto out_check_final;
    }
    p = *head;
    th2 = tcp_hdr(p);
//更新p的头。到达这里说明合并完毕,因此需要更新合并完的新包的头。
    tcp_flag_word(th2) |= flags & (TCP_FLAG_FIN | TCP_FLAG_PSH);

从上面我们可以看到如果tcp的包被设置了一些特殊的flag比如PSH,SYN这类的就必须马上把数据包刷出到协议栈。

下面就是最终的一些flags判断,比如第一个数据包进来都会到这里来判断。

1
2
3
4
5
6
7
8
9
10
11
12
out_check_final:
    flush = len < mss;
//根据flag得到flush
    flush |= (__force int)(flags & (TCP_FLAG_URG | TCP_FLAG_PSH |
                    TCP_FLAG_RST | TCP_FLAG_SYN |
                    TCP_FLAG_FIN));
    if (p && (!NAPI_GRO_CB(skb)->same_flow || flush))
        pp = head;
out:
    NAPI_GRO_CB(skb)->flush |= flush;

这里要知道每次我们只会刷出gro list中的一个skb节点,这是因为每次进来的数据包我们也只会匹配一个。因此如果遇到需要刷出的数据包,会在dev_gro_receive中先刷出gro list中的,然后再将当前的skb feed进协议栈。

最后就是gro最核心的一个函数skb_gro_receive,它的主要工作就是合并,它有2个参数,第一个是gro list中和当前处理的skb是same flow的skb,第二个就是我们需要合并的skb。

这里要注意就是farg_list,其实gro对待skb_shared_info和ip层切片,组包很类似,就是frags放Scatter-Gather I/O的数据包,frag_list放线性数据。这里gro 也是这样的,如果过来的skb支持Scatter-Gather I/O并且数据是只放在frags中,则会合并frags,如果过来的skb不支持Scatter-Gather I/O(数据头还是保存在skb中),则合并很简单,就是新建一个skb然后拷贝当前的skb,并将gro list中的skb直接挂载到farg_list。

先来看支持Scatter-Gather I/O的处理部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//一些需要用到的变量
    struct sk_buff *p = *head;
    struct sk_buff *nskb;
//当前的skb的 share_ino
    struct skb_shared_info *skbinfo = skb_shinfo(skb);
//当前的gro list中的要合并的skb的share_info
    struct skb_shared_info *pinfo = skb_shinfo(p);
    unsigned int headroom;
    unsigned int len = skb_gro_len(skb);
    unsigned int offset = skb_gro_offset(skb);
    unsigned int headlen = skb_headlen(skb);
//如果有frag_list的话,则直接去非Scatter-Gather I/O部分处理,也就是合并到frag_list.
    if (pinfo->frag_list)
        goto merge;
    else if (headlen <= offset) {
//支持Scatter-Gather I/O的处理
        skb_frag_t *frag;
        skb_frag_t *frag2;
        int i = skbinfo->nr_frags;
//这里遍历是从后向前。
        int nr_frags = pinfo->nr_frags + i;
        offset -= headlen;
        if (nr_frags > MAX_SKB_FRAGS)
            return -E2BIG;
//设置pinfo的frags的大小,可以看到就是加上skb的frags的大小
        pinfo->nr_frags = nr_frags;
        skbinfo->nr_frags = 0;
        frag = pinfo->frags + nr_frags;
        frag2 = skbinfo->frags + i;
//遍历赋值,其实就是地址赋值,这里就是将skb的frag加到pinfo的frgas后面。
        do {
            *--frag = *--frag2;
        } while (--i);
//更改page_offet的值
        frag->page_offset += offset;
//修改size大小
        frag->size -= offset;
//更新skb的相关值
        skb->truesize -= skb->data_len;
        skb->len -= skb->data_len;
        skb->data_len = 0;
        NAPI_GRO_CB(skb)->free = 1;
//最终完成
        goto done;
    } else if (skb_gro_len(p) != pinfo->gso_size)
        return -E2BIG;

这里gro list中的要被合并的skb我们叫做skb_s.

接下来就是不支持支持Scatter-Gather I/O(skb的头放在skb中)的处理。这里处理也比较简单,就是复制一个新的nskb,然后它的头和skb_s一样,然后将skb_s挂载到nskb的frag_list上,并且把新建的nskb挂在到gro list中,代替skb_s的位置,而当前的skb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    headroom = skb_headroom(p);
    nskb = alloc_skb(headroom + skb_gro_offset(p), GFP_ATOMIC);
    if (unlikely(!nskb))
        return -ENOMEM;
//复制头
    __copy_skb_header(nskb, p);
    nskb->mac_len = p->mac_len;
    skb_reserve(nskb, headroom);
    __skb_put(nskb, skb_gro_offset(p));
//设置各层的头
    skb_set_mac_header(nskb, skb_mac_header(p) - p->data);
    skb_set_network_header(nskb, skb_network_offset(p));
    skb_set_transport_header(nskb, skb_transport_offset(p));
    __skb_pull(p, skb_gro_offset(p));
//复制数据
    memcpy(skb_mac_header(nskb), skb_mac_header(p),
           p->data - skb_mac_header(p));
//对应的gro 域的赋值
    *NAPI_GRO_CB(nskb) = *NAPI_GRO_CB(p);
//可以看到frag_list被赋值
    skb_shinfo(nskb)->frag_list = p;
    skb_shinfo(nskb)->gso_size = pinfo->gso_size;
    pinfo->gso_size = 0;
    skb_header_release(p);
    nskb->prev = p;
//更新新的skb的数据段
    nskb->data_len += p->len;
    nskb->truesize += p->len;
    nskb->len += p->len;
//将新的skb插入到gro list中
    *head = nskb;
    nskb->next = p->next;
    p->next = NULL;
    p = nskb;
merge:
    if (offset > headlen) {
        skbinfo->frags[0].page_offset += offset - headlen;
        skbinfo->frags[0].size -= offset - headlen;
        offset = headlen;
    }
    __skb_pull(skb, offset);
//将skb插入新的skb的(或者老的skb,当frag list本身存在)fraglist
    p->prev->next = skb;
    p->prev = skb;
    skb_header_release(skb);

 

[转]如何改造 Linux 虚拟终端显示文字

转一篇ibm的文章,读完之后真正了解了tty,console,fb等等之间的区别和联系,浅显易懂。

 

蔡 万钊, 自由职业, 无

 

简介: 本文就 CJKTTY 补丁如何让 linux 虚拟终端显示汉字的原理进行了讨论,为此介绍了 Linux 虚拟终端和其依赖的硬件的工作原理。过程中我们分析了 Linux 字符终端的不足之处,并向读者介绍前沿的 Wayland system compositor 是什么以及为什么 需要它。

CJKTTY 补丁是什么,为什么我写了它

当你不使用 X 的时候,打开电脑,你就在使用虚拟终端。这么多年来它工作的很好,直到它来到了中国。包含中文字符的文件名无法正确显示,中文文档无法阅读。当然可以使用 X , 但是我为什么不能让终端也能显示汉字呢?如果在 X 下我能让屏幕显示汉字,终端下一定也能。为此我开始了 internet 上的搜寻。 我找到了 fbterm,这是个可以利用 /dev/fb0 实现的终端模拟器,和 XTERM 一样,只不过 XTERM 利用的 X 绘制文字,而 fbterm 直接写入 /dev/fb0。

/dev/fb0 是什么?

帧缓冲区设备。帧缓冲区是一块存储区域(内存或者显存或者其他的输出设备的存储空间),内核将其抽象为一个设备。通过访问该设备就能访问帧缓冲区。帧缓冲区的内容既是屏幕映像。由输出设备不停的扫描帧缓冲区生成显示设备的控制信号。

然而我似乎总是忘记登录后开启 fbterm,对我来说,等看到乱码的时候突然想起没有开启 fbterm 并不是那么愉快的经历。这只是其中一个缺点。fbterm 占用了帧缓冲区设备,导致 w3m 这类使用帧缓存绘制图像的终端 www 浏览器不能正常工作。许多依赖帧缓冲区设备的终端程序都不能被 fbterm 良好的兼容。 于是我继续寻找,找到了 youbest 写的中文补丁。 我有个喜欢使用最新内核的习惯,当内核升级导致 youbest 的补丁再也不能使用的时候,我开始到 youbest 发布补丁的页面留言,希望 youbest 百忙中能修改一下他的补丁。

Youbest 似乎很忙,在新内核的诱惑下,我决定自己在修改。那个时候我才真正的开始看内核的代码。终于了解了虚拟终端的工作原理后,我开始修改内核。由于内核内部结构变动导致 youbest 的补丁无法应用,我几乎是从头开始了开发而不是简单的将无法应用的部分进行修整。唯一得到保留的就是 youbest 补丁中的点阵字库。我将其命名为 CJKTTY , 取能显示 CJK 的 TTY 之意。

从那里获得中文显示补丁呢?

我将补丁托管在了 http://repo.or.cz/w/linux-2.6/cjktty.git,依据你使用的内核版本,签出内核版本 -utf8 分支就可以。

控制台是如何显示文字呢?

那么,为了能在控制台下显示出汉字到底需要做什么样的修改么?在开始前,我想先介绍一些名字,并介绍一些控制台在硬件上是如何进行文字显示的。 首先我解释一下几个名词,知道的人可以到这里开始阅读。

UNICODE

为每一个字符分配全球唯一的一个数字,但是并没有规定这个数字的表示方法。数字的表示方法由 UTF 规范规定。UTF-16 使用 2 个字节表示一个 UNICODE 数字,但是对于 >=216的数字使用 4 字节来表达。UTF-8 则对于 <127 的数字采取单字节表示,大于 127 的数字要根据其大小选择 2~6 个字节进行表示。UNICODE 在程序内部则简单的使用 unsigned long 即可表示一个字符。

GLYPH

GLYPH 指的是字体里的字形。字符总是要在特定的字体下表示的,该表示就是字形。比如一个只包含 26 个大小写字母的字体,只包含了 52 个字形,如果该字体是先大写后小写排列的,那么数字 0 就表示字形 ‘A’ , 数字 1 就表示字形 ‘B’。UNICODE 或者 ASCII 到 GLYPH 的映射是由一个称作 CMAP 的映射表做的。如果字体里字符就是按照 UNICODE 排列的,则其 CMAP 就是 UNICODE CMAP。同理也有 ASCII CMAP。 VGA 自带字体没有提供 CMAP,操作系统假定它的 CMAP 是 ASCII CMAP。事实上也是如此。

TTY

内核为终端提供的接口,对应用程序而言就是 TTY 设备。通常是使用 stdin stdout 来访问。TTY 提供各种 IOCTL 用来设置终端的模式。TTY 也提供了用户控制程序的方法,比如 Ctrl-C 终止当前程序。 TTY 可以是显示器 + 键盘构成的控制台,也可以是串口(可以通过猫链接到电话线上),可以通过 pts 模拟。XTERM 即利用 pts 为里面运行的程序提供的模拟的终端 , 对应的设备文件 /dev/pts/* 由模拟终端程序动态创建。

控制台 (CONSOLE)

控制台特指由显示器 + 键盘构成的终端。其中显示器由显卡控制,而且当前 VGA 兼容显卡有两种模式,文字模式和图形模式。Linux 即可以使用文字模式也可以使用图形模式。

控制台对于程序是无法访问的,程序只能通过虚拟终端使用控制台

虚拟终端 (VT)

如果你的电脑只有一个终端,那将是多么乏味。一个需要长时间执行的任务就能导致你什么也做不了,Linux 的多任务机制的好处荡然无存。所以,你需要更多的终端。Linux 内核使用复用机制,将一个控制台复用为多个终端 (63 个,/dev/tty1 到 dev/tty63)。 按键 Alt+F1-F12 ( 如果当前在 X 中,需要再按下 Ctrl 键 ) 能在 12 个终端中进行切换。事实上你拥有 63 个终端,键盘只能切换其中的 12 个,其他的终端你可以通过 chvt 命令进行切换。

当前拥有显示器和键盘的虚拟终端被称为活动终端或者当前终端。

TTY、控制台和虚拟终端有啥区别和联系?

当你按下 Ctrl-C 的时候,当前执行的程序会被终止。因为 Linux 发送了 SIGTERM 信号给此终端的前台程序。该信号并不是由 Shell 产生,而是内核。不论是在虚拟终端下,还是在 X 里的终端模拟器里,这个功能都是一样的。终端的一大功能就是进行任务控制,另一个功能是输入输出。输入输出模式下,还可以选择行编辑模式,回显模式,设置终端速率等等。不管你使用的是何种终端,这些功能都是存在的,因为他们都是一个类型的设备。内核将他们抽象为 TTY 设备。也就是说,应用程序都是在和 TTY 这个抽象层打交道,而不是和具体的设备打交道。 能作为 TTY 的设备除了控制台外,还有串口。将两台电脑的串口连接起来,其中一台电脑为串口打开登录程序(执行 /sbin/agetty ttyS0 38400),另一台就能通过可以进行串口通信的程序 ( 比如 putty、minicom) 登录对方。 控制台可以作为 TTY 设备,但是一台电脑一般只有一个屏幕,也就使用一个控制台,所以 Linux 在控制台和 TTY 之间加了一层虚拟终端。由虚拟终端将控制台复用,这样就可以使用多个终端而不是只有一个了。多个虚拟终端设备合作使用一个控制台。 除了串口和虚拟终端,这些都是在内核实现的 TTY 设备,内核还提供了一个叫 PTY 的为终端设备,XTERM 之类的程序利用 PTY 提供的功能可以在程序里实现 TTY 的功能。 那么,虚拟终端就是利用控制台复用出了多个 TTY 。TTY 逻辑由 TTY 子系统完成,复用逻辑由虚拟终端实现,而具体的显示则交给控制台完成。如果说这是一个观察者模型的话,控制台就是观察者,它将虚拟终端的内容呈现到屏幕上。 在 Linux 下,控制台分文字模式控制台(vgacon)和图形模式控制台 (fbcon)。

文字模式控制台 (VGA 文字模式 )

文字模式控制台使用 VGA 兼容显卡的文字模式实现 VGA 兼容显卡初始化时默认就处于文字模式,能显示 80×25 个字符。

在文字模式下,显卡虽然输出给显示器的是图像,但是显卡提供给内核的却只能显示文字功能。要显示一个字符,内核将要显示的字符的代码和属性写入字符缓冲区相应地址即可。缓冲区如下面所示:

图 1. 缓冲区
图 1. 缓冲区
表 1. 每个字符的格式

属性字节 字符字节
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
闪烁 背景色 前景色 GLYPH

VGA 显卡处于文字模式时,物理地址 0xB8000 即文字缓冲区起始地址,大小由其文字模式决定,最大 32KB 。VGA 显卡内建字符发生器,不断的扫描字符缓冲区并将文字转换为图形驱动显示器显示。其 GLYPH 即为字符的 ASCII 码。

字符发生器和字体

字符发生器内建一个或者多个位图字体。使用何种字体取决于设置的文字模式代码。Linux 默认使用 80×25 字符 16 色模式 , 每字符 8×16 点阵。 字符发生器的字体包含 256 个字形。字符发生器里的文字和字符的对应关系在 DOS 系统下被称为代码页。显卡自带的被称为 OEM 代码页。

字符发生器的字体可以修改。DOS 下通过修改代码页实现。Linux 下可以使用 setfont。

因为这一历史原因,VGA 的文字模式最多同屏出现 256 种不同的字符。对于只有 26 个字母的拉丁文字绰绰有余,但是却无法满足拥有上万字符的中文。 文字模式控制台由 vgacon (drivers/video/console/vgacon.c)实现。 VGA 还有不常使用的单色文字模式,起始地址为 0xB0000 。并且字符格式也有所不同,这里不再介绍。

图形模式控制台

图形模式控制台直接操作帧缓冲区显示文字。帧缓冲区是一块存储区域(通常是在显存中),其内容就是显示在屏幕上的图像。显卡直接将其内容转化为显示器控制信号。对于程序而言,帧缓冲区就是屏幕。

图像模式控制台使用帧缓冲区作为屏幕输出,要想使用图像模式控制台,必须加载帧缓冲区驱动并有相应的硬件。

不同的显示设备通常使用不同的帧缓冲区驱动,对于 VGA 兼容显卡,除了能使用针对显卡写的帧缓冲区驱动 ( 如果有的话 ),还可以使用通用的 VESA 驱动。 VESA 驱动利用 VGA 兼容的显卡的 BIOS 将显卡转入 VGA 图形模式并设置期望的缓冲区模式,由显示模式内核参数 vga= 给出。 VGA 图形模式下,帧缓冲区的起始地址为 0xA0000 ,最大 64KB 大小。像素格式看显示色深。典型的 256 色模式下每像素一个字节。 如果 BIOS 支持 VBE 扩展,能设置更大的分辨率和色彩深度,帧缓冲区大小也会超过 64KB,因而起始地址也不再是 0xA0000,具体细节请参考 基百科. 帧缓存模式下,Linux 内核将需要显示的字符首先转换为位图,然后将位图字体写入帧缓存即可变呈现于屏幕。 也就是说,图形模式下,由内核来承担字符发生器的工作。理论上来说,这个内核自带的字符发生器将能支持显示多于 255 种字符。但是我将在后面告诉读者,因为历史原因,内核实现的字符发生器和 VGA 的字符发生器有着一样的缺点。 图形控制台由 fbcon(drivers/video/console/fbcon.c)实现。图形控制台由于需要生成位图字形,需要带上位图字体,编译内核的时候至少需要选择一个字体。位图字体在内核是一个大数组,不同的字体 ( drivers/video/console/font_* ) 存储在各自的数组中。

为何图形模式控制台也不能显示汉字

要解释图形模式控制台为何不能显示汉字,首先我们来了解一下虚拟终端是怎么管理屏幕上的文字显示的。 虚拟终端的实现在 drivers/tty/vt/vt.c 。代表虚拟终端的数据是 struct vc。

 struct vc{ 
	 struct vc_data; 
	 struct work_struct; 
 }; 
故而 struct vc_data 才是我們要的虛擬終端的定義。我們先來看看 struct vc_data 到底定義了什麼東西吧。
 struct vc_data 的定義在 include/linux/console_struct.h, 定義摘錄如下,爲了不延長篇幅,有省略的部分:
 struct vc_data { 
	 struct tty_port port; 			 /* Upper level data */ 

	 unsigned short 	 vc_num; 			 /* Console number */ 
	 unsigned int 	 vc_cols; 		 /* [#] Console size */ 
	 unsigned int 	 vc_rows; 
	省略 ... 
	 const struct consw *vc_sw; 
	 unsigned short *vc_screenbuf;  /* In-memory character/attribute buffer */
	 unsigned int 	 vc_screenbuf_size; 
	省略 ... 
 };

其中 vc_screenbuf 存储了虚拟终端要显示在屏幕上的文字。 const struct consw *vc_sw 指向控制台驱动提供的函数。虚拟终端利用里面的函数指针调用相应的操作,比如重绘屏幕,绘制一个字符等等。这些操作由 vgacon 和 fbcon 等控制台驱动实现。 当你切换终端的时候,实际上就是把当前终端设置为你要切换过去的终端,并且重新绘制当前终端 vc_data->vc_screenbuf 存储的内容。 当你从键盘输入命令或者程序运行过程中要输出内容的时候,虚拟终端首先将输出的字符进行编码转化,转化为对于字符的 GLYPH 代码,并且将 GLYPH 和当前字符属性结合,最后将合成结果写入当前光标所处的位置。内核中实际的算法要复杂的多,还牵涉到中断,但是为了简单快递的把我们关心的部分的核心表达出来,我使用一下伪代码表示不那么严谨的过程。希望了解全部的读者可以自行查看内核相关的代码,主要代码在 drivers/tty/vt.c 的 do_con_write() 中。

清单 1. 伪代码

				
 vc_write(vc_data * vc, const char * string, int count){ 
            for( ; count ; ){ 
   /* 和当前编码有关,如果是 utf8 就以 utf8 方式解码不是 utf8 就按照 扩展的 ASCII 方式,也就是一个字节就是一个字母。*/
        int glyph = next_char(vc->utf,&string,&count); 
        int c = vc_build_attribute(vc)|(vc_glyph_mask& glyph); 
        // 把当前设置的前景色背景色等属性结合 ,glyph 不能超过描述它的位段 ; 

        vc->vc_screenbuf[vc->vc_pos] = c; // 写入当前位置
		 update_pos(); // 更新当前光标位置
     } 
     notify_redraw(vc); // 调用 cosole 这个观察者重会屏幕
 }

你也许想知道 vc_screenbuf 指向的缓冲区的格式到底是怎样的,现在也可以回答 前面提到过的为何图像控制台依然不能支持汉字显示的问题了:vc_screenbuf 的格式就是 VGA 文字模式时显卡所使用的文字缓冲区格式。上述伪代码中的 vc_glyph_mask 就是 0xFF, 也就是 glyph 被截断只能 8 比特长度。 打从一开始,fbcon 就是按照 VGA 字符发生器设计的。因为当 vc_screenbuf 的格式和 VGA 字符缓冲区的格式一致的时候,切换终端就可以只需要 memcpy ——快速的拷贝到 VGA 字符缓冲区就能实现“重绘”当前终端。现在看来这种做法局限性非常大,但是单年 PC 性能还不够强大的时候,能做到快速的重绘是非常重要的。 重绘例程中,notify_redraw(vc) 用伪代码表示为

清单 2. notify_redraw(vc) 伪代码

				
 notify_redraw(vc_data * vc){ 
   for(int rows = 0 ; rows < vc->rows ; rows ++){ 
    unsigned short * current_line = & 
        vc->vc_screen_buf [vc_size_row * rows + vc->vc_visible_origin ] ; 
     // 在屏幕的第 row 行第 0 列绘制一行 current_line 指向的内容共 vc->cols 个字符。
     vc->vc_sw->con_puts(vc,current_line,vc->cols,0,rows); 
   } 
 }

vc_sw 是个由控制台代码提供的指针,类型为 structconsw * , 控制台驱动的初始化部分会使用 vt_unbind() 将自己绑定为虚拟终端使用的控制台,传入的信息中就包括 vc_sw。 在 vgacon 中 vc_sw->con_puts 事实上就是将要显示的内容简单的拷贝到 VGA 的字符缓冲区。对于 fbcon 而言则要复杂的多。 fbcon 提供的 con_puts 将 glyph 作为一个下标在 vc->vc_font 中找到对应的位图数据,然后拷贝到帧缓冲区。 TTY-> 虚拟终端 -> 控制台 -> 屏幕的路径用一幅图片表达为

图 2. 路径

图 2. 路径
图形模式控制台的改造

fbcon 将 glyph 作为下标到 vc_font 中获取位图数据,而 glyph 要么是一个 unicode (vc->utf=1 的时候,当然是被截断到 8 个比特)要么就是扩展的 ASCII 代码。 由于扩展 ASCII 只有 8 个比特位表示一个字符,所以只能请出 unicode 作为中文的数字表示。要想控制台能支持汉字显示,需要解决 3 个问题:

  1. 必须使用 UTF-8 模式 ( 默认 vc->utf=1 即可 )
  2. 虚拟控制台的 vc_screenbuf 必须修改以为 glyph 提供至少 16bit 的空间。
  3. 图形控制台需要 vc_font 包含更多的字符,不只是 255 个,并提供代码绘制双倍宽度的中文字形,字体中的字符按照 UNICODE 排列,这样 glyph 就是字符的 UNICODE 编码。
  4. 修改虚拟控制台

一开始,我的打算是 vc_screenbuf 修改为 unsigned long long* 类型,32bit 给字符属性,分别表示 16bit 终端前景色和背景色。glyph 则拥有 31bit 的空间 , 因为汉字的宽度为双倍的英文字母 ,其中 1 bit 用来表示双字符宽度。比如 ‘我’ 会表达为 两个 ‘我’,第二个’我’的最高位为 1:绘制任何字形的时候,只绘制字形的左半部分;如果发现最高位为 1 则绘制字体位图中的右半部分。这样同样的绘制代码可以适应英文字母和汉字。 写入 vc_screenbuf 的时候, 如果是双倍宽度的字符,需要同时写入两份,第二份的最高位置 1 就可以。 但是 vc_screenbuf 的格式已经被到处假定为每字符两个字节。如此修改导致牵一发动全身。许多艰涩难懂的代码都依赖 vc_screenbuf 是 每字符两个字节的设定,直接修改定义后,光是编译器能直接检测出来的就有百余个地方需要修改,还有更多的逻辑并不能被编译器检测出来。 如此修改的后果就是会出现许多隐晦的错误,非常难于调式。挣扎后,为最终选择了另一条道路 :

为汉字重新分配一块 vc_unicode_screenbuf

vc_unicode_screenbuf 紧挨着 vc_screenbuf , 事实上 vc_screenbuf 在分配空间的时候,多分配了一倍的空间,多分配的空间充作 vc_unicode_screenbuf,因此 struct vc_data 里并没有添加 vc_unicode_screenbuf 成员。 vc_unicode_screenbuf 同样为每字符 2 个字节,并不包含字符属性,所以 2 个字节如数用来保存 glyph。vc_screenbuf 格式未变,所以 vgacon 不需要修改,这就减少了大量的工作量。 向 vc_screenbuf 写入字符的时候,同时写入一份到 vc_unicode_screenbuf 。如果是汉字,由于其 glyph 大于 254 , 所以 vc_screenbuf 的那两个字符 ( 汉字双倍宽度 ) 实际写入的是 0xff 和 0xfe ( 故而上文提到是 glyph 大于 254 的字符 ,0xfe 被保留它用了 )。0xff 表示该字符的 glyph 要到 vc_unicode_screenbuf 提取,然后绘制左半部分;0xfe 表示该字符的 glyph 要到 vc_unicode_screenbuf 提取,然后绘制右半部分。对于 glyph 大于 254 但是又不是双倍宽度的字符,就不需要 0xfe 作陪了。 比如屏幕上显示的文字是黑底白字的 “牛 B” , vc_screenbuf 的内容就是 “0x00ff, 0x0ffe, 0x0f42 ” , vc_unicode_screenbuf 的内容则是 “牛 , 牛 ,b” 。这是因为一个汉字为两倍的英文字母宽度。在屏幕文字缓冲区上也必须占用两个字符的位置。并且必须有一种机制能知道应该绘制左半部分和右半部分,我使用的就是 0xff 和 0xfe。

修改图形控制台绘制代码

要修改的地方只有 3 个。

  1. struct console_font 添加 charcount 成员。将主线内核的字体设置为 charcount = 255。 主线内核带的字体都是 255 个 glyph 的,所以没有添加字符个数的必要。不过我们即将要添加的字体会有数万字符。
  2. 添加一个新的字体,复盖 UNICODE BMP 基本区域的所有符号。
  3. 修改字符绘制代码,添加 vc_unicode_screenbuf 的支持。

字符绘制代码的修改比较繁琐,代码分布在 drivers/video/console/ 下的多个文件中。fbcon_putc(s) 由由 vc->vc_sw->con_putc(s) 调用, fbcon_putc(s) 转而调用分散于 drivers/video/console/ 的多个 puts 实现。因为终端要支持 console_rotate , decoration , timing , 故而每种模式下的绘制实现都是不同的。 我拿 drivers/video/console/bitblt.c 最常用的不倾斜、不加装饰等的终端模式为例来讲解绘图部分的修改。 由于中文字体为 16×16 点阵,是对齐的字体,故而其绘制代码为 bit_putcs_aligned() 原先的代码以 glyph 为下标到 vc->vc_font->data 获得字体数据,然后调用 fb_pad_aligned_buffer 执行块拷贝操作。 我的修改很简单,原来获得字体数据的代码修改后放入 font_bits() 辅助函数。 在 font_bits 里,要判断 glyph 是否为 0xff 或者 0xfe, 如果不是,使用 glyph 为下标获得字体的左半部分后并返回。 如果是,则从 vc_unicode_screenbuf 获得真正的 glyph 数值,然后再依据现有的 glyph 是 0xff 还是 0xfe 去获得字体的右半部分还是左半部分返回。font_bits 获得字体数据后执行 fb_pad_aligned_buffer 块拷贝。 需要修改的地方还有 drivers/video/console/fbcon_ccw.c fbcon_cw.c fbcon_ub.c 。依原理进行修改即可。

虚拟终端的不足之处

虽然费尽心机添加了中文支持,那只是一个 workaround , 并不能算真正的支持。要真正的支持必须彻底重写虚拟终端和控制台。而要支持中文,就需要更进一步,全面支持 UNICODE , 包括支持从右向左的书写习惯。 在内核里实现一个全面支持 UNICODE 的控制台并不是一件容易的事情,何况内核的政策也不允许将如此庞大的字库装入内核。于是乎,这里出现了死胡同。KMS 和 Wayland 的出现让这死胡同似乎有了个完美的解。

KMS:KMS 是内核模式设置 (Kernel Mode Setting)的缩写。传统上内核使用 VGA 模式,该模式由 BIOS 或者 bootloader 设置。如果启动 Xorg, 则 Xorg 使用自己的驱动将显示模式进行切换。这导致内核并不知道显卡的当前工作状态,虚拟终端切换必须依赖 X 进行。X 锁死会导致整个终端被锁定无法进行切换。待机、休眠等功能必须依靠 X 和内核双方进行深度合作才能实现。让一个用户程序搞垮内核是不可以接受的,故有 KMS , 希望通过把模式设置代码移入内核,减少内核对 X 的依赖。

如果不使用 X , KMS 对于控制台来说就是支持了显示器的本地本分辨率。KMS 优势并不显着。但是 Wayland 的介入使事情发生了变化。 有关于 Wayland 的详细文档请参考 freedesktop.org 上的 项目首页。Wayland 并不只是对桌面带来了福音,同时也为控制台带来了福音,因为 Wayland 可以代替内核自身的虚拟终端和控制台实现。 而这个代替者就是 System Compositor

 System Compositor?

System Compositor 是一个 wayland compositor,只是运行于系统全局范围。

为了懒人我这里稍微讲解一下 wayland compositor 吧。 Wayland 不同于 X , 在 wayland 的世界里,只有 compositor 和 client。Client 利用各种 API (wayland 给出的示例使用的是 OpenGL ES, 但其实 wayland 并不限制使用的绘图 API 类型 ) 进行窗口绘图,然后将窗口的绘制结果直接提交给 compositor 合成到屏幕上。这样 wayland 本身就不包含绘图 API 而大大简化了 wayland 的设计。Wayland compositor 可以同 X 一样操作显卡向屏幕输出合成后的结果,也可以作为另一个 wayland compositor 的 client。

对于多账户同时登录的实现,固然可以让每一个本地 GUI 会话开启一个 wayland compositor,但是存在更好的办法就是固定开启一个 system compositor。而让所有用户会话的 wayland compositor 再作为 system compositor 的 client. 藉由 system compositor 的合成效果,进行快速用户切换也可以进行一些视觉效果。而且 Xorg 本身也已经支持作为 wayland client 运行,这样可以使用传统的 X 提供桌面,而让 wayland system compositor 实现终端切换。 这还有一个好处,只有 wayland system compositor 是以 root 运行的,而用户会话的 compositor 或 X 就不必以 root 权限运行。 因为 Wayland 非常轻量,所以 system compositor 可以作为系统级服务常驻内存运行。而因为有了 system compositor , 内核也不再需要实现虚拟终端了:只需要实现终端模拟器作为 system compositor 的 client 。由于是在用户空间实现的,所有可以加入 UNICODE,矢量字体,国际化的书写习惯等等的支持,再也不用受限于内核啦。 Wayland 还是一个非常年轻的项目,Wayland system compositor 目前还只是设想中的概念,需要更多的人关注参与。笔者相信不久的将来 wayland 一定能大有作为。

本篇简要介绍了 Linux 虚拟终端的工作原理和依赖的硬件细节实现,然后使用了不太优雅的办法让虚拟终端在帧缓存模式下实现汉字的显示。让大家对虚拟终端有了一点点更多的了解,希望本文能对想了解 Linux 的人有所帮助。也再次感谢 IBM DevelopWorks, 它让我有机会把自己的知识共享给更多的人知道。

[转]关于终端

终端其实是一种 输入 输出 设备。

典型的终端使用RS-232之类的串行数据通信与主机相连。

大多数终端的屏幕是绿色或者橙色的,它们与大型计算机相连。典型的终端使用RS-232之类的串行数据通信与主机相连,IBM使用它自己的系统网络体系结构协议通过同轴电缆来连接其主机与终端。

后来所谓的智能终端(如VT52和VT100)被引入。今天依然有许多这两个终端的模拟软件。这些终端之所以被称为“智能”是因为它们理解转义序列,可以定位光标和控制显示位置。

1970年代里世界上有十数个终端生产商,大多数终端的指令不兼容。

早期的IBM个人计算机虽然也适用绿色的荧光屏,但是它不算终端。个人计算机的荧光屏不包括任何产生字母的硬件,所有的视频信号是在个人计算机的视卡里产生的。但是使用相应的模拟程序一台个人计算机可以与大型计算机相连模拟终端。最后使用微处理器的个人计算机大大地取消了对终端的需要。今天大多数个人计算机的Telnet用户端提供最普遍的终端(一般VT100)的模拟。

一个程序最简单的使用终端的方法是向终端串行写或读文字。输出的文字向上推,因此只有最后的数行可见。输入的文字首先被暂时贮存,在获得回车键后程序获得整个输入的文字。在这种情况下程序不需要很了解终端。

对于许多互动程序来说这个方法不够,一个普遍的改善是描述命令行编辑,一般它还提供指令历史的功能。这个改善对不同互动的命令行注释器也非常有用。

更进步的终端提供“全屏幕”应用。这样的程序可以完全控制屏幕上的输出,还可以立刻对键盘输入做反应。这样的终端对文本编辑器、文件管理和网页浏览器这样的应用非常有用。这样的程序可以控制屏幕上文字的亮度、在文字下划线、使文字闪烁和使用特别的字母。这样的程序不但要处理简单的文字,而且还要处理可以将光标放置到屏幕上任何地方、清除屏幕部分地区、改变颜色和显示特殊字母的控制字符和转义序列。这些程序还必须能够对功能键做反应。

但是由于各种终端和它们的模拟程序使用不同的转义序列,加上许多模拟程序多少有些错误,在显示的时候往往会发生错误。此外终端和终端模拟程序一般不支持罗马字母以外的字母或者字形。这些缺点都限制了今天终端和终端模拟器的使用。而且近年来由于图形用户界面的普及终端模拟库的发展和改错的工作越来越趋停滞。

关于各种watchdog

这篇东西本该早些记录的,由于事情太多拖到现在…….    本篇和内核中的nmi_watchdog无关

从硬件上说watchdog 分为ipmi watchdog, superio watchdog, iTCO watchdog

最复杂的是ipmi watchdog 这个最后讲。

superio watchdog:

superio其实就是负责以往过时的ps/2, 串口,并口等接口io的芯片。因为这些接口在最近10年的南桥芯片里不再集成,所以只有台湾人发挥聪明才智搞出来了superio芯片,这枚芯片通过GPIO与南桥相连。然而这枚芯片不旦集成了这些老旧的接口还集成watchdog。可以说superio watchdog是最常见的watchdog,尤其是w83627这枚芯片上的。

就拿w83627举例子吧。

也许板卡厂商会发给你一份w83627的datasheet,上面有watchdog的设定方法,就是用读写端口的方法 in(), out() 来读写指定端口。其实对于大部分的superio watchdog都不用自己写代码。而只需要载入对应的驱动模块即可:

# modinfo w83627hf_wdt
filename: /lib/modules/2.6.34.12_2.19_120525/kernel/drivers/watchdog/w83627hf_wdt.ko
alias: char-major-10-130
description: w83627hf/thf WDT driver
author: P�draig Brady <P@draigBrady.com>
license: GPL
srcversion: 86345BC119DCE658FD6407C
depends:
vermagic: 2.6.34.12_2.19_120525 SMP mod_unload modversions
parm: wdt_io:w83627hf/thf WDT io port (default 0x2E) (int)
parm: timeout:Watchdog timeout in seconds. 1 <= timeout <= 255, default=60. (int)
parm: nowayout:Watchdog cannot be stopped once started (default=0) (int)

载入成功后:

[ 1237.355215] WDT driver for the Winbond(TM) W83627HF/THF/HG Super I/O chip initialising.
[ 1237.355294] w83627hf/thf/hg WDT: initialized. timeout=60 sec (nowayout=0)

#cat /proc/ioports

0000-03af : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
002e-002e : w83627hf/thf/hg WDT
0040-0043 : timer0
0050-0053 : timer1

# ll /dev/watchdog
crw——- 1 root root 10, 130 Dec 4 22:08 /dev/watchdog

看到这个设备文件就算成功了。其实驱动程序内部把各种watchdog抽象了一个设备,操作这个文件将执行对应的ioctl,不同的ioctl对应不同的io操作而已。也就是说只要加载了对应的watchdog驱动(不管是superio的,还是ipmi的,或是iTCO)就会出现这个设备文件。现在该考虑如何在用户层操作了。这里也不需要自己写代码,因为有人已经写过了:http://www.ibiblio.org/pub/linux/system/daemons/watchdog/

配置文件在/etc/watchdog.conf  最后一行

watchdog-timeout = 5 表示watchdog timer的超时时间。

然后直接键入watchdog 开始启动。

iTCO watchdog:

和superio差不多,但这个是intel自家的,集成了南桥里面。

#cat /proc/ioports

0800-087f : pnp 00:08
          0800-0803 : ACPI PM1a_EVT_BLK
          0804-0805 : ACPI PM1a_CNT_BLK
          0808-080b : ACPI PM_TMR
          0810-0815 : ACPI CPU throttle
          0828-082f : ACPI GPE0_BLK
          0830-0833 : iTCO_wdt
          0860-087f : iTCO_wdt

加载完iTCO_wdt 也会出现/dev/watchdog,用户层次的操作就都一样了。

ipmi watchdog:

ipmi watchdog 稍微复杂一些。因为ipmi有自己的规范,或者说有自己的一套管理工具(freeimpi 和 openipmi)。所以除了使用/dev/watchdog设备文件来访问,也能使用其他众多的工具来操作。先说一下用设备文件的方法。

首先系统要有载入这些模块

lsmod |grep ipmi
ipmi_devintf 7073 0
ipmi_si 37564 1
ipmi_watchdog 15801 0
ipmi_msghandler 33151 3 ipmi_devintf,ipmi_si,ipmi_watchdog

其中ipmi_watchdog 是ipmi watchdog的驱动模块,供用户层的watchdog 来访问。 ipmi_devintf 和ipmi_si 则是抽象出一个ipmi层的管理设备  /dev/ipmi0 。然后就和之前一样启用watchdog进程即可。同时还可以安装ipmitool来观察当前的watchdog状态:

# ipmitool mc watchdog get
Watchdog Timer Use: SMS/OS (0x04)
Watchdog Timer Is: Stopped
Watchdog Timer Actions: No action (0x00)
Pre-timeout interval: 0 seconds
Timer Expiration Flags: 0x10
Initial Countdown: 10 sec
Present Countdown: 10 sec

除上述方法外还有另外一套用户态的ipmi watchdog工具集:freeipmi-bmc-watchdog。 比较另类的是它要求从底层对ipmi设备独占。也就是说它要求卸载所有ipmi的内核模块。然后可以用bmc-watchdog -g 来查看状态:

# bmc-watchdog -g
Timer Use: SMS/OS
Timer: Stopped
Logging: Enabled
Timeout Action: None
Pre-Timeout Interrupt: None
Pre-Timeout Interval: 0 seconds
Timer Use BIOS FRB2 Flag: Clear
Timer Use BIOS POST Flag: Clear
Timer Use BIOS OS Load Flag: Clear
Timer Use BIOS SMS/OS Flag: Set
Timer Use BIOS OEM Flag: Clear
Initial Countdown: 10 seconds
Current Countdown: 10 seconds

wathcdog用户态访问由bmc-watchdog 程序处理:

bmc-watchdog -d -u 4 -p 0 -a 1 -F -P -L -S -O -i 900 -e 60

或者由/etc/rc.d/init.d/freeipmi-bmc-watchdog 脚本启动守护进程管理,配置文件在/etc/sysconfig/freeipmi-bmc-watchdog。

 

 

注意:watchdog的用户进程被kill的默认信号kill时,会先停止watchdog timer。