Mar 3

Yet Another False-Sharing Test 不指定

felix021 @ 2013-3-3 23:31 [IT » 硬件] 评论(0) , 引用(0) , 阅读(3151) | Via 本站原创
前天在 coolshell 里看到 并发框架Disruptor译文 以后 ,才感慨了CPU娘的傲娇,没一会儿就看到 Dutor 同学的 A False-Sharing Test ,发现差距好大(4线程4倍- ,16线程8倍+ ,我用dutor的代码实测16线程性能差距接近20倍),于是也写了段小代码来测试它。跟dutor同学不一样,我用的是 c 实现的,看起来可能没那么易读。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include <limits.h>

void *tester(void *arg)
{
    long *nloop = (long *)arg; //这里之前笔误写成int了。
    while ( (*nloop)-- );
    return NULL;
}

int driver(int nthread, int nloop, int npad)
{
    size_t size = npad + sizeof(long); //每个线程占用sizeof(long) + npad的空间
    char buff[size * nthread];
    pthread_t th[nthread];

    struct timeval s, e;
    gettimeofday(&s, NULL);

    for (int i = 0; i < nthread; i++) {
        int *arg = (int *)(buff + size * i);
        *arg = nloop;
        pthread_create(&th[i], NULL, tester, (void *)arg);
    }

    void *pret;
    for (int i = 0; i < nthread; i++)
        pthread_join(th[i], &pret);

    gettimeofday(&e, NULL);

    return (e.tv_sec - s.tv_sec) * 1000000 + e.tv_usec - s.tv_usec;
}

int main()
{
    int nloop = 1024 * 1024 * 128, nthread = 16, npad, best_padding = 0, best_usage = INT_MAX;
    printf("nloop = %d, nthread = %d\n\n", nloop, nthread);
    for (npad = 64; npad >= 0; npad -= 8) { //之所以步长为8是为了避免非8字节对齐long可能有的性能损失
        int i, usage = 0;;
        for (i = 0; i < 3; i++)
            usage += driver(nthread, nloop, npad);
        usage /= 3;
        if (usage < best_usage) {
            best_usage = usage;
            best_padding = npad;
        }
        printf("padding: %2d, time usage: %12d\n", npad, usage);
    }
    printf("\nbest padding: %2d, time usage: %12d\n", best_padding, best_usage);
    return 0;
}

引用
$ gcc false_sharing.c -lpthread -std=c99
$ ./a.out
nloop = 134217728, nthread = 16

padding: 64, time usage:      491395
padding: 56, time usage:      477760
padding: 48, time usage:      853594
padding: 40, time usage:      834318
padding: 32, time usage:      905200
padding: 24, time usage:      940989
padding: 16, time usage:      991595
padding:  8, time usage:      1040412
padding:  0, time usage:      1112716

best padding: 56, time usage:      477760

该机器使用的是4颗4核8线程的Xeon E7520@1.87GHz (16个物理核心32个逻辑核心),64GB RAM,/proc/cpuinfo里的cache_alignment是64

可以看出来,padding=56(也就是正好对齐到一个cache行)的时候效率最高,是没有填充时的2倍+的效率,虽然明显,但是显著地没有dutor的测试那么夸张。

把dutor的代码稍微改了下,s[ith].n = NLOOP,且pthread_create的时候传入的参数改成 (void *)&(s[ith].n),然后hook程序改成
size_t *n = (size_t *)args;
while ( (*n)-- );
return NULL;

其运行效率提升显著,padding=56的时候能快10%左右,而padding=0的时候能快达7倍之巨,最终的性能差距大约可以降至 3 倍的差距。这说明dutor的测试方法并不是测试裸的性能差距,带来的了一定的误差。

由于现在多数CPU都已经有了共享的L2或者L3 Cache,Cache Line失效的问题得到了相当的改善,不过不同物理CPU上仍然需要注意这个问题。

然而有一点我不能理解,这个修改对两种情况的影响竟相差这么大,这里头又有什么玄机呢...... #UPDATE: 后根据dutor的测试,我去掉了 for 循环中用到的循环变量 i 之后,性能差距立即将至2倍左右,修改循环的方向或者将for改成while则无效,因此这很可能是分支预测失效带来的问题了。
Mar 3

Python int缓存的那点事 不指定

felix021 @ 2013-3-3 02:42 [IT » Python] 评论(1) , 引用(0) , 阅读(5114) | Via 本站原创
早先翻python代码的时候是有注意到 intobject 里有缓存这档事,不过没细看。昨天有人在sf问起为什么有如下现象:
引用
>>> a = 1.0
>>> b = 1.0
>>> a is b
False
>>> a = 1
>>> b = 1
>>> a is b
True

