关于 C++ 的碎碎念

关于模板

一定要合理使用模板, 一定要优先保证可读性…

关于指针

个人觉得, 能少用指针就少用, 优先级如下:

引用 > 智能指针 > 指针

关于 const

引入 const 有意义吗? 变量是否应该改变应该由代码确保, 而不需要编译器确保. 相比与 const 带来的安全性, 它带来了更多的麻烦:

  1. const 类型的变量不能传入接受非 const 类型参数的函数, 这要求我们定义函数时参数类型最好都定义成 const 的. (c++ primer 5th, p192)

boost::asio 网络编程简明教程

acceptor 一般使用方法:

using boost::asio::ip::tcp;

boost::asio::io_context     io_context;
boost::system::error_code   ec;

tcp::acceptor   acceptor(io_context)
tcp::socket     socket(io_context);
tcp::endpoint   ep_listen(tcp::v4(), 13000);
tcp::endpoint   ep_peer;

acceptor.open(ep.protocol(), ec);
acceptor.bind(ep, ec);
acceptor.listen(5, ec);
acceptor.accept(socket, ep_peer, ec);

...

这其实是有些繁琐的, 实际上 acceptor 的构造函数还有这个:

basic_socket_acceptor(
    boost::asio::io_context & io_context,
    const endpoint_type & endpoint,
    bool reuse_addr = true);

这里我们可以传入一个 endpoint 作为第二个参数, 提供了这个 endpoint 参数的话,
acceptor 会同时帮我们做 open, bind, listen 三件事,
所以上述代码可以写成如下这么短:

using boost::asio::ip::tcp;

boost::asio::io_context     io_context;
boost::system::error_code   ec;

tcp::endpoint   ep;
tcp::acceptor   acceptor(io_context, tcp::endpoint(tcp::v4(), 13000))
tcp::socket     socket(io_context);

acceptor.accept(socket, ep, ec);

...

嵌入式哈系表的实现

这篇文章是一个例子, 重点在于阐述嵌入式哈系表结构, 而不是针对不同键类型进行哈希的方法.

关于哈希冲突的解决, 我们使用链表存储冲突的节点, 这在一些书里被称为 “Separate Chaning” 方法.

由于在链表中插入以及删除节点需要更新节点的前继和后继的指针, 所以为了方便的从链表中插入以及删除节点, hlist_node_t 结构中定义两个指针, 分别指向前继和后继.

typedef struct hlist_node_s {
    struct hlist_node_s *prev;
    struct hlist_node_s *next;
} hlist_node_t;

hlist_t 结构中的 heads 是一个数组, 每个数组元素都只是一个头节点, 头节点不会嵌入到别的结构体中.

typedef struct xhash {
    hlist_node_t    *heads;
    int             size;
} xhash_t;

void hlist_node_init(hlist_node_t *node)
{
    node->next = node->prev = node;
}

xhash_t *xhash_create(int size)
{
    xhash_t     *hash;
    int         i;

    /* TODO Get next prime bigger than *size* */
    /* size = next_prime(size);*/

    hash = calloc(1, sizeof *hash);
    if (!hash)
        return NULL;

    hash->heads = calloc(size, sizeof *hash->heads);
    if (!hash->heads) {
        free(hash);
        return NULL;
    }

    for (i = 0 ; i < size ; ++i)
        hlist_node_init(&hash->heads[i]);

    hash->size = size;

    return hash;
}

为了方便链表的遍历, 我们还需要定义如下几个宏, 其中 offsetof 宏一般 C 标准库会为我们定义, 并且 C 库的定义会考虑到不同系统上的差异. 为了完整的阐述我们的实现, 这里我还是将它 “bare” 的定义写了出来.

#define offsetof(type, member)              \
    ({                                      \
        type s;                             \
        (char *)(&s.member) - (char *)(&s); \
    })

#define container_of(node, type, member)                \
    ((type *)((char *)(node) - offsetof(type, member)))

#define hlist_for_each(head, node)                      \
    for ((node) = (head)->next ;                        \
            (node) != (head) ; (node) = (node)->next)

#define hlist_for_each_safe(head, node, next)           \
    for ((node) = (head)->next, (next) = (node)->next ; \
            (node) != (head) ;                          \
            (node) = (next), (next) = (node)->next)

void hlist_add_head(hlist_node_t *head, hlist_node_t *new)
{
    head->next->prev = new;
    new->next = head->next;
    head->next = new;
    new->prev = head;
}

void hlist_add_tail(hlist_node_t *head, hlist_node_t *new)
{
    head->prev->next = new;
    new->prev = head->prev;
    head->prev = new;
    new->next = head;
}

void hlist_del(hlist_node_t *node)
{
    node->next->prev = node->prev;
    node->prev->next = node->next;
}

如前所说, 为方便起见我们仅实现整数类型的键, 哈系函数采用简单的取模运算. 其它类型的键的哈系后面只要进一步扩展就可以了, 这里不再多说.

void xhash_int_add(xhash_t *hash, int key, hlist_node_t *node)
{
    key %= hash->size;
    hlist_add_head(&hash->heads[key], node);
}

void xhash_int_del(xhash_t *hash, int key, hlist_node_t *node)
{
    key %= hash->size;
    hlist_del(&hash->heads[key], node);
}

/** return hlist head */
hlist_node_t *xhash_int_find(xhash_t *hash, int key)
{
    key %= hash->size;
    return &hash->heads[key];
}

xhash_destroy 方法中, 要不要帮助用户将所有嵌入的节点从链表中移除是个问题, 可能比较人性化的方法是帮助用户移除. 但假如节点嵌入的结构体是用户动态申请的, 然后用户调用 xhash_destroy 之前就把结构体释放了呢? 这样节点成员所处的内存就是无效的了, 我们的 xhash_destroy 方法就很可能会崩溃. 所以这活儿还是不干了.

void xhash_destroy(xhash_t *hash)
{
    /*hlist_node_t    *node, *next;
    int             i;*/

    /* TODO
     * whether we should del all node from corresponding list? what if user remove
     * the node member from embedded struct?
     */
    /*
    for (i = 0 ; i < size ; ++i) {
        hlist_for_each_safe(&hash->heads[i], node, next) {
            hlist_del(node);
            /* restore to initial state */
            hlist_node_init(node);
        }

    }
    */

    free(hash->heads);
    free(hash);
}

下面我们用一个例子测试一下我们的实现:

typedef struct xy_s {
    int             key;
    int             x;
    int             y;
    hlist_node_t    node;
} xy_t;

int xy_print(xhash_t *hash)
{
    int             idx;
    hlist_node_t    *node;
    xy_t            *xy;

    if (!hash) return -1;

    for (idx = 0 ; idx < hash->size ; ++idx) {
        printf("bucket[%d]: ", idx);
        hlist_for_each(&hash->heads[idx], node) {
            xy = container_of(node, xy_t, node);
            printf("%d, ", xy->key);
        }

        printf("\n");
    }

    return 0;
}

int main()
{
    xhash_t     *hash;
    xy_t        *xy;
    int         i;

    assert((hash = xhash_create(13)) != NULL);

    printf("\ninsert 0 to 99 nodes \n\n");

    for (i = 0 ; i < 100 ; ++i) {
        xy = calloc(1, sizeof *xy);
        xy->key = i;
        xy->x = i * 3;
        xy->y = i * 4;
        hlist_node_init(&xy->node);
        xhash_int_add(hash, xy->key, &xy->node);
    }

    xy_print(hash);

    printf("\ndelete 99\n\n");

    xhash_int_del(hash, 99, &xy->node);
    xy_print(hash);

    return 0;
}

编译运行以上程序, 输出的信息如下:

insert 0 to 99 nodes 

bucket[0]: 91, 78, 65, 52, 39, 26, 13, 0, 
bucket[1]: 92, 79, 66, 53, 40, 27, 14, 1, 
bucket[2]: 93, 80, 67, 54, 41, 28, 15, 2, 
bucket[3]: 94, 81, 68, 55, 42, 29, 16, 3, 
bucket[4]: 95, 82, 69, 56, 43, 30, 17, 4, 
bucket[5]: 96, 83, 70, 57, 44, 31, 18, 5, 
bucket[6]: 97, 84, 71, 58, 45, 32, 19, 6, 
bucket[7]: 98, 85, 72, 59, 46, 33, 20, 7, 
bucket[8]: 99, 86, 73, 60, 47, 34, 21, 8, 
bucket[9]: 87, 74, 61, 48, 35, 22, 9, 
bucket[10]: 88, 75, 62, 49, 36, 23, 10, 
bucket[11]: 89, 76, 63, 50, 37, 24, 11, 
bucket[12]: 90, 77, 64, 51, 38, 25, 12, 

delete 99

bucket[0]: 91, 78, 65, 52, 39, 26, 13, 0, 
bucket[1]: 92, 79, 66, 53, 40, 27, 14, 1, 
bucket[2]: 93, 80, 67, 54, 41, 28, 15, 2, 
bucket[3]: 94, 81, 68, 55, 42, 29, 16, 3, 
bucket[4]: 95, 82, 69, 56, 43, 30, 17, 4, 
bucket[5]: 96, 83, 70, 57, 44, 31, 18, 5, 
bucket[6]: 97, 84, 71, 58, 45, 32, 19, 6, 
bucket[7]: 85, 72, 59, 46, 33, 20, 7, 
bucket[8]: 86, 73, 60, 47, 34, 21, 8, 
bucket[9]: 87, 74, 61, 48, 35, 22, 9, 
bucket[10]: 88, 75, 62, 49, 36, 23, 10, 
bucket[11]: 89, 76, 63, 50, 37, 24, 11, 
bucket[12]: 90, 77, 64, 51, 38, 25, 12, 

