存档

‘编程’ 分类的存档

Consistent Hashing算法

2010年3月12日

网络应用中经常会用到缓存机制,例如memcached。当需要缓存的数据量较大,一个memcached实例存不下的时候,就需要一定的策略将不同的对象分配到不同的memcached实例上。比较常见的方法是对对象的key值做hash,再求余,根据余数分配到不同的cache中。这种简单的方法的扩展性相当差一些,主要体现在cache结点的增加和删除上。假设本来有a个cache结点,因数据量扩大而扩大至b个结点,可以计算得到有 min(a, b) / [a, b]比例的对象的余数没有发生变化,也就是说这些对象仍可以存在于原来的cache结点上。如果每次cache结点数量的扩展都是翻番的,即b=2a,则有50%的对象的缓存失效。但是如果是原有5个结点,增加了2个结点,则会有86%的对象缓存失效,这将对系统的性能造成较大冲击。

所以出现了consistent hashing算法,它可以减少节点变动带来的缓存失效。consistent hashing的实现方法是把缓存对像的key和cache结点本身,分别用一定的算法映射到一个相同的hash空间上,例如一个32位的整数。然后将这个空间想像成一个圆,然后将各key及各cache结点都画在这个圆上。对于每个key,它顺时针碰到的最近的一个cache结点就是它应该存在的地方。如下图所示:

为了防止由于对cache结点的hash算法结果的不均匀,导致cache结点在圆环上的分布过于不均,使得每个cache的负载不同,consistent hashing算法还引用了virtual node的概念。也就是在对cache结点计算hash的时候,通过对hash过程的微调,使每个结点都算出很多(例如200个)hash值,这些值以virtual node的形式添加到环上,所有属于这些virtual node的缓存对象都映射到该实际结点上,这样就基本能保持每个cache结点在hash空间中cover住基本同样多的对象了。

这样,在保证结点基本均匀分布的情况下,将cache结点数从a提高到b,只有(b-a)/b比例的对象缓存失效。对于上文提到的从5个结点添加2个结点的情况,缓存失效率为28%,相比传统算法的86%,性能提高了不少。

不少新的memcached client都已经支持这种算法,如libketama等。但是不同语言的客户端的实现依赖于该语言对对象的serialization实现,可能分配的结果也不一致。我最近做的项目需要同时从Java和C客户端访问同一组cache server,所以我还是自己实现了一个简单的consistent hashing机制。其中,算法中的环结构对应一个SortedMap,每个cache结点均为该树中的一个点,某key顺时针碰到的第一个结点的问题转化成排序树上大于该key的最小结点。(无关代码已略去)

public class ConsistentHashingPartitionStrategy <TTarget, TKey> {

    public static final int                    DEFAULT_REPLICA = 64;

    private final SortedMap <Integer, TTarget> circle          = new TreeMap <Integer, TTarget> ();

    /**
     * Number of virtual nodes we create for a target
     */
    private final int                          replica;

    public void addTarget (TTarget target) {

        for (int i = 0; i < replica; i++) {
            circle.put (this.getTargetHashingFunction ().hash (target.toString() + i), target);
        }
    }

    public void removeTarget (TTarget target) {

        for (int i = 0; i < replica; i++) {
            circle.remove (this.getTargetHashingFunction ().hash (target.toString() + i));
        }
    }

    /**
     * Find the nearest target next to the key hash on the circle.
     */
    public TTarget getTargetForKey (TKey key) {

        if (circle.isEmpty ()) {
            return null;
        }

        int hash = getKeyHashingFunction ().hash (key);

        if (!circle.containsKey (hash)) {
            SortedMap <Integer, TTarget> tailMap = circle.tailMap (hash);
            hash = tailMap.isEmpty () ? circle.firstKey () : tailMap.firstKey ();
        }

        return circle.get (hash);
    }
}

编程 ,

glibc中malloc()的空间overhead

2010年2月13日

在linux下调用malloc()分配内存的时候,实际占用的内存与请求的内存尺寸的关系是什么呢,这个需要研究一下glibc中malloc()的实现。现在常见linux发行版中带的glibc中采用的都是Doug Lea的实现,下面的分析取自他的2.8.4版本的malloc.c

