分类目录归档:Uncategorized

挖矿算法比较

下文介绍的算法基本上是对抗 ASIC 的发展史.

 

什么是哈希?

 

消息摘要

 

SHA256

 

CPU 型, non ASIC  resistant

 

Scrypt

 

Scrypt 大量使用内存, 起初号称 GPU resistant, 结果失败了, 又号称 ASIC resistant, 结果还是失败了.

 

因为早期内存价格高, 而现在内存价格下降, 厂商经过努力也研发出了速度还可以的针对 scrypt 算法的 ASIC 矿机.

 

Scrypt 在 ASIC 对抗上做的努力也不是没有成效, Scrypt 的 ASIC 矿机速度确实是比 SHA256 ASIC 矿机速度低. 蚂蚁 S9 (SHA256) 算力可达到 14T/s, 而同期的 Scrypt 矿机蚂蚁 L3+ (Scrypt) 算力才 500M/s.

 

相关链接:

https://bitcoin.stackexchange.com/questions/49199/how-is-scrypt-asic-resistant

 

Ethash

 

以太坊的哈希算法, 基于 Dagger-Hashimoto 改造. 内存型,并且所需内存周期性增长。

 

关于 Ethash 抵抗 ASIC 属性, https://ethereum.stackexchange.com/questions/16811/is-ethereum-asic-resistant 分析的很好.

 

首先由从以太坊白皮书可知, Ethash 要求矿工从最后几个区块里随机取几个交易算出来. 由于以太坊的交易可能包含智能合约, 智能合约中的运算可能是各种类型, 要计算各种类型的计算这是 CPU 的工作, 这就使得 ASIC 无用武之地.

 

其次, 白皮书里没说的一点是 Ethash 需要内存一定要快, ASIC 需要外挂 DRAM, 而 DRAM 的速度是完全比不上显卡里的 SRAM 的.

 

这两点正中 ASIC 要害, 所以从 whattomine.com 来看 Ethash 目前还是要用显卡挖.

 

但是前段时间也确实爆出了比特大陆要推 E3 以太坊矿机,参数都出来了,从暴露的参数来看,E3 矿机配备了 72G 大的 DDR 内存,相比比特币矿机 S9 只有 512M 内存,真是鲜明对比。

 

深度分析:比特大陆入局以太坊矿机将给行业带来怎样的影响?

https://36kr.com/p/5127311.html

 

以太坊社区针对此事进行了讨论,V 神认为这件事不是太大的威胁: https://cryptoslate.com/vitalik-buterin-ethash-asics-ethereum/

 

Equihash

 

Equihash 是 Zcash 采用的哈希算法,后来也被 ZenCash,BitcoinGold 所采用。

 

Equihash 也是内存型算法,其抵抗 ASIC 的关键是找到两个合适的常数来使得算法耗内存最大,一般所说的 Equihash 算法都是指这两个常数为 <200,9> 时的 Equihash 算法。

 

2018 年 4 月, 比特大陆推出 Z9 mini 矿机针对 Equihash 算法。

 

2018 年 6 月,BitcoinGold 发现了 Equihash 算法的另一对常数 <144,5> 能够耗更多的内存: https://forum.bitcoingold.org/t/our-new-equihash-equihash-btg/1512, 这篇文章也深入介绍了 ASIC 的短板,值得一看

 

Zhash

 

Zhash 是基于 Equihash 的强化版,由 bitcoinz 项目开发引入(待确认)

 

CryptoNight

 

特点是依赖 CPU 三级缓存,L3 缓存是 ASIC 和显卡所不配备的装置。 所以 CryptoNight 不但防 ASIC,还防显卡。

 

然而,2018 年 3 月, 比特大陆推出 X3 矿机针对 CryptoNight 算法。为此,门罗币还特意进行了硬分叉来对抗 X3 矿机。

 

相关链接:

CryptoNight算法沦陷,ASIC矿机即将上市,XMR矿工告急? https://zhuanlan.zhihu.com/p/34632126

 

XMR顺利硬分叉,ASIC矿机出局,门罗社区保卫战胜利

https://mp.weixin.qq.com/s?__biz=MzUyNzMzMjE5MA==&mid=2247485689&idx=1&sn=e9d352a5ed86471e45299d518e8d23f9&chksm=fa006ec8cd77e7de2d0e59a99ea04cafaab67a8ad9cf376311e4e86abf46fcf41e1ed972303d&scene=21#wechat_redirect

 

X11, X13, X15 等

 

当 Scrypt 也被矿机攻陷, 达世币带着 X11 就出来了. X11 是 11 种算法串联计算, 上一个算法的输出作为下一个算法的输入, ASIC 上要实现这个逻辑要花一段时间.

 

X11 的 11 个算法是: blake, bmw, groestl, jh, keccak, skein, luffa, cubehash, shavite, simd, echo, 这 11 个都是 SHA3 家族算法 (bitcoin 出现时 SHA3 还未问世, 所以用的是 SHA2 家族的 SHA256 算法). 代码在 https://github.com/dashpay/dash_hash/blob/master/dash.c.

 

但 X11 最终也被矿机攻陷. 2017 年 8 月, 比特大陆发布蚂蚁 D3 矿机, 算力可达 34G/s.

 

相关链接

https://bitcointalk.org/index.php?topic=599319.0

https://bitcointalk.org/index.php?topic=599319.msg6626544#msg6626544

 

X 系列算法组成: https://getpimp.org/what-are-all-these-x11-x13-x15-algorithms-made-of/

 

另外 D3 矿机的出现还被认为毁了达世币 http://www.qukuaibiji.com/dash-kuangji.html, 看完就知道为什么各个币都要对抗 ASIC 矿机了.

 

X16R

 

RavenCoin 提出的算法. 不仅仅不 X11 多了几个算法, 这些个算法的次序还是可以改变的, 使用前一个区块的哈希值决定本区块的算法次序. 这使的再次在一段时间内不会出现 ASIC 矿机.

 

有句话这样说:

 

POW机制必然会导致中心化,专业程度越高,获得的市场份额就越多。所有的POW算法都无法阻挡ASIC矿机的出现,最多只能拖延一时。你可以用 Scrypt 算法来阻挡 SHA-256 的矿机,你可以用 X11 算法来阻挡 Scrypt 算法的矿机,顶多只能做到这样了。只要币价足够高,一定会导致ASIC矿机的出现。

 

X16R 所用的算法是 X15 + SHA512.

 

相关链接

https://bitcointalk.org/index.php?topic=3242152.0

 

Cifer

五月 13, 2018

我们常常需要根据程序运行的宿主机的 cpu 核心数来决定程序要创建的线程或进程, 在物理机上查看 cpu 核心数可以通过 /proc/cpuinfo, 而在 docker 上就不行了.

因为 docker 容器可以利用 --cpuset-cpus 选项配置可以使用的 cpu 核数, 而 /proc/cpuinfo 反应的却是物理机的情况, 所以就不能用它来获取实际 docker 运行时实际拥有的核数了, 需要通过其他方式来获得, 详情参见:

  1. https://github.com/moby/moby/issues/20770
  2. https://stackoverflow.com/questions/47545960/how-to-check-the-number-of-cores-used-by-docker-container/47547987#47547987

C++ 的左右值与左右值引用

左值与右值

C++ 中左值和右值的概念来源于 C, 在 C 中左值和右值的区别很简单, 能出现在赋值号左侧的就是左值, 否则就是右值. 比如变量是左值, 字面常量或者 const 定义的常量是右值.

然而在 C++ 中, 左值和右值的区别就不再是那么简单了, 甚至和 C 还会有冲突, 比如在 C 中 const 定义的常量对象是右值, 而在 C++ 中却是左值.

实际上在 C++ 中左值和右值的情况非常复杂, 有时区分他们也是非常困难的. Scott Meyers 大师在其 Effective Modern C++ 一书所说的不失为一个好方法, 在理解这句话之前, 我们一定要有一个意识, 就是左值和右值是表达式的属性, 代表着表达式的运算结果是左值还是右值.

A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can’t, it’s usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue.

也就是说, 表达式是左值还是右值, 就看这个表达式的结果是否在内存有对应的存储区域, 有的话就是左右, 没有的话就是右值 (TODO: 补充例子), 这点也可以参考 c++ primer 5th, p121.

另外还有两个有重要的准则:

  1. 对象 (对象是最简单的表达式) 被用作左值时, 用的是对象的地址; 被用作右值时, 用的是对象的值
  2. 在需要右值的地方, 可以用左值代替 (因为地址中存储着值), 这时左值被当成右值使用, 用的是左值里存储的值. 这条准则只有一个例外, 就是左值引用不能当做右值引用使用 (下面会讲到引用)

引用

