« October 2017 | Main | December 2017 »

November 20, 2017

Windows 下重定向当前进程的 stdout 到网络连接

前段时间碰到一个需求,想把当前进程的标准输出重定向到一个 tcp 连接上。

如果依照 posix 标准,调用一下 dup2 这个 api 就能搞定,但是 windows 并不是基于 posix 标准的操作系统,所以做起来要麻烦的多。

我在 stackoverflow 和 msdn 上找了一番,没有看到什么靠谱的做法,所以自己折腾了一天。这里的难点在于:windows 上虽然有 _dup2 来模拟 posix 的 api dup2 ,但 fd 在 windows 上并不是内核对象, HANDLE 才是。fd 是在 runtime 层模拟出来的东西。msdn 上引用最多的一篇是:Creating a Child Process with Redirected Input and Output ,做的事情是创建一个子进程,然后重定向标准输入输出。

重定向子进程和重定向当前进程有什么区别?我是这样理解的:

Windows 虽然也有标准输入输出的概念,但是是基于 HANDLE 的。GetStdHandle 和 SetStdHandle 两个 API 虽然可以读写 Windows 的标准输入输出句柄,但这个句柄似乎(我猜想)是在 runtime 初始化阶段绑定到 fd 0 1 2 上的,之后,基于 fd 的一套 runtime 机制都不再经过这个转换过程。在进程运行过程中,调用 SetStdHandle 并不能重定向 C 的标准输入输出库。

在 stackoverflow 上的一个帖子也谈及了这个问题,SetStdHandle has no effect on cout/printf 。模仿 posix 的做法,使用 GetStdHandle 获得 windows 标准输出,然后利用 DuplicateHandle 复制 handle 的做法是无效的。

帖子里给出的方案是在 runtime 层用 _dup 复制 fd 再重定向到文件。不过想重定向到网络连接却没这么简单。因为 windows 下 socket 并不是 first class 的 handle ,不能直接当普通的文件 handle 使用,也就无法用 dup/dup2 传过去。

所以,我们需要先将 stdout 用 dup2 重定向到匿名管道,再创建一个线程去读这个管道,转发到 socket 。

光启动这么一个转发线程也有潜在的问题:在进程结束的时候,runtime 在处理最后的标准输出时,无法直接让转发线程感知到,这样有可能丢失结束前最后的输出。我们需要额外的机制在进程要结束时通知转发线程停止转发。主动通知可以让转发线程处理完所有已经转发的数据,而不会有遗漏,主线程则可以等待转发线程工作作完再自行退出。

因为读转发管道 Handle 使用的是 ReadFile ,windows 下的文件 Handle 无法和 Windows 的 Event 一起通过 WaitForMultipleObjects 工作(这或许是 windows 要额外引入 IOCP 的原因之一?)。我们不能利用额外的 Event 来做这件通知工作,只能通过关闭管道,让 ReadFile 感知。

这里要小心,dup 内部会增加内核 handle 的引用计数,所以不要漏掉了 close ,否则会让管道没有正确关闭,ReadFile 无法在关闭后返回。

写了一个示例供参考 。它把转发开启和转发结束的过程封装在一个 lua 模块中。

November 09, 2017

四元数的压缩存储

今天在读 https://github.com/guillaumeblanc/ozz-animation 这个动画库的代码时,发现它使用了一个有趣的四元数压缩技术。

我们用四元数来表示 3D 空间中的旋转,通常需要 4 个浮点数。不过用四元数表示旋转时,四元数通常会先做一次归一化,即 x*x + y *y + z*z + w*w = 1 。所以我们只需要保留 x,y,z 和 w 的符号位就够了。

但三个 float 来表示四元数既然有压缩余地。这是因为用来表示旋转量时, float 提供的 23 bit 精度是多余的。 1/2^23 的转角在视觉上完全是忽略不计的。而且 x,y,z 都一定小于 1 ,所以 float 的指数位也浪费了 1bit 。

ozz-animation 这个库采用了一个有趣的方法来做进一步压缩。

如果直接用 half 来代替 float 是达不到,或是浪费了精度的。half 只有 10bit 有效精度,也就是千分之一左右,这是肉眼可察的误差。

因为 x y z 都归一化过了,绝对值都小于 1 ,所以我们可以考虑直接用定点数,保留小数点后 16 位,最小可表示角度就提高到了六万分之一圆周。

由于需要 1 个 bit 记录第四分量的符号位,如果直接在前三个分量的数据位中减去,会导致不平均。所以,可以不去保存固定的四元数的前三个量,而是保存四个分量中最小的三个。第二大的分量最大不会超过1/√2,所以可以把每个分量再乘上√2,这样大约可以增加半个 bit 的精度。由于是使用的定点数,这种方法对于接近零的小数字意义比浮点计数法要大。

这样,还需要花上 2 bit 记录最大的分量是第几个。连同最大分量的符号,一共需要额外的 3 bit ,正好每个分量扣除一个 bit 。

ozz-animation 因为同时还需要记录四元数归属的骨骼编号,所以把这额外的 3 bit 压缩在骨骼编号字中。我想打包在 3 个 16bit 定点数中也可以。每个分量在符号位外还能保留 14bit 的精度,可以比 half 的精度高16 倍。