glibc对内存的管理是以chunk为单位的,未分配的chunk之间用双向链表连接成一个环,遍历的时候用指针遍历,已分配的chunk在其首部有chunk的大小,并以此字节数做遍历。每个chunk的首部要放置一个叫做malloc_chunk的struct,故每个chunk的大小至少是这个struct的大小,如果分配的空间或者free的空间大于这个大小,则struct的后面是raw的数据或者是空白空间。chunk的定义如下所示:

struct malloc_chunk {
  size_t               prev_foot;  /* Size of previous chunk (if free).  */
  size_t               head;       /* Size and inuse bits. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;
};

对于已分配的和未分配的chunk,使用的都这个结构,只是用到的成员不一样。对于这两种情况,head变量存放的都是本chunk的尺寸,由于chunk的大小有对齐的规定(见下文),所以head变量的最后三位一定是0的,用不到,所以这三位被做为三个标志位使用,其实本文涉及的是最低位P,它表示当前chunk紧跟着的上一个chunk是不是一个free的chunk,以及倒数第三位A,它表明当前chunk是否被使用中。如果上一个chunk是free的,prev_foot变量就是有用的,它表明了上一个chunk的尺寸,如果上一个chunk也是分配了的,prev_foot变量就是不使用的(在内存上直接和前一个chunk重叠起来,见下文的图示)。如果当前chunk是个空chunk,那么fd和bk两个指针就会分别指向空闲chunk环中的上一个和下一个,如果当前chunk已经分配了,fd和bk所在的内存存放的就是用户实际申请到的数据空间了。

Doug Lea在malloc.c的源代码中画了图来描述实际分配中的内存使用情况,首先是对于已分配的chunk:

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Size of previous chunk (if P = 0)                             |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Size of this chunk                                     |1| |P|+
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |                                                               |
        +-                                                             -+
        |                                                               |
        +-                                                             -+
        |                                                               :
        +-      size - sizeof(size_t) available payload bytes          -+
        :                                                               |
chunk-> +-                                                             -+
        |                                                               |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Size of next chunk (may or may not be in use)              |1|+
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图中,chunk箭头所指的位置是malloc_chunk struct的起始位置,mem箭头所指的位置是malloc()等函数返回给用户的指针的位置,也就是实际数据的位置,mem和chunk箭头之前的2*size_t字节的空间可认为是overhead。但是注意第二个chunk,它的上一个chunk也就是第一个chunk是已经分配的,所以它的head字段的P标志位为1,并且它的prev_foot字段其实是不存在的,因为它和第一个chunk的payload数据的最后size_t个字节是重叠的。如果第二个chunk也被分配了的话,它的overhead就是第二个mem箭头上面的那一条当前chunk尺寸(也就是head变量)的那size_t字节的空间。

对于空闲的chuck是这样的:

chunk-> +-                                                             -+
        | User payload (must be in use, or we would have merged!)       |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Size of this chunk                                     |0| |P|+
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Next pointer                                                  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Prev pointer                                                  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |                                                               :
        +-      size - sizeof(struct chunk) unused bytes               -+
        :                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Size of this chunk                                            |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Size of next chunk (must be in use, or we would have merged|0|+
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |                                                               :
        +- User payload                                                -+
        :                                                               |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

如图中注释所说,两个free的chunk如果是相邻的,会被合并,所以不会存在两个free的chunk相连的情况。(对于较小的刚刚被free的chunk,glibc中存在一种调度算法,把这种小free chunk加入一个叫fastbins的队列管理,这时尽管被free,它们的A标志位会保持为1,因此就算相邻也不会被合并)。

注意图中的第二个chunk,它是一个被分配了的chunk,它的第一个字段prev_foot表示的是第一个free的chunk的大小。其实这个prev_foot变量也是位于上一个chunk的尾部。真正属于第二个chunk的空间还是从“size of the next chunk”开始的,它距离用户数据的空间mem是size_t字节。

综合上面两个例子可以看出,对于一个已分配了的chunk,如果它的上一个chunk也是分配了的,它的prev_foot就是完全被前一个chunk覆盖的;如果它的上一个chunk是未分配的,它的prev_foot存的是该空间chunk的大小,且prev_foot变量位于前一个chunk的最后size_t个字节。这样看来,已分配的chunk的第一个成员prev_foot总是位于别的chunk内的,所以overhead就是head一个变量的大小,即site_t字节。这样一种chunk之间重叠的设计,使得prev_foot与其说是当前chunk的第一个变量,不如说是上一个chunk结尾尺寸标记(从它的名字foot就可以看出这一点)。这使得对于一个空闲chunk来说,其头(自身的header变量)和尾(下一个chunk的prev_foot)都是此空闲chunk的尺寸,这使得从正向两个方向按字节数遍历都很容易,是个不错的设计。

除了上面讨论的overhead以外,chunk的存储还涉及对齐的问题,glibc中规定chunk的以size_t的两倍大小对齐。与此相关的一些代码如下:

#define MALLOC_ALIGNMENT       (2 * SIZE_SZ)  //在我下载的malloc.c中不是这样定义的,而是分情况讨论的
#define CHUNK_ALIGN_MASK    (MALLOC_ALIGNMENT - SIZE_T_ONE)

#define CHUNK_OVERHEAD      (SIZE_T_SIZE)

#define MCHUNK_SIZE         (sizeof(mchunk))

/* The smallest size we can malloc is an aligned minimal chunk */
#define MIN_CHUNK_SIZE\
  ((MCHUNK_SIZE + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK)

/* pad request bytes into a usable size */
#define pad_request(req) \
   (((req) + CHUNK_OVERHEAD + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK)

/* pad request, checking for minimum (but not maximum) */
#define request2size(req) \
  (((req) < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(req))

request2size这个宏汇合了上文的各个因素,给出一个要申请的空间大小,它返回的就是实际分配的内存大小。只申请1个字节的时候,实际至少也要分配malloc_chunk的大小那么多空间,当申请的字节更多直到一个malloc_chunk的空间放不下后,实际空间会以2倍的size_t为步长增长。

这里写了一个程序验证:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

#define N 1

int main() {
    int i = 0;
    char *p;
    char *p0, *p1;

    size_t *psize;

    for (i = 0; i < 1000000; i++) {
        p = (char*) malloc(N);
        if (i == 0) p0 = p;
        if (i == 1) p1 = p;
    }
    printf("%d\n", p1 - p0);    //看两个chunk的mem指针相差多少

    psize = (size_t*)(p1 - sizeof(size_t));
    printf("%d\n", (*psize) & ~7);    //看head变量的值,& ~7的作用是把后三个标志位过滤掉

    getchar();
    return 0;

}

在32系统下,size_t和指针均为4字节,当N为1至12时,程序的内存占用为16M,当N为13时,程序占用内存涨到24M。
在64系统下,size_t和指针均为8字节,当N为1至24时,程序的内存占用为32M,当N为25时,程序占用内存涨到48M。

上述程序在centos 5.3下,gcc 4.1.2下测试通过。另外,由于C++的new操作底层调用的也是malloc,所以上述讨论对new同样适用。

最后附上本文主要的参考材料,一个讲述glibc的pdf和malloc.c源代码。

Understanding the heap by breaking it
malloc.c

编程

c语言中struct的内存对齐

2010年2月8日

为了让CPU能够更舒服地访问到变量,struct中的各成员变量的存储地址有一套对齐的机制。这个机制概括起来有两点:第一,每个成员变量的首地址,必须是它的类型的对齐值的整数倍,如果不满足,它与前一个成员变量之间要填充(padding)一些无意义的字节来满足;第二,整个struct的大小,必须是该struct中所有成员的类型中对齐值最大者的整数倍,如果不满足,在最后一个成员后面填充。

各种类型的变量的align值如下,参考的是wikipedia的页面:Data structure alignment

The following typical alignments are valid for compilers from MicrosoftBorland, and GNU when compiling for 32-bit x86:

  • char (one byte) will be 1-byte aligned.
  • short (two bytes) will be 2-byte aligned.
  • An int (four bytes) will be 4-byte aligned.
  • float (four bytes) will be 4-byte aligned.
  • double (eight bytes) will be 8-byte aligned on Windows and 4-byte aligned on Linux.
  • long double (twelve bytes) will be 4-byte aligned on Linux.
  • Any pointer (four bytes) will be 4-byte aligned on Linux. (eg: char*, int*)

The only notable difference in alignment for a 64-bit linux system when compared to a 32 bit is:

  • double (eight bytes) will be 8-byte aligned.
  • long double (Sixteen bytes) will be 16-byte aligned.
  • Any pointer (eight bytes) will be 8-byte aligned.

这里写了个程序来验证这些事:

#include <stdio.h>

struct s {
    char a;
    short b;
    char c;
    double d;
    char e;
};

int main() {

    struct s s1;

    printf("%d, %d, %d, %d, %d\n",
        (char*)(&s1.a) - (char*)(&s1),
        (char*)(&s1.b) - (char*)(&s1),
        (char*)(&s1.c) - (char*)(&s1),
        (char*)(&s1.d) - (char*)(&s1),
        (char*)(&s1.e) - (char*)(&s1));
    printf("%d\n", sizeof(struct s));

    return 0;
}

在64位linux下面运行这段代码的结果是:

0, 2, 4, 8, 16
24

由于对齐机制的存在,实际上上面的struct在内存中是长这个样子的,共计24个字节:

struct s {
    char a;             //在地址为0的位置
    char padding1[1];   //由于下面一个元素是short,对齐字节数为2的位数,需要补1字节
    short b;            //对齐到了地址为2的位置
    char c;             //在地址为4的位置
    char padding2[3];   //由于下面一个元素是double,对齐字节数为8的倍数,需要补3字节
    double d;           //对齐到了地址为8的位置
    char e;             //在地址为16的位置
    char padding3[7];   //整个struct的大小需要是对齐数最大者,也就是double的8字节的整数倍
};

如果是在32位的linux下跑上面的程序,由于double的长度还是8字节,但是对齐是4字节的了,所以前面几个成员的位置不变,而最后的padding只需要补3个字节就可以了,所以输出的结果是0, 2, 4, 8, 16及20.

对于windows,其32位和64位下double都是8字节对齐的,所以在32位和64位下跑这个程序结果都是0, 2, 4, 8, 16及24.

最后,整个struct的大小的要求是对齐值最大者的整数倍,没有什么默认的4或者8的倍数一说。如果把上面程序中的a,b,c,d,e的类型全变成char,那么最后的他们的地址会是0,1,2,3,4,整个struct的大小 sizeof(struct s)的值是5,没有任何padding发生。

以上程序实验的环境在64位centos x64上的gcc 4.1.2(32位结果加-m32参数)及Visual Studio 2008上得出。

编程

Facebook神作: HipHop for PHP

2010年2月4日

PHP很容易开发,它的弱类型的变量和近乎万能的array,可以节约很多开发的时间,当然这也导致它的运行效率不高。有一些扩展例如APC 和 eAccelerator 致力于缓存PHP编译成的opcode来提高速度,不过facebook花了一年半的时间做了更彻底的事情,他们不想容忍连$a=1; $b=1; $c = $a + $b这样简单的事情都要去判断zval的类型,于是他们写出了HipHop for PHP——直接把PHP往C++上转换。

主创人员的blog文章:HipHop for PHP: Move Fast
一个关于HipHop for PHP的talk的视频:http://www.ustream.tv/recorded/4409735

PHP的语法大致被分为两类,一种是传统的,几乎可以和C++直接对应的语法,一种是所谓的”magic”语法。magic语法包括诸如:

$$$$$foo = 1;(动态变量)
$$$$foo();  (动态函数)
extract(array(‘a’ => 1, ‘b’ => 2));

从上面的视频可以看到,动态变量,动态函数乃至extract()都被支持了,不过他们没说是怎么实现的,好奇中。而且对于下面这种code,会是如何编译的呢,a到底要声明为一个int还是查表呢,或者莫非只要scope中有extract()的存在一切变量都要查表?

$a = 1;
$arr['a'] = “2″;
extract($arr);
$b = $a + 1;

另外还有明确说了不支持的,包括eval()的功能,还有is_function_exists()这样的运行时的判断,到是不太用得着。另外在视频中的Q&A环节,看意思是PHP的已有extension都要rewrite一下,感觉这个的代价是不是太大了?

还不知道啥时能用上这个技术,facebook说他们已经有90%的服务已经在用了,用了6个月部署。不清楚它的开源度如何,其它人能不能随便用。HTTP Server方面,facebook方面用了一个自已写的http server以及non-multithreading的apache 1.3,multithreading的暂时还不可用的样子,所以说短期内至少未名是不能用上这个技术的。

ps. 这个技术如果真的发达了,是不是广大C++民工身价会小跌呢。。。

编程

活动目录中时间日期格式的总结

2009年5月3日

活动目录中关于日期和时间的存储还是比较复杂的,如果是用ldap直接操作的话,需要了解得比较清楚的。这里查了一些文档,做一个笔记。

AD中关于时间的属性有两种类型:

1.NT时间戳型:

以lastLogon、pwdLastSet、accountExpires等属性为代表,这类属性的类型(syntax)其实就是一个大整数。NT时间戳与Unix时间戳的思想是一样的,只是前者的定义更bt一些:从1601年1月1日0时起经过的1E-7秒(即100纳秒)的个数(时间是GMT的)。

官方文档可以参考:How to convert date/time attributes in Active Directory to standard time format

如果用PHP之类的语言操作AD,可以先求出标准的Unix时间戳,然后乘上1E7,再加上1970年1月1日00:00:00的NT时间戳(常量:116445312000000000)即得到了NT时间戳。这需要用到计算大数值的BC库,如果没有的话也可以改用字符串,不乘那个1E7,而是直接转成字符串接上7个”0″

2. Generalized Time/Universal time 类型

这种以whenCreated、whenChanged属性为代表,这种属性的类型是真正的时间类型。

这种格式的说明在MSDN上的这个页面:http://msdn.microsoft.com/en-us/library/cc223266(PROT.10).aspx

格式的核心是由ISO8601定义的,W3上的 Date and Time Formats一文介绍得比较易读。不过实际使用的格式不包括横线、冒号等分隔符,具体格式是由ITUX680规定的(在64~65页)。

概括起来,这种格式就是年月日时分秒连续地写在一起,加上一个小数点后的秒值(一般为0),再加上时区,小数点和时区都不可省略。
对于UTC时间,它就形如:20090502173227.0Z 
对于本地时间,它就形如:20090503013227.0+0800 (这两个时间是同一个)

一般来说,向AD中写入时,可以用本地的表示也可以用UTC的表示,但是读出来的时候,都是UTC表示(也就是以Z结尾的) 

 

如果你是PHP用户,可以参照下面的代码实现DateTime对象与Generalized Time格式的相互转化:

 

    function DateTimeToGeneralizedTime($datetime) {
        return $datetime->format(‘YmdHis.0O’);
    }

    function GeneralizedTimeToDateTime($str) {
        $ret = strptime($str, ‘%Y%m%d%H%M%S.0%z’);
        if (!$ret) $ret = strptime($str, ‘%Y%m%d%H%M%S.0%Z’);

        $gmtTimestamp = gmmktime($ret['tm_hour'], $ret['tm_min'], $ret['tm_sec'], $ret['tm_mon'] + 1, $ret['tm_mday'], $ret['tm_year'] + 1900);

        $ret = getdate($gmtTimestamp);

        $datetime = new DateTime();
        $datetime->setDate($ret['year'], $ret['mon'], $ret['mday']);
        $datetime->setTime($ret['hours'], $ret['minutes'], $ret['seconds']);

        return $datetime;
    }

 

 

编程, 技术 , ,

长期陷入PHP这类动态语言的思维定式中

2009年3月19日

长期陷入PHP这类动态语言的思维定式中,一个实际的页面就对应着一个物理的文件,有时还要特意借助什么rewrite之类的功能来实现文件结构不暴露给用户。

其实像python这类的语言做web server时先天的就不是这样做的,人家就是用一个url dispatcher来分配什么url交给哪个函数去处理,你访问 /foo/bar,其实根本没有bar那个文件在那里,这很方便嘛。甚至这些python的代码都不需要放在httpd的document目录里了。事实上,The Django Book里赫然就鄙视了原来PHP的这种方法来着:

Where Should This Directory Live?

If your background is in PHP, you’re probably used to putting code under the Web server’s document root (in a place such as /var/www). With Django, you don’t do that. It’s not a good idea to put any of this Python code within your Web server’s document root, because in doing so you risk the possibility that people will be able to view your raw source code over the Web. That’s not good.

Put your code in some directory outside of the document root.

中译版

这个目录应该放哪儿?

 

有过 PHP 编程背景的话,你可能习惯于将代码都放在 Web 服务器的文档根目录 (例如 /var/www 这样的地方)。而在 Django 中,你不能这样做。把任何 Python 代码放到 Web 服务器的文档根目录中都不是个好主意,因为这样一来,你就要冒着别人透过页面直接看到代码的风险。这对于安全可不是件好事。

 

把代码放置在文档根目录 之外 的某些目录中。

编程 ,

用PHP通过LDAP操作活动目录(AD)的笔记

2007年11月12日

最近一周一直在研究AD这方面的问题,主要难题就是用跑在Linux上的PHP通过LDAP来读写Windows上的活动目录中用户的信息。一般来说需要这么做的情况不常见,因为既然是用AD了就可以用很多现在的LDAP模块,但是我现在要做的是把已有的一个非标准的用户/密码的库转换到AD里去,所以必须亲自来完成一些底层的工作。这方面的文章网上不多见,所以这里综述一下。

首先,是安装配置AD本身的这部分工作:
主要包括:装Win2k3(或2008哈),建立域控制器,建DNS服务,把要加入域的几个机器加进域来,然后建设OU(放用户的OU及放计算机的OU),建立安全组(我用它来限制哪些用户可以登录哪些计算机),设立组策略并应用到各OU等。
这部分内容的文章很多,微软给的文档就足够详细了,比如这个Step by Step的教程:http://www.microsoft.com/china/technet/prodtechnol/windowsserver2003/technologies/directory/activedirectory/stepbystep/default.mspx
(ps.这中间在密码策略上浪费了大量时间,后来才弄清一个域中只能在根下设置密码策略,就算把带有密码策略的组策略应用在OU上,也是无效的。2008里已经可以针对安全组甚至用户设置独立的密码策略了,期待~~)

然后是一些为了就乎linux的准备工作:
先去http://www.microsoft.com/windowsserversystem/sfu/downloads/default.mspx下载一套Windows Services for UNIX,这个装上以后AD里用户的属性页里会出现一个UNIX属于页,有uid,gid什么的,这是为了让linux能结合AD验证用户登录的(比如pam),我目前没用上,不过装它没坏处。
还有是要建立一个proxyuser,这个user是ldap客户端连AD时登录用的,想让PHP实现对AD的什么操作,就给这个proxyuser赋予什么权限。
这里推荐一套很好的文档,是微软的Windows Security and Directory Services for UNIX Guide(还是要pf一下m$,文档就是全,这里对linux中安全体系的介绍比专方介绍的文档都详细。。。)

下面就是资料不好找的工作啦,一步一步来:

1.关于AD与ldap的符合性。AD被做成基本符合ldap的模式,每个用户和计算机或是组什么的都有一个DN,形如CN=loudly,OU=bbs,DC=bdwm,DC=net这样子,而用户的每个属性就是这条记录里的一个attribute,所以可以用ldap_search(),ldap_add()这些标准的函数来操作AD。一个特殊但是关键的地方就是用户的密码,很不易地找到这两个文档介绍这个事情:
How To Change a Windows 2000 User’s Password Through LDAP
User Management with Active Directory―LDAP Password Modification
文章中的核心内容就是:用户的密码是一个叫unicodePwd的属性表示的,这个属于只能写不能读,写的时候的格式是用一对双引号括起密码原文,再编码为unicode传给它,还有,要想改用户密码,必须是通过SSL方式连接LDAP(636端口,而不是普通的389端口)。因为如上要求,就要有以下步骤的工作了。

2.生成根证书
用SSL的话嘛,就需要证书。可以在域控制器上开启认证服务来创建企业的CA(注意这个服务开启后域的设置就不能改了,包括域名,这个机器的名等,所以要在进行这步之前设置好)。这里要做的就是创建CA,然后验证一下是不是ok的。其实这部分内容我也不懂,主要就是照着下面两篇参考着做的,具体步骤怎么回事(尤其是验证的时候)我也不知道是在做什么。
a.Install an enterprise root certification authority
b.就是上面提到的Windows Security and Directory Services for UNIX Guide第二卷的第Chapter4的Build a Certification Authority那一节
如果不自己建CA,而是从别处获得CA,那就看下面这篇:
c.How to enable LDAP over SSL with a third-party certification authority
下面这篇也可以参考,不过该文的主题是别的
d.如何在 Windows 2000 域控制器中配置数字证书来确保 LDAP 和 SMTP 复制的安全
上面的工作做好后,如果正常,那么域控制器就会开始listen 636端口,也就可以通过SSL连AD了(一般ldap客户端的规矩是:连的时候直接写DC地址就是用非加密方法连,连的时候如果写ldaps://地址,就是用SSL方法连接DC)

3.LDAP客户端的配置
客户端,就是要去连AD的机器,也就是本文中跑着PHP的那个机器,要做的工作主要是把DC上的CA导出成.cer,然后拷到客户端上来,然后转化为.pem(可是我的.cer和.pem文件是一样的)放到某个位置,然后再修改一下/etc/openldap/ldap.conf,这样连DC的时候系统就可以去查相应的证书。主要参考的文档是这篇:
Communicating between OpenLDAP and Active Directory over SSL
下面两篇也可以参考:
Configuring Microsoft Active Directory for SSL access
LDAP over SSL – Modifying Active Directory with PHP
上面的文章里最后都要用linux下的ldapsearch命令来测试连接是否可以进行,不过语法不同,我最后成功地连上DC用的语句是:
ldapsearch -x -H “ldaps://theDC.yourdomain.net” -s base -D “cn=proxyuser,cn=users,dc=yourdomain,dc=net” -W -b “dc=yourdomain,dc=net”(回车后提示输proxyuser的密码)
还有,要强调的是,连DC一定要用它的域名,不要用IP或是NetBIOS名,因为CA里写的是域名,要匹配
另,上面说的是linux的ldap客户端,win下也有啊,比如说win2k和2k3的support-tools里的ldp.exe,可是我还没搞清楚它应该怎么才能以SSL连AD。。。

4.用PHP来操作活动目录
这步算是最轻松的了,就是学一学PHP的相关函数的语法就好了。不过大概是没几个人用到ldap这方面的功能吧,PHP manual中ldap这部分的说明很简略,特别是用TLS的文档里写着这部分没有文档。。。所以最好的办法还是看一些源代码,这里推荐的是我在SourceForge上找到一个的PHP连AD的类:http://adldap.sourceforge.net/
这个类写的并不是特别好,比如添用户时非要求填几个没用的属性,还有至少在一层的OU里的样子,所以推荐它主要是为了看它的原码(30k左右,不是很多)学习用ldap来添加、查询、修改、加组等操作应该怎么写,如果你的应用比较复杂,建议还是自己来写,不直接用它的类。
下面几个地址也列出来考虑吧,它们都是我google到的别人问的问题,本身没有确切解答,不过还是有功劳的,比如我认识到了没有验证一个用户的密码对不对的函数,要做的就是用那个用户名和密码去bind一下,bind上
就是对,否则就是错。
http://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/Q_21296510.html?qid=21296510
http://topic.csdn.net/t/20060524/10/4774581.html
http://www.ideawu.net/ideablog/category6/article238.html
http://topic.csdn.net/t/20030520/15/1809327.html#

上面列出的文章链接真的不是那么好挑选出来,真怕他们哪天没了,回头我下载完打包传上来吧

编程, 技术 , , ,

关于linux下的编程,不禁发些牢骚

2007年8月13日

虽说是以开源为基础的,而且在简单的程序上写起来是比较方便,不过有些时候确实让人头痛,让人想念微软的.Net或是MFC。

首先是标准不统一,最近在写一个基于线程池的服务器程序,用到了semaphore,还是存在mmap中的无名semaphore,调试得很愉快,于是高兴地把程序分发往各服务器,没料到在第一个服务器上装的时候就郁闷掉了,发现进程会永远地阻塞在sem_wait()上。后来一调查才意识到,我开发的环境是FC6(用的2.6内核),人家是支持整套Posix IPC的,结果分发到的服务器以Rhel3甚至Rh7为主,内核很老(老到不是问题,主要是发行版编译时不开posix支持),我用的Posix的ipc函数就不支持。没办法,只好返工重写,这下改用System V的semaphore,这个东西比Posix的功能强,却也复杂了不少。Posix说是明日这标准,不过System V还是比较老,被支持广,这下写完,到是没啥问题了。这还只是在linux平台上呢,还没提什么solaris,bsd之流的,现在明白了为什么APUE一上来就介绍各种linux/unix版的区别了哎。
然后就是库版本什么的挑剔性啦,我写的这个程序要部署在实验室的20多台服务器上。这个工程很大,因为要首先给每台机器装上要用到的库(比如Xercesc),然后把源码拷过去,然后编译,只能写脚本来做这些工作,不过仍然是很麻烦,因为意外情况还是很多的。不能拷二进制程序吗?没戏。发行版各种各样,glibc版本各种各样,直接拷二进制的话,20台机器里有6、7台能正常就不错了。试想这些服务器要都是Windows 2003呢,直接分发二进制就是理所当然的啦,分发源代码再编译才是多此一举。

还有文档,一直在linux下写东西,能看的也就是man page加上google一些东西,偶尔在win下写程序时看MSDN时确实觉得是享受啊。。。man page,纯文字的不说,里面有各种各样的地方让你参见某某某,却又没个方便的链接过去的方法,影响编程心情啊。。。

编程

php中调用mysql存储过程的两个问题

2007年5月1日

一、存储过程返回一个结果集的问题

正常情况下,如果在php中调用一个mysql存储过程,而该过程返回一个结果集的话,那么会收到“#1312 – PROCEDURE XXX can’t return a result set in the given context”的错误提示。
解决方法是,在用mysql_connect()连接数据库时,使用最后一个参数client_flags,设该参数值为0×20000,即如下:

define(‘CLIENT_MULTI_RESULTS’, 0×20000);
$db = mysql_connect(“dbhost”, “dbuser”, “dbpasswd”,1,CLIENT_MULTI_RESULTS);

二、提示“Lost connection to MySQL server during query”的问题

如果在一个页面中调用了不止一次存储过程,就可能收到上面的提示。一个省事的方法就是每次调用储存过程都重connect一下数据库。
在网上查了一下这个问题,有的人说可以在phpinfo()中看一下mysql client API的版本,至少在5.0.15以上才行,不过我的已然是5.0.18了。还有说要改php.ini,设置这个参数:set-variable=thread_stack=256k

编程 ,

让IE和firefox都支持的半透明语法以及隐藏的语法

2006年10月14日

一般地,想让网页上的东西(如DIV)半透明,方法是在它的style中加入:
filter:alpha(Opacity=XX)
其中XX从0到100,0是全完透明

而firefox中不支持css的filter属性,透明的话要在style中写:
-moz-opacity:XX
这里的XX是个小数,从0到1,0也是完全透明

如果需要半透明,只是要显示或隐藏,就在相应对象的style中改visibility发生,常用的是’hidden’与’visible’。这一属性是两种浏览器都支持的。
JS中修改这一属性的语法是 对象名.style.visibility=’hidden’而不是 对象名.visibility=’hidden’,今天为此也浪费了些时间。

编程 ,