在 C++11 以前, 引用就是对象的别名, 只能绑定到左值上, 并且一旦绑定到了某个对象上就永远绑定了, 不能通过赋值改变引用绑定的对象, 因此在定义引用时必须初始化.

定义引用绑定到对象时, 一般是要求所绑定到的对象的类型和引用的类型必须一致, 不过也有两个例外:

  1. 定义的基类引用可以用派生类对象初始化 (c++ primer 5th, p534)
  2. 常量引用 (对 const 的引用) 可以用任意表达式作为其初始值, 只要该表达式的结果能转换成引用类型即可 (c++ primer 5th, p55)

上面说的引用只能绑定到左值上, 在 C++11 中扩展了引用的概念, 允许引用绑定到右值表达式这样的右值上, 这新引入的引用类型被称为右值引用, 对应的之前的引用被称为左值引用.

那么右值引用有什么用呢? 如果仅仅是让我们能够绑定到字面常量这样的右值的话, 刚刚我们也看到了, 通过常量(左值)引用我们同样也能够绑定到字面常量等右值表达式上, 为什么还要引入右值引用呢?

然而右值引用不仅仅是让我们能够不借助 const 就能引用右值表达式, 它还支持了对象移动以及完美转发这两个重要的能力. 关于这两个能力就是另外的话题了, 我们简单介绍一下对象移动.

对象移动

对象移动的目的是减少对象拷贝, 被移动的对象内容会被 “掏空”, 赋予新对象中, 这个操作依赖于 “移动构造函数” 或者 “移动操作符”, 然而如何定义这两个方法却成了一个问题, 我们知道拷贝构造函数和赋值操作符是以对象的引用 (左值引用) 为参数的, 而如果要定义移动构造函数或移动操作符, 其参数当然也得以代表对象的某种形态为参数, 对象的引用这种形态已经不能用了, 对象的非引用更不能用 (原因见 c++ primer 5th, p422), 那么只能新发明一中语义了, 这个新的语义就是右值引用, 写作 T &&. 于是移动构造函数就可以这么定义了:

Foo (Foo && other);

如此一来, 只要传递给构造函数的是右值引用, 就会调用移动构造函数, 免去拷贝所造成的开销.

然而如何传递右值引用呢? 当发生如下情况时, 该调哪个构造函数又成了一个问题:

Foo (const Foo &other);
Foo (Foo && other);

Foo(foo);    // 该调哪个构造函数?

事实上由于拷贝构造函数先出现, 所以上面第 4 行的写法当然是会调拷贝构造函数的. 为了能够调到移动构造函数, 标准库提供了一个 std::move 方法, 这个方法总是返回右值引用, 因此我们只要这么写就能够调用移动构造函数了:

Foo(std::move(foo));

除了显式的通过 std::move 方法, 有些情况下编译器也能够自动判断是否可以调移动构造函数来降低开销, 比如编译器发现源对象是个临时对象.

关于右值引用带来的另一个功能完美转发就不再敖述, 可以参考文末链接 1 关于右值引用的提案.

参考

  1. 右值引用提案: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html

嵌入式哈系表的实现

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

关于哈希冲突的解决, 我们使用链表存储冲突的节点, 这在一些书里被称为 “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, 

通用 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 $@ $<

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

-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) $< > $@.$$$$; \
    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 关键字.

(译)如何使用 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 导致的.

关于文件系统类型与分区类型的区别

今天碰到一个奇怪的问题, 在这里记录下来.

我的 U 盘上有一个分区, /dev/sdd1, 这个分区本来上面的文件系统是 FAT32, 在 fdisk -l 的输出中可以看到, 最后的 System 字段是 HPFS/NTFS/exFAT. 然后我使用 mkfs 将其分区格式化为 ext4 格式: mkfs -v -t ext4 /dev/sdd1. 然而再次运行 fdisk -l, 发现最后的 System 字段仍然是 HPFS/NTFS/exFAT, 并不是预期的 Linux. 为什么呢?

经过我的调查, 底部的三个链接阐述了答案, 这里概括一下:

首先, 我这块 U 盘是使用的 msdos 也就是 MBR 分区表, MBR 分区表中每一个表项都有一个 partition type 字段, fdisk -l 输出的最后的 System 字段就是对应着分区表项里的 partition type 字段. 这个字段表示的是这个分区的类型, 但是不一定是这个分区里装着的文件系统的类型. 有些应用程序会根据这个字段来判断分区里的文件系统的类型, 但是 Linux 下的大多数程序都不使用这个字段来判断分区里的文件系统类型. 在使用 mkfs 创建文件系统时, mkfs 更是不会去碰 MBR 分区表表项里的 partition type 字段, 所以使用 mkfs 创建了分区之后, fdisk -l 输出的依然是以前的类型.

这样的一个分区 (partition type 字段与文件系统不一致), 在 Linux 系统下工作一般是没什么问题的, 但不排除有一些应用程序, 会检查 partition type 字段来判断里面的文件系统, 这样的话, 最好还是手动改一下 partition type 字段比较好.

fdisk 工具的 t 命令就是可以修改 partition type 的, 具体有哪些 type 呢, 可以用 l 命令看一下. ext2, ext3, ext4, ReiserFS, XFS, 等等都对应着 Linux 分区类型, 类型码是 83.

参考

http://superuser.com/questions/643765/creating-ext4-partition-from-console
http://unix.stackexchange.com/questions/18510/why-do-we-need-to-specify-partition-type-in-fdisk-and-later-again-in-mkfs
http://unix.stackexchange.com/questions/114485/fdisk-l-shows-ext3-file-system-as-hpfs-ntfs

PEAR 与 PECL – PHP 原始包管理系统

PEAR

