Denis's Blog


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 搜索

替换fonts.googleapi.com

发表于 2017-03-13 | 分类于 hexo | | 阅读次数
字数统计 49 | 阅读时长 1

可以换成以下网址:

  • fonts.css.network
  • libs.bangbang93.com
  • fonts.proxy.ustclug.org
  • cdn.moefont.com/fonts

以Next主题为例:

1
2
3
4
5
6
# themes/next/_config.yml
font:
enable: true
# Uri of fonts host. E.g. //fonts.googleapis.com (Default)
host: //fonts.css.network

编译器如何生成汇编

发表于 2017-03-13 | 分类于 jsengine | | 阅读次数
字数统计 1,030 | 阅读时长 4

转载自: https://zhuanlan.zhihu.com/p/25718411
原文作者:Lin Clark
原文链接:A crash course in assembly

本文是关于 WebAssembly 系列的第三篇文章(本系列共六篇文章,后续会将连接补充归整)。如果你没有读先前文章的话,建议先读这里(英文文章)。如果对 WebAssembly 没概念,建议先读这里(中文文章)。

理解什么是汇编,以及编译器如何生成它,对于理解 WebAssembly 是很有帮助的。

在上一篇关于 JIT 的文章中,我介绍了和计算机打交道,就像同外星人打交道一样。

现在来思考一下“外星人”的大脑是如何工作的——机器的“大脑”是如何对我们输入给它的内容进行分析和理解的。

“大脑”中,有一部分负责思考——处理加法、减法或者逻辑运算。还有其他的部分分别负责短暂记忆和长期记忆的。

这些不同的部分都有自己的名字:

  • 负责思考的部分叫做算数逻辑单元(ALU)
  • 寄存器提供短暂记忆功能
  • 随机存取存储器(RAM)提供长期记忆功能

机器代码中的语句称作指令。

那么在指令进入“大脑”以后都发生了什么呢?它们会被切分为不同的部分传送到不同的单元进行处理。

“大脑”切分指令通过不同连接线路进行。举个例子,“大脑”会将指令最开始的 6 比特通过管道送到 ALU 中。而 ALU 会通过 0 和 1 的位置来决定对两个数做加法。

这串 01 串就叫做“操作码”,它告诉了 ALU 要执行什么样的操作。

然后“大脑”会取后面两个连续的 3 比特 01 串来确定把哪两个数加到一起,而这 3 比特指的是寄存器的地址。

注意看上面机器码的注释:“ADD R1 R2”,这对于人类来讲很容易理解其含义。这就是汇编,也叫符号机器码,它使人类也能看懂机器代码的含义。

可以看到汇编和这台机器的机器码之间有直接的映射关系。正是因为如此,拥有不同机器结构的计算机会有不同的汇编系统。如果你有一个机器,它有自己的内部结构,那么它就需要它所独有的汇编语言。

从上面的分析可以知道我们进行机器码的翻译并不是只有一种,不同的机器有不同的机器码,就像我们人类也说各种各样的语言一样,机器也“说”不同的语言。

人类和外星人之间的语言翻译,可能会从英语、德语或中文翻译到外星语 A 或者外星语 B。而在程序的世界里,则是从 C、C++ 或者 JAVA 翻译到 x86 或者 ARM。

你想要从任意一个高级语言翻译到众多汇编语言中的一种(依赖机器内部结构),其中一种方式是创建不同的翻译器来完成各种高级语言到汇编的映射。

这种翻译的效率实在太低了。为了解决这个问题,大多数编译器都会在中间多加一层。它会把高级语言翻译到一个低层,而这个低层又没有低到机器码这个层级。这就是中间代码( intermediate representation,IR)。

这就是说编译器会把高级语言翻译到 IR 语言,而编译器另外的部分再把 IR 语言编译成特定目标结构的可执行代码。

重新总结一下:编译器的前端把高级语言翻译到 IR,编译器的后端把 IR 翻译成目标机器的汇编代码。

总结

本文介绍了什么是汇编以及编译器是如何把高级语言翻译成汇编语言的,在下一篇文章中,我们来介绍 WebAssembly 的工作原理。