于是又翻开python的源码 python2.6.8/Objects/intobject.c ,可以看到这些代码(略做简化):
#define NSMALLPOSINTS          257
#define NSMALLNEGINTS          5
/* References to small integers are saved in this array so that they
  can be shared.
  The integers that are saved are those in the range
  -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

PyObject *
PyInt_FromLong(long ival)
{
    register PyIntObject *v;
    //如果 -5 <= ival && ival < 257, 命中缓存~
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
        return (PyObject *) v;
    }
    if (free_list == NULL) { //这个是另一个优化,相当于内存池,用链表实现
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)Py_TYPE(v);
    PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}

而PyFloat_Object并没有(也不适合)实现这样的缓存,所以就可以解释上面的情况了。

更进一步,可以用257来验证一下,的确是超出了缓存的范围:
引用
>>> a = 257
>>> b = 257
>>> a is b
False


然后手贱做了另一个测试,蛋疼了:
引用
>>> a = 257; b = 257; a is b
True

也就是说如果让解释器一次执行的话,解释器又会再优化它,让a、b引用同一个对象。//注:这里对于float和str类型的常量效果是一样的。

为了搞清楚解释器到底是怎么实现这一点的,又把代码翻出来。之前翻的时候对解释器的执行流程已经有大致的了解了。make得到的python解释器是从 Module/python.c 里的 main() 函数开始的,调用链大约是这样:
引用
main() @Modules/python.c
    Py_Main() @Modules/main.c
        PyRun_AnyFileExFlags() @Python/pythonrun.c
            PyRun_SimpleFileExFlags
                PyRun_FileExFlags()


从 PyRun_FileExFlags 开始,才能看到底层代码正式登场:
引用
PyRun_FileExFlags()
    mod_ty *mod = PyParser_ASTFromFile() //把文件转换成AST(Abstract Syntax Tree)
        node *n = PyParser_ParseFileFlagsEx() //生成CST(Concrete Syntax Tree)
            parsetoke() //逐个解析token
                ps = PyParser_New()
                for (;;)
                    PyTokenizer_Get() //获取下一个token
                    PyParser_AddToken(ps, ...) //将token加入到CST中
        mod = PyAST_FromNode(n, ...)  //将CST转换成AST
            递归调用 ast_for_xxx 生成AST,同时过滤CST中的冗余信息
                其中ast_for_atom中调用了parsenumber, 它调用PyInt_FromLong()
    run_mod(mod, ...) //执行AST
        co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode
            PyFuture_FromAST()
            PySymtable_Build() //创建符号表
            co = compiler_mod() //编译ast为bytecode
        PyEval_EvalCode(co, ...) //执行bytecode
            PyEval_EvalCodeEx()

注:更详细的Python编译解释流程可参见这一系列: http://blog.csdn.net/atfield/article/category/256448

通过加入一些调试代码,可以窥探到内部的执行流。例如,在PyParser_AddToken中输出token的名称和类型编码;在PyParser_ParseFileFlagsEx()之后调用PyNode_ListTree(),可以看到生成的CST树(修改list1node()可以让它打印出更容易阅读的版本);修改PyInt_FromLong(),让它在ival=257的时候输出创建的object的id(CPython实现中 id 其实就是指针的值)。加上一些代码以后,编译python,执行test.py可以看到如下输出:
引用
felix021@ubuntu-server:~/src/python2.7-2.7.3$ cat test.py
a = 257
b = 0x101
print a is b
felix021@ubuntu-server:~/src/python2.7-2.7.3$ ./python -d test.py
PyParser_ParseFileFlagsEx
    type =    1, token: [a]
    type =  22, token: [=]
    type =    2, token: [257]
    type =    4, token: []
    type =    1, token: [b]
    type =  22, token: [=]
    type =    2, token: [0x101]
    type =    4, token: []
    type =    1, token: [print]
    type =    1, token: [a]
    type =    1, token: [is]
    type =    1, token: [b]
    type =    4, token: []
    type =    4, token: []
    type =    0, token: []
PyNode_ListTree:
                    <1>a  //type=1表示是NAME
    <22>=
                    <2>257  //type=2表示是NUMBER
  <4> //这是NEWLINE
                    <1>b
    <22>=
                    <2>0x101
  <4>
    <1>print
                  <1>a
          <1>is
                  <1>b
  <4>
<4>
<0>  //这是ENDMARKER
Before PyAST_FromNode
    name = a
    ival = 257, id = 22699048 //注意这个id和下一个id不一样
    name = b
    ival = 257, id = 22698784
    name = b
    name = a
After PyAST_FromNode
True #这一行是print a is b的输出

从输出可以看到,解析源码生成CST的时候(输出的CST已经滤掉了非TERMINAL的node),每个token还保留着原始的字符串(例如0x101和257),而在CST到AST的转换过程中(PyAST_FromNode),解释器为每一个NUMBER都创建了一个PyIntObject。然而在程序运行的最终结果里可以看到,a is b的结果是True,也就是说,从AST转换到CFG并执行(run_mod)的过程中,解释器做了适量的优化,将 a 和 b 都指向了同一个 int 对象。

由于对CFG不熟,相应的代码还不太看得懂,所以暂时只能烂尾了,如果以后看懂了,再来补充。

续集:Python int缓存的那点事[续]
Feb 4

探探gethostbyname 不指定

felix021 @ 2013-2-4 23:18 [IT » 网络] 评论(0) , 引用(0) , 阅读(3664) | Via 本站原创
开篇先说一下,在它的manpage里面,有这么一句话:
引用
The gethostbyname*() and gethostbyaddr*() functions are obsolete. Applications should use getaddrinfo(3) and getnameinfo(3) instead.
这是因为gethostbyname只能处理ipv4请求(经测试ipv6.google.com不能解析,ipv6.tsinghua.edu.cn只能解析出ipv4地址),因此推荐使用 getaddrinfo 来替代它。

忽略上述问题不管的话, gethostbyname 还是有点意思的。它的原型和用法大概可以这样:
struct hostent *gethostbyname(const char *name);

struct hostent *he = gethostbyname(argv[1]);
printf("%s\n", inet_ntoa(*(struct in_addr *)he->h_addr));

用法特别简单吧,如果不是在提到这个函数的时候附带说明“它使用了内部分配的空间,因此不是线程安全的”,估计会大受欢迎的。
//注:由于windows版用了 thread local 的变量,所以在windows下它是线程安全的(只要不是同一个线程马上再调用覆盖它的话)。

相应的,像ctime、localtime之类的posix接口一样,它的线程安全版就是带了 _r 后缀的 gethostname_r ,虽然名字只复杂了一点点,但是接口可不只是复杂了一点点啊:
int gethostbyname_r(const char *name, struct hostent *ret,
    char *buf, size_t buflen, struct hostent **result, int *h_errnop);
其中的name是hostname(或者ip地址),ret应当指向一个分配好的struct hostent结构体(函数把内容填进去),buf应当指向一块分配好的空间(不小于buflen),result是一个struct hostent的二级指针(如果失败会存个NULL),h_errnop则指向某个int(失败则填对应的herr,这个奇葩函数的errno跟errno.h里面那个还不是一套)。

用起来真是相当地恶心,比如下面这个函数通过它来解析ip地址,跟前面的代码的复杂度完全不是一个量级的:
int host2addr(const char *host, struct in_addr *addr)
{
    struct hostent he, *result;
    int herr, ret, bufsz = 512;
    char *buff = NULL;
    do {
        char *new_buff = (char *)realloc(buff, bufsz);
        if (new_buff == NULL) {
            free(buff);
            return ENOMEM;
        }
        buff = new_buff;
        ret = gethostbyname_r(host, &he, buff, bufsz, &result, &herr);
        bufsz *= 2;
    } while (ret == ERANGE);

    if (ret == 0 && result != NULL)
        *addr = *(struct in_addr *)he.h_addr;
    else if (result == NULL)
        ret = herr;
    free(buff);
    return ret;
}


代码是写出来了,但是觉得不太对头,仔细看了一下 struct hostent 的定义,里头还有多个指针(h_name、h_aliases、h_addr_list)什么的:
struct hostent {
  char  *h_name;            /* official name of host */
  char **h_aliases;        /* alias list */
  int    h_addrtype;        /* host address type */
  int    h_length;          /* length of address */
  char **h_addr_list;      /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
