用NVME提高群晖NAS的性能

群晖的DSM有一个比较鸡肋的功能:SSD Cache,原因如下:

  1. 单条NVME只能做只读Cache,功能有限
  2. 一个SSD Cache只能加速一个存储池(DSM 6.2中的词汇,以前叫存储空间)
  3. 系统分区不在任何存储池里,所以系统分区上的东西都不能加速
  4. NVME只能做Cache,不能用来存数据

DMS的SSD Cache功能是基于Linux的dm-cache来实现的,理论上通过Hack是可以解决上面说的这些问题的,不过得摸透了DSM相关的所有逻辑才能搞,而且对DSM的侵入比较大。所以,有一个想法,直接解决第4条问题,想办法把NVME当成正常的磁盘来存数据。然后就可以按自己的想法,灵活地挑一些需要高速随机IO的数据放在NVME上,其它的数据还是放在磁盘上。

如果问群晖的技术支持可不可以这样做,回答是否定的——非常可以理解,在群晖的设备上,盘位就是钱,白给你增加两个盘位?想都别想。

研究了一番,找到了解决这个问题的办法,虽然不完美,但是可用,并且效果不错。以下内容基于DS918+的硬件,DSM 6.2,系统中所有的磁盘都是Basic模式。由于条件所限,我没有测试RAID或SHR的情况,但原理应该都是一样的,可以参考一下我的思路。DSM的RAID就是标准的Linux RAID,而SHR则是基于LVM搞出来的。

以下步骤请充分理解后自行操作,所有给出的命令仅供参考,切勿依样画葫芦。这里介绍的方法有可能会造成不可逆的数据损失,请自己评估风险。

1. 分区

安装NVME后,系统中会出现/dev/nvme0n*设备,并且在存储管理器中可以看到对应设备。不要在DSM中创建SSD Cache,因为我们准备自己管理这个硬件。

Basic模式下,DSM会把系统中的每块硬盘都分成三个区:

Device      Start         End     Sectors Size Type
/dev/sdd1    2048     4982527     4980480 2.4G Linux RAID
/dev/sdd2 4982528     9176831     4194304   2G Linux RAID
/dev/sdd3 9437184 11720840351 11711403168 5.5T Linux RAID

第一个分区用于存放DSM系统,第二个分区是SWAP,第三个分区是用户可用的存储空间。每块磁盘的前两个分区,分别组成/dev/md0和/dev/md1两个RAID设备,都是RAID 1的模式。

所以,我们也依样画葫芦,把NVME也用相同的方式分区(可以使用系统自带的parted工具),分出与别的磁盘大小一样的前两个分区,类型都设置为RAID。然后剩余的空间分为普通Linux分区,并格式化为你需要的文件系统(ext4或btrfs)。当然,如果你有多块NVME或者你愿意,你也可以把剩余空间分为RAID类型,创建md设备后再格式化。

2. 创建RAID组

分完区后,扩展md0和md1,把NVME的前两个分区,加到这两个RAID组中。

# 如果是2盘位的设备,大概只需要扩展成3就可以了 
$ sudo mdadm --grow --raid-devices=5 /dev/md0 
$ sudo mdadm --grow --raid-devices=5 /dev/md1
$ sudo mdadm --manage /dev/md0 --add /dev/nvme0n1p1
$ sudo mdadm --manage /dev/md1 --add /dev/nvme0n1p2

然后,把第三个数据分区mount到一个你喜欢的位置,比如:

sudo mount /dev/nvme0n1p3 /volume1/homes/admin/nvme

考虑到NVME和磁盘混合组成RAID,可以把所有磁盘(不包括NVME)的对应分区设为writemostly,这样可以让读操作尽量走NVME,提高性能:

sudo echo writemostly | tee /sys/block/md0/md/dev-sdX1/state
sudo echo writemostly | tee /sys/block/md1/md/dev-sdX2/state

这样做完以后,系统分区和SWAP分区都已经可以被NVME加速了。这一步是完全没有风险的,因为即使未来NVME从RAID 1组中意外丢失或损坏,也不会造成任何数据问题。