PEAR 全称是 PHP Extension and Application Repository, 和水果 “梨” 的英文发音是相同的. PEAR 存在的目的是:

  • 提供一个有组织结构的开源代码仓库给 PHP 用户们
  • 提供一个代码发布以及包维护的系统
  • 制定一份 PHP 代码风格规范 (在这里: http://pear.php.net/manual/en/standards.php)
  • 运作 PHP Extension Community Lbrary (PECL) 姐妹组织
  • 维护相关的网站, 邮件列表, 源镜像, PEAR/PECL 社区
  • PEAR 是一个社区驱动的组织, 由开发者管理.

PEAR 的使命

PEAR 的使命就是为 PHP 用户提供良好可重用的组件 (避免让用户自造轮子), 以及领导 PHP 革新, 努力为 PHP 开发者提供最佳的开发体验.

由 PHP 书写的结构良好的代码库以及应用

PEAR 中的代码以 “包” 为单元. 每一个包都是一个独立维护的项目, 有专门的开发团队, 有自己的版本号, 发布周期, 项目文档, 以及与其他包的依赖关系信息.

PEAR 中的包都是以 gzip tar 档案格式发布的. 在你的系统上, 你可以使用 “PEAR installer” (http://pear.php.net/package/PEAR/) 来安装这些包.

不同的包之间可以显示的指定依赖关系, 不要根据包的名字相似程度想当然的认为他们有依赖关系.

代码发布与包维护

所有的要发布的包都需要在 pear.php.net 上注册, 这些包当然也可以被下载. pear.php.net 是 PEAR 的中心服务器. 还有很多第三方的服务器, 也可以在上面发布包以及被 PEAR installer 下载安装它们上面的包. 这些第三方的服务器被称为为 Channel.

上面所说的 pear.php.net 以及其他的 Channel, 所提供的包都是不同的, 就是说, 如果一个包在 pear.php.net 里有了, 就不会发布在别的 Channel 里了, 反之亦然. 而且, 绝大部分的 Channel 都是只提供专门的一个包, 通常他们也不会接受发布其他的包, 比如 pear.dropbox-php.com 这个 channel 就只是 dropbox-php 的几个开发者用来发布 dropbox-php 的.

但是也有例外, 比如像是 pecl.php.net, 这是一个专门提供用 C 语言写的扩展的 Channel, 从这个 Channel 下载的扩展, 需要编译, 安装, 然后在 php.ini 里加上相应的 extension=extname.so 行. 而 pear.php.net 上的扩展都是直接用 PHP 写成的, 下载后不需要编译, 因为就是 PHP 代码, 也不是动态库, 所以也不会在 php.ini 里加 extension=extname.so 这样的行, 直接 include 就行了.

pear.php.net 提供了两种介面来展示它上面的那些包, 一种是对人友好的 HTML, 一种是 PEAR installer 友好的 REST 接口. 这两种介面都使用 HTTP 协议.

前面说了, 每一个包都是以 gzip tar 档案形式发布, 其中包含一个 xml 描述文件, 描述这个包的一些信息, 包括这个包里所包含的文件及其作用, 以及这个包的依赖关系等.

关于 PEAR installer

PEAR installer 指的应该就是系统上的 pear 命令, 以及上面链接 (http://pear.php.net/package/PEAR/) 给出的 PEAR 这个包, 从这个链接可以看出, PEAR 这个包是 PEAR 的基础包, PEAR 仓库里相当一部分的包都依赖于 PEAR 包.

另外, 现在又出来了一个 PEAR2, PEAR2 有一个新的 installer, 叫做 pyrus, 是直接用 php 语言写的 (pear 命令, 应该是 C 写的), 压缩成 phar 格式, 可以直接执行 php pyrus.phar 来安装包. PEAR2 应该和 PEAR 是一班人马维护的, PEAR2 的 installer, pyrus, 目的是比 pear 更易用.

PHP Extension Community Lbrary (PECL)

PECL (pronounced “pickle”) is a separate project that distributes PHP extensions (compiled code written in C, such as the PDO extension). PECL extensions are also distributed as packages and can be installed using the PEAR installer with the pecl command.

(以上部分译自: http://pear.php.net/manual/en/about.pear.php)

PECL 和 PEAR 的关系

» PECL is a repository of PHP extensions that are made available to you via the» PEAR packaging system.

这句话简单明了, PEAR 是 PHP 所使用的包管理系统, 而 PECL 是集中存放 PHP 扩展的一个仓库, 这个仓库的扩展是可以使用 PEAR 来安装的. 所以, PEAR 和 PECL 的关系, 就好比 Protage 和 Gentoo 源的关系.

PECL 扩展的安装

通过以上说明, 想必我们都清楚了 PEAR 和 PECL 的关系.

PECL, pecl.php.net, 实际上也是 PEAR 系统的一个 Channel, pecl.php.net 里的包也是可以通过 PEAR installer (pear 命令) 来安装的. 但是, pecl.php.net 这里面的扩展都是 C 语言写的, 这些扩展下载下来之后还需要编译什么的, 编译的时候还需要准备扩展编译的环境, pear 命令当初设计的时候主要是适合于安装 PHP 语言写的扩展的.

因此, pecl 这个工具也出现了, 它是为 pecl.php.net 定制的, 能够自动下载 pecl.php.net 里的扩展包, 准备编译环境, 编译扩展, 然后安装它. 最后要使用的话我们只需在 php.ini 里添加 extension=extname.so 行.
对于 pecl 这个工具, 一般发行版们会将它直接包含到 pear 工具所在的包里. 所以你安装完了 pear, 也就有了 pecl 了.

phpize

有些时候你可能处于防火墙内, 无法使用 pecl (原因我不知, 难道 pecl 还用了 80 以外的端口?), 这时候, 你可以使用 phpize, phpize 是包含在 php 源码包里的一个脚本 (有待验证, 我猜是脚本. 已经过验证, 确实是脚本), 只要你安装过 php, phpize 也就会被安装到你的系统中.

然后当你想要安装新的 PECL 扩展, 而又不能联网使用 pecl 命令时. 就可以到 php 源代码根目录 (显然, 你需要有一个份 php 源代码) 的 extname 目录下, 执行 phpize, 就会给你生成编译这个扩展所需要的编译环境 (主要是 Zend extension framework 或者 PHP extension framework 这两个扩展框架接口).

然后, 只要运行经典的三部曲: ./configure && make && make install 就可以了.

参考

  1. 很好的解释了 PEAR 和 PECL 的关系: http://stackoverflow.com/questions/1385346/what-are-differences-between-pecl-and-pear
  2. 推荐: http://php.net/manual/en/install.pecl.php

Nginx 与 fastcgi pathinfo 配置

不知为何, Nginx 中配置 PATH_INFO 似乎一直以来是一件不那么明朗的事情, 在网上搜索的话, 会搜到各种各样的配置方式. 很多都是网友们自己 “发明” 的. 各大发行版安装好了 Nginx 之后, 默认也是没有配置对 PATH_INFO 的支持的, 怎么会这样呢? 难道 Nginx 就没有一个官方的解决方案吗?

自然是有的.

PATH_INFO 是 CGI 1.1 标准中规定的一个变量, 在 www 服务器委托 CGI 脚本执行任务时, 需要传递给 CGI 脚本的信息. 这么重要的一个变量, Nginx 当然是会支持的. 参考一中就是官方的方案. 我们在这里重复一下.

首先我们知道, 在 nginx 中, 是可以使用 nginx 自带的一些命令, 给 CGI 1.1 中规定的那些变量赋值的, 而这些命令默认都位于 /etc/nginx/fastcgi.conf 或者 /etc/nginx/fastcgi_params 文件里, 在配置 fastcgi 程序处理我们的请求时, 只要在 nginx 中包含这个两个文件之一, fastcgi 程序就能够取得所需要的变量. 在我的系统上, /etc/nginx/fastcgi.conf 文件是这样的:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

/etc/nginx/fastcgi_params 文件的内容与 /etc/nginx/fastcgi.conf 类似, 只是少了 SCRIPT_FILENAME 变量的赋值 (SCRIPT_FILENAME 变量不是 CGI 1.1 要求的), 不过奇怪的是, 这两个文件中默认都没有对 PATH_INFO 的配置.

这样在我们的 cgi 程序里, 就拿不到 PATH_INFO 变量了, 以 PHP 为例, 假如你访问如下的 URI (下面都是以这个 URI 为例子), 会发现 $_SERVER[‘PATH_INFO’] 是空的.

http://localhost/index.php/foo/bar?query=hello

怎么办呢, 好说, 既然默认配置里没给 PATH_INFO 赋值, 那我们就自己加上. Nginx 的 fastcgi 模块, 提供了一条指令 fastcgi_split_path_info, 使用这条指令, 再配上一个正则就能将 PATH_INFO 信息提取出来, 这样我们的 nginx 中的配置如下:

location ~ ^(.+\.php)(.*)$ {
    fastcgi_split_path_info    ^(.+\.php)(.*)$; 
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }
    fastcgi_param  PATH_INFO       $fastcgi_path_info;
    include /etc/nginx/fastcgi.conf;
    fastcgi_pass unix:/run/php-fpm.sock;
}

其中, fastcgi_split_path_info 指令, 会将后面正则匹配出来的 \1 赋值给 $fastcgi_script_name, \2 赋值给 $fastcgi_path_info, 这两个都是 nginx fastcgi 模块的内置变量.

要注意的是, 即使不调用 fastcgi_split_path_info 指令, \$fastcgi_script_name 变量默认也是有值的, 而 \$fastcgi_path_info 默认却是空值 (我用 add_header X-debug-message \$fastcgi_path_info; 调试看过).

取得了 $fastcgi_path_info, 下面就使用了 fastcgi_param PATH_INFO $fastcgi_path_info; 来给 PATH_INFO 赋值, 经过这之后, $_SERVER[‘PATH_INFO’] 就能被填充上值了.

另外, 关于上面的这段代码:

if (!-f $document_root$fastcgi_script_name) {
    return 404;
}

有人问为什么不使用 try_files $fastcgi_script_name =404;, 原因是 try_files 会导致 $fastcgi_path_info 变为空, 具体的原因可以参见这两个链接:

http://trac.nginx.org/nginx/ticket/321
http://forum.nginx.org/read.php?2,238825,238825

以上就是 Nginx 官方 Wiki 里给出的方法, 链接在参考 1 里.

事实上在找到上面的官方的方法之前, 我先搜到了 @Laruence 的博文, 在参考链接 2 里. @Laruence 的方法很简单, 不过需要借助 PHP 的 fix_pathinfo. 使用 @Laruence 的方法, 只需要你的 nginx 的配置文件这么写就行了:

location ~ .php {
    include /etc/nginx/fastcgi.conf
    fastcgi_param    PATH_INFO    $fastcgi_script_name;
    fastcgi_pass unix:/run/php-fpm.sock;
}

刚看的时候, 让我很疑惑, 怎么能直接把 $fastcgi_script_name 赋值给 PATH_INFO 呢? $fastcgi_script_name 的值不是应该就只是 /index.php 吗, 翻了 nginx fastcgi 模块的文档, google 了两下子, 都没有说 $fastcgi_script_name 变量是否包含 PATH_INFO 信息的, 最后我只好自己在 nginx 配置里加 add_header X-debug-message $fastcgi_script_name; 调试了一下才知道, 原来 $fastcgi_script_name 的值不是 /index.php, 而是 /index.php/foo/bar.[1]

原来如此, PATH_INFO 里现在的值是 /index.php/foo/bar, 然后 PHP 的 fix_pathinfo 特性修正 PATH_INFO 的值为 /foo/bar, 就是这样. 所以说, 这种方式是把 PATH_INFO 的解析工作交给 fastcgi 程序去做了, 这里也就是 PHP. 而第一种方式中, 这个工作其实是事先让 nginx 做好.

按照 CGI 的规范, PATH_INFO 本来就是要服务器程序准备好, 传给 CGI 程序的, 所以我个人倾向于官方的方式. 而且, 早先 PHP fix_pathinfo 这种方式已被爆出有 bug, 还是不用的为妙.

另外, 网上还有另外的一些方法, 各种转贴, 千篇一律, 这里就不详细说了, 只贴个配置吧:

location ~ .php {
    include /etc/nginx/fastcgi.conf
    set $script_name $fastcgi_script_name;
    set $path_info    "";

    if ($uri ~ "^(.+?.php)(/.*)$") {
        set $script_name $1;
        set $path_info $2;
    }

    fastcgi_param    PATH_INFO    $path_info;
    fastcgi_param    SCRIPT_NAME    $script_name;
    fastcgi_pass unix:/run/php-fpm.sock;
}

可以看出, 这种方式实际上和官方的方式原理是一样的.

脚注

  1. 如果采用了 Nginx 官方的方式, 在 add_header 输出之前加上了这句: fastcgi_split_path_info ^(.+.php)(.)$;, 那么 $fastcgi_script_name 的值还是会是 /index.php 的. 这里说的是不用 fastcgi_split_path_info ^(.+.php)(.)$; 的方式.

参考

  1. Nginx Wiki, 官方的方法, 推荐: http://wiki.nginx.org/PHPFcgiExample
  2. Laruence 前辈的博文: http://www.laruence.com/2009/11/13/1138.html

FreeRADIUS + PPTP 系统架设指南

搭建这个系统时, 我所使用的服务器是 CentOS 6.5, 下面的步骤如非特别说明, 均以 CentOS 为主.

  1. 首先配置好 Poptop 正常工作, 这个可以找我以前的笔记或者博客

  2. 安装 freeradius

    # yum install freeradius

    在 Debian 下

    # apt-get install freeradius

注意1: 在 Debian 下, 安装完 freeradius 之后, freeradius 就会自动启动, 这点不太好
注意2: 在 Debian 下, 安装 freeradius 时, freeradius-utils 这个包也会被安装上, 所以如果是用的 Debian, 那么下面第三步应该就不用看了

  1. (Centos)  安装 freeradius-utils 包, 下面的步骤里我们会用到 radtest 这个程序, 在 centos 源里, 这个程序在 freeradius-utils 包中

    # yum install freeradius-utils

接下来我们来测试以下我们安装的 freeradius 是否正常工作

  1. 首先修改 /etc/raddb/users 文件 (对于 Debian, 这个配置文件位于 /etc/freeradius/users), 添加一个用户, 找到下面这一行, 取消其注释.

    # steve Cleartext-Password := “testing”

    注意: freeradius 项目主页的文档告诉我们, 尽可能的不要修改所有默认的配置, 这些配置一般是会适合你, 除非清除要修改的配置是干什么的. 在参考 1 的链接中, 它还将下面的几行也取消注释了, 这对于你从另一台机器上测试是是有必要的, 但下面一步我们只是想在 freeradius 服务器上测试, 所以只需取消注释这一行即可

  2. 启动 freeradius

    CentOS: 

    # radiusd -X

Debian:

    # freeradius -X

(在 CentOS 中 man radiusd, 建议我们一开始搭建时应该总是使用 -X 启动 freeradius)

  1. 在另一个终端中执行

    $ radtest steve testing localhost 1812 testing123

这句里面, steve 和 testing 就是刚刚我们配置的用户名和密码, localhost 和 1812 就是 freeradius 运行的 IP 地址和端口号, testing123 则是用于加密 freeradius 服务器与客户端通信的共享 key (虽然在此处我们的 freeradius 服务器端与客户端位于同一台主机上), 这个值是在 /etc/raddb/clients.conf 中定义的. clients.conf 的注释强烈建议这个值一定要被修改, 不要使用默认的 testing123.

radtest 测试成功后会输出类似如下的信息:

   Sending Access-Request of id 211 to 127.0.0.1 port 1812
   User-Name = "steve"
   User-Password = "testing"
   NAS-IP-Address = 127.0.0.1
   NAS-Port = 1812
   Message-Authenticator = 0x00000000000000000000000000000000
   rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=211, length=20

在你运行 radiusd -X 的终端也会看到相应的信息的输出.

  1. 很多 NAS 都自己集成了 radius 客户端的功能, 比如思科华为等公司的路由器交换机等. 但是一般这些产品没有强大到集成了 radius 服务器端. 如果你们的公司是属于这种情况的话, 那么可能你们会直接使用路由器内置的 radius 客户端功能, 这样的话网络拓朴应该是这样的:

                     公网 —————— NAS (RADIUS 客户端) ———— RADIUS Server
                                                       \ 
                                                         \ 
                                                           \
                                                        Devices

对于处于公网的员工来说, 想要通过 PPTP 连接到内网, 就要首先连接到 NAS (这种情况下, NAS 也要有 PPTP server 的功能), 然后 NAS 会到处于内网的 RADIUS Server 那查询身份验证, 授权以及计费信息.

如果, 你们的路由器没有 RADIUS 客户端的功能, 那么可以在内网里找一台服务器, 在其上搭建 PPTP server 以及 RADIUS server, 而且, RADIUS client 的功能也得在这台服务器上实现了. 这种情况下, 你需要在路由器上做一个端口映射, 将 1723 (PPTP 端口) 映射到 RADIUS 这台服务器上.

如果是第一种情况, 也就是 RADIUS Server 和 RADIUS 客户端是分开的, 那么就需要在 RADIUS server 上面配置使其能够不仅接受来自自己本机的连接, 还能接受来自其他主机的连接. 你可以查看 /etc/raddb/clients.conf, 里面有提示告诉你如何去做, 很详细, 比如:

   #
   # You can now specify one secret for a network of clients.
   # When a client request comes in, the BEST match is chosen.
   # i.e. The entry from the smallest possible network.
   #
   # client 192.168.0.0/24 {
   #    secret = testing123-1
   #    shortname = private-network-1
   # }

取消上面的注释, 然后保存, 重启 radius -X, 就可以在和 RADIUS 处于一个内网的其他主机上连接 RADIUS 服务器了. 可以在那台机器上安装 freeradius-utils 包, 然后运行:

    $  radtest steve testing 192.168.0.9 1812 testing123-1     # 假设 RADIUS 服务器 IP 地址是 0.9

mysql 与 freeradius 连接

  1. 创建 radius 数据库

    $ mysql -uroot -p
    > create database radius default character set utf8;

然后退出 mysql 就可以了.

  1. 安装 freeradius-mysql 包

    安装这个包之后, 你会发现 /etc/raddb/sql 这个目录下多了两个目录, mysql 目录和 ndb 目录 (根据源里面 freeradius 包的不同可能你只有一个 mysql 目录). 其中 ndb 只有两个脚本: schema.sql 和 admin.sql. admin.sql 脚本用来创建管理 radius 数据库的专门用户并为其赋予管理 radius 数据库的全部权限. schema.sql 脚本用来创建 radius 数据库里的表.

    mysql 目录下也有着两个脚本, 只不过 ndb 目录下的 schema.sql 脚本创建数据表的时候会指定使用是 ndb 存储引擎. 而 mysql 目录下的 schema.sql 脚本创建数据库时不会指定存储引擎, 也就是使用默认的存储引擎.

    这两个目录下的 admin.sql 脚本的区别是对权限控制粒度的不同, mysql 目录下的 admin.sql 在创建 radius 专门的管理账户时只对其需要写入的数据表赋予写权限, 其他的表都是读权限. 而 ndb 下的 admin.sql 脚本是对这个专门的账户服务 radius 数据库的全部权限.

    freeradius 不会在乎数据库用的是什么存储引擎, 如果你不熟悉 ndb 引擎或者对引擎没有要求, 那么用 mysql 目录下的脚本即可, 何况, 它对权限粒度把握的还细, 更安全.

    mysql 目录下除了 admin.sql 和 schema.sql 之外, 还包含很多其他的 sql 脚本用于不同用处, 我们暂时不用管它们.

  2. 导入 admin.sql 和 schema.sql

    为了简单, 我们就都使用 root 账户导入了:

  $ mysql -uroot -p < admin.sql
    $ mysql -uroot -p radius < schema.sql

其中 admin.sql 创建的账户的名称和密码是 radius / radpass, 我没有做修改

这样, msyql 数据方面的工作就完成了, 然后我们需要再在 freeradius 上添加相关的配置.

  1. 配置 freeradius 让其知道 mysql

    在 /etc/raddb/radiusd.conf 配置文件中, 找到这一行:

    # $INCLUDE sql.conf

取消其注释.

然后编辑 /etc/raddb/sql.conf 文件, 设置数据库类型, 设置访问数据库的账户及密码等, 一般来说下载包的时候源里都会给你做这些事, 如果你没有修改 admin.sql 的话, 那这步应该不用修改什么, 但必须是要确认一下的.

在强调一遍, freeradius 项目主页有文档说, 建议我们尽可能的不要修改默认参数, 除非你知道修改后的影响.

  1. 配置相应的设备, 令其使用 mysql 作为数据存储设备

    # vim /etc/freeradius/sites-available/default

  • 把authorize{}字段下的file注释掉、反注释sql、这里的file指的就是usrs文件、将不再把用户信息写在users而使用mysql来存储用户信息、
  • 把accounting{} 字段下的sql反注释、启用sql来记录统计信息、
  • 把session{}字段下的sql反注释、启用用户同时登录限制功能、这里还需要修改其它地方、一会再说
  • 把post-auth{} 字段的sql反注释、启用用户登录后进行数据记录功能、

    整个文件如下所示:

    authorize {


    # files
    sql

    }


    accounting {

    sql

    }


    session {
    radutmp

       #
       #  See "Simultaneous Use Checking Queries" in sql.conf
       sql
    

    }

    post-auth {

    sql


    }

    如果迩之前如莪一样启动了启用用户同时登录限制功能、那么接下来还要做这一步

    编辑dialup.conf文件

    $ vim /etc/freeradius/sql/mysql/dialup.conf

    找到这几行、将之反注释

    # Uncomment simul_count_query to enable simultaneous use checking
    simul_count_query = “SELECT COUNT(*) \
    FROM ${acct_table1} \
    WHERE username = ‘%{SQL-User-Name}’ \
    AND acctstoptime IS NULL”
    之后整个对mysql的radius配置就已经完成了

整合 PPTP 与 FreeRADIUS

首先我们要搭建一个能够正常工作的 PPTP 服务器, 关于 PPTP 服务器的搭建, 可以参考我另外的日志

另外, 要整合的话可定需要一个 radius 客户端, pptp 自己可没有 radius 客户端的功能. 如果你的路由器已经有了 radius 客户端的功能, 那就不需要了.

  1. 我们从 CentOS 源里下载 radiusclient-ng 这个包:

    # yum install radiusclient-ng

安装完之后, 配置文件位于 /etc/radiusclient-ng 目录下.

  1. 设置共享密钥

    首先在 /etc/radiusclient-ng/radiusclient.conf 文件中, 你应该可以找到如下几行:

    # RADIUS settings

    # RADIUS server to use for authentication requests. this config
    # item can appear more then one time. if multiple servers are
    # defined they are tried in a round robin fashion if one
    # server is not answering.
    # optionally you can specify a the port number on which is remote
    # RADIUS listens separated by a colon from the hostname. if
    # no port is specified /etc/services is consulted of the radius
    # service. if this fails also a compiled in default is used.
    authserver localhost
     
          # RADIUS server to use for accouting requests. All that I
          # said for authserver applies, too.
          #
          acctserver localhost

    这代表 radiusclient-ng 默认认为 radius 服务器是在本机上的, 这和我们的情况相符, 不需改动, 然后我们编辑 /etc/radiusclient-ng/servers 文件, 在其末尾添加:

     localhost         testing123

其中 localhost 就对应上面的 authserver, acctserver 指定的主机. testing123 是我们前面在 /etc/raddb/clients.conf 中指定的值.

  1. 增加 microsoft 字典

    这一步很重要, 如果不加的话, windows 用户将无法通过 freeradius 的验证. 安装完 radiusclient-ng 包之后, 可以从 /etc/radiusclient-ng/radiusclient.conf 文件看出, 它包含了 /usr/share/radiusclient-ng/dictionary 文件, 这个文件是一个总的字典文件, 要包含其它字典文件, 只需在这个文件包含它们即可, 其他 dictionary 文件也位于 /usr/share/radiusclient-ng/ 目录, 这个目录里有很多字典文件, 但是不幸的是, 唯独没有 microsoft 的字典文件, 具体原因我暂时不清楚. 不过 microsoft 的字典文件可以通过其他方式找到. 在 freeradius 项目的 wiki 里 (参考 1) 有提供这个 microsoft 的字典, 将其拷贝下来即可. 另外这个页面还提供了一个 merit 字典, 不过 /usr/share/radiusclient-ng/ 目录里已经有这个字典了, 就不用再下载了.

    然后我们找到 /usr/share/radiusclient-ng/dictionary 文件, 在末尾添包含 merit 和 microsoft 字典 (/usr/share/radiusclient-ng 目录还包含了很多其他字典, 可能以后会需要, 参考 1 中只指定了这两个字典, 我们也先只添加这两个):

    
    INCLUDE /usr/share/radiusclient-ng/dictionary.merit
    INCLUDE /usr/share/radiusclient-ng/dictionary.microsoft

  1. 修改 pptpd 的配置以及 radiusclient-ng 的配置

    /etc/pptpd.conf 需要作如下修改:

  • 确保 noipparam 选项没有被注释
  • 确保 delegate 选项没有被注释
  • 确保 logwtmp 选项被注释

    /etc/ppp/options.pptpd 文件末尾添加如下几行: 

    plugin radius.so
    plugin radattr.so
    radius-config-file /etc/radiusclient-ng/radiusclient.conf         # 如上面所见, 我们的 radiusclient.conf 是位于 /etc/radiusclient-ng 目录下的

    这两个 so 库是 pppd 的插件, 在安装 pppd 的时候就应该已经是安装了. 而 radius-config-file 指定 radiusclient.conf 的位置, 具体看后面的问题 1.

    /etc/radiusclient-ng/radiusclient.conf 这个文件里, 把 bindaddr * 这一行注释掉, 即

     # local address from which radius packets have to be sent
     #bindaddr *

  1. 把 pptp 账户信息转移到 mysql radius 数据库

  2. 重启 pptpd 和 radiusd 服务

    /etc/init.d/pptpd restart
    radiusd -X

现在你在自己电脑连接 pptp, 就是通过 freeradius 验证了.

FreeRADIUS 的 web 管理界面

有很多开源的项目能够帮助你从 web 界面管理 FreeRADIUS 系统, 像我随便一搜就搜到下面这么多:

  1. http://daloradius.com/
  2. http://freeradius.org/dialupadmin.html
    3. http://phpradiusadmin.sourceforge.net/
    4. http://labs.asn.pl/ara/wiki

不过我又稍微深入的了解了下:

  • 第 2 个是 freeradius 官方的, 但是已经不维护了, 最新版本是 2003 年发布的, freeradius 页面 http://wiki.freeradius.org/guide/Dialup-admin 上面明确说了这是 PHP4 写的, 可能在新版本 PHP 上不能好好工作. 弃之.
  • 第 3 个最新版本是 1.0, 是 2010 年 12 月发布的.  4 年没更新了. 弃之.
  • 第 4 个最新版本是 2009 年发布的, 弃之.

daloradius 最新版本虽然是 2011 年 5 月发布的, 距今也有 3 年多, 但是 daloradius 的社区氛围很好, 很活跃, 相信有问题一定能够及时得到解决. daloradius 代码有 74000 多行, 功能强大. 就是它了!

安装过程

下载下来 daloradius 包之后, 看 README, 然后是 INSTALL, 最后可以看看 FAQS.

详细的安装过程都在 INSTALL 文件里, 这里只说重点的. INSTALL 文件和网上的教程一般都是讲的 apache 服务器, 我们用的是 nginx. 有些地方不一样, 重点说一下.

依赖

首先是确保 PHP DB Abstraction Layer (may require PHP Pear) 安装了, 这点 INSTALL 文档说了, 我当时没有注意到这点, 结果搭完 daloradius 登录后页面空白, 看 nginx 日志看到如下内容:

2014/12/11 15:51:41 [error] 14311#0: *1 FastCGI sent in stderr: "PHP message: PHP Warning: include_once(DB.php): failed to open stream: No such file or directory in /srv/www/daloradius-0.9-9/library/opendb.php on line 84
PHP message: PHP Warning: include_once(): Failed opening 'DB.php' for inclusion (include_path='.:/usr/share/pear:/usr/share/php') in /srv/www/daloradius-0.9-9/library/opendb.php on line 84
PHP message: PHP Fatal error: Class 'DB' not found in /srv/www/daloradius-0.9-9/library/opendb.php on line 86" while reading response header from upstream, client: 192.168.0.117, server: dalo.yeedev.com, request: "GET /dologin.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php-fpm/php-fpm.sock:", host: "dalo.yeedev.com"

安装可以使用 pear 也可以使用 yum, 建议用 yum 吧, 方式是这样的:

    # yum search php db
    # yum install php-pear-DB

  1. 我是将 daloradius-0.9-9.tar.gz 解压到了 /srv/www, 由于打包的人的机器上的用户 ID 和我机器上的用户 ID 不同, 解压之后 /srv/www/daloradius-0.9-9/ 这个目录的权限变成了:

    drwxr-xr-x 11 33 tape 12288 May 6 2011 daloradius-0.9-9/

看来, 打包 daloradius 的开发者的机器上的用户 ID 是 33, 而我维护的机器上没有编号为 33 的用户, 所以就显示数字了. 后面我们需要让 nginx (以及 php-fpm, 我维护的服务器上运行 php-fpm 的用户和 nginx 一样) 对这个目录有读写权限, 我们将 nginx 用户设为这个目录的属主. (INSTALL 文档里让你把 www-data 设为它的属主, www-data 是默认运行 apache 的用户). 我们运行如下命令:

    # chown -R nginx:nginx daloradius-0.9-9/

另外, 我们确保一下 library/daloradius.conf.php 文件的权限是 644, 以使得 nginx 用户对它有读写权限 (经过上面的一步, 应该就已经是 664 了)

      # chmod 644 library/daloradius.conf.php

  1. 导入 daloradius 数据库

    daloradius 需要使用 freeradius 数据库, 并在里面创建一些自己需要的数据表, freeradius 的数据库我们前面已经创建过了. 那么现在需要的就只是把 daloradius 自己的数据表导进去就可以了. 这些个数据表位于 contrib/db/mysql-daloradius.sql

    还记得我们前面也为 freeradius 的数据库 — radius, 创建了一个专门的用户吧, 但是当时没有给这个用户赋予 radius 数据库的所有权限, 所以他就没有权限在 radius 数据库里创建数据表, 所以我们还是使用 root 用户导入这些数据表:
       
        # mysql -uroot -p radius < mysql-daloradius.sql

    导完之后, 我一看, 我去, 给我新创建了那么多表! (daloradius 会创建十多张, radius 本来就五六张表)

  2. 配置 daloradius 的数据库连接信息

    修改 library/daloradius.conf.php 中的信息, 需要修改的信息有:

    CONFIG_DB_USER = ‘radius’
    CONFIG_DB_PASS = ‘radpass’    # 这是我们前面配置过的
    CONFIG_DB_NAME = ‘radius’
    …..
    ….. 不需改变
    …..
    CONFIG_FILE_RADIUS_PROXY = ‘/etc/raddb/proxy.conf’    # 默认是 /etc/freeradius/proxy.conf, 这是基于 Debian 系统的, 与我们不符, 需改成我们的
    CONFIG_PATH_DALO_VARIABLE_DATA = ‘/srv/www/daloradius-0.9-9/var’

    其它保持默认即可. 这个文件还可以配置邮箱通知 smtp 信息, 我们暂时先不配置.

  3. 设置 radius 数据库 radius 用户的权限

    第二步的时候导入了很多 daloradius 需要的数据表, 第三步我们配置 daloradius 使用 radius 用户来访问数据库, 但是 radius 用户对新导入那些表没有任何权限  (我们前面的配置中, radius 用户只对 freeradius 的几个表有读写权限). 由于 daloradius 一下子创建了这个么表, 之前 freeradius 有哪些表我也没记住. 我也分不清哪些是 daloradiu 创建的了, 简便起见, 我们直接这样吧:

    GRANT ALL on radius.* TO ‘radius’@’localhost’;

  1. 收尾

至此就按装完了, INSTALL 文档说了默认的管理员用户和密码: administrator/radius.

最后, INSTALL 文档还建议我们修改 administrator 用户的密码 (daloradius 管理界面就能修改), 以及将 /update.php 文件改个名, 免得被别人不小心启动了升级.

关于 daloradius 的使用在我的另一篇文章中

参考

  1. daloradius INSTALL 文档推荐的参考文章: http://www.howtoforge.com/authentication-authorization-and-accounting-with-freeradius-and-mysql-backend-and-webbased-management-with-daloradius   

问题记录

  1. 找不到 radiusclient.conf 文件

    我在用手机开启 3G, 然后开 pptp 连接的时候, 连接不上, 到服务器开监控这 log 文件再连接一次, 日志如下:

    Dec 10 10:48:21 localhost pptpd[6780]: CTRL: Client 153.119.195.169 control connection started
    Dec 10 10:48:21 localhost pptpd[6780]: CTRL: Starting call (launching pppd, opening GRE)
    Dec 10 10:48:21 localhost pppd[6781]: Plugin radius.so loaded.
    Dec 10 10:48:21 localhost pppd[6781]: RADIUS plugin initialized.
    Dec 10 10:48:21 localhost pppd[6781]: Plugin radattr.so loaded.
    Dec 10 10:48:21 localhost pppd[6781]: RADATTR plugin initialized.
    Dec 10 10:48:21 localhost pppd[6781]: pppd 2.4.5 started by root, uid 0
    Dec 10 10:48:21 localhost pppd[6781]: Using interface ppp0
    Dec 10 10:48:21 localhost pppd[6781]: Connect: ppp0 <--> /dev/pts/9
    Dec 10 10:48:21 localhost pppd[6781]: rc_read_config: can’t open /etc/radiusclient/radiusclient.conf: No such file or directory
    Dec 10 10:48:21 localhost pppd[6781]: RADIUS: Can’t read config file /etc/radiusclient/radiusclient.conf
    Dec 10 10:48:21 localhost pppd[6781]: Peer birdee failed CHAP authentication
    Dec 10 10:48:21 localhost pptpd[6780]: CTRL: EOF or bad error reading ctrl packet length.
    Dec 10 10:48:21 localhost pptpd[6780]: CTRL: couldn’t read packet header (exit)
    Dec 10 10:48:21 localhost pptpd[6780]: CTRL: CTRL read failed
    Dec 10 10:48:21 localhost pppd[6781]: Modem hangup
    Dec 10 10:48:21 localhost pppd[6781]: Connection terminated.
    Dec 10 10:48:21 localhost pppd[6781]: Exit.
    Dec 10 10:48:21 localhost pptpd[6780]: CTRL: Client 153.119.195.169 control connection finished

    可以看出, 建立 ppp 连接之后, pppd 正确加载了 radius.so 和 radattr.so, 但是找不到 radiusclient.conf, rc_read_config 应该就是这两个库里的某个函数调用之类的, 猜测可能还得在 /etc/pptpd.conf 里告诉去哪里找 radiusclient.conf.

    可以看出 pppd 这两个 so 库是从 /etc/radiusclient/ 目录找 radiusclient.conf, 而我在安装 radiusclient 的时候, 源里面已经没有 radiusclient 这个包了, 取而代之的是 radiusclient-ng 这个包, 所以, 按理说 CentOS 源的维护者应该会帮我们解决这些问题. 我想可能我系统上 pppd 的版本低了, 升级一下也许就可以了. 结果执行 yum check-update, yum info ppp 之后发现我系统上的 pppd 就是最新的. 唉 CentOS 源维护者没做好这块啊.

    于是我 google 了一下 “rc_read_config: can’t open /etc/radiusclient/radiusclient.conf: No such file or directory”, 无奈竟然 google 了半天没结果, 最终在参考 2 中看到, 说是在 /etc/options.pptpd 末尾, 也就是 radius.so  radattr.so 这两行后面加上一句:

  radius.so
      radattr.so
     radius-config-file /etc/radiusclient-ng/radiusclient.conf         # 如上面所见, 我们的 radiusclient.conf 是位于 /etc/radiusclient-ng 目录下的

  1. 上面那个问题解决重启 pptpd 之后, 又出现一个新的问题, 从我手机连 pptp 依然连不上, 这此服务器上日志是这样:

    Dec 10 11:18:14 localhost pptpd[7045]: MGR: Manager process started
    Dec 10 11:18:51 localhost pptpd[7048]: CTRL: Client 153.119.195.169 control connection started
    Dec 10 11:18:51 localhost pptpd[7048]: CTRL: Starting call (launching pppd, opening GRE)
    Dec 10 11:18:51 localhost pppd[7049]: Plugin radius.so loaded.
    Dec 10 11:18:51 localhost pppd[7049]: RADIUS plugin initialized.
    Dec 10 11:18:51 localhost pppd[7049]: Plugin radattr.so loaded.
    Dec 10 11:18:51 localhost pppd[7049]: RADATTR plugin initialized.
    Dec 10 11:18:51 localhost pppd[7049]: pppd 2.4.5 started by root, uid 0
    Dec 10 11:18:51 localhost pppd[7049]: Using interface ppp0
    Dec 10 11:18:51 localhost pppd[7049]: Connect: ppp0 <--> /dev/pts/9
    Dec 10 11:18:54 localhost pppd[7049]: /etc/radiusclient-ng/radiusclient.conf: line 75: unrecognized keyword: bindaddr
    Dec 10 11:18:54 localhost pppd[7049]: rc_read_dictionary: invalid include entry on line 241 of dictionary /usr/share/radiusclient-ng/dictionary
    Dec 10 11:18:54 localhost pppd[7049]: RADIUS: Can’t read dictionary file /usr/share/radiusclient-ng/dictionary
    Dec 10 11:18:54 localhost pppd[7049]: Peer birdee failed CHAP authentication
    Dec 10 11:18:54 localhost pptpd[7048]: CTRL: EOF or bad error reading ctrl packet length.
    Dec 10 11:18:54 localhost pptpd[7048]: CTRL: couldn’t read packet header (exit)
    Dec 10 11:18:54 localhost pptpd[7048]: CTRL: CTRL read failed
    Dec 10 11:18:54 localhost pppd[7049]: Modem hangup
    Dec 10 11:18:54 localhost pppd[7049]: Connection terminated.
    Dec 10 11:18:54 localhost pppd[7049]: Exit.
    Dec 10 11:18:54 localhost pptpd[7048]: CTRL: Client 153.119.195.169 control connection finished

    可以看出有两个错误, 一个是 radiusclient.conf 中有个关键字 bindaddr 不被识别, 我依然是看了参考 2 这个仁兄的解决方法: 到 radiusclient.conf 中注释掉 bindaddr 这一行, 再次用手机连接, 这次 bindaddr 不被识别的错误没了:

    Dec 10 11:26:31 localhost pptpd[7102]: CTRL: Client 153.119.195.169 control connection started
    Dec 10 11:26:32 localhost pptpd[7102]: CTRL: Starting call (launching pppd, opening GRE)
    Dec 10 11:26:32 localhost pppd[7103]: Plugin radius.so loaded.
    Dec 10 11:26:32 localhost pppd[7103]: RADIUS plugin initialized.
    Dec 10 11:26:32 localhost pppd[7103]: Plugin radattr.so loaded.
    Dec 10 11:26:32 localhost pppd[7103]: RADATTR plugin initialized.
    Dec 10 11:26:32 localhost pppd[7103]: pppd 2.4.5 started by root, uid 0
    Dec 10 11:26:32 localhost pppd[7103]: Using interface ppp0
    Dec 10 11:26:32 localhost pppd[7103]: Connect: ppp0 <--> /dev/pts/9
    Dec 10 11:26:32 localhost pppd[7103]: rc_read_dictionary: invalid include entry on line 241 of dictionary /usr/share/radiusclient-ng/dictionary
    Dec 10 11:26:32 localhost pppd[7103]: RADIUS: Can’t read dictionary file /usr/share/radiusclient-ng/dictionary
    Dec 10 11:26:32 localhost pppd[7103]: Peer birdee failed CHAP authentication
    Dec 10 11:26:32 localhost pptpd[7102]: CTRL: EOF or bad error reading ctrl packet length.
    Dec 10 11:26:32 localhost pptpd[7102]: CTRL: couldn’t read packet header (exit)
    Dec 10 11:26:32 localhost pptpd[7102]: CTRL: CTRL read failed
    Dec 10 11:26:32 localhost pppd[7103]: Modem hangup
    Dec 10 11:26:32 localhost pppd[7103]: Connection terminated.
    Dec 10 11:26:32 localhost pppd[7103]: Exit.
    Dec 10 11:26:32 localhost pptpd[7102]: CTRL: Client 153.119.195.169 control connection finished

    看来我们前面在 /usr/share/radiusclient-ng/dictionary 后面加的那两行 INCLUDE 不被识别, 看来 microsoft 和 merit 字典不是这么加的. 本来想 google 的, 但是我无意中在 /usr/share/radiusclient-ng/ 目录下的另一个字典文件: dictionary.ascend 顶部介绍中看到如下的内容:

    #
    # Ascend dictionary.
    #
    # Enable by putting the line “$INCLUDE dictionary.ascend” into
    # the main dictionary file.
    #
    # Version: 1.00 21-Jul-1997 Jens Glaser jens@regio.net
    #

    而且, 在参考 1 中, 也提到了用 $INCLUDE 指定来包含其他字典文件, 于是我试了以下, 果然解决了这个问题

  2. 解决了上面两个问题, 还有问题阿, 问题就是这个:

    Dec 10 12:33:33 localhost pptpd[7437]: CTRL: Client 153.119.195.169 control connection started
    Dec 10 12:33:33 localhost pptpd[7437]: CTRL: Starting call (launching pppd, opening GRE)
    Dec 10 12:33:33 localhost pppd[7438]: Plugin radius.so loaded.
    Dec 10 12:33:33 localhost pppd[7438]: RADIUS plugin initialized.
    Dec 10 12:33:33 localhost pppd[7438]: Plugin radattr.so loaded.
    Dec 10 12:33:33 localhost pppd[7438]: RADATTR plugin initialized.
    Dec 10 12:33:33 localhost pppd[7438]: pppd 2.4.5 started by root, uid 0
    Dec 10 12:33:33 localhost pppd[7438]: Using interface ppp0
    Dec 10 12:33:33 localhost pppd[7438]: Connect: ppp0 <--> /dev/pts/9
    Dec 10 12:33:36 localhost pppd[7438]: rc_avpair_new: unknown attribute 11
    Dec 10 12:33:36 localhost pppd[7438]: rc_avpair_new: unknown attribute 25
    Dec 10 12:33:37 localhost pppd[7438]: Peer sqltest failed CHAP authentication
    Dec 10 12:33:37 localhost pptpd[7437]: CTRL: EOF or bad error reading ctrl packet length.
    Dec 10 12:33:37 localhost pptpd[7437]: CTRL: couldn’t read packet header (exit)
    Dec 10 12:33:37 localhost pptpd[7437]: CTRL: CTRL read failed
    Dec 10 12:33:37 localhost pppd[7438]: Modem hangup
    Dec 10 12:33:37 localhost pppd[7438]: Connection terminated.
    Dec 10 12:33:37 localhost pppd[7438]: Exit.
    Dec 10 12:33:37 localhost pptpd[7437]: CTRL: Client 153.119.195.169 control connection finished

    这个问题参考 1 中有说到, 并且参考 1 对其解释很全. 我也很清楚了这个问题的原因, 但是我严格按照参考 1的步骤, 以及我还采取了其它巧妙的步骤, 但都是解决不了. 最后只好用了不太合常规的做法解决了.

    建议先看以下参考 1 对这个错误的描述再往下看我的解决过程.

    这个错误的重点在于:

    Dec 10 12:33:36 localhost pppd[7438]: rc_avpair_new: unknown attribute 11
    Dec 10 12:33:36 localhost pppd[7438]: rc_avpair_new: unknown attribute 25

    如果你打开 dictionary.microsoft, 就会看到, 11 和 25 这两个 attribute 分别是 MS-CHAP-Challenge 和 MS-CHAP2-Response, 那么问题就很明了了. 就是 dictionary.microsoft 这个字典文件似乎没有被 radiusclient 读到, 参考 1 中说到的所有的点我都排除了. 参考 1 让将 $INCLUDE 改成 INCLUDE, 但是你知道的, 改成 INCLUDE 就会导致上面第 2 个问题.

    我采取的巧妙地方法是: 我觉得可能是 pppd 的 bug, 虽然在 /etc/options.pptpd 中指定配置文件路径: /usr/share/radiusclient-ng/radiusclient.conf, 但是有可能 pppd 还是没处理好, 于是我注释掉了 /etc/options.pptpd 中的这句:

     radius-config-file /etc/radiusclient-ng/radiusclient.conf

然后位 /etc/radiusclient-ng 建立了一个软连接:

    ln -s /etc/radiusclient-ng /etc/radiusclient

但结果也是解决不了

4. 

   Dec 10 15:16:49 localhost pptpd[8445]: MGR: Manager process started
   Dec 10 15:16:56 localhost pptpd[8449]: CTRL: Client 153.119.196.0 control connection started
   Dec 10 15:16:56 localhost pptpd[8449]: CTRL: Starting call (launching pppd, opening GRE)
   Dec 10 15:16:56 localhost pppd[8450]: Plugin radius.so loaded.
   Dec 10 15:16:56 localhost pppd[8450]: RADIUS plugin initialized.
   Dec 10 15:16:56 localhost pppd[8450]: Plugin radattr.so loaded.
   Dec 10 15:16:56 localhost pppd[8450]: RADATTR plugin initialized.
   Dec 10 15:16:56 localhost pppd[8450]: pppd 2.4.5 started by root, uid 0
   Dec 10 15:16:56 localhost pppd[8450]: Using interface ppp0
   Dec 10 15:16:56 localhost pppd[8450]: Connect: ppp0 <--> /dev/pts/2
   Dec 10 15:16:56 localhost pppd[8450]: peer from calling number 153.119.196.0 authorized
   Dec 10 15:16:57 localhost pppd[8450]: MPPE 128-bit stateless compression enabled
   Dec 10 15:17:21 localhost pptpd[8449]: CTRL: EOF or bad error reading ctrl packet length.
   Dec 10 15:17:21 localhost pptpd[8449]: CTRL: couldn't read packet header (exit)
   Dec 10 15:17:21 localhost pptpd[8449]: CTRL: CTRL read failed
   Dec 10 15:17:21 localhost pppd[8450]: Modem hangup
   Dec 10 15:17:21 localhost pppd[8450]: MPPE disabled
   Dec 10 15:17:21 localhost pppd[8450]: Connection terminated.
   Dec 10 15:17:21 localhost pppd[8450]: Connect time 0.5 minutes.
   Dec 10 15:17:21 localhost pppd[8450]: Sent 3656 bytes, received 6726 bytes.
   Dec 10 15:17:21 localhost pppd[8450]: Exit.
   Dec 10 15:17:21 localhost pptpd[8449]: CTRL: Client 153.119.196.0 control connection finished

我尝试了以下许多种方法:

  1. 修改 radiusd 的 ippool 模块的 ip 地址范围, 改成和以前不用 freeradius 时, 只用 pptpd 分配地址时的范围一样, 不能解决
  2. 再次翻看参考 1中的步骤, 发现我有一步露了, 就是这一步, 但是操作了这一步, 依然不能解决
      ### If it is not available
      # modprobe ppp-compress-18 && echo MPPE Module is ok
      FATAL: Module ppp_mppe not found.
      ### If it is available
      # modprobe ppp-compress-18 && echo MPPE Module is ok
      MPPE Module is ok
    
  3. google 了一下午, 无果, 主要是上面的 log 信息跟本看不出 freeradius 或者 radiuclient 是否有问题

  4. 最后, 我开始使用排除法, 将 /etc/pptpd.conf 以及 /etc/ppp/options.pptpd 配置文件一步一步, 一个参数一个参数的改, 直到恢复成不和 freeradius 配合时的状态 (这时是能够连接成功的), 然后查看 log 有什么不同
  5. 正所谓, 当你把所有的资源都用尽, 依然解决不了问题的时候, 只要不要气馁, 静下心来继续干, 就会超越瓶顶, 找到解决方案. 在第 4 步的过程中, 我突然想明白一个问题. 突然想明白了 pptpd/pppd 和 freeradius 的关系. 我上面配置的参数, 大多是参考了参考 1 和参考 2 的内容, 这些参数的含义我并没有完全理解. 但是在第 4 步的过程中, 我渐渐的理解了.

    首先看一下, 我们在 /etc/ppp/options.pptpd 中加入的这几行的意义:

    # put plugins here
    # (putting them higher up may cause them to sent messages to the pty)
    plugin radius.so
    plugin radattr.so
    radius-config-file /etc/radiusclient-ng/radiusclient.conf

    不要多想, 这几行其实意思很简单, 前面说了 radius.so 和 radattr.so 是 pppd 的插件, 它们的作用就是让 pppd 能够和 freeradius 通信. 让 pppd 使用 freeradius 的身份验证以及授权机制以及计时功能.

    而我们在 /etc/pptpd.conf 中将 delegate 和 noipparam 取消注释是什么意思呢, 将它们取消注释就会使得 pptpd 不会为连接过来的客户端分配 IP 地址, 转而将这个工作交给 freeradius 去做.

    我在第 4 步中一步一步试的过程中, 发现只要 /etc/pptpd.conf 中的参数是这样: noippram 和 delegate 注释掉, logwtmp 随意. /etc/ppp/options.pptpd 中继续留着这三行:

    plugin radius.so
    plugin radattr.so
    radius-config-file /etc/radiusclient-ng/radiusclient.conf

    就是可以成功的, 说到底, 只要不把分配 IP 的工作委托给 freeradius, 连接就是可以成功的, 我测试过, 可以正常使用 freeradius 授权, 验证以及计时功能 (mysql radius 数据库里都写入了相关的内容).

    我现在仍有一点不明白的是, pptpd 是怎么把授权和身份验证的过程交给 freeradius 去做的 (其实不和 freeradius 配合时, pptpd 是怎么选择使用 chap 身份认证方式的我也不清楚), 其实相比以前不和 freeradius 配合的方式, 我们的配置的东西只有:

    /etc/pptpd.conf 中的 delegate, noippram 和 logwtmp 选项. /etc/ppp/options.pptpd 中的那三行. 所以委托 freeradius 对客户端进行身份验证以及授权的功能只能是 radius.so 和 radattr.so 这两个模块完成的了.

    但是, pptpd 以前自己是会使用 /etc/ppp/chap-secrets 文件对客户端进行身份验证的, 这个过程又是怎么被 “废除” 的呢? 难道说使用了 radius.so/radattr.so 模块之后, pptpd 就会自动不再使用自己的验证机制吗?

    这点有待继续研究.

为什么在和 freeradius 配合时, /etc/pptpd.conf 中的 logwtmp 参数需要注释掉

delegate 和 noipparam 都是不注释, 但是为何 logwtmp 需要注释呢? 我发现, 如果不注释的话, 会有如下错误, 原因我暂时不知道.

但是如果 delegate 和 noipparam 注释了 (不委托 freeradius 给客户端分配 IP), 这时将 logwtmp 不注释, 是不会有这个错误的.

我猜可能是 logwtmp 的内容被 pppd (还是 pptpd?) 读取到引起的.

Dec 11 11:09:59 localhost pptpd[13024]: CTRL: Starting call (launching pppd, opening GRE)
Dec 11 11:09:59 localhost pppd[13025]: Plugin radius.so loaded.
Dec 11 11:09:59 localhost pppd[13025]: RADIUS plugin initialized.
Dec 11 11:09:59 localhost pppd[13025]: Plugin radattr.so loaded.
Dec 11 11:09:59 localhost pppd[13025]: RADATTR plugin initialized.
Dec 11 11:09:59 localhost pppd[13025]: Plugin /usr/lib64/pptpd/pptpd-logwtmp.so loaded.
Dec 11 11:09:59 localhost pppd[13025]: pptpd-logwtmp: $Version$
Dec 11 11:09:59 localhost pppd[13025]: pppd 2.4.5 started by root, uid 0
Dec 11 11:09:59 localhost pppd[13025]: Using interface ppp0
Dec 11 11:09:59 localhost pppd[13025]: Connect: ppp0 <--> /dev/pts/8
Dec 11 11:10:02 localhost pppd[13025]: peer from calling number 153.119.195.144 authorized
Dec 11 11:10:02 localhost pppd[13025]: MPPE 128-bit stateless compression enabled
Dec 11 11:10:02 localhost pppd[13025]: Unsupported protocol 'IPv6 Control Protocol' (0x8057) received
Dec 11 11:10:02 localhost pppd[13025]: Could not determine local IP address
Dec 11 11:10:02 localhost pppd[13025]: pptpd-logwtmp.so ip-down ppp0
Dec 11 11:10:02 localhost pppd[13025]: Connect time 0.1 minutes.
Dec 11 11:10:02 localhost pppd[13025]: Sent 102 bytes, received 116 bytes.
Dec 11 11:10:02 localhost pppd[13025]: MPPE disabled
Dec 11 11:10:02 localhost pppd[13025]: Connection terminated.
Dec 11 11:10:02 localhost pppd[13025]: Connect time 0.1 minutes.
Dec 11 11:10:02 localhost pppd[13025]: Sent 142 bytes, received 120 bytes.
Dec 11 11:10:02 localhost pppd[13025]: Exit.
Dec 11 11:10:02 localhost pptpd[13024]: GRE: read(fd=6,buffer=6124a0,len=8196) from PTY failed: status = -1 error = Input/output error, usually caused by unexpected termination of pppd, check option syntax and pppd logs
Dec 11 11:10:02 localhost pptpd[13024]: CTRL: PTY read or GRE write failed (pty,gre)=(6,7)
Dec 11 11:10:02 localhost pptpd[13024]: CTRL: Client 153.119.195.144 control connection finished

参考

  1. freeradius wiki 主页上的关于集成 pptp 和 freeradius/radiusclient-ng 的介绍: http://wiki.freeradius.org/guide/PopTop-HOWTO
  2. 这位博主很全面的介绍了搭建 freeradius 及其配置, 我第一次安装 freeradius 就是看这位博主的文章安装的, 感谢: http://www.cnblogs.com/klobohyz/archive/2012/02/01/2334811.html
    3. http://www.cnblogs.com/klobohyz/archive/2012/02/04/2338675.html
  3. pptpclient 项目主页
  4. freeradius 和 SQL 的介绍: http://wiki.freeradius.org/guide/SQL-HOWTO