如果不释放的话,不会造成内存泄漏么?

于是去看了下gethostbyname和gethostbyname_r的源码,进入 eglibc-2.15 的源码, ctags -r 然后 vim -t gethostbyname ,竟然没有这个函数。然后翻了翻,看到有个 inet 目录,里面有个 gethstbynm.c 这样一个奇葩的文件,里面除了include一些头文件之外,只有一些简单的宏定义和一个奇怪的include:
#define LOOKUP_TYPE struct hostent
#define FUNCTION_NAME  gethostbyname
#define DATABASE_NAME  hosts
#define ADD_PARAMS  const char *name
#define ADD_VARIABLES  name
#define BUFLEN      1024
#define NEED_H_ERRNO    1

#define HANDLE_DIGITS_DOTS  1

#include <nss/getXXbyYY.c>

打开一看 getXXbyYY.c 这个文件更奇葩了,根本就是用宏来实现C++中的模板功能啊。。。grep一下还发现,这个文件被18个其他文件include,也就是说,有18个函数(诸如gethostbyaddr, getnetbyaddr, 甚至getpwuid、getpwnam)都是用的这个模板。

凑合着看了一下,代码倒是不复杂(具体就不贴出来了),基本逻辑是这样的:
LOOKUP_TYPE *
FUNCTION_NAME (ADD_PARAMS)
{
    获取锁
    如buffer==NULL,分配初始化的空间
    如定义了HANDLE_DIGITS_DOTS(如gethostbyname)则检查name是否是ip地址并处理(是的话就不用发dns请求了)
    类似的while循环:调用对应的线程安全版函数,并检查是否buffer大小不够(如果是的话realloc)
    释放锁
    如果定义了NEED_H_ERRNO,设置对应的值
    返回
}