同时在DSM中,正常访问home/nvme就是访问NVME上的内容,这个目录可以当作普通的存储空间来使用,存放一些需要高速随机IO的数据。

3. 迁移数据

为了达到更好的性能,我们还应该把一些常用的对IO性能敏感的东西迁移到NVME上,包括但不限于:

  1. 系统的PostgreSQL/MariaDB数据库,通常位于/volume1/@database,但也可能分布在多块盘上
  2. 软件包,通常位于/volume1/@appstore,但也可能分布在多块盘上
  3. CloudSync的SQLite数据库,通常位于/volume1/@cloudsync
  4. Docker的镜像和容量数据,通常位于/volume1/@docker
  5. VMM的虚拟磁盘文件,通常位于/volume1/@Repository

迁移的方法很简单,先rsync,然后再mount –bind,比如:

sudo rsync -a /volume1/\@appstore/ /volume1/homes/admin/nvme/\@appstore/
sudo mount --bind /volume1/homes/admin/nvme/\@appstore /volume1/\@appstore

理想情况下,应该先把相关服务停止后(用synoservice命令可以启停服务)再迁移数据,避免迁移过程中有新数据写入造成不一致。为了保证万无一失,建议可以写个迁移脚本,在系统启动过程中所有服务还没有启动前运行一下这个脚本。

这一步是有风险的,因为万一未来某一次mount –bind没有成功或者没有做mount –bind,系统就无法访问到正确的数据了,这对于系统数据库之类的,还是有一定影响的,会造成数据不一致。

4. 重启自动生效

写一个脚本,用于在未来重启时重建RAID和mount相关目录,比如:

# 先把NVME上的数据盘mount起来
mount /dev/nvme0n1p3 /volume1/homes/admin/nvme
# 把SWAP的RAID 1扩容,并把NVME上的分区加进去
# 系统分区不需要,因为会自动完成。
# 但SWAP的RAID每次重启都会重建,所以每次都需要扩容。
mdadm --grow --raid-devices=5 /dev/md1
mdadm --manage /dev/md1 --add /dev/nvme0n1p2
# 把相关的目录mount --bind上去
mount --bind /volume4/homes/admin/nvme/\@appstore /volume4/\@appstore
mount --bind /volume4/homes/admin/nvme/\@cloudsync /volume4/\@cloudsync
mount --bind /volume4/homes/admin/nvme/\@database /volume4/\@database
mount --bind /volume4/homes/admin/nvme/\@docker /volume4/\@docker
mount --bind /volume4/homes/admin/nvme/\@Repository /volume4/\@Repository
# 重新设置RAID writemostly策略
echo writemostly > /sys/block/md0/md/dev-sda1/state
echo writemostly > /sys/block/md0/md/dev-sdb1/state
echo writemostly > /sys/block/md0/md/dev-sdc1/state
echo writemostly > /sys/block/md1/md/dev-sda2/state
echo writemostly > /sys/block/md1/md/dev-sdb2/state
echo writemostly > /sys/block/md1/md/dev-sdc2/state

修改/etc/rc,在SYNOINSTActionPostVolume这一行后面,增加一行对上述脚本的调用。SYNOINSTActionPostVolume执行完后,刚好是所有磁盘都mount好但是没有任何服务启动的时刻,所以这时做mount –bind是最合适的。如果你第三步数据迁移想在重启时做,也是加在这个位置。

这一步是比较不完美的,因为需要修改系统文件,而系统文件有可能会在更新DSM时被覆盖回去,万一被覆盖回去,系统启动后就是一个没有mount –bind的状态了,即使那时再改一遍脚本再重启,DB的一致性可能已经无法保证了。我暂时选用了一个带有一点防御性的做法(同样不是万无一失的):在更新DSM前,把/usr/sbin/reboot改名,这样更新完DSM后系统不会被自动重启,我就可以有机会检查/etc/rc有没有覆盖,如果被覆盖,可以自己改回来以后再重启系统。