[转载] GTK+实现进程间嵌入窗口

发表于 2017-03-11 | 分类于 gtk编程 | | 阅读次数
字数统计 906 | 阅读时长 4

转载自:http://blog.csdn.net/absurd/article/details/600637

Socket/Plug

Windows下的托盘(tray)是不是很酷呢?利用这种机制,你可以方便的把自己的应用程序嵌入到任务栏里。大多数时候,应用程序在后台工作,不会干扰用户,当用户想查看某些信息时,只点一下这个小图标就行了。应用程序在响应点击事件时,可以把应用程序提到前台来,可以弹出一个对话框,可以显示一个菜单,或者做其它任何事情,这完全是应用程序自己的事,与任务栏一点关系都没有。

在Linux下的桌面环境里,不但有这个功能,而且功能更加强大。Linux下的桌面环境有好几种,在PC上最为流行的当然是KDE和GNOME。它们往往都有一套自己的机制,搞得不同桌面环境下开发的应用程序之间的兼容性很差。为了让这些应用程序之间能够互相嵌入,当然得有一个标准才行。为此,freedesktop.org组织制定了一个XEBEDDED协议(https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html)。

只要遵守XEBEDDED协议,用Qt写的应用程序可以嵌入到GNOME的任务栏里,用GTK+写的应用程序可以嵌入到KDE的任务栏里。不但如此,在需要的情况下,两个应用程序之间也可以任意嵌入,而不必关心它们是用哪个库实现的。

虽然说这个协议很简单,自己要从头实现一个,未免太麻烦了。为了简化应用程序开发,GTK+已经封装一套函数。本文用一个简单的实例,介绍如何开发这类应用。在此之前,我们先熟悉几个概念:

插座(socket):这里指宿主窗口,它可以让其它应用程序,把窗口嵌入到它里面。如,任务栏就是一个插座(socket)。

插头(plug): 顾名思义,它就是被嵌入的窗口,可以插入到插座(socket)上。相对任务栏而言,应用程序的窗口就是插头(plug)。

插头(plug)/插座(socket)两者可以在同一个应用中,也可以在不同的应用程序中。在同一个应用程序里,这种做法意义不大,而且可以说是自找麻烦。大多数情况下,它们分别位于不同的进程之中,一个插座(socket)窗口可以容纳多个插头(plug)窗口中,而一个插头(plug)窗口只能处于一个插座(socket)窗口之中。

示例(C)

插座(socket)端应用程序实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//socket.c
#include <stdlib.h>
#include <gtk/gtk.h>
int main( int argc, char *argv[] )
{
GtkWidget *window;
GtkWidget *socket;
gtk_init (&argc, &argv);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_widget_set_size_request(window, 80, 40);
socket = gtk_socket_new();
gtk_widget_show (socket);
gtk_container_add (GTK_CONTAINER (window), socket);
gtk_widget_show (window);
g_message("socket_id=%d/n", gtk_socket_get_id(socket));
gtk_main ();
return 0;
}

插头(plug)端应用程序实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// plug.c
#include <stdlib.h>
#include <gtk/gtk.h>
int main( int argc, char *argv[] )
{
gint socket_id = 0;
GtkWidget *window;
GtkWidget *button;
gtk_init (&argc, &argv);
if(argc != 2)
{
g_message("usage: %s [socket]/n", argv[0]);
return -1;
}
else
{
socket_id = atoi(argv[1]);
}
window = gtk_plug_new(socket_id);
button = gtk_button_new ();
gtk_widget_show (button);
gtk_container_add (GTK_CONTAINER (window), button);
gtk_widget_show (window);
gtk_main ();
return 0;
}

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CC = gcc
CFLAGS = -Wall -Wunused \
-DG_DISABLE_DEPRECATED \
-DGDK_DISABLE_DEPRECATED \
-DGDK_PIXBUF_DISABLE_DEPRECATED \
-DGTK_DISABLE_DEPRECATED
all: plug socket
plug: plug.c
$(CC) plug.c -o plug $(CFLAGS) `pkg-config gtk+-2.0 --cflags --libs`
socket: socket.c
$(CC) socket.c -o socket $(CFLAGS) `pkg-config gtk+-2.0 --cflags --libs`
clean:
rm -f *.o plug socket