也就是说,代码流程基本上跟前面调用 gethostbyname_r 的代码一样,只是多了一个获取/释放锁的操作。根据里面的流程可以看出,代码里面不是使用静态的空间,而是动态malloc的(所以要获取锁,否则在libc内部崩溃,那不好)。由此,所谓的不是线程安全的,其实并不是函数本身,而是返回的数据。

但是,看懂了这个代码,并没有解决上面的问题啊。。。于是到 stackoverflow 上面去提了个问,很快就得到大牛的回答了:
引用
You should print out the values of the pointers in that struct to find out the answer to your question. You'll discover that they all point to data inside the buffer you allocated.

So a single free is all you need to free up all the memory.

But this also means that you must not free that allocation until you've finished using or copying whatever data you're interested in.

也就是说,我完全忽略了塞给 gethostbyname_r 的 buf 这个参数:hostent 里面的指针指向的数据就存在 buf 里面,因此最后只要 free 掉它就够了。

由于有些域名可能绑定了好多个ip(比如google.com可能就绑了数十个),buf 的空间可能不够大,所以才需要一个realloc的机制。根据实测,buf的大小从512或者1024开始比较合适,普通的域名就不需要realloc了。

到此差不多该结束了,最后吐槽一句,这两个接口真奇葩啊。。。
Jan 20
这周写了个模块,让python可以跟后台的C服务通信了。

总体上来说给python写个模块还是比较容易的,比给php写模块要舒服多了,但是还是遇到一个问题:给php写模块的时候可以用MINIT完成初始化、MSHUTDOWN完成清理;给Python写模块,有对应的模块初始化函数,但是却没有对应的清理函数,实在是令人蛋疼。

需求其实很简单(应该也很普遍吧?),只是要在python退出之前执行一点代码,保证在初始化的时候分配的一些资源能够被释放;但是模块本身并没有提供这样的机制,只好想其他办法了。

最简单的是用C语言本身提供的 atexit ,不过这是在main()结束或者调用exit()时 - 也就是说,在整个C环境要结束了的情况下才会运行(但是在关闭所有打开的文件之前)。

虽然有效,但是由于它不是python提供的机制,多少让人有点不太放心,所以还是看看别人怎么用的吧。

Google搜了一下"python  module  destructor",stackoverflow上说的是,可以用python的 atexit 模块。这是一个纯python模块,源码可以在 /usr/lib/pythonX.X/atexit.py 看到,其实就是通过 atexit.register() 注册退出函数,并将 atexit._run_exitfuncs() 函数绑定到 sys.exitfunc 。sys.exitfunc会在Py_Finalize()这个函数中被调用(call_sys_exitfunc())。

在C模块里调用它,最简单的办法是在模块的初始化函数里加入类似这样一句(注意换行和缩进):
    PyRun_SimpleString(
            "import atexit\n"
            "def __modname_clean():\n"
            "    import modname\n"
            "    modname.clean()\n"
            "atexit.register(__modname_clean)\n");

这大致相当于在python脚本里用eval执行了一段代码(没有指定globals、locals和compile_flag)。也可以用如下几乎等价的纯C代码
//注:m是在模块初始化函数中的定义的当前模块对象
    PyObject *pClean = PyObject_GetAttrString(m, "clean");
    if (pClean != NULL)
    {
        PyObject *pName = PyString_FromString("atexit");
        PyObject *pMod = PyImport_Import(pName);
        Py_DECREF(pName);
        if (pMod != NULL)
        {
            PyObject *pFunc = PyObject_GetAttrString(pMod, "register");
            if (pFunc && PyCallable_Check(pFunc))
            {
                PyObject *pArgs = Py_BuildValue("(O)", pClean);
                PyObject *ret = PyObject_CallObject(pFunc, pArgs);
                //if (ret == NULL) ; //sth went wrong
                Py_XDECREF(pArgs);
                Py_XDECREF(ret);
            }
            Py_XDECREF(pFunc);
        }
        Py_XDECREF(pMod);
    }
    Py_XDECREF(pClean);