5. 其它经验和坑

  • 数据迁移必须用mount –bind,不能用软链,已知有些应用组件在软链的情况下不能正常工作
  • /volume1/@tmp不能迁移到NVME,即使用mount –bind,也会造成Drive等组件不能正常工作
  • 现在的做法,NVME的第二个分区是后加到SWAP分区的RAID里的,所以每次重启都会有个重新同步的过程,IO会打高一会儿,并且完成后DSM会弹一个提示:一致性检查完成。但总体是可以忍的,所以我就没有去深究用什么方法可以在建立SWAP的RAID时就直接把它一起建进去了,因为我还是想尽可以少侵入DSM系统
  • 用户的共享文件夹也可以通过mount –bind迁到NVME上,但是这样做会造成在共享文件夹管理界面上不能正常查看共享文件夹相关信息,所以建议只对共享文件夹里的目录进行mount –bind
  • 如果你只有一条NVME,或者有两条但没有做RAID,那么迁移上去的那些系统文件都是单点(虽然原本在磁盘没有RAID的话是单点),需要留意其中的风险

Synology PhotoStation性能优化

本文的内容已经过时,6.7.0以上的Photo Station已经没有严重的性能问题了。

我折腾过不少家用NAS方案,包括最早的WD My Book World Edition,后来的Joggler,再到后来的Raspberry Pi,这些方案除了性能存在一些问题以外,最大的缺点就是易用性存在问题,不但非“专业”人士用起来存在困难,就连我自己也对土法泡制的照片管理功能感到不满。直到两年前入了群晖(Synology)的家用NAS DS214play,事情才变得安逸起来。

平心而论,Synology的系统虽然功能强大,体验也还不错,但细节上做得其实真是挺糙的。最近就发现了它的照片管理软件PhotoStation出现了严重的性能问题,在照片库里只存了8万余张照片的情况下,打开首页要花费的时间已经超过了20秒,每打开一个文件夹都需要等待10秒以上,几乎不可用了。

以下分析以DSM 6.0为例,PhotoStation版本为6.4-3166。PhotoStation的安装路径为/volume1/@appstore/PhotoStation。

Synology的DSM系统,后台使用PostgreSQL数据库,前端是PHP页面。简单推理一下就可以知道,PhotoStation的性能瓶颈主要是在对照片索引数据库的访问上。性能调优的第一步就是先要找到哪些SQL查询占用了太多的时间。打开PostgreSQL记录SQL查询的开关,并查看所有SQL执行情况:

$ sudo su postgres
$ vi ~/postgresql.conf
log_statement = 'all'
$ psql photo postgres
# SELECT pg_reload_conf();
# \q
$ exit
$ sudo tail -f /var/log/postgresql.log

通过查看SQL执行记录,很容易发现几个明显的慢查询:

1.

SELECT COUNT(*) as total FROM photo_image; 
SELECT COUNT(*) as total FROM video;

2.

SELECT COUNT(logid), MAX(logid) FROM photo_log;

3.

SELECT * FROM (
    SELECT path AS filename, timetaken AS takendate, 
        create_time AS createdate, 'photo' AS type
    FROM photo_image
    WHERE path LIKE '/volume1/photo/%'
        AND path NOT LIKE '/volume1/photo/%/%' AND disabled = 'f'
    UNION
    SELECT path AS filename, mdate AS takendate,
        date AS createdate, 'video' AS type
    FROM video
    WHERE path LIKE '/volume1/photo/%'
        AND path NOT LIKE '/volume1/photo/%/%'
        AND disabled = 'f'
    ) AS totalCount; 

4.

SELECT path, resolutionx, resolutiony, version FROM (
    SELECT path, resolutionx, resolutiony, version,
        create_time, privilege_shareid, disabled
    FROM photo_image WHERE privilege_shareid IN (
        SELECT shareid FROM photo_share WHERE ref_shareid = (
            SELECT shareid FROM photo_share WHERE sharename = '2016'))
    AND disabled = 'f'
    UNION ALL
    SELECT path, resolutionx, resolutiony, 0 AS version, 
        date AS create_time, privilege_shareid, disabled
    FROM video WHERE privilege_shareid IN (
        SELECT shareid FROM photo_share WHERE ref_shareid = (
            SELECT shareid FROM photo_share WHERE sharename = '2016'))
    AND disabled = 'f') temp