当然,这里为了便于理解,程序写得很简单,实际的应用程序要比这复杂一些,它们的原理都是一样的,大家可参考GTK+的API手册。

示例(PyGTK)

https://www.moeraki.com/pygtktutorial/pygtk2tutorial/sec-PlugsAndSockets.html

[转载] JavaScript Just-in-time (JIT) 工作原理

发表于 2017-03-10 | 分类于 jsengine | | 阅读次数
字数统计 2,769 | 阅读时长 10

译文作者:胡子大哈
转载自知乎:https://zhuanlan.zhihu.com/p/25669120

原文作者:Lin Clark
英文原文:https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/

本文是关于 WebAssembly 系列的第二篇文章。如果你没有读先前文章的话,建议先读这里(英文文章)。如果对 WebAssembly 没概念,建议先读这里(中文文章)。

JavaScript 的启动比较缓慢,但是通过 JIT 可以使其变快,那么 JIT 是如何起作用的呢?

JavaScript 在浏览器中是如何运行的?

如果是你一个开发者,当你决定在你的页面中使用 JavaScript 的时候,有两个要考虑的事情:目标和问题。

目标:告诉计算机你想做什么。

问题:你和计算机说不同的语言,无法沟通。

你说的是人类的语言,而计算机用的是机器语言。机器语言也是一种语言,只是 JavaScript 或者其他高级编程语言机器能看得懂,而人类不用他们来交流罢了。它们是基于人类认知而设计出来的。

所以呢,JavaScript 引擎的工作就是把人类的语言转换成机器能看懂的语言。

这就像电影《降临》中,人类和外星人的互相交流一样。

在电影里面,人类和外星人不仅仅是语言不同,两个群体看待世界的方式都是不一样的。其实人类和机器也是类似(后面我会详细介绍)。

那么翻译是如何进行的呢?

在代码的世界中,通常有两种方式来翻译机器语言:解释器和编译器。

如果是通过解释器,翻译是一行行地边解释边执行

编译器是把源代码整个编译成目标代码,执行时不再需要编译器,直接在支持目标代码的平台上运行。

这两种翻译的方式都各有利弊。

解释器的利弊

解释器启动和执行的更快。你不需要等待整个编译过程完成就可以运行你的代码。从第一行开始翻译,就可以依次继续执行了。

正是因为这个原因,解释器看起来更加适合 JavaScript。对于一个 Web 开发人员来讲,能够快速执行代码并看到结果是非常重要的。

这就是为什么最开始的浏览器都是用 JavaScript 解释器的原因。

可是当你运行同样的代码一次以上的时候,解释器的弊处就显现出来了。比如你执行一个循环,那解释器就不得不一次又一次的进行翻译,这是一种效率低下的表现。

编译器的利弊

编译器的问题则恰好相反。

它需要花一些时间对整个源代码进行编译,然后生成目标文件才能在机器上执行。对于有循环的代码执行的很快,因为它不需要重复的去翻译每一次循环。

另外一个不同是,编译器可以用更多的时间对代码进行优化,以使的代码执行的更快。而解释器是在 runtime 时进行这一步骤的,这就决定了它不可能在翻译的时候用很多时间进行优化。

Just-in-time 编译器:综合了两者的优点

为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。

不同的浏览器实现这一功能的方式不同,不过其基本思想是一致的。在 JavaScript 引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。

起初,监视器监视着所有通过解释器的代码。

如果同一行代码运行了几次,这个代码段就被标记成了 “warm”,如果运行了很多次,则被标记成 “hot”。

基线编译器

如果一段代码变成了 “warm”,那么 JIT 就把它送到编译器去编译,并且把编译结果存储起来。

代码段的每一行都会被编译成一个“桩”(stub),同时给这个桩分配一个以“行号 + 变量类型”的索引。如果监视器监视到了执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push 出来给浏览器。