由于错误处理、引用计数的代码占了一大半,所以代码这么长……

这种方法的好处是,它在python运行环境结束之前执行注册的函数,所以注册的函数仍然可以使用绝大部分python提供的功能(你甚至可以再import新的模块,但是最好别用thread...)。然而atexit有一个蛋疼的缺陷:尽管python在sys模块的doc里写的是“Assigning to sys.exitfunc is deprecated; use the atexit module instead.”,但是sys.exitfunc仍然是任意python脚本都可以修改的!所以通过atexit.register()注册的函数并不能100%保证被运行。

再回过头来看Py_Finalize()函数,这里其实是有很大槽点的:它有一个长达十一行的"sundry finalizers"段!
    /* Sundry finalizers */
    PyMethod_Fini();
    PyFrame_Fini();
    PyCFunction_Fini();
    PyTuple_Fini();
    PyList_Fini();
    PySet_Fini();
    PyString_Fini();
    PyByteArray_Fini();
    PyInt_Fini();
    PyFloat_Fini();
    PyDict_Fini();

也就是说,它为许多内置的module逐一硬编码了清理函数,却没有实现一个清理机制!

甚至在这后面还有一段注释:
    /* XXX Still allocated:
      - various static ad-hoc pointers to interned strings
      - int and float free list blocks
      - whatever various modules and libraries allocate
    */
坑爹啊。。。

再往下看,在Py_Finalize()的最后一行终于出现了本篇的另一个主角:call_ll_exitfuncs(); 这个函数的内容很简单:逐一执行 exitfuncs 这个数组里保存的函数。而 exitfuncs 这个数组的内容,则是由 Py_AtExit() 函数填进去的:
#define NEXITFUNCS 32  //另一个坑:最多只能注册32个exitfunc
static void (*exitfuncs[NEXITFUNCS])(void);
static int nexitfuncs = 0;

int Py_AtExit(void (*func)(void))
{
    if (nexitfuncs >= NEXITFUNCS)
        return -1;
    exitfuncs[nexitfuncs++] = func;
    return 0;
}

我最终采用的解决方案是这个:Py_AtExit()。

最后总结对比一下上述3种方案:

1. C语言的 atexit() : 使用链表保存注册的函数,只要内存够,数量没限制。在Python完全结束后执行。
2. Python的 atexit 模块:使用 list 保存注册的函数,在register的时候还可以带参数,在Python解释器仍然完整的情况下执行,一般来说很够用;但由于使用的sys.exitfunc可能会被其他脚本使用,并不能100%保证有效,故,有强迫症的慎用。或者应当在doc里明确说明。
3. Python C API的 Py_AtExit() :在Python环境几乎完全结束的时候被调用,最多只能注册32个函数(所以最好要检查返回值)。其实从流程上来说,在这之后马上就是main的return或者exit()函数调用,所以跟atexit基本上差不多。

p.s. 另一个尝试但是失败了的方案:建立一个新的type X_Type,在它的tp_dealloc里头写入清理代码,以X_Type建立一个新的class X,在模块的初始化函数中new一个X,撂着,并期望Py_Finalize()里的PyGC_Collect()会把它回收掉。不知道还差了点什么,如果哪位大牛知道,还望不吝赐教。
Dec 31
昨天@Zind同学找到我之前的一篇blog(已经修改),里面提到了mysql_ping和MYSQL_OPT_RECONNECT的一些事情。

之所以写那篇blog,是因为去年写的一些代码遇到了“2006:MySQL server has gone away”错误。这个问题是因为wait_timeout这个参数的默认值是28800,也就是说,如果一个连接连续8个小时没有任何请求,那么Server端就会把它断开。在测试环境中一个晚上没有请求很正常……于是第二天早上来的时候就发现这个错误了。

其实我有考虑这个问题的,真的……因为我知道php里面有个函数叫做mysql_ping(),PHP手册上说:“mysql_ping() 检查到服务器的连接是否正常。如果断开,则自动尝试连接。本函数可用于空闲很久的脚本来检查服务器是否关闭了连接,如果有必要则重新连接上。”

