安全代码的一些小tip

历经一年多的flex+java游荡,总算又回归linux下C/C++开发,虽然这一年中曾经客串写过daemon,但是看到大段大段的C++ code,仍然有种事过境迁激动到要内牛满面的地步。好久不碰是一方面,基础差也是一方面。最近修了点Klockwork扫出的bug,看到一点有趣的东西,记下来备忘。

Klockwork扫出的问题很大一部分是buffer overrun的问题,基本上是因为不安全API引起的。比如说下面的这几个经典API就应该替换成右边对应的safe API(以下来自飞哥http://zoufeiblog.appspot.com/的经典总结):

    1. sprintf –> snprintf
    2. strcat –> strncat
    3. strcpy –> strncpy
    4. sscanf –> fgetc
    5. system –> fork + exec
    6. execvp –> execve

但是在修的过程中,发现几个很好玩的特例。

    1. sscanf
    2. snprintf
    3. new
    4. stl iterator

sscanf

看到两个特例,没有替换API的提要。一个是用format字符串限制字符长度,

char * src = "abc 123";
char key[3] = {0};
int count = 0;
sscanf(src, "%2s %d", &key, &count);

这种写法可以避免buffer overrun,但是不能保证数据的正确性。下一个特例就非常经典,对sscanf结果的错误判断很tricky

int TmDbConnectImpl::getColValInt(const char* szColName)
{
        const char* szBuf = getColVal(szColName);
        if (NULL == szBuf)
                throw "Wrong column name";

// Check the numeric string is valid
        int  iValue=0;
        char cBad;

switch(sscanf(szBuf, "%d%c", &iValue, &cBad))
        {
        case 1: // One numeric value matched, data is valid.
                break;
        case 2:
                LogPrintf("Invalid character = %c\n", cBad);
        default:
                LogPrintf("Could not convert string to int, column name:[%s], value:[%s]",
                          szColName, szBuf);
        //throw "Could not convert data to number";
       }
}

当然其实上面这段code可以用strtol来代替。

snprintf

这是个safe API没错,但在windows下面_snprintf不能保证null terminated,所以需要_snprintf(buf, sizeof(buf) – 1, "%s", value);而在linux下面snprintf又是null terminated,所以snprintf(buf, sizeof(buf), "%s", value)。

为了达到平台统一性,在linux下面是不是也写作snprintf(buf, sizeof(buf)-1, "%s", value)好了呢,可以。但是也发现一个特例,

mechstr = (char*) malloc(strlen(mechanism) + 5);
if (!mechstr)
    return 0;
sprintf(mechstr, "{ %s }", mechanism);

字符数组分配的长度刚刚好,如果用-1的写法的话,除去snprintf为null terminator保留的一个字符外,还会丢掉一个字符,那么得到的结果是不正确的。所以最后的修法是,

snprintf(mechstr, len, "{ %s }", mechanism);
mechstr[len - 1] = ”;

强制末尾置0,以防万一,就算在windows下面也没有问题。

除此之外,还看见下面一个避开这个问题的解法:sprintf((char *)(SessionKey + i*2), "%2.2hx", HA1[i]),有人能看懂这个sprintf的意图么

new

一个常识是,new完了要检查内存分配是否成功。在大学的时候老师说的是用指针为不为空来判断,但这个是不准确的。在1993年前,C++一直要求在内存分配失败时operator new要返回0。但是现在是要求抛出std::bad_alloc异常。

所以检查new是否成功的方式不是Object *p = new Object; if (p == NULL) xxx,而是

try {
        Object *p = new Object;
        p->name = "test";
}
catch (bad_alloc& ba) {xxx}

对于malloc,仍然用NULL进行判断

stl iterator

对于stl的iterator,一般的用法是for(vector::iterator p = objectVector->begin(); p != objectVector->end(); p++)。在这个例子中需要注意的是,如果在循环体内对objectVector里的Object进行了删除的话,那么p指针可能失效,导致死循环。

问题是,这样的写法for(vector::iterator p = objectVector->begin(); p end(); p++)对不对。还有一个问题是,下面的这个函数,里面的iterator用的对不对Smile 相当tricky的用法

 

int getAllParentGroupsByMember(const string &user_filter, ldap_set &groups)
{
     std::string szDn;
    std::string filter = user_filter;
   
    std::vector<std::string>::iterator iGroupQ;
    std::vector<std::string> groupQ;
   
    //reserve memory for vector to avoid iterator invalidation after inserting or deleting
    //plus, it adds efficiency
    groupQ.reserve(128);
    groupQ.clear();
   
    //iterate from the first member in the group queue
    iGroupQ = groupQ.begin();
 
    do
    {       
        vector< vector<string> > *res = 0;
        vector< vector<string> >::iterator p;
        vector<string>::iterator q;
       
        int rc = getEntityAttrs(filter, xxx, res, xx);
        if(rc > 0 && res != NULL){   
              for(p = res->begin(); p != res->end(); p++)
              {
                for(q = (*p).begin(); q != (*p).end(); q++)
                { 
                    if(*q != "" && (*q).c_str() != NULL)
                    {
                         //check for duplicate group objects in ldap set
                         //Prefast:local declaration hides declaration in an outer space
                         ldap_set::iterator pset = groups.find(*q);
                         if(pset == groups.end())
                         {
                              //enqueue new group object found in the group queue
                              groupQ.push_back(*q);   
                              //insert this unique group object in the sorted ldap set
                              groups.insert(*q);
                           }
                    }   
                }
              }
        }
       
        freeQueryResults(res);
       
        //groupQ is empty, no need to do further search, bails out
        if(groupQ.empty())
            break;
           
        //magic number 128 is chosen based on common practice.
        if(groupQ.size() > 128){
            break;
        }
       
        if(iGroupQ != groupQ.end())
        {
            getEntityDn((xxxx, iGroupQ->c_str(), szDn, xxxx);
            filter = "(&(member=" + szDn + ")" + "(objectCategory=group))";
        }
       
    }while(iGroupQ++ != groupQ.end());
   
    if(groups.empty() || groupQ.empty())
        return 0;
       
    return 1;
}

这些是修klockwork扫描结果时看到的一部分有意思的东西,有不对的地方请指正。这也只是书写安全代码很小的一部分,有一本书《Writing Secure Code》,有心修炼的可以去看看。

另:贴下非线程安全的API(同样来自飞哥的总结,refer自http://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html

unsafe_api

网络蜗居 之 添加Mobile页面支持

入手android手机快两个星期,忙里抽闲折腾熟手机之后,开始为blog添加mobile页面显示。用的是高人推荐的wordpress插件:WPtouch(http://wordpress.org/extend/plugins/wptouch/changelog/

自动安装方法:

  1. 登入wordpress管理页面
  2. 进入plugins管理页面,点击Add New
  3. 用Search Plugins搜索到WPtouch,直接Install Now

手动安装方式:

  1. 从上述链接下载WPtouch安装包
  2. 解压缩
  3. 将压缩后的wptouch目录拷贝到wordpress的wp-content/plugins/wptouch目录下
  4. 进入wordpress管理页面
  5. 进入plugins管理页面
  6. WPtouch会列在已安装插件列表中,Activate之,大功告成

安装过程相当简单,安装完本应大功告成,但在我的手机上(Android+ucweb)并没有生效。一开始以为是WPtouch只支持iPhone系列,查了主页,声明在1.9.25版本中确实支持android和blackberry。查了半天,原来是ucweb的原因。

在wptouch的设置中,高级选项中有一项关于user-agent的描述。描述中列出了所有支持的User-Agents,其中有一项android。测试过拿android自带的浏览器访问没有问题,ucweb不行。查看ucweb的网络设置,发现ua设置为默认,除此之外还有openwave和opera,经测试三者都无法支持。通过在wptouch管理页面中加入Custom user-agents,并更改ucweb的ua设置,可以解决这个问题。

management

不过经这样设置后,ucweb的显示样式依旧不好看,将浏览模式从适应屏幕改为缩放模式,可以增强显示效果。下面三张图分别是android默认浏览器、ucweb适应屏幕浏览模式、ucweb缩放模式浏览模式、ucweb上不支持mobile页面的显示效果

豌豆荚截屏_default 豌豆荚截屏_2 ucweb_change 豌豆荚截屏_original

确实用WPtouch样式效果会好很多,最重要的是,省流量。

BTW:所有手机截屏用USB连接电脑由豌豆荚完成。途中意外发现moto的手机管理页面,在浏览器地址栏中敲入http://192.168.16.2:8080/,就可以进入管理页面,还不错。moto真是靠android原地满血复活啊

moto_management

苏州两日游

俗话说,上有天堂,下有苏杭

这次去苏州并非是想去天堂,而是每年的春天,春宝宝都会像笑话里磕了药的那只兔子叫“sdouble,我们出去玩吧”,然后我总是那只不争气的长颈鹿,忙前忙后伺候着跟着瞎跑。今年这是个艰难的决定,因为忙到精神挂不住生理想吐,但是没法子,春宝宝有个“是不是兄弟就看这一次”的理由,于是我又从了……

杭州去过两次,苏州一直拖着没去,算起来对天堂的另一半有点偏心。因为听说苏州以园林著称,而我对园林着实不感冒。但是亲身经历过才知道,美好的东西得靠自己发现。这是个很神奇的小城,让我想起去过的很多地方

本来是打算走这个http://www.watertownhostel.com/recommend.htm攻略骑车玩的,结果天公不作美,从火车站落地就开始飘雨。但这绝不能阻挡人民游玩的决心!淡定的改公交

第一站,平江街

第一站来这里,是因为酒店在这里http://www.yhachina.com/ls.php?id=117,于是误打误撞进到这么个有名的地方。小桥流水人家,一度让我想到丽江。连两边的商铺都很像,酒吧、咖啡厅、个性商铺,还有小吃,我们在这里消灭了5个青团,还有个排队很长的烤鸭店,因为下雨外带实在不方便错过了,现在想起来很馋很馋……

小桥流水人家2 写给未来的明信片

第二站,观前街

这条街应该算是苏州的新街口了吧,大型饭店有,金鹰也有。但是建筑还是老式的建筑,看到金鹰购物中心嵌在这么古老的建筑里,很有种穿越回古代的感觉。来这里是为了吃午饭,苏州小吃很有名,可是不知道我们那天中午吃的是不是苏州小吃……我一直以为是,结果看过的同学们有的说是港式有的说是台湾的……还看到个很雷人的大标牌,我极力说服春宝宝把头镶嵌在圆心里让我拍一张黄金版的自由女神像,无奈死活不从……

不记得谁家的小吃了 IMG_2102

第三站,虎丘

吃饱喝足,正式上路。没有事先准备地图,七问八问好不容易到了北门。估计来北门的人少,显得相当冷清,但才另有一番风味,以至于后来觉得南门人多的像个菜市场……这里很让我想起瘦西湖

虎丘寂寞的北门  IMG_2107

不知道是不是时间的原因,可能大批的旅行团上来了,南门相当热闹,以至于下面两张没人的照片拍的相当辛苦。很好玩儿,我跟春宝宝说,第一张是杨过十六年练剑的地方,第二张是紫霞仙子的盘丝洞

虎丘杨过练剑的地方? 虎丘盘丝洞

而春宝宝则表示我很没有文化,说来虎丘一定要看的是斜塔,云岩寺斜塔,来苏州看这个就够了。一开始我还以为这是故意这么设计的,就跟传说中鲁班的故事一样,结果这就是个危楼……嗯,举凡快没了的东西才是好东西,快失去才懂得珍惜嘛对吧。我对危楼没有意见,但是对从后门层层叠叠的石阶很有意见,为了到顶,爬得气都快掉了。不过石碑石阶都很有味道,一直觉得周董的《烟花易冷》MTV应该在这里和平江街取景才对

虎丘云岩寺的斜塔 虎丘爬到腿软的一层层的山门

第四站,山塘街

我们没能找到山塘街,很遗憾很遗憾。后来在昆曲博物馆里那个热心的保安告诉我们山塘街现在还有昆曲表演,很遗憾很遗憾。所以虽然没去,也要列在这里,告诉大家这么个所在!

第五站,不知名的小路

爬完虎丘是真的累,没去留园和寒山寺,先回酒店睡了觉。快五点了出门沿平江街乱逛,顺便觅食。路过评弹和昆曲博物馆,可惜都关门了(四点半就关门了啊!!)。不过小雨中和春宝宝沿河边走走聊聊私房事也不错,一路的河景很有意思,难怪叫东方威尼斯。还找到个嵌在民宅里的故居,左边一个福到右边一个空调,倒……这个又让我想起了淮安

不记得哪里的小路 嵌在民宅中的故居

第六站,评弹纪念馆

这已经是第二天上午了。这个纪念馆和后来的昆曲纪念馆应该算是这次旅游非常意外也非常满意的收获。一来个人对中国戏曲一直都有种敬畏的感情,二来这两个地方幽静和我胃口,三来是碰到了很多好心的陌生人。第二张照片里有个老爷爷,太可爱了,说话漏风还回答我们的问题,虽然他说的我都听不懂,但我说谢谢他听懂了,咧着大嘴开心的跟我们笑,老温馨了。后来知道说这里周日下午仍然有评弹演出,亏了。现在想来应该上午去拙政园,下午来这里听一段评弹

评弹纪念馆 评弹纪念馆里

第七站,昆曲纪念馆

我是真不想把那辆煞风景的车拍进来,但是保安同学非常坚持,说屋顶才是最有特色的,不许拉近景,一定要拍全貌,于是我从了……

昆曲博物馆 昆曲博物馆走廊

这里面古时候的东西真的好多啊,其中就有这么个庞然大物。第一眼见到,惊艳!第二眼见到,还是惊艳!雕工相当精致。知道这是干嘛的么,这是古代移动的昆曲表演平台啊。据说大户人家请了,戏班子就把这个拆了,搬到大户人家里装了,然后坐在里面表演。丫丫的,原来还是组装式的啊,他们怎么下得了手舍得拆啊

很惊艳的移动昆曲平台 很惊艳的移动昆曲平台正面

里面的建筑也很有特色,苏州式园林

昆曲博物馆庭院1 昆曲博物馆庭院2

但真正惊艳的是个种昆曲模型,巧夺天工,看起来有没有像真的?第二张拍的模型从外面看就是个粉白盒子,但是里面两层楼,建筑茶具一应俱全,连门都是活动的。我把相机贴在二楼窗口才拍到一点点里面的情况,很不清楚,有机会自个儿去看,叹为观止

昆曲博物馆模型1 IMG_2170

第八站,拙政园

这是我们逛的最快的一个地方,很苏州园林,且人太多太多,走哪儿都撞。去苏州你就得去这儿,没拍照不解释

第九站,苏州博物馆+忠王府

这地儿强烈推荐,免费但有安检。春宝宝带了带中药被拦下来查包,我很想跟检测的阿姨说那是液体炸药,但是……游玩的玩大过于玩笑的玩,硬生生吞回去了。这地儿让我想起了台北的故宫,东西没有太多,但值得去看看。且风景也不错,有个水池可以喂鱼。里面的鱼是真胖啊,见过双下巴的人类,没见过双肚皮的鱼,想见么,去那儿吧。每次他们摆着个将军肚转弯的时候我都很担心,不会被肉咯到吧……

出口是忠王府,拍了他们的教堂??和昆曲台

忠王府太平天国的教堂? 忠王府昆曲台

庭院依旧很美

忠王府庭院1 忠王府庭院2

第十站,狮子林

一开始纯粹抱着打发时间的目的去的,没多大的院子,就一堆假山,有什么好看的呀,怎么着20分钟也该完事了吧。结果……结果后来明白了为什么旁边的导游说,一直往前走别走回头路,也明白了他们为什么说乾隆皇帝在上面呆了2个时辰,同志们,这么一点点大的地方,4个小时啊!!因为里面的假山跟迷宫一样,能耐着性子走完真不容易。有时候走走眼看就快到头了,一低头假山堆里这么一拱,出来差不多又会园地了……古代有钱人没事儿干,喜欢折腾。不过钻假山是确实好玩啊。当然除了假山还有庭院和小瀑布,风景依旧的美

狮子林 狮子林除了假山还有瀑布

第十一站,民俗博物馆

这里就在狮子林的旁边,太早去火车站了所以来这里打发时间,地儿不大,抱着学习的态度可以去看看。模型依旧很赞,左边是厨房的模型,右边放一张苏州民俗活动一览表来对称下布局

民俗博物馆厨房模型 IMG_2204

苏州不大,两天闲庭信步慢慢走,非常适合出门散心,就算哪儿都不去,街边小路都相当有特色,慢慢压路边也是种享受。且要古典有古典要现代有现代,吃喝玩乐一样不缺,方便。貌似婚纱事业相当发达,本来就是嘛,衣服一换路边一站,直接就能拍写真。

唯一遗憾的是没找到七里山塘。也许正因为有遗憾,所以才美丽

NoSQL学习笔记 – Dynamo

    要想入门NoSQL,先读圣经Dynamo。

    Amazon的这篇论文《Dynamo: Amazon’s Highly Available Key-value Store》网上随处可以下得到,据说搞NoSQL的人都是从这里爬出去的。短短16页,不大好看,但非常精彩。不好看不是说写的不好,而是里面提到了很多分布式系统的概念和算法,要引经据典不容易弄懂(好吧,其实是个人基础太差);精彩在于从数据存储格式到分布式管理算法到系统布局架构都有,已有技术的运用也非常漂亮,就算不是玩NoSQL,学了也有益处。

    废话太多,入正题。分下面几部分一步步介绍(注:有不对的地方欢迎提出)

    1. 需求背景
    2. 算法设计
    3. 系统实现
    4. 总结陈词

1. 需求背景

第一个问题,为什么amazon要做Dynamo这个东西,用传统的数据库不行么?回答这个问题之前我们先看看Amazon网站架设的需求。

Amazon拥有5900万活跃用户、25万多可在线全文阅读的图书、数据存储总量达到42TB。2010年有个统计,amazon每年通过每部kindle卖出的图书数量为24本,按2010年销售kindle300万台来算,amazon光kindle上每天卖出的图书就有19.7万。amazon还提供了服务水平协议(SLA),简单的例子来说,amazon服务器要求对99.9%的用户请求在300ms内提供响应。

别误会,我不是在给Amazon做广告,而是想说,对应这样的存储率、吞吐量和性能要求,传统数据库很难全部兼顾。还记得CAP原理么?传统数据库没有P,数据集中存储。存储率可能不是问题,但是对吞吐量和性能来说就有点吃力。再加上为了防止单点故障,数据必然有备份与复制,在强一致性的要求下,又必将以损失性能为代价。而且人家生意越做越大,数据越来越多,传统数据库的可扩展性也是个问题,这不是一部强大的机器不停的挂硬盘就能解决的问题。

所以,时势造英雄,Dynamo应运而生。它的最大特点是去中心化的分布式系统,整个Dynamo存储平台由多个物理异构的机器组成(可以是廉价的普通机器),每台机器角色一样,可以随意添加或去除,且不需要太多人为干预。每台机器存放一部分数据,这些数据的备份同步完全由系统自己完成,单台机器故障甚至一个数据中心的断电故障都不会影响系统对外的可用性,是具有高可用性和高扩展性的分布式数据存储系统。

神奇吧,嗯,胃口先得吊足了。下面这张图是Amazon平台的服务架构,先看看Dynamo在整个平台中的作用和位置,后面会有对其实现的具体讨论。每个物理的Dynamo instance我们叫做一个结点。

architecture

2. 算法设计

要实现上面所说的那么神奇的系统,用到的技术非常的不少。下图列出了Dynamo用来解决问题的一些技术。包括Consistent hashing、Vector clock、Merkle tree、Quorum、Gossip protocol等等一系列的相关概念。

techniques

这部分会用需要解决的问题,一个一个来解释他们的使用。

2.1 数据存储格式(key-value)

既然是数据存储系统,存储格式是首先要看看的。

和传统关系型数据库用表来组织数据不同,Dynamo用key-value来存储数据。这是由Amazon服务的特点决定的。Amazon的大部分服务只需要根据primary key来查询存储操作数据,不需要复杂查询或类似JOIN那样的SQL操作,所以key-value结构就够了。(其实这也是NoSQL的一个缺点,应用场景关联,如果业务逻辑需要复杂RDBMS的功能,NoSQL可能并不是一个好的选择)

举个例子,对交易数据了来说,(key,value)对应(交易id,交易信息(包含用户id和书本id等等)),文章没提具体的实现,所以不知道value的具体存储格式。在实际操作中,除了key和value,每个key还会附加一个context内容用来记录一些数据上下文,这个后面再说。

这些value会根据key值的md5不同,被存放到不同的分区结点上。

2.2 数据分区(Partitioning)

Dynamo是怎样将那么多数据平均放在多个结点上的呢?做Hash平均分配就行了嘛。假设有N个结点,hash(key)模N就可以把key值平均分配在结点上。但这样有个问题,当有新结点加入的时候,因为N变了,所有的数据必须重算并重新迁移分配。一般的应用可能无所谓,考虑Amazon那么大的数据量和SLA要求,把所有的数据重算一遍就要了命了。

Dynamo用Consistent Hashing(http://en.wikipedia.org/wiki/Consistent_hashing)来解决这个问题

consistent_hashing

一样是hash,但是不是根据取模结果,而是根据区间范围来存放。每个结点被分配一个区间,比如说A分配的区间是[A,B),那么只有满足A<=hash(key)<B的key值会存放在A结点上。这样,当有新结点加入时,受影响的只有相邻结点而不是所有结点。比如,在AB之间加入一个结点H,只要把A上的数据拆成[A,H)和[H,B),并把[Q,B)迁到Q上就行了

这样迁移时还是需要对A的所有数据进行扫描拆分,会影响结点A的响应速度。有没有更好的方法?有,Dynamo引入虚结点的概念。假设有N个结点,把所有key值分成Q个区间(Q>>N),一个区间就是一个虚结点,这Q个虚结点轮番依次放在N个真实结点上,每个结点上存放Q/N个虚结点。比如说,has(key)的结果是128位,那么K的范围在[0,2^128)。把这个范围平均分成2^8份,也就是说Q=256个虚结点,第一个虚结点范围就将是[0,2^16)。再假设一共有N=10个物理结点,那么每个结点将平均存放25个虚结点。1号结点上存放的key值范围是[0,2^16),[2^144,2^160),[2^304,2^320)……一共25个,二号结点放的是[2^16,2^32),[2^160,2^176),[2^320,2^336)等25个。那么当再添加一个新结点时,平均每个结点上存放23个,它需要从其他10个上挨次拿2个出来。

这样的好处是,结点的添加和删除只需要重新分配虚结点,省去了数据扫描和拆分的工作,而且迁移时负载平均。

2.3 数据分区的复制(Replication)

为了达到数据高可用性和数据持久型,所有的结点都要求备份。在Consistent Hashing Ring上面,用后续结点为前面的结点做备份。在上面的图上,B、C、D为A做备份,一共备份3份。也就是说key值K同时存在4个结点上。

虽然K值存在4个结点上,但这4个结点会记在一个persist list上面,以这个例子来说list=(A,B,C,D),只有排在第一的结点负责K值的操作以及将它复制到其他结点。当A结点挂掉时,才按persist list的排序由后续结点接手。

这里有两点需要考虑,一个是虚结点的问题,另一个是备份数量的问题。上面提到Consistent Hashing Ring上的每一个结点其实都是一个虚结点,ABCD可能位于同一个物理结点上,如果这样的话,备份就是去了意义。因为如果这个结点故障了,所有备份都会丢失。所以在选择备份结点时,必须保证挑选不同的物理结点。关于备份的数量,少了容易丢失数据,多了备份间同步开销增大。在Amazon系统中备份数量是3,具体怎么得出来的,下面再看

2.4 多复制分区的数据版本管理(Data Versioning)

复制的版本多了,那么对这些不同复制版本的同步写会造成数据冲突,或者说数据不一致性。比如说K=1在ABCD上都有备份,客户请求1由A结点接手,要求K值加1,那么在A结点上K=K+1=2。这个改动还没来得及复制到BCD,客户请求2到了,又要求加1,这次由B接手,那么B结点上K=K+1=2。这时候,当客户请求读时,K到底是几?

传统数据库有集中管理ACID(http://en.wikipedia.org/wiki/ACID)保证,分布式数据库系统该怎么做呢?写涉及磁盘操作,如果要求对所有的备份都写完成才算事物结束,会严重影响性能。

Dynamo用vector clock允许多版本数据存在。首先先来看下vector clock(http://en.wikipedia.org/wiki/Vector_clock)的概念,它是在分布式系统中用来记录事件发生时序的算法。

vector_clock

上面的图给了个用clock vector记录数据版本的例子。一共有3个结点处理4次写请求。第一次写请求由Sx完成。第二次写请求依旧发生在Sx上,所以数据版本直接加1。第三次请求由Sy处理,这时候它从Sx复制过来的数据版本是2,但是有可能在复制完到处理请求这段时间内,Sx上的版本变成了3但是没有复制过来,所以不能直接在Sx上操作,另加[Sy,1]这个版本信息。第四个写请求同理。在这个时候,读请求将读出所有的版本,根据业务逻辑算出合理的值返回客户,并将合并后的结果写回。

假设W是代表写成功的结点的个数,R是读成功的结点的个数,N表示复制版本的数量。这个例子用W=1,R=N的模式,通过由请求客户自己完成多版本合成的方式保证数据同步。这种例子的好处是写很快,但是读很慢。但是如果对于读要求高但是写要求不高的应用就不适用。而且如果刚写入的那个结点还没来得及复制就挂掉了,会造成数据丢失。

在Dynamo系统中,NRW是可以配置调优的。根据最终测评结果,(N,R,W)=(3,2,2)。也就是说,一份数据会有3份拷贝,写操作时,当有2个结点返回写成功,就代表事务成功。读操作一样,至少读到两个结点的数据才算读成功。至于多版本的数据之间怎样合并,以及NRW更多的考量,在后面的系统实现部分会有更多的讨论。

2.5 故障处理(Handling Failures)

前面说过,虽然有很多复制版本,但只有persist list上最前面一个负责拥有的key值的操作。所有它上面的key值得操作请求都会路由给它。拿上面的例子举例,对K值的操作会路由给A,当A挂掉时,如果后续请求依旧路由给A处理显然不合适。

Dynamo用Hinted Handoff(http://wiki.apache.org/cassandra/HintedHandoff)来解决这个问题。当A挂掉后,persist list上后面一个结点B负担起A原来的责任,对K值得操作由B来负责。同时B定期查询A活了没有,如果活过来了,将所有数据复制回A,并将操作权交还给A。保证任何时刻数据都有高可用性

2.6 分区数据的同步机制(Replica synchronization)

多复制版本之间进行数据同步在分布式系统中是不可避免的,那么怎样同步么?系统怎样快速的知道各个结点上数据版本的差别并进行快速同步么?

Dynamo用Merkle Tree(Hash Tree)来解决这个问题。Merkle Tree(http://en.wikipedia.org/wiki/Hash_tree)是一种数据结构,它只在叶子结点存放数据,父结点用来存放子结点的摘要信息。父结点的变化只受子结点决定。比如说一棵Merkel tree有一个跟结点Z,两个子结点XY,子结点X拥有叶子结点ABC,子结点Y拥有叶子结点DEF。那么A变化时,X受A牵连重算,Z受X牵连重算,而Y不变。这样,当这棵树有两个备份同步时,通过比较结点是否一样,就可以找出最小变化集合,并同步相应内容。上面的例子中,Y分支不需要任何同步操作,需要同步的只有结点AXZ。

如果知道Git的同学这部分会相当好理解,貌似Git的branch和submit的管理和合并也是通过Merkel Tree实现的

2.7 新结点加入和故障侦测(Membership and Failure Detection)

一个Dynamo神奇的地方就是很少需要人工干预,这点有个问题。当新加入一个结点或者移除一个结点,或者当一台机器挂掉之后,怎么通知所有的结点这个信息呢?Dynamo系统是完全去中心化的,没有一个结点有管理员这样的角色,通知大家一起更新,所有的机器都是peer-to-peer的。

在这种分布式环境下,各个结点之间也有peer-to-peer的通信方式,gossip protocol(http://en.wikipedia.org/wiki/Gossip_protocol)。它是一系列用于P2P的通信协议。简单来说,就是模拟人类社会中流言传播的方式。每个节点随机地把消息发给它的邻居,接到消息的节点,如果之前没收到这个消息,则会继续随机地转发给它的邻居,否则不转发。这样,失败的结点或者成员信息的变化会像流言一样迅速到达Dynamo的所有结点。

当然根据传播内容的不同,可以用gossip protocol来达到不同的目的。其中一种类型就是Anti-entropy protocols,用于复制版本之间的错误侦测和错误校正

3. 系统实现

好,基于以上的算法设计,对分布式数据管理中的问题都有了相应的技术解决方案,现在对照最开始的系统架构图,来看看怎么由这些算法设计拼出Dynamo的整个系统实现。

3.1 物理架构

先从物理结构来看,整个Dynamo系统由多台异构的机器组成,就是我们上面所说的结点。这些机器分布在多个数据中心,数据中心之间通过高速网连接,persist list的top N结点也会尽量保证分布在不同的数据中心上。这样,即使发生地域性的整体数据中心损坏,系统还是可以照样提供服务。

每台机器上都有三个功能模块:请求协调器(request coordinator),成员关系和失败监测器(membership and failure detector)和一个本地引擎(local persistence engine)。其中,协调器用来接收来自前端server的请求。

3.2 请求的发送

前端的请求有两种:get(key)和put(key,value,context),分别对应数据库系统的读和写请求。他们由前端server利用基于HTTP协议的Amazon的infrastructure-specific request processing framework(别问我这是个什么东西,我也不知道,文章里面没说……)触发。

那么server是怎样把put或者get请求发送到对应的包含该key的机器上呢?有两种方式。一、随便发到任一机器,由任一机器根据成员关系图找到对应的结点,转发请求过去。二、在server端调用Dynamo结点分配算法直接找到对应key的结点把请求发过去。第一种方法的好处是server端不需要依赖Dynamo的实现,第二种方法的好处是请求不需要转发,低延迟。实际选用哪种文章也没提,估计是对不同应用选不同方式吧。

3.3 请求的处理

当一个请求被发送到对应结点后(persist list上的第一个结点),该结点上的协调器接收这个请求并作出相应的处理。

在解释具体怎么处理put和get请求之前,是时候解释下NRW模型了,这个模型的背后是分布式数据库系统中多个数据备份之间的读写效率、数据一致性和持久性之间一系列的取舍。在数据版本的时候已经说过一点了,N表示数据备份的数量,R表示至少读R个备份才算读请求成功,W表示至少写入W份才算写入成功。如果要求强一致性,所有数据备份必须都是一致的,那么W=N、R=1,这样永远不会有数据不一致的问题。但是这样对写请求来说就不是高效的,甚至在某一结点失效的情况下,写请求将永远失败,这对Amazon的购物车之类的请求是无法忍受的。那么是不是设成W=1,R=N就可以解决这个问题了呢?好,假设W=1,那如果刚写完这个结点,这个结点就挂了呢?数据丢失,同样不可接受。那Dynamo是怎么做的呢?

在Dynamo里(N,R,W)是可配置的,根据调优的结果(有兴趣看怎么调优的可以看原文)最终选定(N,R,W)=(3,2,2)加上writer thread的解决方案。这样既符合Quorum W+R>N的要求,又保证了数据持久性,同时利用writer thread方案提高响应速度。

好,一切就绪,来看看具体put和get请求的处理吧。

当一个结点的协调器收到put请求时,它会向top N里的结点都发出写请求,当其中2个返回写成功后,它会向请求server发回确认写请求成功的相应。为了减少写磁盘造成的响应延迟,Dynamo只要求N个结点中有一个真正写入磁盘就行,其他的暂时写入内存就算写入成功。这些内存中的数据将来会由writer thread批量写回磁盘,跟PostgreSQL的logger进程有异曲同工之妙。利用writer thread,只要N>W+1,就能达到既不影响写效率有不丢失数据的效果

当收到get请求时,同样向top N里的所有结点发出读请求,当其中2个返回数据时就算读成功。在这里,不是光读出数值那么简单,除了读出的数值外,还包含一个context结构,里面包含了数据版本等一些额外信息。协调器需要对应不同的应用需求对多个版本的数据进行合并返回。

这其中还有很多有趣的东西,比如多个结点对put和get的响应时间都会随context一起返回,作为从persist list挑选合适结点达到SLA的依据,又比如写请求之后一般会跟随一个读请求,可以把它们分配给同一个结点处理达到read-your-write的一致性要求等等。这块其实看的也不是很明白,就不再说了

4. 总结陈词

总而言之,Dynamo用很多已有的技术拼出这样一个神奇的系统,做法非常的漂亮。相对于传统数据库设计偏重于schema的设计来说,它的重点更偏向于分布式系统中的一些问题的处理。而传统数据库的一些基本要求,比如说数据一致性的同步处理和多个复制版本合并的问题,不再从属它的范畴,而是暴露给程序员自己解决,麻烦但是灵活,应用相关。

但它也有它的致命伤,应用场景的限制。对于要求复杂RDBMS操作的应用,它也很难处理。举个简单的例子,如果所有书本信息都是用(书本id,书本信息)的方式存放在多个数据结点上的话,那么按书名排序这样的应用该如何解决。如果Amazon有提供这个功能,那么我很好奇实现会是怎样的。

上面这两点,也刚好是NoSQL与传统数据库的差别。

NoSQL学习笔记 — 开篇

不管是不是玩数据库的,相信都知道近来有个名词很火:NoSQL。其实这个概念已经火很久了,现在大批成熟的NoSQL数据库产品已经汹涌而出并占据各大主要应用市场。前几年一直在玩PostgreSQL,到了现在,自个儿都觉得不看点NoSQL的东西实在说不过去。这阵子挤了点时间,看了点NoSQL的东西,记录下来怕忘了。

这是第一篇,好比会议开头领导致词,虚了点儿但必须得有,好歹多多少少先有个概念不是。要想真正了解NoSQL,后面的路长着呢

  1. 什么是NoSQL
  2. NoSQL的发展史
  3. NoSQL的特点
  4. NoSQL的分类
  5. NoSQL的现状
  6. NoSQL的应用

什么是NoSQL

NoSQL是Not Only SQL的缩写,而不是Not SQL,它和传统的关系型数据库不一样,不一定遵循传统数据库的一些基本要求,比如说遵循SQL标准、ACID属性、表结构等等等等。比如说后面介绍的Dynamo,在我看来相比传统数据库,叫它分布式数据管理系统更贴切,数据存储被简化更灵活,重点被放在了分布式数据管理上。

这个问题太难,好比问什么是数学。但考试又经常会考,头疼。有兴趣的同学可以去这里(http://en.wikipedia.org/wiki/NoSQL)自己看

NoSQL的发展史

其实NoSQL的概念早在1998年就被提出来了,随后跟随web2.0逐步发展起来,近年更是跟着云计算发展到巅峰。在各种版本的2011十大热门技术预测里,云计算、社交网站、电子商务都在其中,而这三个方向都把NoSQL推到了烫手的地步。Guy Harrison有一篇关于云计算推动NoSQL发展的访谈(http://database.51cto.com/art/201010/229420.htm),没找到原文,有兴趣的同学可以自己找找

NoSQL的特点

换句话说,为什么要选NoSQL?或者为什么NoSQL能火?网上有一堆这样的故事,随便找个背背就行了。在我看来没那么多虚招,说白了,传统型数据库搞不定现在的应用了呗。哪里搞不定了?这就要扯到CAP原理(http://en.wikipedia.org/wiki/CAP_theorem)。

CAP:一致性(Consistency),可用性(Availability)和分区容忍性(Partition tolerance)。CAP原理就一句话,这三个条件最多只能满足两个,不可能三者兼顾。

这下明白了吧,传统数据库遵循ACID,在CAP的C和A上有很严格的要求,所以P很难达到。比如说,在大规模数据条件下,扩展性和性能要求就受到了很大的约束。做企业级应用的兄弟们,应该多多少少会碰到大量存储下数据库性能的问题吧。其实传统数据库已经在做一些改进来应付这些问题,但是CAP原理挡那儿了,算先天不足。当然这并不是说NoSQL就比传统数据库优秀,应用需求不同而已。网上有很多孰优孰劣的对比,实在没必要。当然大规模数据只是一个例子,NoSQL数据库还有其他的一些特点,比如说不拘泥于严格的DBMS存储方式,用key-value轻量灵活存储等等,这个看具体应用需求。

NoSQL数据库一般以P为基本要求,所以就必须牺牲C或者A,一般是C。一般的分布式应用虽然不需要像传统关系型数据库的强一致性,但是最终一致性还是要求的。关于最终一直性学问又大了,可以拉出一篇文章太谈,有时间再表,有兴趣的看这里(http://www.allthingsdistributed.com/2008/12/eventually_consistent.html),有一篇中文的(http://www.infoq.com/cn/news/2008/01/consistency-vs-availability

NoSQL的分类

NoSQL数据库根据实现的不同又可以分为以下几类:Document store,Graph,Key-value store,Object database,Multivalue databases,Tabular,Tuple store等等。这是NoSQL wiki的分类(每个分类代表性的数据库可以去http://en.wikipedia.org/wiki/NoSQL找),和网上传得很多版本有点不一样,比如说盛传的Column based database在wiki的分类中并未找到,归在这个分类里的HBase和Cassandra在wiki上则分属于Tabular和key-value分类。无所谓,没必要深究,知道是什么东西就行了

NoSQL_taxonomy

NoSQL的现状

NoSQL现在有很多成熟的产品被广泛应用。网上找了张NoSQL族谱,现在知名的一些NoSQL数据库,比如说Cassandra,HBase,MongoDB,CouchDB等都在其中

NoSQL_developments

从图上除了可以看到热门的NoSQL数据库,还大致可以看到这些数据库的起源派系。Tokyo Cabinet早了,当年看它的时候NoSQL还没这么火,当然也可能自己孤陋寡闻了。比较强大的两类是BigTable系和Dynamo系,一个属Google一个属Amazon,来头都不小。两个公司都有这两个NoSQL数据库系统的论文发表,这两篇论文被称为NoSQL入手圣经。想要研究NoSQL,那就从这两篇论文开始吧

NoSQL的应用

NoSQL数据库在社交网站和电子商务上的应用应该是非常广泛了。社交网站龙头facebook放弃了自己研发的数据库系统用了Cassendra,有个选用测评报告的,懒得翻出来了有兴趣的自己找。twitter从MySQL切到了Cassendra很不幸又切回了MySQL。电子商务类的就不用再说了吧,amazon自己搞出来的Dynamo还成鼻祖了。

所以基本上可以看出来,NoSQL在某些应用上绝对是趋势,但是就目前来看肯定还有点问题,毕竟走向成熟总有过程。这个纯属个人看法,不述

以上是近期学习来的一些总结,刚开始,有不正确的地方欢迎指正讨论。后续还会有具体NoSQL数据库的学习笔记和问题讨论

在Linux下玩转MIDI

  曾经我非常热衷于MIDI音乐,原因是一直没有机会正儿八经的学习一种我喜欢的乐器,于是在电脑上用HappyEO和Cakewalk制作一些音乐(或者说噪音?)便成为了生活中的一个重要部分。但是最近相当长的一段时间中,我都没有再玩过MIDI音乐,原因只不过是因为我从台式机换成了用笔记本电脑和开始使用Linux……

  先科普一下有关MIDI音乐的简单知识:MIDI音乐文件中存放的是音乐的乐谱信息,而不是实际的声音。乐谱信息以一系列的消息组成,比如:用最大力度的50%按下钢琴上小字一组的C键、持续1秒、松开小字一组的C键。电脑在播放MIDI文件时,需要由音源设备把MIDI消息转换成真正声音,再播放出来,音源设备的好坏就直接决定了MIDI音乐的回放质量。

  在Linux中玩MIDI,本身也不该是件什么难事,但问题是在于Linux的声音系统一直以来处于非常混乱的状态,各家争鸣百花齐放,使得配置起来很是复杂。另外,现在很多电脑的的声卡上都不带音源设备,这样就没有办法直接用声卡合成声音。

  要在Linux下玩转MIDI,就得从这以下方面入手:

  驱动层:早期Linux用OSS来驱动声卡,而现在这一块基本上已经由ALSA来一统天下,当然ALSA也提供了OSS的兼容层来保证一些古老的应用。这年头,电脑装好Linux后能正常发声的话,ALSA应该就已经是在正常工作了。

  中间层:一些新的发行版都用PulseAudio做为一个应用中间层,通过它去统一应用软件与ALSA的接口。不过这个东西对于玩MIDI这种“专业”的事情来说,不是很有用。我们需要的是Jack。Jack是大部分音频处理软件所使用的输入输出组件,它具有高性能低延迟等优秀的特质。

  音源:如果声卡没有硬件的音源,或者硬件音源烂到只能发出电子味很重的声音。我们就需要一个软件音源,通过加载音色库(SoundFont)文件,把MIDI信号合成为声音。Linux下常用的软音源有fluidsynth或Timidity++。另外还需要准备合适的音色库文件。

  音序器:简单的说就是用来作曲的软件啦,由于把各种MIDI信号组合到一起形成一个音乐作品。上面说的Cakewalk就是一款Windows下的古董级的音序器了。在Linux下,最有名的当属Rosegarden了。

  最终的工作流程就是用Rosegarden谱曲,在fluidsynth或Timidity++中加载好音色库文件,用Jack把Rosegarden的General MIDI Device输出端口连接到fluidsynth或Timidity++生成的Synth输入端口。

  以Debian Squeeze为例,在ALSA工作正常的前提下,安装以下软件包及其依赖包:jackd, qjackctl(Jack的图形前端), qsynth(fluidsynth的图形前端), rosegarden, fluid-soundfont-gm (一个音色库文件)。

sudo aptitude install jackd qjackctl qsynth rosegarden fluid-soundfont-gm

  安装完后就可以启动JACK Control和Qsynth,首先点Qsynth界面上的Setup按钮,在Soundfonts页中指定要用的音色库文件,如:/usr/share/sounds/sf2/FluidR3_GM.sf2,重启音源引擎。然后可以打开Rosegarden,这时在JACK Control的Connect按钮对应的窗口的ALSA页中会可以看到Rosegarden的输出端口和Qsynth的输出端口,选中它们点“Connect”把它们连起来。同时确认一下在"Audio“页中,Qsynth的输出端口也已经与”system“的playback端口连接正常。

Qsynth设置
Qsynth设置
JACK Control ALSA设置
JACK Control ALSA设置
JACK Control Audio设置
JACK Control Audio设置

  大功告成,在Rosegarden中打开一个MIDI文件,或者自己涂鸦几个音符吧,点播放以后就应该可以听到MIDI音乐声响起了。

Rosegarden界面
Rosegarden界面

  除了Rosegarden,Linux中也有其它一些音序器软件,比如tuxguitar,就是专门为吉他爱好者准备的。

  HappyEO是一款在Windows下用电脑键盘模拟电子琴的软件,也能当成音序器的MIDI信号输入输出设备。这个软件在2000年到2006年间占用了我无数的时间,我乐在其中的用它制造了无数的“噪音”,直到我换了笔记本电脑(没有了标准键盘)和开始用Linux。在把Linux下的MIDI环境配置好后我就迫不急待的用wine尝试运行这个软件,软件运行成功了!看到久违的界面,听着自己已经不再熟练的弹奏,还真是颇有一点激动呢。

在Debian下用wine运行的HappyEO
在Debian下用wine运行的HappyEO
Posted in Uncategorized

明朝那些事儿

我就纳了闷儿了,你说咱汉族多少代人的故事电视台不说,非抱着清宫戏一顿猛拍。拍完了正史拍野史,拍完了政治篇拍情感篇,拍的没有可拍了还能搞出个现代穿越,有完没完。这两天为验kindle看了《明朝那些事儿》,那叫一个赞,精彩程度完全不输清朝那两百多年。

怎么说呢,要战场,有!从游僧乞丐朱重八被逼无奈起兵造反,忽悠无数良民与流氓,一路斗倒陈友谅、张士诚,赶得元兵回蒙古,坐的明朝开国皇帝。与陈友谅在鄱阳湖的恶斗是血雨腥风,八次北征与蒙古的交战更是斗志斗勇。

要英雄,有!从开国功臣徐达、常遇春等,到后来的抗蒙古名将蓝玉,到北京保卫战的于谦,再到同样鄱阳湖平定宁王反叛的王守仁,任哪个单独拿出来那都是一本书。

要阴谋,有!都说赵匡胤杯酒释兵权,谁晓朱元璋为保子孙基业稳固杀尽开国名将,更不要提后期东厂太监之乱以及明史各大案。

要亲情伦理,有!朱棣夺了侄子的江山,朱祁钰夺了哥哥的江山,宁王企图多朱厚照的江山未遂,反正没有一任皇帝是一个人做安稳的就对了,如果有那也是短命。

要8g感情戏?有!朱见深为了大他十五岁的一宫女搞到断子绝孙,儿子被太监宫女瞒着养大才活下来,5岁才见着亲爹。

要奇士?有!这位低调的思想家、哲学家、文学家和军事家的王守仁同学浑身上下沾满了奇遇,小时候格竹一心立志做圣贤气得老爹没法子,年轻当官当到要装死,年近不惑驳了把朱老夫子,后来飞黄腾达却跑去江西剿匪,最后以一己书生之力平叛宁王保了朱厚照的江山。

要野史,有!都说风流才子唐伯虎3点秋香终抱得美人归,实际才子因科场舞弊案被贬终生不得为官,后半辈子误入宁王府靠装疯卖傻才保得性命,成就了诗画文采却潦倒一生

要悬疑,有!建文帝是死是活、朱厚照真正死因到今天都没有具体的答案

真亏了作者,把一部历史写得风趣幽默。幽默的有,比如说,郑和下西洋,其实是为了找失踪的建文帝啊,找就找呗通商就通商呗,还路见不平拔刀相助,居然把不听招呼的锡兰山国王抓回大明坐牢……深奥的有,比如说,朱熹的格物穷理,浅显的普及了把哲学知识;历史盲普及的更有,比如高丽是怎样和元朝脱离关系的,并且是朱元璋同学赐名改为朝鲜

反正就是精彩就对了,且励志。换个皇帝换朝臣,多少人动辄从飞黄腾达到坐牢再到飞黄腾达再到掉脑袋再……哪怕是皇帝!朱祁镇同学从皇帝到蒙古人质到亲弟弟的囚犯到复位皇上,真是可歌可泣,折腾啊……

唯一的不好是出场人物太多,记得累。你想啊,清宫戏拍得最多的就九子夺嫡了,就自己家人搁那儿斗。而且记名字不累,大哥、太子,三哥四哥五哥六哥就完了,大不了多一两个好事的臣子年羹尧之类的。你在看明朝,皇帝的名字就三字,这还不加年号,再看看那些出名的臣子,要多少有多少,这要再算上蒙古的那波儿脱脱帖木儿什么的……难怪明朝戏不拍,阵容太强大,群众演员都要找很多……

还没看完,刚看到朱厚璁,还好,连这位皇帝在内明朝还剩六位皇帝,短期应该是没得看了,估计得搁浅一阵子,有空,日后再续

kindle 3入手

这两天神速入手枚利器:kindle 3。

他们问我为什么要买。想来想去找了三个理由交差:一、2012要到了,说不定哪天不测就要被困地下20多天,到时候有kindle做消遣打发时间也不错。二、最近莫名其妙的事儿比较多,一会儿电视台被统一预设了,一会儿appspot关了,说不定哪天网络也不通了,kindle还是可以打发时间。三、姐最近吧,那个心情不好,花钱讨点喜气。其实,就只是想看看书而已……

事实证明这个冲动的决定相当的赞。

买的kindle 3 wifi版,6寸珍珠屏,经同志们实地对比比同类6寸e-ink屏显示效果确实要好。刚去同事那里看样机的时候,以为屏保是张纸,还纳闷贴张纸在上面做什么……屏幕显示效果非常不错,6寸全屏显示A4大小pdf完全没有压力,字迹粒粒清楚。此外,外形小重量轻,单手就可以拿得住,在地铁上站着看书应该没问题。至于费眼的问题,这三天看书没停歇过,眼睛没什么明显不适,比看电脑屏幕轻松。待机时间据说可以有30天左右,个人的是从买来到现在共计4天,一共使用wifi差不错6小时,看书差不多24小时,耗电50%,可能翻书设置写批注是频繁了点。内存4G,实际可用3G多一点点,貌似少了点,不过人家就一看书的东西,这个价位要求就表太高了。

此外,Kindle 3还内置了浏览器和音乐播放器。kindle 3G版的还可以免费上网。不过个人觉得吧,拿kindle的浏览器上网那是没事找事儿,手机上网都比它好使。因为键盘太硬,而且需要双手操作,没有鼠标全键盘操作是在不容易。上网最大的用途应该还是拿来网上买书,还可以更新RSS feed。浏览器还内置了facebook、twtter等社交网站标签,据说还可以经过设置将图书批注直接同步到这类网站。但是你知道,地域局限性,这么好的东西我们玩不起来……

到手先试了原生系统,中文貌似确实不太给力,英文pdf非6寸版的排版也是问题,于是当天刷了多看2.18。因为固件是3.1版,需要额外的破解包,刷了两次才刷好,紧张的要死。刷完双系统,由多看可以一键进入kindle原系统,使用无压力。

在中文支持上,多看确实很给力。试了txt、pdf(6寸和A4)、mobi、rss feed mobi,中文和显示都没有问题,pdf支持分栏显示。音乐播放器的用户体验也比原声系统好。最赞的是中文输入法,支持单词智能联想。这两天给书下批注,输入法使用完全不是问题,别手的点还是在于键盘。唯一的问题是多看没有浏览器,不过这个有没有都无所谓,大不了一键切回原系统

以下分别是多看系统下屏保、6寸pdf+输入法,A4pdf、系统切换,晚上室内灯光不行,意思意思看看吧

屏保 6寸pdf

A4pdf 多看双系统

这几天最主要的感触是,看书就得用kindle。手机能看,屏太小;电脑能看,诱惑太多;纸质书能看,旅行带着占空间(费体力倒是小事,减肥嘛)。不过找书确实费劲,虽然资源多,那也得找不是……希望有资源的同学们多多分享