通过这样的做法可以加快执行速度,但是正如前面我所说的,编译器还可以找到更有效地执行代码的方法,也就是做优化。

基线编译器可以做一部分这样的优化(下面我会给出例子),不过基线编译器优化的时间不能太久,因为会使得程序的执行在这里 hold 住。

不过如果代码确实非常 “hot”(也就是说几乎所有的执行时间都耗费在这里),那么花点时间做优化也是值得的。

优化编译器

如果一个代码段变得 “very hot”,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。

为了生成一个更快速的代码版本,优化编译器必须做一些假设。例如,它会假设由同一个构造函数生成的实例都有相同的形状——就是说所有的实例都有相同的属性名,并且都以同样的顺序初始化,那么就可以针对这一模式进行优化。

整个优化器起作用的链条是这样的,监视器从他所监视代码的执行情况做出自己的判断,接下来把它所整理的信息传递给优化器进行优化。如果某个循环中先前每次迭代的对象都有相同的形状,那么就可以认为它以后迭代的对象的形状都是相同的。可是对于 JavaScript 从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性。

正是由于这样的情况,所以编译代码需要在运行之前检查其假设是不是合理的。如果合理,那么优化的编译代码会运行,如果不合理,那么 JIT 会认为做了一个错误的假设,并且把优化代码丢掉。

这时(发生优化代码丢弃的情况)执行过程将会回到解释器或者基线编译器,这一过程叫做去优化。

通常优化编译器会使得代码变得更快,但是一些情况也会引起一些意想不到的性能问题。如果你的代码一直陷入优化<->去优化的怪圈,那么程序执行将会变慢,还不如基线编译器快。

大多数的浏览器都做了限制,当优化/去优化循环发生的时候会尝试跳出这种循环。比如,如果 JIT 做了 10 次以上的优化并且又丢弃的操作,那么就不继续尝试去优化这段代码了桩。

一个优化的例子:类型特化(Type specialization)

有很多不同类型的优化方法,这里我介绍一种,让大家能够明白是如何优化的。优化编译器最成功一个特点叫做类型特化,下面详细解释。

JavaScript 所使用的动态类型体系在运行时需要进行额外的解释工作,例如下面代码:

1
2
3
4
5
6
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}

+= 循环中这一步看起来很简单,只需要进行一步计算,但是恰恰因为是用动态类型,他所需要的步骤要比你所想象的更复杂一些。

我们假设 arr 是一个有 100 个整数的数组。当代码被标记为 “warm” 时,基线编译器就为函数中的每一个操作生成一个桩。sum += arr[i]会有一个相应的桩,并且把里面的 += 操作当成整数加法。

但是,sum 和 arr[i] 两个数并不保证都是整数。因为在 JavaScript 中类型都是动态类型,在接下来的循环当中,arr[i] 很有可能变成了string 类型。整数加法和字符串连接是完全不同的两个操作,会被编译成不同的机器码。

JIT 处理这个问题的方法是编译多基线桩。如果一个代码段是单一形态的(即总是以同一类型被调用),则只生成一个桩。如果是多形态的(即调用的过程中,类型不断变化),则会为操作所调用的每一个类型组合生成一个桩。

这就是说 JIT 在选择一个桩之前,会进行多分枝选择,类似于决策树,问自己很多问题才会确定最终选择哪个,见下图:

正是因为在基线编译器中每行代码都有自己的桩,所以 JIT 在每行代码被执行的时候都会检查数据类型。在循环的每次迭代,JIT 也都会重复一次分枝选择。

如果代码在执行的过程中,JIT 不是每次都重复检查的话,那么执行的还会更快一些,而这就是优化编译器所需要做的工作之一了。

优化编译器中,整个函数被统一编译,这样的话就可以在循环开始执行之前进行类型检查。

一些浏览器的 JIT 优化更加复杂。比如在 Firefox 中,给一些数组设定了特定的类型,比如里面只包含整型。如果 arr 是这种数组类型,那么 JIT 就不需要检查 arr[i] 是不是整型了,这也意味着 JIT 可以在进入循环之前进行所有的类型检查。

总结