回想起来,以前真是很傻很天真。根据MySQL官方C API里mysql_ping()的文档:"Checks whether the connection to the server is working. If the connection has gone down and auto-reconnect is enabled an attempt to reconnect is made. ... Auto-reconnect is disabled by default. To enable it, call mysql_options() with the MYSQL_OPT_RECONNECT option",也就是说,它实际上还依赖于MYSQL_OPT_RECONNECT这个配置,而这个配置默认(自5.0.3开始)是关闭的!

虽然想起来很愤怒很蛋疼,不过看到 libmysql/client.c: mysql_init() 里的注释就淡定了:
引用
By default we don't reconnect because it could silently corrupt data (after reconnection you potentially lose table locks, user variables, session variables (transactions but they are specifically dealt with in mysql_reconnect()).  This is a change: < 5.0.3 mysql->reconnect was set to 1 by default. 


好吧,既然有问题,那就正视它。解决办法是调用 mysql_options ,将MYSQL_OPT_RECONNECT设置为1:
char value = 1;
mysql_options(mysql, MYSQL_OPT_RECONNECT, &value);


但是!! 在mysql 5.0.19 之前,mysql->reconnect = 0 这一句是放在 mysql_real_connect() 里面的!也就是说,如果你不能像处理其他选项一样,而是必须在mysql_real_connect()之前设置MYSQL_OPT_RECONNECT,坑爹啊!

好吧好吧,总之,关于坑的问题暂告一段落,结论就是,不管是哪个版本,如果你想要启用自动重连,最好都是在mysql_real_connect()之后,反正不会错。

然后这篇的重点来了(前面似乎太罗嗦了点):MYSQL_OPT_RECONNECT的文档里头说了,这个选项是用来启用/禁用(当发现连接断开时的)自动重连,那么,MYSQL什么时候会发现链接断开呢?

这个问题可能太大了,不过不妨先去追一下,mysql_ping()做了啥。

下载源码 http://cdn.mysql.com/Downloads/MySQL-5.1/mysql-5.1.67.tar.gz ,解压以后ctags -R,再vim -t mysql_ping ,马上就定位到了,似乎太简单了点:
int STDCALL
mysql_ping(MYSQL *mysql)
{
  int res;
  DBUG_ENTER("mysql_ping");
  res= simple_command(mysql,COM_PING,0,0,0);        //试着向服务器发送一个ping包
  if (res == CR_SERVER_LOST && mysql->reconnect)    //如果server挂了,而mysql->reconnect为true
    res= simple_command(mysql,COM_PING,0,0,0);      //再ping一次??
  DBUG_RETURN(res);
}


好吧,看来关键在于这个simple_command了。ctrl+],原来是这样:
#define simple_command(mysql, command, arg, length, skip_check) \
  (*(mysql)->methods->advanced_command)(mysql, command, 0, 0, arg, length, skip_check, NULL)