LoxiGen 与 Indigo 项目介绍

Indigo 是由 Big Switch 开发, 现在托管在 Floodlight 组织下的 OpenFlow agent 开源实现. OpenFlow 控制器的开源实现有很多, 像是 Ryu, ODL, POX 等等, 但是 agent 方面的开源实现可能相对少一些, Indigo 是一个非常精巧的实现, 但是似乎网上 SDN 相关的中文社区对其介绍的文档却很少, 本文将对 Indigo 及其所依附的 LoxiGen 项目做一个简单的介绍.

关于 LOXI 与 Loxigen

LOXI (Logical OpenFlow eXtensible Interface), 是一种描述 OpenFlow 协议的逻辑语言.

LoxiGen 项目能够解读 LOXI 语言, 进而用来生成各种编程语言的 OpenFlow 协议库. 所以说 LoxiGen 实际上是一个 “编译器” 项目, 可想而知 LoxiGen 包含一个前端用来解析 LOXI 语言, 以及包含各种编程语言的后端来生成这些编程语言的代码. 目前包含 Java, Python 以及 C 语言的后端, 生成的 C 版本的协议库叫做 LOCI, Java 版本的叫做 OpenFlowJ, Python 版本的叫做 pyloxi.

LoxiGen is a tool that generate OpenFlow protocol library for a number of languages. It is composed of a frontend that parses wire protocol descriptions and a backend for each supported language (currently C, Python and Java, with an auto-generated wireshark dissector in Lua on the way).

LoxiGen 项目组维护了一个快照, 定期的生成各种语言的协议库, 并将其放在 loxigen-artifacts 仓库下, 这样对于使用者来说只需要定期从 loxigen-artifacts 仓库拿现成的就行了, 连构建都不需要自己构建.

Indigo 项目源码构成

Indigo 项目 (https://github.com/floodlight/indigo) 为我们提供了一些与平台无关的基础功能, 包括:

  • IO 复用与定时器管理框架: 提供了通用的 socket 回调注册机制与定时器回调处理机制, 让我们能够在单一线程中同时处理多个 sockets 的读写以及定时器的回调事件.
  • OpenFlow 连接管理: 维护每一条与控制器之间的连接, 包括连接的建立, 心跳维持, 消息收发以及连接的终止.
  • OpenFlow 状态管理: 包括与控制器之间的各种消息的处理以及交换机状态的上报都会在这里处理
  • 配置模块: 提供了平台无关的配置接口

另外 Indigo 中有一些平台相关的功能是需要厂商自己来实现的, 这包括:

  • Forwarding 模块: 负责实现将控制器下发的流表下发到交换机芯片中的接口
  • Port Manager 模块: 负责实现端口管理的接口

芯片厂商需要负责实现后两个模块, 一般来讲就是将这两个模块中 indigo 所定义的接口用自己芯片的 sdk 来实现. 关于 Indigo 适配有一个很好的例子就是 Broadcom 的 OF-DPA, 我们可以从 Broadcom 的 OF-DPA 项目仓库中获得实现了 Forwarding 和 Port Manager 模块之后的 indigo 代码.

OF-DPA 项目托管在 https://github.com/Broadcom-Switch/of-dpa/, 其对 Indigo 项目做了些许代码上的改动, 从这个地址获得 OF-DPA 的代码, indigo 的代码位于 ofagent/ 目录下:

ofagent/
    application/
    indigo/
    ofdpadriver/

其中 indigo/ 目录中就是 indigo 的源码; ofdpadriver/ 是 OF-DPA 实现好的 Forwarding 和 Port manager 模块的代码, 这两部分会调用芯片 sdk; application/ 目录中只有一个源文件: ofagent.c, 这是一个示例程序, 向我们展示了如何初始化以及启动 indigo.

在 indigo 的项目代码也就是 indigo/ 目录下, 又有两个子目录: modules/submodules/, 其中 modules/ 里面是 indigo 自身的代码, submodules/ 里是 indigo 所用到的非自身的代码, 目前有 bigcode, infra, loxigen-artifacts.

bigcode 和 infra 都是 Floodlight 开发的通用工具库, 另外你可能注意到了 Indigo 使用了 loxigen-artifacts 中的代码. 下面就看一下 indigo 具体是如何使用 loxigen 的.

Indigo 与 LoxiGen

Indigo 使用了 LoxiGen 项目生成的 LOCI 协议库, 并且是直接把源代码拿来使用的. 一开始将其放在 indigo/modules/loci/ 目录下, Indigo 的维护者从 loxigen-artifacts 获得最新的代码, 然后再将其与 modules/loci/ 下原本的 loxigen 项目代码合并或者直接替换. 因为这样每次都需要维护者手动去 pull loxigen-artifacts, 然后与 Indigo 的 codebase 合并, 后来估计是 Indigo 的维护者也觉得这样太笨了, 于是就干脆不往 modules/loci/ 里面合了, 而是直接借助了 git submodule 机制, 把 loxigen-artifacts 用 git pull 拉下来放到了 submodules/loxigen-artifacts/, 然后 modules/loci/make.mk 做了如下的改动, 直接指向 submodules/loxigen-artifacts/.

-loci_INCLUDES := -I $(dir $(lastword $(MAKEFILE_LIST)))inc
-loci_INTERNAL_INCLUDES := -I $(dir $(lastword $(MAKEFILE_LIST)))src
+LOCI := $(SUBMODULE_LOXIGEN_ARTIFACTS)/loci

+loci_INCLUDES := -I $(LOCI)/inc
+loci_INTERNAL_INCLUDES := -I $(LOCI)/src

+LIBRARY := loci
+loci_SUBDIR := $(LOCI)/src
+include $(BUILDER)/lib.mk

这样一来, 编译 indigo 项目时就会直接从 submodules/loxigen-artifacts/ 处获取 loxigen 的源码.

(End)

记一次 Indigo 与 Ryu 的连接建立问题

之前在研究 Indigo 和 Ryu 的衔接时碰到一个 OpenFlow 连接建立失败的问题, 特此记录下, 希望能够供别人参考.

在了解这个问题之前, 我们先回顾一下 OpenFlow 连接建立过程.

OpenFlow 协议的连接建立过程

根据 OpenFlow 协议标准的陈述我们能够知道, 交换机和控制器之间使用 TCP (或者 SSL) 传输协议, 交换机必须能够主动发起连接 (实际应用中, 连接一般都是都由交换机主动发起), 另外就是所有的 OpenFlow 消息, 都要用网络序 (大端序) 发送 (参见 OpenFlow Spec v1.3 以及 v1.4 的第 7 章)

TCP 连接的建立我们很熟悉了, 就是典型的三此握手过程. 在 TCP 连接建立以后, 交换机和控制器双方在 TCP 连接建立后需要立即发送给 OF_HELLO 消息给对方, 并且 OF_HELLO 必须是双发发送给对方的第一个消息, OF_HELLO 消息同时起到协商 OpenFlow 版本的功能.

当双方都收到了对方的 OF_HELLO 消息并且两边都共同支持一个最小版本, OpenFlow 连接就成功建立了, 接下来控制器就可以向交换机发送其它的消息, 比如一般第一次要发送的就是 OFPT_FEATURES_REQUEST 消息.

遇到的问题

在这部分工作中基于 Ryu 框架我写了个简单的 Ryu 小程序令它与 Indigo 通信, 但是发现似乎连接建立都不成功, 好在 indigo 项目的错误日志部分做的很好, 我打开了 verbose 级别的日志, 发现连接建立过程中 indigo 在收取 OF_HELLO 这个消息失败了, 结合代码发现是在读 socket 时发生了 EAGAIN (Resource temporary unvailable) 错误. 熟悉 Linux 开发的人都知道这个错误还有另一个名字叫做 EWOULDBLOCK, 其含义是指我去读一个非阻塞的 socket, 这个 socket 本来是被期望可读的, 但是实际读它的时候发现并没有数据.

这就有点奇怪了, 控制器发送 OF_HELLO 消息给交换机, 然而交换机读这个消息时却发生了这样的系统错误. 原本我怀疑是我的交换机系统环境有问题, 于是我写了一个最基本的 tcp socket server 和一个 tcp socket client, 让 tcp socket server 运行在控制器主机并监听控制器的标准端口 6653, tcp socket client 则跑在我的交换机系统上, 发现这两者是能够正常收发包的, 而且, 我按照 OpenFlow 协议组了一个 OF_HELLO 消息, 从 tcp socket server 发送给 tcp socket client, 我的 socket client 也能够正常的接收这个消息, 并没有出现 EAGAIN 错误.

所以说交换机系统环境是没有问题的, 可是为什么 indigo 的代码里收不到这个 OF_HELLO 消息呢? 看来必须得窥探一下 Ryu 发过来的这个 OF_HELLO 消息是什么样的.

问题的分析

为了确定 Ryu 发过来的 OF_HELLO 消息没问题, 用 tcpdump 抓包看一下两者通信的细节, 下面就是交换机和控制器连接建立过程中抓取的数据包:

09:59:26.578677 ethertype IPv4 (0x0800), length 74: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [S], seq 992465816, win 43690, options [mss 65495,sackOK,TS val 15346082 ecr 0,nop,wscale 7], length 0
        0x0000:  4510 003c fa3a 4000 4006 426f 7f00 0001  E..<.:@.@.Bo....
        0x0010:  7f00 0001 8296 19fd 3b27 d398 0000 0000  ........;'......
        0x0020:  a002 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  00ea 29a2 0000 0000 0103 0307            ..).........