简而言之 JIT 是什么呢?它是使 JavaScript 运行更快的一种手段,通过监视代码的运行状态,把 hot 代码(重复执行多次的代码)进行优化。通过这种方式,可以使 JavaScript 应用的性能提升很多倍。

为了使执行速度变快,JIT 会增加很多多余的开销,这些开销包括:

  • 优化和去优化开销
  • 监视器记录信息对内存的开销
  • 发生去优化情况时恢复信息的记录对内存的开销
  • 对基线版本和优化后版本记录的内存开销

这里还有很大的提升空间:即消除开销。通过消除开销使得性能上有进一步地提升,这也是 WebAssembly 所要做的事之一。

Debootstrap安装MATE桌面

发表于 2017-03-10 | 分类于 linux | | 阅读次数
字数统计 109 | 阅读时长 1

安装debootstrap

1
2
3
4
5
6
7
8
9
10
$ sudo apt-get install debootstrap
$ debootstrap --arch=arm64 --include=mate-desktop-environment,lightdm-gtk-greeter,vim,net-tools,xorg,language-pack-zh-hans,fonts-wqy-microhei,ttf-wqy-microhei --components=main,restricted,multiverse,universe xenial /mnt/ http://mirrors.ustc.edu.cn/ubuntu-ports
$ cd ubuntu-xenial
$ sudo mount -B /proc proc
$ sudo mount -t tmpfs tmpfs tmp
$ sudo cp /etc/resolv.conf etc
$ xhost +
$ export DISPLAY=:0.0
$ sudo chroot .
$ useradd -m -p 123123 sk

修改/etc/apt/sources.list

1
deb http://mirrors.ustc.edu.cn/ubuntu-ports xenial main restricted multiverse universe

!!! rtkit包会导致进桌面卡住

开启核心转储(Core Dump)设置

发表于 2017-03-10 | 分类于 linux | | 阅读次数
字数统计 32 | 阅读时长 1
1
2
3
4
5
6
7
8
#修改/etc/sysctl.conf
kernel.core_pattern = /corefile/core-%e-%p-%t
kernel.core_uses_pid = 1
fs.suid_dumpable = 1
#修改/etc/profile
ulimit -S -c unlimited 2>&1
#或修改 /etc/security/limits.conf

Hexo创建个人博客

发表于 2017-03-10 | 分类于 hexo | | 阅读次数
字数统计 75 | 阅读时长 1

Hexo安装

1
2
3
4
5
6
7
8
9
10
11
$ sudo apt install nodejs nodejs-legacy
$ sudo npm install hexo-cli -g
# git部署
$ npm install hexo-deployer-git --save
# RSS
$ npm install hexo-generator-feed
# 百度站点地图,方便搜索引擎收录
$ npm install hexo-generator-sitemap
$ npm install hexo-generator-baidu-sitemap --save
# 本地搜索
$ npm install hexo-generator-search --save

Hexo配置(插件、主题)

TODO

[翻译] Gecko日志记录

发表于 2017-03-10 | 分类于 firefox | | 阅读次数
字数统计 769 | 阅读时长 3

原文链接:Gecko_Logging

Gecko核心代码提供了一个基础的C++日志记录框架。所有构建 (Debug、Release)默认开启,线程安全,是NSPR日志记录的首选。

日志记录框架

声明日志模块

LazyLogModule以线程安全的方式推迟创建后台LogModule,并且是声明日志模块的首选方法。

1
2
3
#include "mozilla/Logging.h"
static mozilla::LazyLogModule sFooLog("foo");

提供了2个宏和1个枚举类做为基本接口

Mozilla日志记录宏

MOZ_LOG(module, level, message)

输出指定信息,如果模块日志级别允许。

  • module - 要使用的日志模块
  • level - 消息的日志输出级别
  • message - 要输出的printf样式消息。必须括在括号中
MOZ_LOG_TEST(module, level)

检查模块是否开启相应的日志输出级别。

  • module - 要使用的日志模块
  • level - 日志输出级别

Mozilla日志级别