ORDER BY create_time DESC LIMIT 1; 

优化的思路很简单,由于PhotoStation在正常情况下访问数据库所需要的读性能是远远大于写性能的,所以就通过牺牲写性能来逐一击破上面这些慢查询:

1. 程序的目的就是想知道系统中有多少张照片和多少个视频(而且其实并不需要精确知道,差不多就行),可惜对于PostgreSQL来说,由于它采用MVCC来解决并发问题,SELECT COUNT(*)是一个需要进行全表扫描的慢操作。解决方案就是用另外用一张表来存这两个表的总记录条数,并在原表上添加触发器来更新记录数。

CREATE TABLE photo_count (table_oid Oid PRIMARY KEY, count int);
ALTER TABLE photo_count OWNER TO "PhotoStation";
CREATE FUNCTION count_increment() RETURNS TRIGGER AS $_$
BEGIN
  UPDATE photo_count SET count = count + 1 WHERE table_oid = TG_RELID;
  RETURN NEW;
END $_$ LANGUAGE 'plpgsql';
CREATE FUNCTION count_decrement() RETURNS TRIGGER AS $_$
BEGIN
  UPDATE photo_count SET count = count - 1  WHERE table_oid = TG_RELID;
  RETURN NEW;
END $_$ LANGUAGE 'plpgsql';
CREATE TRIGGER photo_image_increment_trig AFTER INSERT ON photo_image 
  FOR EACH ROW EXECUTE PROCEDURE count_increment();
CREATE TRIGGER photo_image_decrement_trig AFTER DELETE ON photo_image 
  FOR EACH ROW EXECUTE PROCEDURE count_decrement();
CREATE TRIGGER video_increment_trig AFTER INSERT ON video 
  FOR EACH ROW EXECUTE PROCEDURE count_increment();
CREATE TRIGGER video_decrement_trig AFTER DELETE ON video 
  FOR EACH ROW EXECUTE PROCEDURE count_decrement();
INSERT INTO photo_count VALUES 
  ('photo_image'::regclass, (SELECT COUNT(*) FROM photo_count));
INSERT INTO photo_count VALUES 
  ('video'::regclass, (SELECT COUNT(*) FROM video));

然后在PHP程序中修改需要统计表数记录数的逻辑,在这里可以看到似乎同一个Session中只会查一次,但即使就是这一次,也已经慢得让人不开心了:

diff --git a/photo/include/file.php b/photo/include/file.php
index 541c5cb..7caa5de 100755
--- a/photo/include/file.php
+++ b/photo/include/file.php
@@ -536,8 +536,11 @@ class File {
 		if ($key && isset($_SESSION[SYNOPHOTO_ADMIN_USER][$key])) {
 			return $_SESSION[SYNOPHOTO_ADMIN_USER][$key];
 		}
-		$query = "SELECT count(*) as total FROM $table";
-
+		if ('photo_image' == $table || 'video' == $table) {
+			$query = "SELECT count as total FROM photo_count where table_oid='$table'::regclass";
+		} else {
+		    $query = "SELECT count(*) as total FROM $table";
+		}
 		$result = PHOTO_DB_Query($query);
 		if (!$result) {
 			// db query fail, won't update session value

2. 其实可以用跟前一个问题类似的方法去解决。但是这个其实只是一个没太多用处的操作日志表,所以我用更为简单粗暴的方法去解决这个问题:减少在数据库中保留日志的条数。直接修改PHP程序:

diff --git a/photo/include/log.php b/photo/include/log.php
index 1c982af..56385db 100644
--- a/photo/include/log.php
+++ b/photo/include/log.php
@@ -1,8 +1,8 @@
 <?php
 
 class PhotoLog {
-	const LIMIT = 100000;
-	const PURGECOUNT = 10000;
+	const LIMIT = 1000;
+	const PURGECOUNT = 100;
 	public static $SupportFormat = array("html", "csv");
 
 	public static function Debug($msg)

3. 第三个问题主要体现在对照片路径的处理上,为了选出位于某个路径下(不含子目录)的照片,程序采用了path LIKE ‘/path/%’ AND path NOT LIKE ‘/path/%/%’这样的查询条件。其实PostgreSQL在一定程度上是可以利用path字段上的索引来很好的优化这个查询的,但是实际运行中发现(通过在PostgreSQL的客户端中用explain和explain analyze分析查询)在某些情况下索引会失效,造成非常差的查询性能。解决方案还是用写性能来换读性能,先在表上加一个dirname字段并建立索引,按path算好文件所在的目录名写入dirname,然后把查询条件改为对dirname的查询,避免使用通配符和LIKE运算即可:

ALTER TABLE photo_image ADD COLUMN dirname TEXT NOT NULL DEFAULT '';
UPDATE photo_image 
    SET dirname = LEFT(path,LENGTH(path)-STRPOS(REVERSE(path),'/')+1);
ALTER TABLE video ADD COLUMN dirname TEXT NOT NULL DEFAULT '';
UPDATE video SET dirname = LEFT(path,LENGTH(path)-STRPOS(REVERSE(path),'/')+1);
CREATE INDEX dirname_index ON photo_image USING btree(dirname);
CREATE INDEX dirname_index ON video USING btree(dirname);
CREATE OR REPLACE FUNCTION set_dirname()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
  NEW.dirname := LEFT(NEW.path,LENGTH(NEW.path)-STRPOS(REVERSE(NEW.path),'/')+1);
  RETURN NEW;
END $function$
CREATE TRIGGER set_dirname_trigger 
BEFORE INSERT OR UPDATE ON photo_image 
FOR EACH ROW 
  EXECUTE PROCEDURE set_dirname();
CREATE TRIGGER set_dirname_trigger 
BEFORE INSERT OR UPDATE ON video 
FOR EACH ROW 
  EXECUTE PROCEDURE set_dirname();

同时修改PHP程序:

diff --git a/photo/include/photo/synophoto_csPhotoDB.php b/photo/include/photo/synophoto_csPhotoDB.php
index ac8f932..43e58ee 100755
--- a/photo/include/photo/synophoto_csPhotoDB.php
+++ b/photo/include/photo/synophoto_csPhotoDB.php
@@ -1607,10 +1607,8 @@ class csSYNOPhotoDB {
 		} else {
 			$albumRealPath = self::EscapeLikeParam(SYNOPHOTO_SERVICE_REAL_DIR_PATH."{$albumName}/");
 		}
-		$cond = "path LIKE ? {$this->escapeStr} AND path NOT LIKE ? {$this->escapeStr} AND disabled = 'f' ";
-		array_push($pathSqlParam, "{$albumRealPath}%");
-		array_push($pathSqlParam, "{$albumRealPath}%/%");
-
+		$cond = "dirname = ? {$this->escapeStr} AND disabled = 'f' ";
+		array_push($pathSqlParam, "{$albumRealPath}");
 		if (!$removePhoto) {
 			$photoQuery = "SELECT path as filename, timetaken as takendate, create_time as createdate, 'photo' as type
 FROM photo_image WHERE $cond";
 			$sqlParam = array_merge($sqlParam, $pathSqlParam);

4. 这个查询只是为了查询一个目录及其所有子目录中最新一个照片或视频,用其缩略图来作为目录的封面图片。群晖的工程师自己也知道这个查询很慢,所以还在程序中加了个逻辑,当照片视频数量大于200000时,放弃按日期排序,直接随机选一张。然而,这个查询实际上是可以简单优化的,明明不需要把所有的照片视频UNION到一起后再找出最新的一个,可以直接分别找出最新的照片和最新的视频,然后再到这两个中去取一个相对更新的就可以了。直接修改PHP代码实现:

diff --git a/photo/include/photo/synophoto_csPhotoAlbum.php b/photo/include/photo/synophoto_csPhotoAlbum.php
index ca128f0..f0e57e7 100755
--- a/photo/include/photo/synophoto_csPhotoAlbum.php
+++ b/photo/include/photo/synophoto_csPhotoAlbum.php
@@ -145,9 +145,11 @@ class csSYNOPhotoAlbum {
 		$cond .= " AND disabled = 'f'";
 
 		$table = "(" .
-				"SELECT path, resolutionx, resolutiony, version, create_time, privilege_shareid, disabled FROM pho
to_image WHERE $cond " .
+				"(SELECT path, resolutionx, resolutiony, version, create_time, privilege_shareid, disabled FRO
M photo_image WHERE $cond " .
+				"ORDER BY create_time DESC LIMIT 1)" .
 				"UNION ALL " .
-				"SELECT path, resolutionx, resolutiony, 0 as version, date as create_time, privilege_shareid, disa
bled FROM video WHERE $cond " .
+				"(SELECT path, resolutionx, resolutiony, 0 as version, date as create_time, privilege_shareid,
 disabled FROM video WHERE $cond " .
+				"ORDER BY create_time DESC LIMIT 1)" .
 		") temp";
 
 		// this may cost lots of time, so it won't sort by create_time if the total count exceeds the threshold (200,000)

做完以上优化,我的PhotoStation已经基本可以做到点进文件夹秒开了,至少我自己已经比较满意了。声明一下,其实我并不太懂数据库相关理论和技术,以上“优化”只能说是在我自己的实验中起到了优化的效果,也许其中一些并不太科学,希望这篇文章能起到抛砖引玉的作用。

重新安装PhotoStation或升级DSM系统会造成我们对程序所作的修改丢失,所以在修改完成后,务必做好备份。

另外,适时对PostgreSQL数据库进行VACUUM操作似乎可以起到提高访问性能的目的,尤其是在做过大量照片更新后。

解密GW-BASIC的加密文件

终于解密了一份1993年左右的BASIC代码,这么多年一直想看这份代码的内容,现在终于看到了,颇有一些唏嘘之感。

目前网上似乎搜不到中文资料介绍如何解密加密过的BASIC代码,我总结一下放在这里。聪明人可以直接跳到“解密方法二”阅读。

背景:

DOS下的GW-BASIC在保存代码时,可以存成tokenized、纯文本和加密三种格式,分别对应SAVE命令的默认参数、“,a”和“,p”参数。

对于用了,p参数保存的源代码,以后就只能LOAD到内存中执行,而不允许再LIST查看源代码了。本文介绍的就是用于解密查看,p参数保存的源代码的方法。

解密方法一:

参考GW-BASIC tokenised program format

原理是找到GW-BASIC中标记代码是否是加密的那个内存地址,然后用VAL命令的一个溢出Bug,修改这个内存地址的值。

第一步,运行一下如下的程序,找到加密标记的地址:

FOR I=1000 TO 16000:PRINT I: J=PEEK(I): POKE I,((J=0)AND 255) OR J: POKE I,J:NEXT I

这个程序会导致Illegal function call错误,记下出错前程序打印出来的数字。

第二步,正常用LOAD命令加载要加密的.BAS文件。

第三步,输入下面的程序,并把其中的a%[9]的值”1450″改为第一步中记录下来的数字。

dim a%[14]
a%[0]=0:a%[1]=&h2020:a%[2]=&h2020:a%[3]=&h2097
a%[4]=&h4553:a%[5]=&h2047:a%[6]=&H203A:a%[7]=&H2098
a%[8]=&H1C20:a%[9]=1450:a%[10]=&h112C
a%[11]=&h903A:a%[12]=0
b$=""
b$="123"+chr$(28)+":::"+chr$(137)+chr$(13)+mki$(varptr(a%[0]))+":"
print val(b$) 456

完成,现在已经可以用LIST命令正常列出解密后的代码了。

解密方法二:

当我还沉浸在成功解密了BAS文件的喜悦中时,无心的一次搜索让我又找到了更简单的解密方法

创建一个只有两个字节的UNPROT.BAS文件,这两个字节是0xFF 0x0A。

先LOAD要解密的.BAS,然后再LOAD一下这个UNPROT.BAS,然后就解密成功了。

如果是在DOS下要创建这么个文件还真有点麻烦,比较简单的做法是用DEBUG:

C:\>debug
-e 0100 ff 1a
-rcx
CX 0000
:0002
-n unprot.bas
-w
Writing 00002 bytes
-q

在Linux下使用“360随身WiFi 2”

某人说“360随身WiFi”价格还算良心,我也认同。昨天无意中看到2代开售,就随手撸了一个。当然,为了免邮费,不得不买了点别的东西凑单,于是还买了本价格是这个“360随身WiFi 2”近两倍《C语言点滴》回来看看。

到货,插到电脑上,Linux下没反应。Ralink的无线网卡系统不自带驱动我不惊呆,于是lsusb看了下。

Bus 001 Device 006: ID 148f:760b Ralink Technology, Corp.

好嘛,二代换芯片了,原来是RT5370的,现在换成不认识的了。不怕,把148f:760b放狗搜一下。不过搜完了就怕了,因为结果是0个。

打算先用Windows确认一下设备是好的,结果装了360官网的驱动后发现设备完全识别不出来……这可真是个大乌龙。到360官网的歪粉交流论坛上看看,有类似问题的看来不是个案。换了论坛上公布的新版本的驱动后Windows下工作正常。(截止我写这篇文章的时候,官网上的驱动已经更新成新的了,文件大小为10797000字节,论坛上讨论说设备识别不出来的那些贴子貌似也都直接消失了。)

继续回到Linux下折腾,可是不知道芯片是什么还是为难。从Ralink网站(现在叫Mediatek)上瞎找了几个Linux驱动,里面也没有符合760b这个idProduct的,抓瞎。

用百度搜了一下148f:760b,结果找到了360论坛上一篇新觧出炉的贴子,确认了芯片是MT7601。

剩下的事就简单了,在Ralink网站下载MT7601的Linux驱动,修改common/rtusb_dev_id.c文件,在

{USB_DEVICE(0x148f,0x7601)}, /* MT 6370 */

下面加一行

{USB_DEVICE(0x148f,0x760b)}, /* 360 Wifi */

按照README_STA_usb中的说明make和make install。然后modprobe一下mt7601Usta.ko这个内核模块,后面的事就妥妥的了。

还有个遗留问题,连不上WPA2 Enterprise的无线网络,暂时不管了,我对这个需求不强烈。

啥?这文章只说了怎么驱动这个网卡没说怎么在Linux实现AP的功能?哦,我本来也没打算用它在Linux下做AP来着。有兴趣的话可以试试hostapd/dnsmasq/iptables这老三样吧,我不知道能不能行,如果哪位朋友弄成了麻烦汇报一下,我很想学习学习,多谢了~

2013-10-12更新:MT7601的Linux驱动中似乎没有实现nl80211的接口,所以hostapd没法直接用。不知道还有什么办法能实现AP的功能,如有朋友知道,希望能不吝指教。

2014-08-12更新:留言区中的轩辕志瑜同学找到了一个支持AP模式的驱动,详细的信息请查看相关的文章:http://blog.csdn.net/sumang_87/article/details/38168877,github上的源代码:https://github.com/eywalink/mt7601u。感谢他的分享。

2015-03-12更新:如果是在Raspberry Pi上尝试编译驱动并且遇到困难,请参考《为Raspberry Pi 2编译内核模块

《爱上Raspberry Pi》诞生记

我的第一本译著《爱上Raspberry Pi》上个月底终于正式出版销售了。从开始有译书的想法到拿到成品书,正好5个月时间。我的Blog也正好长了这5个月的草,不过这之间没有因果关系。

《爱上Raspberry Pi》封面

《爱上Raspberry Pi》封面

今年上半年,闲暇的时间比较多,就玩了玩Raspberry Pi和Arduino,兴趣正浓时,看到论坛上有出版社的编辑在征集Raspberry Pi相关图书的译者,于是就与编辑联系了一下。那时正好看了几本Pi的英文书,第一次感觉到原来老外们也出了很多烂书,在看过的几本书中,Getting Started with Raspberry Pi这本书是那个时候内容相对说得过去的一本。由于编辑手头有几本书都已经找到译者,我就随口问了问这一本,没想到这位编辑正好在跟O’Reilly谈这本书的引进合同,真可谓无巧不成书了。

为了争取到翻译这本书的资格,我主动根据手头的电子书(盗版的,惭愧)先试译了一章,发给了编辑。于是,很顺利的,3月20日,我就与出版社签定了“委托翻译合同”。

“委托翻译合同”从名字就可以看出,译者的事就是翻译,翻完了就没你什么事了。稿费按不含空格的字数计算,每千字稿费低到难以想象。译稿的版权出版社以稿费的形式直接买断,译者只有属名权,出版后的书卖多卖少跟译者都无关了。出版社违约的话,只需部分支付稿费。现在大环境就是这个样子,也没啥办法。

4月15日,通过电子邮件联系了原书作者Matt Richardson,请他给写了个中文版序。后来才知道,出版社好像比较忌讳译者与作者直接联系,不过那时无知者无畏,反正中文版序是到手了。

合同交稿时间是4月20日,不过由于出版社到4月19日才拿到O’Reilly给的英文样书,而我又想根据英文纸书对译稿进行正式的审校,所以虽然那时翻译已经完成了,实际交稿的时间还是协商延迟到了5月15日,期间我自己把译稿通读了七八次,并请好友帮忙审阅了几轮。幸亏书不厚,才有机会这么细致的审核,不过看到最后,真是想吐了。

6月5日,出版社提供了打印出来的初校样,由译者进行审阅。实话说,这个初校样让我大跌眼镜,上面有各种排版错误,甚至还会出现原译稿中没有的错别字。校了3天,200多页的书中找出了100多处错误,其中属于原译稿错误的不到10处。

6月22日,我在家里的Raspberry Pi上用gor这个用Golang写的Blog系统搭建了一个小网站,用作本书的支持网站,提供勘误表和电路图,域名是http://rpi.freemindworld.com。不过后来为了保证网站稳定性,迁移到我的Linode上去了。Raspberry Pi上的那个Web服务换了个了域名,同步运行,域名改为http://rpirpi.freemindworld.com

7月中旬,进入三校阶段,遗憾的是三校的版本中依然是错误多多。咬着牙把三校版又仔细看了几遍,改了六七十处错误。8月上旬,核红稿校对中又发现了少量问题,不过总算错误数开始收敛了。

7月18日,出版社领到了书号:ISBN 978-7-03-038196-5。有了书号,我就在豆瓣上创建了相关的图书页面。

8月8日,又仔细把最后一个版本的核红稿给看了一遍,这次问题终于比较少了,修改了几个不痛不痒的小问题后,终于在8月8日下午进厂印刷。

8月19日,出版社拿到了印刷厂的校正本样书。8月25日,我拿到了第一本校正本样书,劳动终于结成了果实。

销售方面,各主要B2C电商中,8月23日,当当网第一个上架了此书,不过一直处于缺货状态,直到9月5日。其次是亚马逊中国,但很长一段时间上面只有第三方卖家出售,也是到9月初才开始有自营的。最早有现货可以正常发货的是北发图书网,而且还是所有B2C电商中售价最便宜的。

中文版图书的封面,最终是由O’Reilly来设计的,显然采用了与原书和这一系列书中文版一样的公版设计。7月底给了第一稿,按我的要求微调了一下以后,8月5日定稿。

总结一下出版过程,作为译者来说:与出版社编辑联系,确定翻译意向,试译,签定翻译合同,翻译,交稿,审校样,拿样书。

身边也有不少朋友表示对出书有兴趣,其实现在作译者与编辑那里存在一个信息不对称的问题。编辑们手上有很多书或选题找不到人来翻译或原创,而有心出书的人则不知道应该怎么去跟编辑联系。所以与编辑联系是整件事情的第一步,其实借助搜索引擎或微博,这件事情并不难完成。同时,与编辑之间建立友好、信任、合作的关系,会对整本书的出版过程起到重要推动作用。

最后帮我的编辑做个广告,目前还有几本有关Raspberry Pi的书在寻找合适的译者。书都不厚,如果想体验一下出书的乐趣和痛苦的朋友,可以通过我与编辑取得联系。 这几本书主要是Step-by-step的教一些Raspberry Pi的入门操作知识,技术含金量不高,但翻译起来会比较轻松。