09:59:26.578703 ethertype IPv4 (0x0800), length 74: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [S.], seq 710500324, ack 992465817, win 43690, options [mss 65495,sackOK,TS val 15346082 ecr 15346082,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 3cba 7f00 0001  E..<..@.@.<.....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe4 3b27 d399  ........*Y_.;'..
        0x0020:  a012 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  00ea 29a2 00ea 29a2 0103 0307            ..)...).....
09:59:26.578723 ethertype IPv4 (0x0800), length 66: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [.], ack 710500325, win 342, options [nop,nop,TS val 15346082 ecr 15346082], length 0
        0x0000:  4510 0034 fa3b 4000 4006 4276 7f00 0001  E..4.;@.@.Bv....
        0x0010:  7f00 0001 8296 19fd 3b27 d399 2a59 5fe5  ........;'..*Y_.
        0x0020:  8010 0156 fe28 0000 0101 080a 00ea 29a2  ...V.(........).
        0x0030:  00ea 29a2                                ..).
10:10:56.193800 ethertype IPv4 (0x0800), length 76: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [P.], seq 992465817:992465827, ack 710500325, win 342, options [nop,nop,TS val 15518486 ecr 15346082]
        0x0000:  4510 003e fa3c 4000 4006 426b 7f00 0001  E..>.<@.@.Bk....
        0x0010:  7f00 0001 8296 19fd 3b27 d399 2a59 5fe5  ........;'..*Y_.
        0x0020:  8018 0156 fe32 0000 0101 080a 00ec cb16  ...V.2..........
        0x0030:  00ea 29a2 0400 0800 0000 0000            ..).........                  (注: 交换机发给控制器的 OF_HELLO 消息, 即 0400 0800 0000 0000)