日志级别 数值 作用
Disabled 0 表示日志记录已禁用。这不应该直接在代码中使用。
Error 1 发生错误,通常你会考虑在调试生成中断言。
Warning 2 警告通常表示意外状态。
Info 3 通常指示当前程序状态。
Debug 4 调试消息,对调试有用,但过于冗长,无法正常打开。
Verbose 5 将打印很多有用的调试程序流,并可能会影响性能。

日志记录接口

使用示例

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "mozilla/Logging.h"
using mozilla::LogLevel;
static mozilla::LazyLogModule sLogger("example_logger");
static void DoStuff()
{
MOZ_LOG(sLogger, LogLevel::Info, ("Doing stuff."));
int i = 0;
int start = Time::NowMS();
MOZ_LOG(sLogger, LogLevel::Debug, ("Starting loop."));
while (i++ < 10) {
MOZ_LOG(sLogger, LogLevel::Verbose, ("i = %d", i));
}
// Only calculate the elapsed time if the Warning level is enabled.
if (MOZ_LOG_TEST(sLogger, LogLevel::Warning)) {
int elapsed = Time::NowMS() - start;
if (elapsed > 1000) {
MOZ_LOG(sLogger, LogLevel::Warning, ("Loop took %dms!", elapsed));
}
}
if (i != 10) {
MOZ_LOG(sLogger, LogLevel::Error, ("i should be 10!"));
}
}

开启日志输出

模块的日志级别通过在启动应用程序之前设置环境变量来控制。

1
export MOZ_LOG="example_logger:3"

有一些特殊的模块名可以改变日志行为。除日志级别以外,你可以指定一个或多个特殊模块名称。
|模块名||
|—-|—|
|append|将新日志附加到现有日志文件。|
|sync|同步打印每个日志,这对于实时检查行为或在崩溃之前获取日志很有用。|
|timestamp|插入时间戳开始每个日志行。|
|rotate:N|这限制了生成的日志文件的大小。仅保存最近的N兆字节的日志数据。我们使用.0,.1,.2,.3扩展名旋转四个日志文件。注意:此选项禁用“附加”和强制的时间戳。|


例如,如果要指定“sync”,“timestamp”和“rotate”:

1
export MOZ_LOG="example_logger:3,timestamp,sync,rotate:10"

重定向日志到文件

日志输出可以通过环境变量传递其路径来重定向到文件。

默认情况下,日志输出到stderr。

1
export MOZ_LOG_FILE="log.txt"

上述转储和附加选项仅适用于记录到文件。

E10S注意

当content进程在沙盒中时,它不能写入stderr或任何文件。可能需要将首选项security.sandbox.content.level设置为0才能查看日志。

Linux远程桌面-x11vnc

发表于 2017-03-09 | 分类于 linux | | 阅读次数
字数统计 183 | 阅读时长 1

x11vnc类似QQ远程控制,显示的图像是经过硬件加速的。

x11vnc安装

1
$ sudo apt install x11vnc

x11vnc密码设置

1
$ x11vnc -storepasswd

x11vnc启动

1
x11vnc -rfbport 5903 -rfbauth ~/.vnc/passwd -display :0 -forever -bg -repeat -nowf -o ~/.vnc/x11vnc.log

其它参数:

1
2
3
--localhost 在本地地址监听
-autoport PORT 从PORT开始选择可用端口监听
-rfbport PORT 指定监听端口

验证是否启动成功

1
2
$ ps aux | grep x11vnc
odroid 1979 1.4 0.4 29960 8700 ? Ss 03:41 0:01 x11vnc -rfbauth /home/odroid/.vnc/passwd -display :0 -forever -bg -repeat -nowf -o /home/odroid/.vnc/x11vnc.log

设置自动登录

MATE桌面

修改/usr/share/lightdm/lightdm.conf.d/60-lightdm-gtk-greeter.conf

[SeatDefaults]
greeter-session=lightdm-gtk-greeter
autologin-user=sk

参考资料

  1. https://help.ubuntu.com/community/VNC/Servers
  2. http://www.karlrunge.com/x11vnc/
Denis

Denis

9 日志
5 分类
19 标签
RSS
GitHub
0%
© 2017 Denis