好吧,先去追一下MYSQL,里头有个 const struct st_mysql_methods *methods ,再追一下 st_mysql_methods ....
typedef struct st_mysql_methods
{
  my_bool (*read_query_result)(MYSQL *mysql);
  my_bool (*advanced_command)(MYSQL *mysql, enum enum_server_command command,
                  const unsigned char *header, unsigned long header_length,
                  const unsigned char *arg, unsigned long arg_length,
                  my_bool skip_check, MYSQL_STMT *stmt);
  ......

坑爹啊!又是这种鸟代码!蛋疼的C语言!struct只有属性没有方法!没办法,只能暴力了:
引用
find -name '*.c' -exec /bin/grep '{}' -Hne 'mysql->methods *=' ';'
./libmysql_r/client.c:1907:  mysql->methods= &client_methods;
./sql-common/client.c:1907:  mysql->methods= &client_methods;
./libmysql/client.c:1907:  mysql->methods= &client_methods;
./libmysqld/libmysqld.c:120:  mysql->methods= &embedded_methods;
./sql/client.c:1907:  mysql->methods= &client_methods;


果断追到client_methods:
static MYSQL_METHODS client_methods=
{
  cli_read_query_result,                      /* read_query_result */
  cli_advanced_command,                        /* advanced_command */
  ...

也就是说simple_command最后调用了cli_advanced_command这个函数。前面的 simple_command(mysql,COM_PING,0,0,0) 相当于是调用了 cli_advanced_command(mysql, COM_PING, 0, 0, 0, 0, 0, NULL) 。

这个函数做了啥呢。。。其实也不复杂:
1. 设置默认返回值为1 (意外出错goto时被返回)
2. 设置sigpipe的handler(以便忽略它)
3. 如果 mysql->net.vio == 0 ,那么调用mysql_reconnect重连,失败的话就返回1
4. mysql没准备好,返回1
5. 清除之前的信息(错误码、缓冲区、affected_rows)等等
6. 调用net_write_command将命令发送给server,如果失败:
    6.1 检查错误信息,如果是因为发送包太大,goto end
    6.2 调用end_server(mysql)关闭连接
    6.3 调用mysql_reconnect尝试重连,如果失败goto end
    6.4 再次调用net_write_command将命令发送给server,失败则goto end
7. 设置result = 0(发送成功)
8. 如果参数中要求检查server的返回,则读取一个packet进行检查(失败的话就result=1)
9. (end标签)
10. 恢复sigpipe
11. 返回result

可以看到,这里两次调用了mysql_reconnect,但都是有条件的:第一次是在mysql->net.vio == 0的情况下,第二次是net_write_command失败且不是因为包太大的情况。vio相关的代码看得一头雾水,实在找不出头绪,于是决定暴力一点:直接修改这个函数,加入一堆fprintf(stderr, ...)(具体加在哪里就不说了,反正使劲塞就是了),然后写了一个C代码:
#include <stdio.h>
#include <stdlib.h>
#include <mysql/mysql.h>

void do_err(MYSQL *mysql) {
    if (mysql_errno(mysql)) {
        fprintf(stderr, "%d:%s\n", mysql_errno(mysql), mysql_error(mysql));
        exit(mysql_errno(mysql));
    }
}

int main()
{
    MYSQL * mysql = mysql_init(NULL);
    do_err(mysql);

    mysql_real_connect(mysql, "127.0.0.1", "root", "123456", "test", 3306, NULL, 0);
    do_err(mysql);

    char value = 1;
    mysql_options(mysql, MYSQL_OPT_RECONNECT, &value);
   
    char cmd[1024] = "SELECT * FROM t";
    while (1) {
        mysql_query(mysql, cmd);
        do_err(mysql);

        MYSQL_RES *result = mysql_store_result(mysql);

        MYSQL_ROW  row;
        while ((row = mysql_fetch_row(result)) != NULL) {
            int i, num_fields = mysql_num_fields(result);
            for (i = 0; i < num_fields; i++)
                printf("%s\t", row[i] ? row[i] : "NULL");
            //注意上一句是不是二进制安全的,因为row里头可能包含\0,也可能末尾没有\0
            printf("\n");
        }

        mysql_free_result(result);
        printf("press enter..."); getchar();
    }
    mysql_close(mysql);
    return 0;
}


运行输出:
引用
inside mysql_real_query
mysql->net.vio = 0x90e760
mysql->status = 0
net write_command
after send_query
---
1
2
press enter...//按回车之前先重启一下mysql server,下面这几句按照函数调用层次进行手动缩进了……
inside mysql_real_query
    mysql->net.vio = 0x90e760 //进入cli_advanced_command
    mysql->status = 0
    net_write_command
    end_server //说明net_write_command失败了
        inside mysql_reconnect //它会调用mysql_real_query
            inside mysql_real_query
                mysql->net.vio = 0x919990 //于是又回到了cli_advanced_command
                mysql->status = 0
                net_write_command //这次成功了
            after send_query  //这句我是写在mysql_real_query里面的
        reconnect succeded
    after reconnect: mysql->status = 0
after send_query //所以又来一次。。


根据fprintf的输出,发现在正常情况下,mysql->net.vio这个指针并不等于0,所以第一个mysql_reconnect不会被调用。而net_write_command也是正确执行,第二个reconnect也没被调用。

而在执行完一个query,然后重启mysql server再执行query (mysql_query => mysql_real_query => mysql_send_query => cli_advanced_command),就会发现,mysql->net.vio仍然不等于0,但是net_write_command失败了,于是先调用了end_server()(这里面会将mysql->net.vio设置为0,不过不影响后面的流程...),然后调用了第二个reconnect,这个reconnect会调用mysql_init()以及mysql_real_query()执行一些初始化的命令,于是又回到cli_advanced_command,再一步一步回溯。。。

综上可知,如果设置了MYSQL_OPT_RECONNECT(),那么mysql_query()是可以完成自动重连的。实际上,由于cli_advanced_command会在必要情况下调用mysql_reconnect(实际上这个函数也只在这里被调用),因此,所有用到了cli_read_query_result的地方(或者simple_command),也都可以完成自动重连。

完结。

//混蛋,这篇纯粹是为了凑一月至少一篇这个目标啊!
Dec 13
这事儿其实只要有了拼音库,就挺简单的。我从 pinyin4j 这个项目里搞了一份出来。虽然这个库是为java写的,不过要提取倒是相当简单:到这里 下载pinyin4j-2.5.0.zip,解压得到里头的 lib/pinyin4j-2.5.0.jar ,再解压得到里头的 pinyindb/unicode_to_hanyu_pinyin.txt 。

这个文件的结构很简单,每一行的基本结构是这样的:
引用
4E04 (shang4,shang3)

前面的 4E04 是汉字的Unicode编码,空格分隔,然后括号里面包含了所有读音(而且看起来像是按频率排序好了的),每个读音分别给出了拼音的声母韵母以及声调。

在这个基础上解析它就相当简单了。
f = open("unicode_to_hanyu_pinyin.txt", "r")

py = {}
for l in f:
    l = l.strip()  #行末回车
    key, val = l.split(' ')
    sd = val[1:-1].replace('u:', 'v').split(',')  #去掉左右括号,把u:转为v(驴 lv),然后按逗号分隔
    arr_sd = []
    for i in sd:
        arr_sd.append({'py': i[0:-1], 'tone': i[-1]})  #把每个读音的声调分离出来
    py[int(key, 16)] = arr_sd  #把unicode编码转成10进制作为key

f.close()


当然,你也可以把它存入一个key/value数据库(memcachedb什么的就挺好)。以上面的例子的话,把一个字符串中的汉字转换成拼音就很简单了:
def convert(str, encoding = 'utf-8'):
    ret = ''
    for i in str.decode(encoding):
        w = ord(i)
        if py.has_key(w):
            ret += "%s-%s " % (py[w][0]['py'] , py[w][0]['tone'])
        else:
            ret += i
    return ret


要注意的一点是,“驴”的拼音被标记为 lu:2 ,而不是打字时习惯使用的 lv ,如果有需要的,还得再加个简单的转换逻辑。

实际上这个库里头还支持通用拼音(貌似是台湾地区使用过的)等其他转换方式,有需要的同学可以自己考据一下其结构。

p.s. 在python里头可以用 unichr(0x4E04) 得到这个unicode对应的字符,也可以用 ord('率'.decode('utf-8'))  得到这个字符的unicode编码(注意替换字符的原始编码)。至于十进制和十六进制的转换,也很简单, hex(32768) 得到 '0x8000' 而int('0x8000', 16)就能得到32768。python真好用。
Nov 27
昨天关注了下 微信公众平台,的确是个好东西,赞一下tx近来的开放。

它的api 乍一看还是挺简单的,于是就写了个小东西玩玩。

但是在开发的过程中遇到了多个坑,通过有故事王国的@ctqmumu同学把问题转了过去。实际开发的过程中遇到了好几个问题,但是其他问题(包括发一条消息请求我两次)都莫名其妙消失了,只有一个问题(具体见后文)通过被喷的方式解决了。

总之最后是可以用了,于是多花了点时间,把代码完善了下,写好了注释和样例,放在了Google Code上面,有需要的同学可以拿去用。


下载地址:http://code.google.com/p/mmsdk/downloads/list
    注:目前callback url仅支持80端口(@2012.11.27)

#UPDATE 2012.11.29 添加了对图片消息的支持(样例)、对调试的支持(DUMP请求/回复的xml文件),测试SAE可用(调试功能除外)

== 分割线,下面是纯吐槽 ==
Nov 25
C/C++标准里头都没有正则表达式,C++还好,可以用上boost::regex,C的话,最简单的还是用系统自带的正则库了。

这个正则库真是相当简单,如果不关心内部琐碎的细节,实际上它只有2个类型、4个函数和7个常量,详细的后面会列出来(或者直接man regex),这里还是直接看例子比较实在:

代码1:email格式检测
#include <stdio.h>
#include <regex.h>
#include <assert.h>

int main() {
    //分配一个regex_t
    regex_t reg;
    //编译(使用POSIX扩展模式、并忽略大小写),确认编译成功(返回0)
    assert(regcomp(&reg, "^[a-z0-9_]+@([a-z0-9-]+\\.)+[a-z0-9]+$", REG_EXTENDED | REG_ICASE) == 0);
    int ret = regexec(&reg, "steve@rim.jobs", 0, NULL, 0); //执行搜索
    //看看返回值:0表示匹配成功,1表示REG_NOMATCH
    printf("ret = %d, nomatch = %d\n", ret, REG_NOMATCH);
    regfree(&reg); //记得释放空间
}

分页: 9/94 第一页 上页 4 5 6 7 8 9 10 11 12 13 下页 最后页 [ 显示模式: 摘要 | 列表 ]