10:10:56.193822 ethertype IPv4 (0x0800), length 66: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [.], ack 992465827, win 342, options [nop,nop,TS val 15518486 ecr 15518486], length 0
        0x0000:  4500 0034 7647 4000 4006 c67a 7f00 0001  E..4vG@.@..z....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe5 3b27 d3a3  ........*Y_.;'..
        0x0020:  8010 0156 fe28 0000 0101 080a 00ec cb16  ...V.(..........
        0x0030:  00ec cb16                                ....
10:11:01.246773 ethertype IPv4 (0x0800), length 75: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [P.], seq 710500325:710500334, ack 992465827, win 342, options [nop,nop,TS val 15519749 ecr 15518486]
        0x0000:  4500 003d 7648 4000 4006 c670 7f00 0001  E..=vH@.@..p....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe5 3b27 d3a3  ........*Y_.;'..
        0x0020:  8018 0156 fe31 0000 0101 080a 00ec d005  ...V.1..........
        0x0030:  00ec cb16 0400 0008 eed3 0ebf            ............                  (注: 控制器发给交换机的 OF_HELLO 消息, 即 0400 0008 eed3 0ebf)
10:11:01.246800 ethertype IPv4 (0x0800), length 66: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [.], ack 710500334, win 342, options [nop,nop,TS val 15519749 ecr 15519749], length 0
        0x0000:  4510 0034 fa3d 4000 4006 4274 7f00 0001  E..4.=@.@.Bt....
        0x0010:  7f00 0001 8296 19fd 3b27 d3a3 2a59 5fee  ........;'..*Y_.
        0x0020:  8010 0156 fe28 0000 0101 080a 00ec d005  ...V.(..........
        0x0030:  00ec d005                                ....

在这里, 10.0.0.1 是交换机地址, 10.0.0.2 是控制器地址. 其中前三条报文显然就是 tcp 三次握手, 后四条就是两者相互发送的 OF_HELLO 消息, 上面两个括号里的注释是我加的. 在 OpenFlow 协议中, OF_HELLO 消息是一条只有 header 没有 payload 的消息, 所以 OF_HELLO 消息的长度就是 OpenFlow 消息头的长度: 8 字节, 其中第一个字节是 Ryu 与 Indigo 协商好的 OpenFlow 版本号, 这里是 0x04, 代表 OpenFlow v1.3 (注意不是 v1.4), 接下来的一个字节 0x00 表示消息类型是 OF_HELLO, 后四个字节是消息的唯一标识码我们可以不用理会, 中间的两个字节表示整个 OpenFlow 消息的长度, 有意思的地方就在这里.

Ryu 和 Indigo 相互发给对方的 OF_HELLO 消息中, 头部的长度字段字节序不一致, OpenFlow 协议头部的长度字段是两字节, OF_HELLO 消息长度为 8, 即 0x0008, 前面说过在 OpenFlow 协议中要求, 所有的 OpenFlow 消息, 都要用大端序发送. 所以这两个字节用网络序也就是大端序表示应该为 0x0008, 即 MSB 在低字节. 显然, 在这一点上 Ryu 的报文是正确的, indigo 的报文是错误的.

那么 EAGAIN 错误是怎么出现的呢? 既然 indigo 发出消息的长度字段的端序有问题, 可想而知其对于收到的消息的长度字段的端序理解也很可能有问题. 下面这段是 indigo 读取消息的代码逻辑, 为了清晰起见我做了一些删减:

/* read header */
if (READING_HEADER(cxn)) {
    INDIGO_ASSERT(cxn->bytes_needed + cxn->read_bytes ==
                  OF_MESSAGE_HEADER_LENGTH);
    if ((rv = read_from_cxn(cxn)) < 0) {
        return rv;
    }

    msg = (of_message_t)(cxn->read_buffer);
    msg_bytes = of_message_length_get(msg);
    if (msg_bytes < OF_MESSAGE_HEADER_LENGTH) {
        ++ind_cxn_internal_errors;
        return INDIGO_ERROR_PROTOCOL;
    }
    cxn->bytes_needed = msg_bytes - OF_MESSAGE_HEADER_LENGTH;
}

if (cxn->bytes_needed == 0) {
    return INDIGO_ERROR_NONE;
}

/* read the rest */
if ((rv = read_from_cxn(cxn)) < 0) {
    return rv;
}

稍微解释一下这段代码, 在 indigo 的代码中, 与 ryu 通信的 socket 是非阻塞的, 用 poll 系统调用来判断是否可读, indigo 在读 socket 时还有一个字段叫做 bytes_needed 用来表示我想要从 socket 中读多少个字节, read_from_cxn(cxn) 方法会按照 bytes_needed 的值明确读取这些字节数. 当内核第一次通知 indigo 这个 socket 可读时, bytes_needed 字段总是 8, 因为 OpenFlow 的消息头最小就是 8, 小于 8 就是非法的. 读取了 8 字节的头之后, of_message_length_get(msg) 会解析头部中的两字节的长度字段获知整条 OpenFlow 消息的长度, 用它减去头部的长度来获知我还需要读多少字节.

可想而知, 当 indigo 收到 ryu 的 OF_HELLO 时, 它也把 0x0008 处理成了 0x0800, indigo 以为 ryu 给它发送了 8 x 256 = 2048 个字节, 但显然实际上 ryu 只给它发送了 8 字节. 在 indigo 认为后面应该还有 2040 个字节, 于是继续调用 read_from_cxn(cxn), 但实际上 socket 上没有字节可读了, 于是引发 EAGAIN 错误.

问题的解决

综上来看, 问题出在 indigo 对长度字段的解析上, 也就是上面代码段里的 of_message_length_get() 方法中, 我层层看下去发现 of_message_length_get() 方法的调用链是这样的:

of_message_length_get() >> buf_u16_get() >> U16_NTOH()

这几个方法的含义一看名字就知道, 最后的 U16_NTOH() 是个条件编译的宏, 定义如下:

#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 
#define U16_NTOH(val) (val) 
#else 
#define U16_NTOH(val) (((val) >> 8 |(((val) & 0xff) << 8)) 
#endif 

所以现在一切都清晰了, indigo 没有使用 POSIX 标准的系统调用 htons()/ntohs(), 而是自己定义了一套主机序到网络序的转换方法, 可能是 indigo 想能够编译在更多的平台上而不只是 POSIX 兼容的系统. 只不过这样一来, 我们就需要在编译 indigo 的时候根据我们自己的系统架构来定义 __BYTE_ORDER__ 宏. 在我上面的实验环境中, 交换机系统架构是小端序的, 所以收到网络上的消息时, 对于二字节的长度字段应该做颠倒高低字节的处理, 然而 __BYTE_ORDER____ORDER_BIG_ENDIAN__ 这两个宏我都没定义, 所以在预处理阶段 U16_NTOH(val) 宏就直接被定义成了 (val).

那么解决办法就是, 视系统架构而定, 在编译 indigo 的 CPPFLAGS (或者你可能用的是 CFLAGS) 加上几个宏定义:

小端序系统:

CPPFLAGS += -D__ORDER_BIG_ENDIAN__=0 -D__ORDER_LITTLE_ENDIAN__=1 -D__BYTE_ORDER__=__ORDER_LITTLE_ENDIAN__

大端序系统:

CPPFLAGS += -D__ORDER_BIG_ENDIAN__=0 -D__ORDER_LITTLE_ENDIAN__=1 -D__BYTE_ORDER__=__ORDER_BIG_ENDIAN__

(End)

通用 Makefile 模板骨架

Makefile 仍然是绝大多数 Linux 平台 C 项目用的最多的构建方案, 可能由于 Makefile 本身写起来麻烦, 后来又出现了 CMake 这样的帮助人们自动生成 Makefile 的方案. 但我个人觉得 Makefile 本身写起来也并不麻烦, 反倒是使用 CMake 的话还要再去学习一套语法, 多此一举. 人生苦短, 学习那么多冗余的东西干什么呢.

所以我希望最好是能直接就有一个通用一点的纯 Makefile 模板, 便于我们在开始新项目时快速的拿过来套用上, 但是搜索了一圈也没找到有这样的项目, 所以就自己大概研究一下吧.

于是这篇文章的目的, 是展示如何绘制一个基本的, 方便扩展的, 通用的 Makefile 项目构建模板, 以便于将来需要开展新项目时直接套用.

项目的地址在这里: https://github.com/cifer-lee/Makefile.skel

暂时本着两点原则

  1. 能够自动推断依赖
  2. 结构精简, 易读

骨架

最原始

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
        cc -o edit main.o kbd.o command.o display.o \
                   insert.o search.o files.o utils.o

main.o : main.c defs.h
        cc -c main.c
kbd.o : kbd.c defs.h command.h
        cc -c kbd.c
command.o : command.c defs.h command.h
        cc -c command.c
display.o : display.c defs.h buffer.h
        cc -c display.c
insert.o : insert.c defs.h buffer.h
        cc -c insert.c
search.o : search.c defs.h buffer.h
        cc -c search.c
files.o : files.c defs.h buffer.h command.h
        cc -c files.c
utils.o : utils.c defs.h
        cc -c utils.c
clean :
        rm edit main.o kbd.o command.o display.o \
           insert.o search.o files.o utils.o

引入变量

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)
main.o : main.c defs.h
        cc -c main.c
kbd.o : kbd.c defs.h command.h
        cc -c kbd.c
command.o : command.c defs.h command.h
        cc -c command.c
display.o : display.c defs.h buffer.h
        cc -c display.c
insert.o : insert.c defs.h buffer.h
        cc -c insert.c
search.o : search.c defs.h buffer.h
        cc -c search.c
files.o : files.c defs.h buffer.h command.h
        cc -c files.c
utils.o : utils.c defs.h
        cc -c utils.c
clean :
        rm edit $(objects)

引入隐式规则

隐式规则是 gnu make 中的一个大话题, 包含很多方面, 其中一方面就是 recipe 的自动推导.

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
        rm edit $(objects)

自动生成依赖

缺陷

截至目前, 我们拥有了如下的 Makefile

# Where are the source files
src_dir = src

# Where the object files go
obj_dir = obj

# The name of the executable file
elf_name = ryuha

CFLAGS =
LDFLAGS =
LDLIBS =

# All the source files ended in '.c' in $(src_dir) directory
srcs := $(wildcard $(src_dir)/*.c)

# Get the corresponding object file of each source file
objs := $(patsubst $(src_dir)/%.c,$(obj_dir)/%.o,$(srcs))

# Get the dependency file of each source file
deps := $(patsubst $(src_dir)/%.c,$(obj_dir)/%.d,$(srcs))

all : $(obj_dir)/$(elf_name) ;

$(obj_dir)/$(elf_name) : $(objs)
    $(CC) $(LDFLAGS) -o $@ $(objs) $(LDLIBS)
    @echo
    @echo $(elf_name) build success!
    @echo

$(obj_dir)/%.o : $(src_dir)/%.c | $(obj_dir)
    $(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@

lt;

$(obj_dir)/%.d : $(src_dir)/%.c | $(obj_dir)
$(CC) -MM $(CFLAGS) $(CPPFLAGS) -MF $@ -MT $(@:.d=.o)


lt;

-include $(deps)

$(obj_dir) :
@echo Creating obj_dir ...
@mkdir $(obj_dir)
@echo obj_dir created!

clean :
@echo "cleanning..."
-rm -rf $(obj_dir)
@echo "clean done!"

.PHONY: all clean

这个方式看起来工作的不错, 但是有一个缺陷, 假设程序目录如下:

test/
    src/
        a.c
        a.h
        b.h
    Makefile

a.c:

#include "a.h"

int main()
{
    return 0;
}

a.h:

#define A   1

b.h:

#define B   1

现在执行 make, 会输出如下信息:

cli@sanc:/tmp/test$ make
Creating obj_dir ...
obj_dir created!
cc -MM  -w -pthread -pipe  -MF obj/a.d -MT obj/a.o src/a.c
cc -c  -w -pthread -pipe  -o obj/a.o src/a.c
cc  -o obj/ryuha obj/a.o 

ryuha build success!

经过检查, 目录结构变成了

test/
    src/
        a.c
        a.h
        b.h
    obj/
        a.d
        a.o
        ryuha
    Makefile

而且 obj/a.d 的内容如下:

obj/a.o: src/a.c src/a.h

工作的不错! 然后这里有一个隐患, 就是在我们的规则中 obj/a.d 会随着 src/a.c 的更新而更新, 然而假如 src/a.c 引用的头文件中又增加了新的头文件, obj/a.d 却不会跟着更新, 但是 src/a.c 的头文件依赖链确实改变了.

我们将 src/a.h 改成如下来验证一下:

#define A   1

#include "b.h"

然后再次执行 make

cli@sanc:/tmp/test$ make
cc -c  -w -pthread -pipe  -o obj/a.o src/a.c
cc  -o obj/ryuha obj/a.o 

ryuha build success!

然后再来看一下 obj/a.d 文件, 结果依然是如下内容, 没有任何变化:

obj/a.o: src/a.c src/a.h

这下隐患揪出来了, a.o 的生成现在绝对依赖 b.h, 但是 a.d 里却没有记录它! 后面的例子就显而易见了, 下面我们修改 b.h 的内容为如下:

b.h:

#define B   3

然后再次执行 make, 结果你应该也想到了, 就是 “Nothing to be done” !!!

cli@sanc:/tmp/test$ make
make: Nothing to be done for 'all'.

那么如何解决呢?

让 a.d 的生成也依赖于 a.o 所依赖的头文件

上面的问题, 说白了, 就是 a.d 的依赖只有 a.c, 导致了即使 a.c 引用的头文件变了, 但只要 a.c 不变, a.d 就不会重新生成, 所以解决办法就是让 a.d 也依赖 a.c 的那些头文件. 这里引用 gnu make 官方文档中的一个巧妙的方法, 那就是在生成 a.d 文件之后, 把 a.d 也放进目标字段中, 放在 a.o 的后面, 也就是 a.d 的内容升级成如下的样子:

obj/a.o obj/a.d: src/a.c src/a.h

怎么实现呢, 就是将 Makefile 中生成 .d 的规则作如下修改, 这就是 gnu make 官方文档的方法:

$(obj_dir)/%.d : $(src_dir)/%.c | $(obj_dir)
    @set -e; rm -f $@; \
    $(CC) -MM $(CFLAGS) $(CPPFLAGS) -MT $(@:.d=.o)

lt; > $@.$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$ > $@; \
rm -f $@.$$

这样再重复上面的实验, 就会发现没有问题了.

优雅?

上面的写法看似完美, 但实际上还有一点不够优雅的地方. 就在于 include $(deps) 指令

-include $(deps)

我们知道, make 在解析 Makefile 文件时碰到 include 时, 会把 include 后面的每一个文件都作为一个需要更新的目标, 这里 include $(deps) 中是所有 .c 文件对应的 .d 文件, 所以只要 .d 不存在, .d 的菜谱就会被执行.

接着上面的实验, 让我们现在执行一下 make clean, 很好, 输出如下:

cli@sanc:~/Makefile.skel$ make clean
cleanning...
rm -rf obj
clean done!

然而如果我们再执行一次 make clean 呢?

cli@sanc:~/Makefile.skel$ make clean
Creating obj_dir ...
obj_dir created!
cleanning...
rm -rf obj
clean done!

嗯? 怎么会多出一个创建 obj dir 的动作? 这就是因为第一次 clean 时将 obj/ 下面的内容清理了, 第二次 clean 时 include $(deps) 指令发现 .d 文件没了, 于是执行 .d 的菜谱.

这个要解决这个问题, 一种办法是判断 make 的目标, 如果目标是 clean 的话, 就不调用 include $(deps):

ifneq ($(MAKECMDGOALS), clean)
-include $(deps)
endif

更好的解决方法

上述方法的缺点是如果将来加入更多的与生成最终程序无关的目标, 那就需要将那个目标也加入这个条件语句中, 这样又显得难看了.

所以终极解决方法就是,

CPPFLAGS += -MMD

并去掉生成 .d 的规则, -include $(deps) 指令保留着前面的 -, 这样一来, 在 make clean 时, 即使找不到 .d, 但由于没有生成规则, 也就不会执行多余的命令了.

.d 会在 .o 的规则中生成, 第一次 make 的时候, .d 和 .o 都是没有的, 因此 .o 只依赖 .c, 一旦菜谱执行, .d 就生成了, 包含了 .o 所有的依赖. 后续只要这些依赖变动过, .o 就会更新 — 并且顺便把 .d 也更新.

套用第一个问题, 很容易发现这个方法能够解决第一个问题的:

  1. 第一次 make 时, 此时 a.d 不存在; a.d 生成, 包含了 a.c, a.h 这两个依赖
  2. 在 a.h 中添加 include "b.h"
  3. 第二次 make, a.d 已经存在, 被包含进来, a.o 的两个依赖 a.c, a.h 也被感知. 而且 a.h 变化了, 所以 a.o 重新生成, 顺便 a.d 也重新生成了, 而 a.d 这次重新生成发觉了 a.h 对 b.h 的依赖, 于是 b.h 也被加到 a.d 中

各种自动生成依赖方法的比较

下面这篇文档是 gnu make 的维护者写的, 分析比较了各种自动生成依赖的方式的区别

Auto-Dependency Generation

Tips

include 指令的用意

  1. 让各个模块各自的 Makefile 使用一些公共的变量以及 pattern rules.
  2. 当自动生成目标的依赖时, 生成的依赖可以放在一个单独的文件里, 然后使用 include 命令包含这个文件

Makefile 的重新生成

make 如何读入多个 Makefiles

  1. 通过指定多个 -f 选项
  2. 通过 MAKEFILES 环境变量
  3. 通过 include 指令

二阶段式

阶段一

隐式规则

关于隐式规则中, 目标和依赖中的 / 问题, 时常看一看 https://www.gnu.org/software/make/manual/make.html#Pattern-Match 就都明白了

禁忌

不要使用如下的风格

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

这种风格以依赖为中心, 所有依赖此项目的目标们放在一起写, 可读性非常差

不要使用 FORCE 规则

这里说的 FORCE 规则指的是没有依赖没有菜谱的规则, 这种规则的唯一作用是作为其它规则目标的依赖, 好在执行其它规则时无视目标的新旧, 总是执行菜谱.

这个小 trick 仅用在其它版本的不支持 .PHONY 关键字的 make 上, 在 gnu make 中, 我们应该使用表意更明确的 .PHONY 关键字.

OpenFlow 协议匹配结构进化与 OXM TLV 解读

在 OpenFlow 流表定义中, 报文匹配表项是个重要的行为, 我们下发的每一个流表表项都可以包含一到多个匹配项, 报文进来时会与这些匹配项比较, 如果匹配成功的话, 表项中动作也相应的被附加到报文上. 典型的匹配项有入端口, 源 MAC 地址, 目的 MAC 地址, VLAN Id, 源 IP 地址, 目的 IP 地址, MPLS 标签等等. 不难想象, 网络中的报文种类繁杂, 想要能够匹配每一种报文, 光匹配项就能列一个长长的列表出来, 并且随着各种网络通信协议的不断演进以及越来越复杂的网络业务, 这些匹配项将来还会可能还会增加. 所以在 OpenFlow 协议中, 选择一种合适的数据结构来描述这些匹配项就显得重要起来.

早期的匹配结构

在 OpenFlow 协议早期的版本中, 使用一种固定的数据结构来表述所有的匹配项, 这个数据结构长度固定并且所有的匹配项都包含在里面:

enum ofp_match_type {
    OFPMT_STANDARD,             /* The match fields defined in the ofp_match
                                    structure apply */
};

/* Fields to match against flows */
struct ofp_match {
    uint16_t type;              /* One of OFPMT_* */
    uint16_t length;            /* Length of ofp_match */
    uint32_t in_port;           /* Input switch port. */
    uint32_t wildcards;         /* Wildcard fields. */
    uint8_t dl_src[OFP_ETH_ALEN]; /* Ethernet source address. */
    uint8_t dl_src_mask[OFP_ETH_ALEN]; /* Ethernet source address mask. */
    uint8_t dl_dst[OFP_ETH_ALEN]; /* Ethernet destination address. */
    uint8_t dl_dst_mask[OFP_ETH_ALEN]; /* Ethernet destination address mask. */
    uint16_t dl_vlan;           /* Input VLAN id. */
    uint8_t dl_vlan_pcp;        /* Input VLAN priority. */
    uint8_t pad1[1];            /* Align to 32-bits */
    uint16_t dl_type;           /* Ethernet frame type */
    uint8_t nw_tos;             /* IP ToS */
    uint8_t nw_proto;           /* IP protocol or lower 8 bits of ARP code */
    uint32_t nw_src;            /* IP src address */
    uint32_t nw_src_mask;       /* IP src address mask */
    uint32_t nw_dst;            /* IP dest address */
    uint32_t nw_dst_mask;       /* IP dest address mask */
    uint16_t tp_src;            /* TCP/UDP/SCTP source port */
    uint16_t tp_dst;            /* TCP/UDP/SCTP dest port */
    uint32_t mpls_label;        /* MPLS label */
    uint8_t mpls_tc;            /* MPLS TC */
    uint8_t pad2[3];            /* Align to 64 bit */
    uint64_t metadata;          /* Metadata between tables */
    uint64_t metadata_mask;     /* Mask for metadata */
}

这个是 OpenFlow v1.1 中的匹配结构体的定义, 其中 type 字段的取值只有一个就是 OFPMT_STANDARD. length 代表整个匹配结构体的长度, 它的值可想而知也是固定的, 就是整个结构体的长度 88 字节. wildcards 字段是一个位标志符, 在一次流表下发中, ofp_match 中的匹配字段并不都会被用到, 协议规定哪些匹配字段被用到, 其 wildcards 中的对应位就被清 0, 否则就置 1. 其余的字段就是各个匹配项了, 其含义想必一目了然, 不需多做解释.

通过以上的定义不难看出, 早期的 OpenFlow 协议对于匹配项处理的不是太好, 一个是匹配结构体固定, 所有的匹配项都包含在一起, 下流表时即时不需要这个匹配项, 也要一并下发下来, 加大了网络开销; 另外最重要的是这个定义毫无扩展性, 想要增加新的匹配项就等于再更新一版新的 OpenFlow 协议.

在后续的 OpenFlow 协议中, 采用了另一种新的定义解决了这些问题.

新的匹配结构

从 OpenFlow 1.2 开始, 一种新的匹配结构被定义出来, 这种结构被称作 OpenFlow Extensible Match, 简称 OXM, 它采用 Type-length-value 结构, 所以也被称作 OXM TLV.

enum ofp_match_type {
    OFPMT_STANDARD = 0,       /* Deprecated. */
    OFPMT_OXM      = 1,       /* OpenFlow Extensible Match */
};

/* Fields to match against flows */
struct ofp_match {
    uint16_t type;             /* One of OFPMT_* */
    uint16_t length;           /* Length of ofp_match (excluding padding) */
    /* Followed by:
     *   - Exactly (length - 4) (possibly 0) bytes containing OXM TLVs, then
     *   - Exactly ((length + 7)/8*8 - length) (between 0 and 7) bytes of
     *     all-zero bytes
     * In summary, ofp_match is padded as needed, to make its overall size
     * a multiple of 8, to preserve alignement in structures using it.
     */
    uint8_t oxm_fields[0];     /* 0 or more OXM match fields */
    uint8_t pad[4];            /* Zero bytes - see above for sizing */
};
OFP_ASSERT(sizeof(struct ofp_match) == 8);

这个结构体定义以及注释都是取自 OpenFlow 1.4 的源码, 从其注释中可以看到 OFPMT_STANDARD 类型的匹配项已经废弃不用了, 所以 ofp_match 结构体的 type 字段今后的取值将总是 OFPMT_OXM. oxm_fields 字段表示的是一组 OXM TLV 的集合, 可能是 0 个, 也可能多个, 从这可以看出整个 ofp_match 结构是一个变长的结构, 在控制器下发消息时, 只需要包含需要的匹配项, 不需要的匹配项无需包含在消息体中, 省去了不必要的开销.

那么 OXM TLV 的格式到底是如何的呢?

OXM TLV

每一个 OXM TLV 都一定包含一个 4 字节的头, 对于 OpenFlow 标准所定义的匹配项, oxm_class 的取值固定为 0x8000. oxm_field 表示具体的匹配项, 比如源 MAC, VLAN ID 等. oxm_length 表示此 OXM TLV 的值的长度, 以字节为单位. M 字段则表示这个 OXM TLV 是否包含掩码, 在 OXM TLV 中, 掩码的长度和值的长度是一样的, 掩码中的某位为 1 表示报文中匹配项对应位必须和值的对应位相同才能匹配, 掩码中的某位为 0 则表示对报文中匹配项对应位的值不做限制. 如果包含了掩码, 那么报文的匹配将会变成先和做掩码按位与操作, 结果再和 OXM TLV 的值进行比较. 如果下发的消息里没有包含掩码, 那就需要报文与 OXM TLV 的值完全匹配才行.

31                         15          8             0
------------------------------------------------------
|       oxm_class         | oxm_field |M| oxm_length |
------------------------------------------------------

OpenFlow 1.4 中定义的标准匹配项有如下这些:

/* OXM Flow match field types for OpenFlow basic class. */
enum oxm_ofb_match_fields {
    OFPXMT_OFB_IN_PORT        = 0,  /* Switch input port. */
    OFPXMT_OFB_IN_PHY_PORT    = 1,  /* Switch physical input port. */
    OFPXMT_OFB_METADATA       = 2,  /* Metadata passed between tables. */
    OFPXMT_OFB_ETH_DST        = 3,  /* Ethernet destination address. */
    OFPXMT_OFB_ETH_SRC        = 4,  /* Ethernet source address. */
    OFPXMT_OFB_ETH_TYPE       = 5,  /* Ethernet frame type. */
    OFPXMT_OFB_VLAN_VID       = 6,  /* VLAN id. */
    OFPXMT_OFB_VLAN_PCP       = 7,  /* VLAN priority. */
    OFPXMT_OFB_IP_DSCP        = 8,  /* IP DSCP (6 bits in ToS field). */
    OFPXMT_OFB_IP_ECN         = 9,  /* IP ECN (2 bits in ToS field). */
    OFPXMT_OFB_IP_PROTO       = 10, /* IP protocol. */
    OFPXMT_OFB_IPV4_SRC       = 11, /* IPv4 source address. */
    OFPXMT_OFB_IPV4_DST       = 12, /* IPv4 destination address. */
    OFPXMT_OFB_TCP_SRC        = 13, /* TCP source port. */
    OFPXMT_OFB_TCP_DST        = 14, /* TCP destination port. */
    OFPXMT_OFB_UDP_SRC        = 15, /* UDP source port. */
    OFPXMT_OFB_UDP_DST        = 16, /* UDP destination port. */
    OFPXMT_OFB_SCTP_SRC       = 17, /* SCTP source port. */
    OFPXMT_OFB_SCTP_DST       = 18, /* SCTP destination port. */
    OFPXMT_OFB_ICMPV4_TYPE    = 19, /* ICMP type. */
    OFPXMT_OFB_ICMPV4_CODE    = 20, /* ICMP code. */
    OFPXMT_OFB_ARP_OP         = 21, /* ARP opcode. */
    OFPXMT_OFB_ARP_SPA        = 22, /* ARP source IPv4 address. */
    OFPXMT_OFB_ARP_TPA        = 23, /* ARP target IPv4 address. */
    OFPXMT_OFB_ARP_SHA        = 24, /* ARP source hardware address. */
    OFPXMT_OFB_ARP_THA        = 25, /* ARP target hardware address. */
    OFPXMT_OFB_IPV6_SRC       = 26, /* IPv6 source address. */
    OFPXMT_OFB_IPV6_DST       = 27, /* IPv6 destination address. */
    OFPXMT_OFB_IPV6_FLABEL    = 28, /* IPv6 Flow Label */
    OFPXMT_OFB_ICMPV6_TYPE    = 29, /* ICMPv6 type. */
    OFPXMT_OFB_ICMPV6_CODE    = 30, /* ICMPv6 code. */
    OFPXMT_OFB_IPV6_ND_TARGET = 31, /* Target address for ND. */
    OFPXMT_OFB_IPV6_ND_SLL    = 32, /* Source link-layer for ND. */
    OFPXMT_OFB_IPV6_ND_TLL    = 33, /* Target link-layer for ND. */
    OFPXMT_OFB_MPLS_LABEL     = 34, /* MPLS label. */
    OFPXMT_OFB_MPLS_TC        = 35, /* MPLS TC. */
    OFPXMT_OFP_MPLS_BOS       = 36, /* MPLS BoS bit. */
    OFPXMT_OFB_PBB_ISID       = 37, /* PBB I-SID. */
    OFPXMT_OFB_TUNNEL_ID      = 38, /* Logical Port Metadata. */
    OFPXMT_OFB_IPV6_EXTHDR    = 39, /* IPv6 Extension Header pseudo-field */
    OFPXMT_OFB_PBB_UCA        = 41, /* PBB UCA header field. */
};

可以看出相比 OpenFlow 1.1, 这里的匹配项多了很多, 新的匹配结构也提供了方便的扩展匹配项的机制. 前面说了对于 OpenFlow 定义的标准的匹配项, 其 oxm_class 字段的值固定为 0x8000, 如果是其它厂商或组织定义的匹配项, 则可以使用 0xFFFF 这个值. OpenFlow 协议还规定了 oxm_class 取值在 [0x8000, 0xFFFF) 范围内的都留给 OpenFlow 标准, 以备将来协议更新之用; 而 [0x0000, 0x7FFF] 范围内的值则留给 ONF 组织.

OXM TLV 中 payload 的长度

对于某个 OXM 类别下的某个确定的 OXM TLV, 显然其值的长度是一定的, 如果这个 OXM TLV 不包含掩码, 那么其值的长度就是 payload 的长度; 如果包含掩码, 那么 payload 的长度就是值长度的两倍.

关于值的长度, 有一点需要注意的是, 虽然很多 OXM TLV 的值的长度都按 bit 计算的, 比如 IP 报文的 DSCP 字段其实是 6 bits 字段, Vlan Id 是个 12 bits 字段, 但是 oxm_length 字段的单位是字节, 就是说即时 OXM TLV 值用不了一个字节, 也会占用一字节的空间.

这里还有一个细微的问题, 协议 Spec 中没有明确说明的, 就是如果某个 OXM TLV 包含掩码, 并且其值的长度小于 4 bits, 那么这个 OXM TLV 的 payload 最终占用的空间是 1 字节还是 2 字节呢? 这个问题微妙的地方在于值的长度小于 4 bits, 通过上面所说的我们知道如果没有掩码 payload 肯定也是占用 1 字节, 但如果有掩码呢? 在值和掩码加起来还是不超过 1 字节的情况下, 协议会为这种情况做一点空间优化, 让值和掩码 “挤” 进 1 个字节里吗?

答案是不会, 这种情况下 payload 还是会占用 2 字节的空间, 这个问题 OpenFlow 的文档里没有明确的回答, 答案我也是从源码里找到的. 在 OpenFlow 中 OXM TLV 的定义是用 OXM_HEADER 以及 OXM_HEADER_W 这两个宏来定义的, 前者定义不包含掩码的 OXM TLV, 后者则定义包含掩码的 OXM TLV, 而这两个宏其实又都引用了 OXM_HEADER__ 这个宏. 可以看到, 定义中并没有对值小于 4 bits 的 OXM TLV 做什么特殊处理, 就算值只有 1 bit, 定义包含掩码时也要对长度乘以 2 变成 2 字节.

/* Components of a OXM TLV header. */
#define OXM_HEADER__(CLASS, FIELD, HASMASK, LENGTH) \
    (((CLASS) << 16) | ((FIELD) << 9) | ((HASMASK) << 8) | (LENGTH))
#define OXM_HEADER(CLASS, FIELD, LENGTH) \
    OXM_HEADER__(CLASS, FIELD, 0, LENGTH)
#define OXM_HEADER_W(CLASS, FIELD, LENGTH) \
    OXM_HEADER__(CLASS, FIELD, 1, (LENGTH) * 2)

扩展 OXM TLV

我们可以通过 Experimenter 特性来扩展 OXM TLV, 前面已经说了当 oxm_class 字段取值 0xFFFF 时, 就代表这个 OXM TLV 是扩展字段. 另外对于扩展 OXM TLV 最重要的一点就是, 紧跟在 4 字节头后面的一定得是一个 32 bit 的 Experimenter ID, 而不是 OXM TLV 的值. Experimenter ID 可以用来唯一标志厂商或组织, 可以向 ONF 申请分配, 也可以是厂商自己已有的 IEEE 分配的 OUI 号码.

Experimenter ID 之后的内容 OpenFlow 协议就不关心了, 完全由厂商自己来解释.

参考

  1. OpenFlow Spec v1.4.0

关于 OpenFlow 协议中 Instruction, Action 概念的解读

(首发于 sdnlab: http://www.sdnlab.com/17952.html)

阅读任何一个协议都要注意的一点是这个协议中所定义的专有术语, 对这些术语的理解不到位的话也会造成对协议的理解偏差. 本文想和大家分享几个可能容易混淆的术语.

在 OpenFlow 协议文档中经常会看到这么几个词语: Instruction, Action, Apply-actions, Action Set, Action List, Clear-actions, … 有点迷惑人, 实际上这里面只有两个实体的概念: Instruction 和 Action. 为了保持后文的易读性, 这两个概念分别用中文 “指令” 和 “动作” 来描述. 下文中的 “指令” 和 “动作” 都特指在 OpenFlow 协议中的含义.

指令这个词, 特指流表表项中的指令, 当某个报文匹配了这个表项之后, 表项中的指令就会被应用于这个报文; 而动作是比指令更细粒度的概念, 但它并不是局限于流表表项的概念, 动作可以独立于指令而存在, 也可以被包含在指令中, 具体说来, 我们在下流表的时候, 可以为某个表项的某种指令指定一些列的动作, 但是动作并不是只有下流表的时候才会被用到.

本文以目前较新的 Openflow 1.4 版本为准, 来分别看一下指令和动作的含义.

指令

每一个流表的表项都包含一系列的指令, 当报文匹配上了这个表项后, 这些指令就会被执行, 这些指令的执行结果有几种: 改变报文, 改变 action set, 改变 pipeline. 这些指令可以按照其执行结果的不同而分类, 不同的流表的表项包含的指令种类也不同, 前面说了指令可以包含动作, 但也并非所有种类的指令都包含动作, 下面我们一起来看一下指令的分类.

指令的分类

OpenFlow 1.4 中规定了 6 种类型的指令, 但并不要求交换机支持所有的类型. 另外要注意的是, 在 OpenFlow 协议文档中指令的类型名字几乎全都以 “actions” 后缀结尾, 我觉得这是非常容易令人混淆的地方, 我们一定要记住指令类型名中的 “actions” 字样和我们上面说的 “动作” 的概念完全没有关系. 然而 OpenFlow 协议文档的这种写法看起来似乎每种指令都包含了一组动作, 而实际上只有几种指令是真正包含动作的, 下面我们来看一下这 6 种指令与动作的关系.

为了避免我们更加混淆, 6 种指令类型的名字我还是保持和 OpenFlow 1.4 的 Spec 一样. 还有一点需要注意的是, 括号里的 “可选”, “必选” 字样指的是交换机是否必须支持这个指令, 而不是说下流表时表项中是否必须包含这个指令.

  • (可选指令) Meter meter_id, 不包含动作, 行为是将报文送往指定的 meter
  • (可选指令) Apply-actions action(s), 这个指令是真正包含动作的指令, 它的行为是立即对报文应用这些指令, 不要改变报文的 action set
  • (可选指令) Clear-actions, 这个指令并不包含任何的动作, 它的行为是立即清除报文的 action set 中所有的动作
  • (必选指令) Write-actions actions(s), 这个指令真正的包含动作, 它的行为是将自己包含的动作合并到报文的 action set 中
  • (可选指令) Write-Metadata metadata / mask, 这个也不包含动作, 用的不多
  • (必选指令) Goto-Table next-table-id, 这个指令也不包含动作, 它表示把报文交给后续的哪张流表处理. OpenFlow 协议要求交换机必须支持这个 action, 但有一个例外是假设你的交换机本身就只支持一张流表, 那可以不支持这个 action.

动作

前面说了动作也有分类, 但是相比指令的分类, 动作的分类就比较好理解了, 我们稍加带过, 然后解释下 Action Set 和 Action List 两个概念.

同样, OpenFlow 协议不要求交换机支持所有的动作种类, 我们只看几个常见的:

  • (可选) Output, 表示将报文从某个特定的端口送出去
  • (必选) Drop, 丢弃报文
  • (必选) Group, 表示将报文交给指定的组
  • (可选) Change-TTL, 改变报文的 TTL 字段 (可以是 IPv4 TTL, MPLS TTL 或者 Ipv6 Hop Limit)

关于 Action Set

Action set 是一个与报文相关联的概念, 只要提起 action set, 它就一定是报文的 action set, 它包含了当报文离开流表时要附加于这个报文上的动作. 我们前面看到了有一种 Apply-actions 指令, 它是在报文匹配了表项的时候将它包含的动作立即应用到报文上, 而 Write-actions 则是将它包含的动作合并到报文的 action set 中, 另外还有 Clear-actions 指令, 是将报文的 action set 清空. 最终报文走完所有流表时, 其 action set 里面有什么动作, 就执行什么动作, 这就是 action set 的作用了.

关于 Action List

Action list 实际上就是一系列动作的有序序列, 一定要注意其有序性. 在上面说到的流表中的 Apply-actions 指令中, 以及 OpenFlow 协议中同样能够包含动作的 Packet-out 命令中, 都要求所包含的动作被有序执行. 所以就出来了这么个 action list 的概念, 这是与 action set 的一点区别. 另一个区别是 action list 并不是和报文相关联的概念, action list 可以直接夹带在 controller 发给 agent 的消息中, 比如 Packet-out 消息; 也可以存在于流表表项的指令中, 比如 Apply-actions 指令.

协议源代码

说实话, 光看协议 Spec 我是没有理清楚这些个指令与动作的关系的, 真正完全理清楚是看了 OpenFlow 源码之后. 在 OpenFlow 源码中, 指令与动作的结构头分别如下:

struct ofp_instruction_header {
    uint16_t type;          /* One of OFPIT_*. */
    uint16_t len;           /* Length of this struct in bytes. */
};
OFP_ASSERT(sizeof(struct ofp_instruction_header) == 4);

struct ofp_action_header {
    uint16_t type;          /* One of OFPAT_*. */
    uint16_t len;           /* Length of action, including this
                               header. This is the length of action,
                               including any padding to make it
                               64-bit aligned. */
};
OFP_ASSERT(sizeof(struct ofp_action_header) == 4);

Meter, Write-metadata, Goto-Table 这三类指令的结构如下, 它们的前两个字段和 struct ofp_instruction_header 是相同的, 另外可以看出, 它们都不包含 struct ofp_action_header 结构体, 所以这三个指令是不包含动作的.

/* Instruction structure for OFPIT_METER */
struct ofp_instruction_meter {
    uint16_t type;            /* OFPIT_METER */
    uint16_t len;             /* Length is 8. */
    uint32_t meter_id;        /* Meter instance. */
};
OFP_ASSERT(sizeof(struct ofp_instruction_meter) == 8);

/* Instruction structure for OFPIT_GOTO_TABLE */
struct ofp_instruction_goto_table {
    uint16_t type;             /* OFPIT_GOTO_TABLE */
    uint16_t len;              /* Length is 8. */
    uint8_t table_id;          /* Set next table in the lookup pipeline */
    uint8_t pad[3];            /* Pad to 64 bits. */
};
OFP_ASSERT(sizeof(struct ofp_instruction_goto_table) == 8);

/* Instruction structure for OFPIT_WRITE_METADATA */
struct ofp_instruction_write_metadata {
    uint16_t type;                    /* OFPIT_WRITE_METADATA */
    uint16_t len;                     /* Length is 24. */
    uint8_t pad[4];                   /* Align to 64-bits */
    uint64_t metadata;                /* Metadata value to write */
    uint64_t metadata_mask;           /* Metadata write bitmask */
};
OFP_ASSERT(sizeof(struct ofp_instruction_write_metadata) == 24);

而 Apply-actions, Clear-actions, Write-actions 三种指令则共用如下的结构体, 可以看到它是包含 struct ofp_action_header 的, 你可能会奇怪 Clear-actions 指令不是也不包含动作吗, 为什么也用了这个结构体, 实际上对于 Clear-actions 指令来说, struct ofp_instruction_actions 结构体的最后一个 actions 字段是大小为 0 的数组.

/* Instruction structure for OFPIT_WRITE/APPLY/CLEAR_ACTIONS */
struct ofp_instruction_actions {
    uint16_t type;           /* One of OFPIT_*_ACTIONS */
    uint16_t len;            /* Length is padded to 64 bits. */
    uint8_t pad[4];          /* Align to 64-bits */
    struct ofp_action_header actions[0];        /* 0 or more actions associated with
                                                   OFPIT_WRITE_ACTIONS and
                                                   OFPIT_APPLY_ACTIONS */
};
OFP_ASSERT(sizeof(struct ofp_instruction_actions) == 8);

另外上面还说到了一个 Packet-out 消息也是包含动作的, 它的定义如下, actions 字段包含了一个动作列表, 也就是 action list.

/* Send packet (controller -> datapath). */
struct ofp_packet_out {
    struct ofp_header header;
    uint32_t buffer_id;              /* ID assigned by datapath (OFP_NO_BUFFER if none). */
    uint32_t in_port;                /* Packet’s input port or OFPP_CONTROLLER. */
    uint16_t actions_len;            /* Size of action array in bytes. */
    uint8_t pad[6];
    struct ofp_action_header actions[0]; /* Action list - 0 or more. */
                                         /* The variable size action list is optionally followed by packet data.
                                          * This data is only present and meaningful if buffer_id == -1. */
    /* uint8_t data[0]; */               /* Packet data. The length is inferred from the length field in the header. */
};
OFP_ASSERT(sizeof(struct ofp_packet_out) == 24);

参考

  1. OpenFlow Spec v1.4.0

经典的括号匹配问题与最小栈实现

Valid Parentheses

我觉得我实现的这个栈应该是挺简单的了吧.. 回头查查看别人的更短的实现. 不要在意 pop 里的 free 为何被注释掉了, 为了胜率…

typedef
struct stack {
    char            c;
    struct stack   *next;
} Stack;

void create(Stack **ptop)
{
    *ptop = NULL;
}

void push(Stack **ptop, char c)
{
    Stack   *tmp;

    tmp = malloc(sizeof *tmp);
    tmp->c = c;
    tmp->next = *ptop;

    *ptop = tmp;
}

void pop(Stack **ptop, char *c)
{
    Stack   *tmp;

    if (! *ptop) return;

    tmp = *ptop;
    *c = tmp->c;
    *ptop = (*ptop)->next;
    //free(tmp);
}

int empty(Stack **ptop)
{
   return *ptop == NULL; 
}

bool isValid(char* s) {
    Stack   *ss;
    char    c;

    create(&ss);
    while (*s) {
        switch (*s) {
        case '(':
        case '[':
        case '{':
            push(&ss, *s);
            break;
        case ')':
        case ']':
        case '}':
            if (empty(&ss)) return 0;
            pop(&ss, &c);
            if (c + 1 != *s
                && c + 2 != *s) return 0;
            break;
        default:
            break;
        }
        ++s;
    }

    if (!empty(&ss)) return 0;
    return 1;
}

Min Stack

这题得吐槽一下 Leetcode 的系统, 为什么到了这题就只能用 C++, Java, Python 三种语言? 为什么不能用 C 语言了???

typedef
struct stack {
    int            c;
    struct stack   *next;
} C_Stack;

void c_create(C_Stack **ptop)
{
    *ptop = NULL;
}

void c_push(C_Stack **ptop, int c)
{
    C_Stack   *tmp;

    tmp = (C_Stack *)malloc(sizeof *tmp);
    tmp->c = c;
    tmp->next = *ptop;

    *ptop = tmp;
}

void c_pop(C_Stack **ptop, int *c)
{
    C_Stack   *tmp;

    if (! *ptop) return;
    tmp = *ptop;
    *c = tmp->c;
    *ptop = (*ptop)->next;
    //free(tmp);
}

int c_empty(C_Stack **ptop)
{
   return *ptop == NULL; 
}

class MinStack {
public:

    C_Stack   *stack;
    int     min;

    /** initialize your data structure here. */
    MinStack() {
        c_create(&stack);
        min = INT_MAX;
    }

    void push(int x) {
        c_push(&stack, x);
        if (x < min) min = x;
    }

    void pop() {
        int tmp;
        C_Stack *p;

        c_pop(&stack, &tmp);
        if (tmp != min)
            return;

        min = INT_MAX;
        for (p = stack ; p ; p = p->next) {
            if (p->c < min)
                min = p->c;
        }
    }

    int top() {
        int tmp;
        c_pop(&stack, &tmp);
        c_push(&stack, tmp);
        return tmp;
    }

    int getMin() {
        return min;
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

(译)如何使用 C 语言中的 volatile 关键字

(原文: http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword, 已取得翻译许可)

很多 C 程序员都不真正懂得 volatile 关键字的用法. 这无需奇怪, 因为大多数的 C 教程对 volatile 的介绍都比较简单. 这篇文章的目的就是告诉你 volatile 的正确使用方式

你有碰到过下面的几个情形吗?

  • 代码编译运行没问题 — 直到你打开了编译器优化
  • 代码运行的很好 — 直到一个中断发生
  • 古怪的硬件驱动程序
  • RTOS task 各自单独运行时很好 — 直到有其它 task 被 spawned

如果你碰到过上述任何一个问题, 那么就可能是你没有使用 volatile 关键字的原因. 你并不孤单, volatile 关键字为很多程序员所不熟悉. 不幸的是, 很多 C 相关的书籍都没有好好的介绍 volatile 关键字.

volatile 关键字和 const 一样, 是一个限定符, 用于一个变量被声明时. 它告诉编译器, 被声明的变量的值可能随时都会被改变 — 就算使用这个变量的代码的附近 (附近有多近, 要看编译器了, 可能是同一个源文件) 没有任何修改这个变量值的语句也是如此. 给编译器的这个暗示是很严肃的, 在我们继续讲解之前, 我们先来看一下 volatile 的语法.

volatile 关键字的语法

要将一个变量声明为 volatile 的, 需要在声明时将 volatile 关键字写到数据类型关键字的前面或后面. 比如, 下面两条语句都将 foo 声明为一个 volatile 的整数:

volatile int foo;
int volatile foo;

然后下面的例子是声明指向 volatile 变量的指针, 我们经常需要这么做, 尤其是涉及到 memory-mapped I/O 寄存器的时候. 下面两条语句都将 pReg 声明为一个指向 volatile 的, 无符号的 8 位整数的指针:

volatile uint8_t *pReg;
uint8_t volatile *pReg;

指向 non-volatile 变量的 volatile 指针一般不多见 (我可能曾经用过一次), 不过, 我还是给你展示一下语法:

int *volatile p;

为了公平起见, 我再展示一个:

int volatile * volatile p;

Incidentally, for a great explanation of why you have a choice of where to place volatile and why you should place it after the data type (for example, int volatile * foo), read Dan Sak’s column “Top-Level cv-Qualifiers in Function Parameters” (Embedded Systems Programming, February 2000, p. 63).

最后, 如果你将 volatile 关键字应用于一个 struct/union, 那么整个 struct/union 中的所有内容都会是 volatile 的. 如果你不想这样, 那么将 volatile 应用于 struct/union 中的单独的成员就可以了.

volatile 关键字的正确用法

任何时候, 只要这个变量的值可能在不确定的时刻被修改, 那么它都应该被声明为 volatile. 什么是 “不确定的时刻” 呢, 其实总共不过是只有三种情况:

  1. Memory-mapped peripheral registers
  2. 被 ISR 修改的全局变量
  3. 多线程编程中, 被多个 task 共同访问或修改的全局变量

Peripheral Registers

嵌入式系统包含真实的已经, 一般都有着精细复杂的外围. 这些外围包含一些寄存器, 这些寄存器的值可能会改变, 而且是与程序的执行流毫不相干的. 考虑一个非常简单地例子, 一个 8 bit 的状态寄存器, 被应射到内存地址 0x1234. 现在需要你轮询这个状态寄存器, 直到它的值不是 0. 你可能想当然的这样实现:

uint8_t *pReg = (uint8_t *)0x1234;
// Wait for register to become non-zero
while (*pReg == 0) {} // Do something else

只要你打开编译器的优化选项, 上面的代码基本上在哪个架构哪个编译器上都会得到错误的结果, 因为编译器只会生成类似如下的汇编代码:

    mov ptr, #0x1234
    mov a, @ptr

loop:
    bz loop

编译器这么做的道理很简单: 既然代码里没有任何地方会修改地址 0x1234 处的值, 那么对 0x1234 地址的访问一次就够了, 没必要老是访问. 殊不知, 这只是代码里没有改变 0x1234, 但是能够改变 0x1234 处的值的不光是我们的代码, 还有外围啊. 要解决这个问题我们需要给 pReg 加上 volatile 限定符:

uint8_t volatile *pReg = (uint8_t volatile *) 0x1234;

这样一来生成的汇编代码将会是下面这样的:

    mov ptr, #0x1234

loop:
    mov a, @ptr
    bz loop

这样便达到我们的要求了.

Subtler problems tend to arise with registers that have special properties. For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.

Interrupt Service Routines

ISRs 常常会设置或修改那些在主代码里的标识变量. 比如, 串行口中断可能会检查每一个接受到的字符, 来看一下它是不是 ETX 字符 (代表消息结束). 如果这个字符是 ETX, 这个 ISR 就会设置一个全局的标识变量. 一个错误的实现是下面这样的:

int etx_rcvd = FALSE;

void main()
{
    ...
    while (!ext_rcvd)
    {
        // Wait
    }
    ...
}

interrupt void rx_isr(void)
{
    ...
    if (ETX == rx_char)
    {
        etx_rcvd = TRUE;
    }
    ...
}

如果编译器优化选项没打开, 这段代码或许能够正常运行. 但现在像样的编译器都会 “破坏” 上面代码的逻辑, 问题在于 “编译器不知道 etx_rcvd 会在一个 ISR 中被改变. 在编译器看来, !ext_rcvd 总是正确的, 所以你将永远无法推出循环. 更甚者, 所有 while() {} 循环体后面的代码会全部被移除 — 因为它们永远得不到执行. 幸运的话, 你的编译器会警告你的; 不幸运的话, 或者你从不在意编译器的警告的话, 你的代码将会失败的让你很难找到原因.

解决方法也是, 将 etx_rcvd 声明为 volatile.

多线程 (tasks) 应用程序

在实时操作系统的通信机制中, 撇开队列, 管道这些高大上, 共享内存 (这里说的就是全局变量) 仍然不失为一个多 tasks 间通信的好办法. 就算你使用了一个抢占式的调度器, 编译器也依然无法知道什么是 context switch, 更不知道 context switch 何时会发生. 因此, 另一个 task 修改一个全局变量, 概念上就和 ISR 修改这个全局变量是一样的. 因此所有的共享的全局变量都应该被声明为 volatile.

int cntr;

void task1(void)
{
    cntr = 0;

    while (cntr == 0)
    {
        sleep(1);
    }
    ...
}

void task2(void)
{
    ...
    cntr++;
    sleep(10);
    ...
}

同样你需要将 cntr 声明为 volatile, 否则, 编译器优化选项一开, 代码运行就很可能不是你想要的结果.

最终幻想

有一些编译器允许你隐式的将所有的变量声明为 volatile 的. 请抵制这种诱惑! 这会让你不再去思考. 同样也会导致产生低效率的程序.

同样, 抵制住想要责备编译器优化或者要关闭它的想法. 当今的编译器都非常优秀, 我已经不记得上次发现编译器优化的 bug 是哪年哪月了.

如果你有一段运行结果令你匪夷所思的代码, 你要修复它, 那么你可以先 grep 一下 volatile, 如果结果是空, 那么就可以按照上面的思路考虑一下是不是没加 volatile 导致的.