[评论]systemd @ Facebook — a year later[All Systems Go! 2017]

今天看了All Systems Go! 2017上,systemd @ Facebook — a year later这个talk。记录下自己的感想。

首先,FaceBook软件更新还是比较及时的,CentOS 7 和 systemd 都上了:

可怜我司还在Ubuntu 14.04上

另外,很多基础组件都是和上游有良好的沟通的:

长远来看,紧跟上游对公司的技术实力、技术影响力都有好处。当然,短期来看,我的系统工作的很好,不跟进上游没毛病啊。国内,大部分是后者吧……

当然,大公司都有自己的repo:

FaceBook居然都已经接入了Meson,很让我吃惊。

资源管理

资源管理用的是cgroup2:

结合systemd中的slice:

在docker的潮流之下,FaceBook还是比较 old school style 的。不过,slice确实很够解决这个问题,后面可以看看

服务监控

systemd也提供了监控接口,通过dbus暴露出来:

一个daemon收集数据,上报:

使用Cython包装的sd-bus来获取数据,IPython写原型:

案例学习

一般都是直接关闭rpm里面重启的脚本,FaceBook还做的一个工具。

journalctl可以看单机日志,不知道分布式日志有没有用journald。

嗯,一个小坑

systemd的这个转义很反人类,但是他们并没有打算改!

FaceBook的建议

文档要贴近用户,积极参与上游,技术分享!

评论

Reddit上,很多人都不喜欢systemd这种经常改API,不考虑兼容性的做法。

当然,也有人讨论视频、文字交流的效率……


实际上,之前有一个系统需要部署。

我考察过很多daemonize的方式,从最经典的init.d,到中间的supervisord、upstrat,再到激进的systemd。

综合来看,systemd比较灵活,而且支持user-service,非常适合初期服务的部署。

但是,由于我司用的是Ubuntu 14.04,故只能切回到supervisord。服务的reload,对于supervisord是一个大问题。

个人而言,还是很期待systemd普及的。

另外,关于最后的建议。国内公司倒是经常tech talks,但是很多时候,并不会将自己的东西放开,也不会推向上游,这对长期的技术发展很不利。

如何调试iptables [CentOS 7]

首先,来一张非常有用的图:

基础知识

iptables可以jump到TRACE目标,开启包追踪(packet tracing)选项。

iptables有一个raw表,基本上是iptables最先处理包的地方。

 开始调试

在最入口的位置给符合条件的包开启包追踪,比如对所有进入的icmp包:

iptables -t raw -I PREROUTING -p icmp -j TRACE

有的时候,还需要加载需要的模块(CentOS 7下):

modprobe nf_log_ipv4

然后你就可以在/var/log/messages看到包追踪信息了。

关闭

删除掉TRACE规则即可:

iptables -t raw -D PREROUTING 1

这是一次对机器ping产生的trace信息:

 可以看到,firewalld强行加戏:)

Oct 21 15:34:50 vultr kernel: TRACE: raw:PREROUTING:policy:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING_ZONES_SOURCE:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING_ZONES:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public_log:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public_deny:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public_allow:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PRE_public:return:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:PREROUTING:policy:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING_ZONES_SOURCE:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING_ZONES:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public_log:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public_deny:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public_allow:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PRE_public:return:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:rule:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:DOCKER:return:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:PREROUTING:policy:5 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:INPUT:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:INPUT_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: mangle:INPUT:policy:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT:rule:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT_ZONES_SOURCE:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT:rule:5 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:INPUT_ZONES:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public_log:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public_deny:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:3 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public_allow:return:13 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: filter:IN_public:rule:4 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: security:INPUT:rule:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: security:INPUT_direct:return:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: security:INPUT:policy:2 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0
Oct 21 15:34:50 vultr kernel: TRACE: nat:INPUT:policy:1 IN=eth0 OUT= MAC=56:00:00:7c:40:0c:fe:00:00:7c:40:0c:08:00 SRC=183.192.89.160 DST=1.2.53.225 LEN=84 TOS=0x04 PREC=0x00 TTL=43 ID=32583 PROTO=ICMP TYPE=8 CODE=0 ID=8765 SEQ=0

参考资料

Hello Wayland — Wayland教程

翻译自https://hdante.wordpress.com/2014/07/08/the-hello-wayland-tutorial/

TLDR

我写了一个wayland下的hello world,源码在 https://github.com/hdante/hello_wayland

介绍

从最终用户的角度,很容易理解wayland是什么:它是一个新的窗口系统,它将显示服务器和窗口管理器合并了[1]。从技术角度来看,wayland是为了摆脱传统,使用现代设计来实现一个高效的窗口系统,解决X窗口系统中长期存在的效率问题和一些极端情况[2]。这个教程展示如何实现一个作为wayland客户端的hello world程序、解释基础的wayland概念、创建一个GUI程序的必要流程。hello world程序不需要任何GUI工具包,它直接使用底层的wayland协议,以便解释wayland的基础概念。本教程是我自己研究wayland协议的结果。教程分为两部分。这是第一篇教程,解释所有的概念和程序的高级部分。

再问一次,什么是wayland?

wayland窗口系统的的完整设计分为好几层。如果你下载了wayland library的代码[3],或者你看了下wayland的API[4],你会注意到两层:

  1. 最基础的一层是进程间通讯功能的实现,以及一些实用工具。比如主循环调度器和一些数据类型。大部分这些代码都出现在wayland library中(所有在src文件夹[5]中的内容),并且和窗口系统无关。
  2. 第二层是窗口系统协议。它的描述在protocol/wayland.xml[6]文件中,这个文件应该算是一种接口定义语言。IDL文件可以用wayland-scanner 工具处理,并在 wayland-client-protocol.h 和 wayland-server-protocol.h中生成代理方法。协议定义了客户端程序和显示服务器的基础功能。比如访问输入设备、注册共享缓存以便显示在屏幕上。wayland library并不实现这些协议。这些协议的实现被分割到一个第三方层。服务端的参考实现是weston的一部分[7],它在客户端和服务端都定义了一些附加层。以实现wayland协议。在hello world程序中,我们并不需要了解任何关于weston的东西。我们仅仅需要IDL文件。

从上面关于wayland library的描述中,我们发现wayland的三个定义。它是一个(额听用途的)IPC库,不像D-Bus库,它仅仅用于显示服务器,并且它没有定义wayland到底是什么、wayland协议的定义以及如何找到协议定义。我相信即使人们在阅读官方文档后,大部分人也不明白wayland到底是什么。我想,这三个定义澄清了“什么是wayland”这个问题,每一个定义都能够用在不同的上下文中。

Hello, World!

介绍的部分已经够长了,我们来看看hello world的代码:

#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wayland-client.h>

#include "helpers.h"

static const unsigned WIDTH = 320;
static const unsigned HEIGHT = 200;
static const unsigned CURSOR_WIDTH = 100;
static const unsigned CURSOR_HEIGHT = 59;
static const int32_t CURSOR_HOT_SPOT_X = 10;
static const int32_t CURSOR_HOT_SPOT_Y = 35;

static bool done = false;

void on_button(uint32_t button)
{
    done = true;
}

int main(void)
{
    struct wl_buffer *buffer;
    struct wl_shm_pool *pool;
    struct wl_shell_surface *surface;
    int image;

    hello_setup_wayland();

    image = open("images.bin", O_RDWR);

    if (image < 0) {
        perror("Error opening surface image");
        return EXIT_FAILURE;
    }

    pool = hello_create_memory_pool(image);
    surface = hello_create_surface();
    buffer = hello_create_buffer(pool, WIDTH, HEIGHT);
    hello_bind_buffer(buffer, surface);
    hello_set_cursor_from_pool(pool, CURSOR_WIDTH,
        CURSOR_HEIGHT, CURSOR_HOT_SPOT_X, CURSOR_HOT_SPOT_Y);
    hello_set_button_callback(surface, on_button);

    while (!done) {
        if (wl_display_dispatch(display) < 0) {
            perror("Main loop error");
            done = true;
        }
    }

    fprintf(stderr, "Exiting sample wayland client...\n");

    hello_free_cursor();
    hello_free_buffer(buffer);
    hello_free_surface(surface);
    hello_free_memory_pool(pool);
    close(image);
    hello_cleanup_wayland();

    return EXIT_SUCCESS;
}

wayland协议太啰嗦,所以我将代码分成两部分,包含了协议细节的部分以及其他部分。主模块,如上所述,包含了高级层面的hello world实现。main函数代表与显示服务器进行通信以显示hello world窗口并接受指针设备输入所需的完整步骤,在单击时关闭应用程序。代码的相关部分的描述如下:

#include "helpers.h"

helper模块包含了hello_*函数和wayland中的一大堆全局对象。全局根对象是一个名字为display的对象,代表了一个与显示服务器的连接,并且被用于发送请求、接受事件。在运行主循环的时候,也会用到display对象。在本教程的下一部分中将详细介绍helper模块。

static const unsigned WIDTH = 320;
static const unsigned HEIGHT = 200;
static const unsigned CURSOR_WIDTH = 100;
static const unsigned CURSOR_HEIGHT = 59;
static const int32_t CURSOR_HOT_SPOT_X = 10;
static const int32_t CURSOR_HOT_SPOT_Y = 35;
在本教程中,我们将一个图像显示为主窗口,另一个用于显示光标。 它们的位置是硬编码的。 然而,在一般应用中,值将被动态计算。
void on_button(uint32_t button)
{
    done = true;
}

这是按钮回调。当一个按钮被点击的时候,我们设置done为true,可以让我们离开main中的主循环。

hello_setup_wayland();

程序开始就会调用setup函数,连接到显示服务器,从服务器请求一系列的全局对象,填充到代理变量中去。

image = open("images.bin", O_RDWR);

然后,我们打开图片文件。这个图片文件包含了硬编码的图片,已经是显示用的数据格式了:它是显示用的像素值。

pool = hello_create_memory_pool(image);

wayland的主要设计哲学就是处理图像的时候更加高效。wayland使用在客户端和服务器端共享内存的方式达到这个目标,所以数据不会在服务器和客户端之间复制。客户端和服务器端共享的基本元素就是共享内存池,就是一段内存被mmap到客户端和服务器端。在内存池中,一系列图片能够被当作缓存对象添加到其中,这些图片也都是共享的。

在hello world程序中,我们mmap了我们的硬编码图像文件。在一般程序中,会创建一个空的内存池,比如用shm_open()创建一个共享内存对象,然后用展示widget的图片,动态构造图片缓存。在写hello world程序的时候,我必须决定我应不应该创建空的内存池、分配内存,以及哪种方式更容易理解。或者我使用一个不太直观的例子来创建一个预建内存池。我决定用这个直观的例子,有一个重要的原因:如果你阅读整个hello world的源代码,你会发现在任何地方都没有内存复制操作。图像文件打开一次,并显示一次。不需要额外的副本。这样做是为了明确指出,如果认真实施,wayalnd应用程序可以实现效率最大化。

surface = hello_create_surface();

代表了可见元素的对象变成了平面。平面是一个长方形区域,有自己的位置和大小。平面被缓冲对象填充。在平面的生命周期中,将附加几个缓冲区作为平面内容,并且会求服务器重绘平面。在hello world例子中,平面对象的类型是wl_shell_surface,被用来创建顶级窗口。

buffer = hello_create_buffer(pool, WIDTH, HEIGHT);

缓冲对象里面有平面要显示的内容。缓冲在内存池中被创建(它们是内存池切片),以便它们由客户端和服务器共享。在我们的例子中,我们不会创建一个空的缓冲对象,我们用一个已经填充好数据的的缓冲对象。

hello_bind_buffer(buffer, surface);

为了使缓冲区显示到屏幕,我们需要将缓冲区数据绑定到平面,也就是将平面内容设置为缓冲区数据。绑定操作也会讲平面提交到服务器。在wayland中,有一个平面所有权的概念:要不就是客户端拥有平面:客户端可以在平面上绘制(服务端复制了一份旧的平面);要不就是服务端拥有平面:客户端无法修改平面,因为服务器正在屏幕上绘制它。为了将所有权转移到服务器,我们给服务器提交请求;为了将所有权返回给客户端,服务器发送一个release事件。在一般应用程序中,表面将来回转移所有权,但在hello应用程序中,只需执行一次即可,作为绑定操作的一部分。

hello_set_cursor_from_pool(pool, CURSOR_WIDTH,
    CURSOR_HEIGHT, CURSOR_HOT_SPOT_X, CURSOR_HOT_SPOT_Y);

设置了主窗口了之后,我们来配置鼠标指针。这是必须的步骤:客户端必须设置鼠标指针。在这儿,我们将鼠标指针设置为内存池中的内容。helper模块为鼠标指针创建一个平面和缓冲对象。我必须决定是将指针配置放在helper里面,还是将它放到外面的高层逻辑里面来。我决定用后者,来展示wayland中的角色划分:客户端能够控制绘制什么、如何绘制。

hello_set_button_callback(surface, on_button);

设置的最后一步是设置回调:将界面点击和on_callback回调关联起来。

while (!done) {
    if (wl_display_dispatch(display) < 0) {
        perror("Main loop error");
        done = true;
    }
}

这段代码调用主循环,将全局变量display作为参数。当done为true的时候,主循环退出——或者因为出错了,或者因为按钮被点击了。

hello_free_cursor();
hello_free_buffer(buffer);
hello_free_surface(surface);
hello_free_memory_pool(pool);
close(image);
hello_cleanup_wayland();

程序结束了,清理所有我们申请的资源。

总结

这是wayland的hello world教程的第一部分。主要讨论什么是wayland,如何描述操作,在一个高级的例子中。在下一部分,我将会详细介绍helper函数。

截图

原作者截图:

我的截图:

参考

[1] http://wayland.freedesktop.org/architecture.html
[2] http://mirror.linux.org.au/linux.conf.au/2013/ogv/The_real_story_behind_Wayland_and_X.ogv
[3] http://cgit.freedesktop.org/wayland/wayland/tree/
[4] http://wayland.freedesktop.org/docs/html/
[5] http://cgit.freedesktop.org/wayland/wayland/tree/src
[6] http://cgit.freedesktop.org/wayland/wayland/tree/protocol/wayland.xml
[7] http://cgit.freedesktop.org/wayland/weston/tree/

如何在Linux/Fedora下编译安装为知笔记

虽然为知收费了,但是目前只有为知笔记的Linux客户端做的不错,也只能用它了。

首先,安装编译期间的依赖:

# git拉代码,cmake编译
sudo dnf install -y git cmake
# 编译器
sudo dnf install -y gcc gcc-c++
# qt5相关的包
sudo dnf install -y qt5-qtbase-devel qt5-linguist qt5-qtwebengine-devel qt5-qtwebsockets-devel

sudo dnf install -y zlib-devel

拉代码(此处以v2.5.0分支为例):

git clone https://github.com/WizTeam/WizQTClient.git
cd /path/to/WizQTClient
git checkout v2.5.0

编译:

# 安装到~/.local/目录
cmake . -DCMAKE_INSTALL_PREFIX=$HOME/.local/
# -j 参数加速编译
make -j $(nproc)
# 安装
make install

安装完成后,为知笔记位于$HOME/.local/bin/WizNote,也可以在应用列表中找到。


  • 编译的时候cmake检测模块不通过,会提示如下错误:
CMake Error at cmake/QtChooser.cmake:1 (find_package):
  By not providing "FindQt5.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "Qt5", but
  CMake did not find one.
  Could not find a package configuration file provided by "Qt5" with any of
  the following names:
    Qt5Config.cmake
    qt5-config.cmake

你只需要通过dnf provides */Qt5Config.cmake 命令来找找哪个包提供了,比如qt5-qtbase-devel

  • make的时候会报:
lib/quazip/zip.h:56:18: fatal error: zlib.h: No such file or directory
 #include "zlib.h"

也是一样的处理方式,使用dnf provides */zlib.h 找到包,比如zlib-devel

  • 为什么使用 $HOME/.local/ 作为安装目录:

首先,/usr/local 作为安装目录,需要root权限才能安装,删除的时候也不好删除,因为/usr/local/ 下可能也有别的软件。

其次,/opt/wiznote 作为安装目录,也需要root权限,而且也会导致wiznote.desktop中申明的图标名称找不到。

$HOME/.local/ 作为安装目录,生成的wiznote.desktop开箱即用,也不需要root权限,比较方便。

[译]输入系统如何工作——键盘输入

本文是 How input works – Keyboard input 的翻译。

上一篇博文中,我[作者]解释了KWin中,输入设备是如何打开和处理的。在这篇博文中,我们将仔细关注下键盘设备和键盘事件。

键盘并不总是键盘

在Linux中,键盘是一个非常奇特的设备。你不会只有一个键盘,你肯定会有很多个。很多设备宣称自己是键盘,但是只支持一个键。比如,电源按钮或者带静音按钮、音量加减按钮的外置耳机。从输入系统的角度来看,这些设备也是键盘。

对于KWin,发现真正支持的键盘是很重要的。如果没有一个“真的”键盘连接(或者说开启),我们的虚拟键盘应该自动激活。比如,如果你从平板PC二合一设备上拔了键盘,它应该转换为平板模式,即有虚拟键盘的模式。如果连接上了键盘,虚拟键盘就不是主要的输入设备了。libinput提供了函数来检测键盘支持哪些键。我们使用这个功能来区分不同的键盘类型。

键盘事件

在这里,键盘是最简单的输入设备。libinput仅仅触发一个LIBINPUT_EVENT_KEYBOARD_KEY事件,并包含了被按下或者释放的键。KWin有一个专用线程,用来从libinput读取事件。所以这些事件现在仅仅是放在处理队列中,主线程同时也得到新事件通知。一旦主线程处理事件,事件就被转换成我们的输入重定向类。不管事件是通过哪种源头抵达,所有输入事件都会通过输入重定向。KWin不仅仅支持来自libinput的事件,并且支持嵌套会话(即KWin运行在X11之上,或者在另一个Wayland服务器之上),还有在集成测试中使用的模拟事件。这意味着一旦事件到达输入重定向,我们基本上就丢失了事件来自哪个设备这些信息。最近,我们扩展了内部API,可以在事件处理函数中包含设备源,这是可选的。可以在调试终端中使用,来显示哪些设备产生了哪些事件。

xkbcommon

现在,一个键的按下、释放事件到达了中心分发方法KeyboardInputRedirection::processKey。第一件也是重要的一件事就是在xbkcommon中更新键盘状态。xkbcommon被用将物理键为事件,转换过程根据键盘布局、键盘状态(比如激活的修饰符)来进行。比如:如果我按下了“y”键(键码是21)并且“Shift”键是按下的状态,在德语键盘布局下,就会创建“Z”按下的事件;但是在英语键盘布局下,它会是“Y”。

在KWin中,我们将所有需要的xkbcommon功能都封装到Xkb类中。这个类追踪活跃的键盘布局、执行键盘布局切换(当布局变换的时候展示OSD)。它知道上一次按键的符号,当前活跃的修饰符和快捷键相关的修饰符。

当xkb的状态更新的时候,我们也检查很多键盘状态变更。用户是不是激活了数字锁定?如果是这样,我们需要让键盘上的LED做对应的变化,所以我们的libinput代码能够更新物理键盘上LED的状态。修饰副是不是变掉了?如果是这样,我们要将新的修饰符集合通知给Wayland窗口。尽管实际上从key到符号的翻译发生在Wayland客户端,但刚刚讲的翻译过程也在Wayland服务端进行了。为什么KWin也要做这一层翻译呢?这是因为KWin也需要在很多地方使用键盘符号,比如在当前窗口中过滤按键,来实现全局快捷键。

调试终端中的键盘状态

Xkb状态更新功能也能够处理哪些只需要修饰符键就能触发的快捷键。实际上,在这儿处理是不对的,但我们的输入过滤代码没法保证能捕获到所有的输入事件。对于快捷键中的修饰符,看多所有的事件是很基础的需求。所以,唯一的处理地方只能在Xkb中了。这不是最优雅的解决方案,但是能工作。这个功能也被X11使用,我在之前的博文中讲过。

KWin的过滤

现在KWin有足够的信息来处理键盘事件。对于此,它创建自定义的QKeyEvent并将其送到输入过滤链。KWin的输入处理使用了输入过滤器组成的链。每个过滤器都能够对事件做一系列操作,并决定这个事件是不是应该被继续处理,或结束处理。

比如,我们有锁屏过滤器。如果屏幕是锁屏状态,这个过滤器就会过滤事件处理,确保事件只会被送到屏幕锁定程序,不会被其他窗口处理。或者这儿有一个过滤器,确保ctrl+alt+f1在锁屏情况下仍然能够工作。另一个过滤器用来处理全局快捷键,一个将事件传递给特效系统,比如窗口轮转。

在调试终端中,一个输入事件过滤器显示被按下的键

最后一个过滤器是我们的转发过滤器。这个过滤器的任务是转发事件到窗口。它将事件从当前活动的Wayland平面,传递到KWayland::Server。

获得键盘输入焦点的Wayland表面

Wayland服务器需要获取了键盘焦点的平面。在获取了键盘焦点的情况下,KWin相比而言微不足道。KWin有一个叫活动窗口的概念。在转发事件之前,KWin会检查哪个是键盘焦点窗口。如果有一个活动窗口,则该窗口平面被标记为KWayland::Server中的键盘焦点平面。

我们的KWayland::Server库关注发送丢失键盘焦点和获取键盘焦点事件到窗口,所以KWin不需要关注这些。这是我们有一个KWayland::Server抽象层的优势——和混成器无关的一切都在库中被处理了。

Wayland中键盘事件的处理

之前提到的转发过滤器更新键盘平面,并且将当前键盘事件发送到Wayland客户端。对于所有在keysymbol中的处理,都不需要了。key code已经送到了客户端。

客户端从回调中得到键盘事件,现在也将其送到xkbcommon。在Wayland中,keymap从服务端送到客户端,以便服务端和客户端有同样的keymap。客户端现在将key code转换成key symbol,和之前KWin做的一样。

接下来的事件处理在客户端完成。比如在QT中,这一步会产生QKeyEvent,然后送到当前输入焦点的widget。

重复输入键

键盘输入也有一些特殊的模式:重复键。当一些键被按下,产生重复的键输入。KWin使用来自键盘模块的配置来决定什么时候、什么频率来产生重复的按键。一个重复的键不会被转发到KWayland客户端。而是KWin通过Wayland键盘协议告诉键重复的设定,而不是被客户端直接处理。

不幸的是,QT中这个是不能工作的,使用的是硬编码。所以当前在Plasma Wayland的会话中,根据使用的GUI库不同,对重复键的支持很破碎。如果运行在Wayland上的话,KWin支持是正常的,X11程序是对的,GTK程序是对的,QT程序是错的。


如果你想支持我们[作者]的工作,请考虑捐助

[译]输入系统如何工作——创建设备

本文是How input works – creating a Device的翻译。

最近,我[作者]在KWin/Wayland输入栈上做了一些工作,比如实现了鼠标手势和鼠标限制协议(pointer constraints protocol),并且想写一些系列博客来描述输入事件是如何从设备传递到应用的。在第一篇博文中,我将关注于创建和配置输入设备,以及与此相关的其他事情。

evdev

Linux内核通过evdev API来产生输入事件。如果你对evdev如何工作感兴趣,我推荐你读一读Peter Hutterer写的优秀的博文。对于我们关注的层面来说,输入事件API太底层了,我们仅仅想用对它的一个抽象。

libinput 和设备文件

对输入事件API的抽象叫做libinput。它允许我们得到通知,不论何时添加、删除输入设备、不论何时产生输入事件。但是别急,我们首先需要打开输入设备,这是我们的第一个挑战。

正常情况下,设备文件不能被用户读取。这有好的一面,否则每个程序都能够读取所有的键盘事件。这种情况下,键盘记录器太容易实现了。

但是如果KWin以普通用户的方式运行,普通用户又无法读取设备文件,那KWin如何获取输入?为了让KWin获取输入,我们需要特殊的支持。libinput就是为这种情况准备的,它自己不会尝试打开文件,它会使用用户提供的open_restricted函数。KWin就是这么干的,并将打开文件的任务交给了logind。logind允许一个进程控制当前登录会话。并且这个登录会话的控制进程可以打开一些设备文件。所以KWin和logind的dbus API交互,成为登录会话的控制进程,通过logind的API打开设备文件,并将其传递回libinput。

这也是为什么对于整个Wayland会话,KWin在运行时依赖logind的DBus接口。请注意,这并不意味着你需要使用logind或者sysemd。这仅仅意味着需要一个进程来同logind的DBus接口通讯。

KWin中的设备

现在libinput已经准备好打开设备文件,并且为每个设备触发了LIBINPUT_EVENT_DEVICE_ADDED事件。KWin为每一种设备类型创建了一个小的门面类,来应用设备的配置。KWin支持读取Plasma的鼠标配置模块中的配置选项。并且KWin打开了LED灯,如果设备支持LED灯的话,比如数字锁定灯和大写锁定灯,这也是设备配置过程的一部分。

所有的输入设备都被KWin创建,并且可以在调试终端显示(打开KRunner,输入“KWin”)。KWin从libinput中读取了很多关于设备的信息,并展示在调试终端中。在输入事件选项卡中,每种事件都包含了设备产生该事件的一些信息。

所有的设备也通过DBus暴露出来,和KWin的调试终端信息一样。这意味着可以通过DBus来动态修改配置。不过,KWin在成功应用配置后,会将其保存,以确保你在重启系统、或者重新插拔外部设备后,你的设置能够被恢复。这是一个很重要的特性,用来支持触摸板配置模块。

如果你想支持作者的工作,考虑捐赠给作者,让世界变得更美好!——KDE 2016年年终募款

[译]什么是HiDPI?以及它为什么这么重要。

本文是What is HiDPI的翻译。

我是一名web开发者和用户体验设计师,使用System 76电脑、Ubuntu系统;也是elementary OS的联合创始人。elementary OS是一个开源的桌面操作系统。我和桌面、web、硬件开发者一起工作,来实现HiDPI支持。我注意到很多人很难理解HiDPI,因为对此没有一个全面的介绍。这是我尝试介绍HiDPI和去除一些常见误解的博文。

HiDPI显示器在计算机上变得越来越流行:苹果最新的MacBook、MacBook Pro和iMac;微软的Surface,Surface Book,和新的Surface Studio;戴尔,联想,惠普和其他厂商将HiDPI作为笔记本电脑的可选配置;LG,戴尔和飞利浦等的HiDPI桌面显示器;和System76(我的雇主)刚刚发布的旗舰Oryx Pro和Bonobo WS笔记本电脑也支持了HiDPI。

由于价格,图形性能需求和功耗的增加,HiDPI不是默认配置,但我们肯定会朝这个方向发展的。那么,HiDPI解决了什么问题呢?

像素数翻倍

HiDPI的核心是像素数量加倍:每个维度中的物理像素数量是所需的虚拟像素数的两倍。

例如,图标或图像的高度是64虚拟像素,但是在HiDPI显示器上,它用128个物理像素绘制(总共是4倍的像素——在每个方向上是原来的两倍)。这使得图标在任何角度都变得两倍清晰,或者说容纳了了两倍的细节。

普通显示和HiDPI。4倍的像素数量。HiDPI允许更精细的形状和更好的抗锯齿。

对于用户界面,这意味着它们比原来的一大堆像素集更加清晰、完美。对于照片,HiDPI使它们看起来更像一张打印的照片,而不仅仅是数字图像。对于文本,HiDPi使它看起来更像一个实体的杂志,而不仅仅是电脑屏幕。对于视频,HiDPI允许更多的细节和沉浸体验:屏幕渐渐消失,成为电影故事的一个窗口。

半个像素是不存在的

那为什么是像素数加倍,而不仅仅是增加15寸显示器的像素密度呢?比如,1080p提升到2880×1620?为了让用户界面的元素具有同样的物理尺寸,在1080p的显示器上,你必须把显示内容缩放到1.5倍。这意味着一个虚拟像素点需要1.5个物理像素点来绘制。

半个像素是不存在的,所以软件必须抵消锯齿。锯齿=模糊。所以随着高分辨率的显示器,你会得到一个模糊的UI。和1倍的显示比率相比,在1.5倍的显示比率下,字体有对应的处理机制,所以显示比率不是一个大问题,但是UI元素,比如图标和按钮周围的描边看起来会非常糟糕。

1像素点的1,2,1.5倍缩放。在1倍和2倍缩放下,点看起来是一样的。在1.5倍缩放下,软件必须处理锯齿,创建一个模糊的边界。

有人认为像素足够小,以至于锯齿/模糊不是很明显。对于某些大小/分辨率的组合来说,这是可能的。但如果是这种情况,在同样的物理尺寸下,1.5倍并不比1倍好,你只是通过处理更多的像素来浪费性能和电量。

* 有些人(比如Linus Torvalds)认为这是“疯狂的”,4倍的分辨率就是4倍的价格,却只带来了微小,不可读的字体和图标。软件开发者有自己的意见,但对于大多数消费者来说,这会使设备完全无法使用。仅仅有少数的技术人员可以通过手动设置缩放因子来解决这个问题。

4K?Quad HD?5K?UHD?Retina?HiDPI?

HiDPI是一个好主意,但很难和客户解释清楚(一些制造商甚至没有从中获得好处,见下一节)。因此,工业上已经提出了几个不同的流行语,努力向客户解释这些,这些解释获得了不同程度的成功。

Retina

可以说,最有效的解释来自苹果和他们的Retina品牌。他们将Retina描述为,在正常观看距离处,该像素密度使人类视网膜不能辨别出来单个像素。这是对的,但也有点市场营销的意思。 对于苹果,Retina似乎只是意味着像素倍增。

当他们发布第一个Retina显示器,它有4倍像素(每个维度2倍)。这避免了旧软件在新显示器上的模糊(它们在新显示器上看起来和原来相同,不会更好但也不会更糟),并且这使得设计者/开发者更加容易设计新的图形界面。

苹果的每个Retina品牌的显示器都遵循这个约定;它们都只是旧显示器的像素翻倍,并在软件中设置为2倍缩放。

4K,Quad HD,5K和UHD

从技术来讲,4K、Quad HD、5K和UHD与像素密度和HiDPI完全没有关系。然而,由于它们是用于销售投影机和电视的术语,计算机制造商喜欢使用这些术语来对它们的普通显示器进行加售。

不要过多关注这些营销术语;相反,看看分辨率在显示器上是否有意义。如果分辨率将每个维度缩放到两倍,那就足够高。如果这个分辨率没法达到这个比率,那就没有意义,不要买。因为制造商做出了一个糟糕的决定:设备需要一半或分数个像素才可以开箱即用。

一般来说:Quad HD或者1440p(2560×1440)在两倍缩放下和720p是一样的大小;4K(3840×2160)在两倍缩放下和1080p是一样的;5K(5120×2880)在两倍缩放下和1440p是一样的;UHD没法通过这种像素除以2来对应到现有的尺寸。和物理尺寸一起来看的话,14到24寸显示器的分辨率一般是1080p,所以4K是同样尺寸下的HiDPI版本。24到30寸的显示器的分辨率是1440p,所以5K是其HiDPI版本。在更小的显示器上(11到13寸),1600×900是原生的分辨率,所以HiDPI版本是3200×1800(联想和戴尔的13寸HiDPI显示器就是这个分辨率)。

HiDPI

在桌面Linux世界(在Windows世界也是如此),HiDPI表示像素倍增或者“Retina”,HiDPI是一个厂商无关的术语。很多设备生产商避免使用这些术语(System76使用这些术语!),但是软件开发者倾向于了解并使用它。如果使用得当的话,我希望HiDPI成为一个工业标准。

一些厂商在分辨率上作出了错误的选择

分辨率不是越高越好。事实上,一些厂商为了能够从低分辨率的显示器上拿到加售,他们采用了4K的变种,而没有考虑到物理尺寸和像素倍增怎么工作。

在15寸的显示器上,1080p(1920×1080)在1倍缩放的情况下,显示效果不错,也没有字体或者图标变得太小的情况。1080p经过像素倍增后是4K(3840×2160),所以4K在这个物理尺寸上的显示也不错:所有的元素经过缩放,都和原来一样大。但是,在更小的显示器上,比如12寸或者13寸,1080p在1倍缩放下就太密了(这意味着UI看起来太小了)。所以4K在12寸或者13寸的物理尺寸上比3200×1800(1600×900像素倍增后的结果)差。

有人问elementary OS是否会支持非整数的缩放因子,来帮助那些买了不好的大小/分辨率组合的显示器的用户。但是因为半个像素是不存在的,我们没法设置非整数的缩放。设备生产商作出了错误的决策、销售了差的产品。对此,我们做不了太多。

总结

  • HiDPI 就是每个维度上的像素增加了一倍。
  • 半个像素是不存在的
  • 4K 不一定是 HiDPI,并且它不一定是最好的分辨率
  • 一些生产商在分辨率上作出了错误的选择

如果这篇文章帮到了你,或者你有自己的想法,请给我发twitter或者在这儿评论。

Linux 3.4中acpi_pad的一个Bug分析

今天,同事被Bug #42981坑了,看了同事发的文章,觉得有必要分析下这个bug。这篇博客主要讲acpi_pad是如何工作的。


模块注册

module加载的时候首先会执行init函数,acpi_pad注册的init函数acpi_pad_init。acpi_pad_init最终调用driver_register来将acpi_pad_driver.drv 注册到系统中。

acpi_pad_driver的定义如下:

static struct acpi_driver acpi_pad_driver = {
        .name = "processor_aggregator",
        .class = ACPI_PROCESSOR_AGGREGATOR_CLASS,
        .ids = pad_device_ids,
        .ops = {
                .add = acpi_pad_add,
                .remove = acpi_pad_remove,
        },
};

没有.drv 字段?看下struct acpi_driver定义

struct acpi_driver {
        char name[80];
        char class[80];
        const struct acpi_device_id *ids; /* Supported Hardware IDs */
        unsigned int flags;
        struct acpi_device_ops ops;
        struct device_driver drv;
        struct module *owner;
};

这边需要注意的是,acpi_driver里面直接嵌套了一个device_driver结构体,而不是用指针引用,这一点很重要。

但是,acpi_pad_driver.drv没有初始化!后来找了找,发现了初始化的代码(在acpi_bus_register_driver中):

driver->drv.name = driver->name;
driver->drv.bus = &acpi_bus_type;
driver->drv.owner = driver->owner;

这个时候,driver是指向acpi_pad_driver的指针。

acpi_bus_type的定义如下:

struct bus_type acpi_bus_type = {
        .name           = "acpi",
        .suspend        = acpi_device_suspend,
        .resume         = acpi_device_resume,
        .match          = acpi_bus_match,
        .probe          = acpi_device_probe,
        .remove         = acpi_device_remove,
        .uevent         = acpi_device_uevent,
};

注册了driver之后,我们就应该关注acpi_device_probe函数了,这个函数真正在sysfs中创建了idlecpus文件(这个文件是用户控制acpi_pad特性的入口)。

static int acpi_device_probe(struct device * dev) 函数是被内核调用的,相当于回调:

static int acpi_device_probe(struct device * dev)
{
        struct acpi_device *acpi_dev = to_acpi_device(dev);
        struct acpi_driver *acpi_drv = to_acpi_driver(dev->driver);
        int ret;

        ret = acpi_bus_driver_init(acpi_dev, acpi_drv);
        //。。。。。。
        return ret;
}

to_acpi_driver就是container_of宏,可以将struct acpi_driver的drv的地址,转化微acpi_driver的地址(就是根据子结构体地址,获取父级结构体地址):

#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

acpi_device_probe函数最终在acpi_bus_driver_init中调用acpi_pad_driver.ops.add 函数,即acpi_pad_add函数。最终使用在acpi_pad_add_sysfs中将idlecpus绑定到了sysfs:

static int acpi_pad_add_sysfs(struct acpi_device *device)
{
        int result;
        result = device_create_file(&device->dev, &dev_attr_idlecpus);
        //。。。。。。
        return 0;
}

dev_attr_idlecpus的定义:

static DEVICE_ATTR(idlecpus, S_IRUGO|S_IWUSR,
        acpi_pad_idlecpus_show,
        acpi_pad_idlecpus_store);

被展开为结构体变量定义struct device_attribute dev_attr_idlecpus

该文件的读写函数分别是acpi_pad_idlecpus_show和acpi_pad_idlecpus_store。


至此,acpi_pad模块加载完成,idlecpus文件也在sysfs中加载完成了。

通过acpi_pad修改cpu状态

根据bug重现说明

to make the failure more likely:

# echo 1 > rrtime
# echo 31 > idlecpus; echo 0 > idlecpus
# echo 31 > idlecpus; echo 0 > idlecpus
# echo 31 > idlecpus; echo 0 > idlecpus
(it usually takes only a few attempts)

etc. until the echo does not return

我们通过idlecpus节点,先空置31个cpu,再激活,多试几次就可以重现该bug了。

在此过程中,调用了acpi_pad_idlecpus_store函数:

static ssize_t acpi_pad_idlecpus_store(struct device *dev,
        struct device_attribute *attr, const char *buf, size_t count)
{
        unsigned long num;
        if (strict_strtoul(buf, 0, &num))
                return -EINVAL;
        mutex_lock(&isolated_cpus_lock);
        acpi_pad_idle_cpus(num);
        mutex_unlock(&isolated_cpus_lock);
        return count;
}

将用户输入的buf转化为long,获取isolated_cpus_lock锁(这个就导致了前面提到的bug)。然后通过acpi_pad_idle_cpus将用户需要的cpu数置空:

static void acpi_pad_idle_cpus(unsigned int num_cpus)
{
        // 对cpu想关数据加锁
        get_online_cpus();

        // 当前在线cpu,将要置空的cpu 这两个数字,选一个小的
        num_cpus = min_t(unsigned int, num_cpus, num_online_cpus());
        // 将置空的cpu数目同步到num_cpus个
        set_power_saving_task_num(num_cpus);
        // 对cpu相关数据解锁
        put_online_cpus();
}

set_power_saving_task_num的逻辑很简单,根据当前的power_saving_thread线程数量,少了就通过create_power_saving_task补足,多了就通过destroy_power_saving_task去掉一些。


destory_power_saving_task调用kthread_stop来结束多余的power_saving_thread线程。kthread_stop设置对应kthread的should_stop为1,然后等待该kthread结束:

kthread->should_stop = 1;
wake_up_process(k);
wait_for_completion(&kthread->exited);

但是它在等待kthread结束的时候,还拿着isolated_cpus_lock这个锁呢!!


我们来看下power_saving_thread到底干了啥,导致了bug。

static int power_saving_thread(void *data)
{
        //。。。。。。

        while (!kthread_should_stop()) {
                int cpu;
                u64 expire_time;

                try_to_freeze();

                /* round robin to cpus */
                if (last_jiffies + round_robin_time * HZ < jiffies) {
                        last_jiffies = jiffies;
                        round_robin_cpu(tsk_index);
                }
                //。。。。。。
        }
        //。。。。。。
}

看起来,没有问题,我们来看下round_robin_cpu的代码:

static void round_robin_cpu(unsigned int tsk_index)
{
        //。。。。。。
        mutex_lock(&isolated_cpus_lock);
        cpumask_clear(tmp);
        // 。。。。。
        mutex_unlock(&isolated_cpus_lock);

        set_cpus_allowed_ptr(current, cpumask_of(preferred_cpu));
}

代码中有对isolated_cpus_lock加锁的操作,这导致了这个bug。


Bug是如何出现的

一边,acpi_pad_idlecpus_store函数拿到ioslated_cpus_lock锁,调用kthread_stop等待power_saving_thread结束。

另一边,要结束的这个kthread/power_saving_thread,在round_robin_cpu函数中,等待isolated_cpu_lock锁。

两个kthread互相等待,成了死锁。

Continue reading Linux 3.4中acpi_pad的一个Bug分析

php-fpm的reload过程

背景

谈谈PHP的Reload操作中提到reload会让sleep提前结束,所以就探究了下fpm的reload操作如何实现。

本文在php7.0 fpm下分析,process_control_timeout设置不为0。


重启信号

首先,我们从PHP源码可以知道,fpm的reload操作实际上就是对fpm进程发送了USR2信号。

fpm的master进程中,fpm_signals_init_main函数通过sigaction注册了信号处理函数sig_handler

int fpm_signals_init_main() /* {{{ */
{
	struct sigaction act;

	// 。。。。。。

	memset(&act, 0, sizeof(act));
	act.sa_handler = sig_handler;
	sigfillset(&act.sa_mask);

	if (0 > sigaction(SIGTERM,  &act, 0) ||
	    0 > sigaction(SIGINT,   &act, 0) ||
	    0 > sigaction(SIGUSR1,  &act, 0) ||
	    0 > sigaction(SIGUSR2,  &act, 0) ||
	    0 > sigaction(SIGCHLD,  &act, 0) ||
	    0 > sigaction(SIGQUIT,  &act, 0)) {

		zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");
		return -1;
	}
	return 0;
}
/* }}} */

简而言之,通过sigfillset设置为block掉所有的信号,然后通过sigaction设置对应的信号处理函数。


当我们reload fpm时,systemctl向fpm的master进程发送USR2信号,执行函数sig_handler函数

static void sig_handler(int signo) /* {{{ */
{
	static const char sig_chars[NSIG + 1] = {
		[SIGTERM] = 'T',
		[SIGINT]  = 'I',
		[SIGUSR1] = '1',
		[SIGUSR2] = '2',
		[SIGQUIT] = 'Q',
		[SIGCHLD] = 'C'
	};
	char s;
        // ***
	s = sig_chars[signo];
	zend_quiet_write(sp[1], &s, sizeof(s));
	errno = saved_errno;
}
/* }}} */

关键点在zend_quiet_write,它就是write函数。sig_handler函数就是向sp[1]中写入了一个字符串2。

此处需要注意的是,sp[0]和sp[1]是通过socketpair创建的本地套接字。


master开始重启

之前的信号处理函数,在信号发生的时候会被调用,但是程序的主逻辑仍然不会被打乱,那fpm master进程怎么知道要reload呢?

答案就在fpm_event_loop中,这是master进程的事件循环。

在循环之前,我们需要用sp[0]创建一个struct fpm_event_s,添加到监听的fd中:

int fpm_event_set(struct fpm_event_s *ev, int fd, int flags, void (*callback)(struct fpm_event_s *, short, void *), void *arg) /* {{{ */
{
	if (!ev || !callback || fd < -1) {
		return -1;
	}
	memset(ev, 0, sizeof(struct fpm_event_s));
	ev->fd = fd;
	ev->callback = callback;
	ev->arg = arg;
	ev->flags = flags;
	return 0;
}
/* }}} */

然后将这个struct fpm_event_s,也就是代码中的ev,添加到监听的fd中。

实际上,这个添加过程也和fpm不同的异步模型有关(都是由对应fpm_event_module_s的add方法实现的),比如epoll的实现就是将ev参数整体放到epoll_event的data.ptr中的。(poll的add可以参考源码

当所有的fd都添加了之后(当然不仅仅是signal相关的fd咯),我们就可以使用wait方法等待事件来临了。(epoll和poll也都各自实现了wait方法)

好,回到sig_handler给sp[1]写了个字符串2。wait方法接到了信号,拿到对应的ev,调用 fpm_event_fire ,实际上就是调用了ev的callback方法,就是fpm_got_signal方法

static void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
{
	char c;
	int res, ret;
	int fd = ev->fd;

	do {
		res = read(fd, &c, 1);

		switch (c) {
			// 。。。。。。
			case '2' :                  /* SIGUSR2 */
				zlog(ZLOG_DEBUG, "received SIGUSR2");
				zlog(ZLOG_NOTICE, "Reloading in progress ...");
				fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);
				break;
		}

		if (fpm_globals.is_child) {
			break;
		}
	} while (1);
	return;
}
/* }}} */

如果接收到了字符串2,则执行

fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET)

实际上就这么几行

void fpm_pctl(int new_state, int action) /* {{{ */
{
	switch (action) {
		case FPM_PCTL_ACTION_SET :
			//。。。。。。
			fpm_signal_sent = 0;
			fpm_state = new_state;

			zlog(ZLOG_DEBUG, "switching to '%s' state", fpm_state_names[fpm_state]);
			/* fall down */

		case FPM_PCTL_ACTION_TIMEOUT :
			fpm_pctl_action_next();
			break;
		//。。。。。
	}
}
/* }}} */

即,将fpm_state设置为FPM_PCTL_STATE_RELOADING后,没有break,继续执行fpm_pctl_action_next

static void fpm_pctl_action_next() /* {{{ */
{
	int sig, timeout;

	if (!fpm_globals.running_children) {
		fpm_pctl_action_last();
	}

	if (fpm_signal_sent == 0) {
		if (fpm_state == FPM_PCTL_STATE_TERMINATING) {
			sig = SIGTERM;
		} else {
			sig = SIGQUIT;
		}
		timeout = fpm_global_config.process_control_timeout;
	} else {
		if (fpm_signal_sent == SIGQUIT) {
			sig = SIGTERM;
		} else {
			sig = SIGKILL;
		}
		timeout = 1;
	}

	fpm_pctl_kill_all(sig);
	fpm_signal_sent = sig;
	fpm_pctl_timeout_set(timeout);
}
/* }}} */

即,给所有子进程发送SIGQUIT信号。

这边还有一个fpm_pctl_timeout_set,这个等会讨论。


子进程处理信号

父进程发送完信号了,就该子进程处理啦。

子进程只有SIGQUIT交给sig_soft_quit处理。子进程初始化完成后,收到了SIGQUIT信号,由sig_soft_quit处理,最终调用fcgi_terminate处理:

void fcgi_terminate(void)
{
	in_shutdown = 1;
}

就是将in_shutdown设置为1。


子进程退出

子进程的循环主体在fcgi_accept_request中,其中多出判断in_shutdown,若为1则直接退出:


超时处理

前面提到的超时处理的回调函数fpm_pctl_timeout_set。执行了如下操作:

fpm_pctl(FPM_PCTL_STATE_UNSPECIFIED, FPM_PCTL_ACTION_TIMEOUT);

在这种条件下,发送的信号变成了SIGTERM,直接退出了子进程。


为何sleep会被打断?

我们可以看到,sleep的实现就是系统调用sleep(php_sleep是sleep的一个宏):

/* {{{ proto void sleep(int seconds)
   Delay for a given number of seconds */
PHP_FUNCTION(sleep)
{
	zend_long num;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &num) == FAILURE) {
		RETURN_FALSE;
	}
	php_sleep((unsigned int)num);

}
/* }}} */

sleep函数执行时,此时进程的状态是S:

interruptible sleep

此时一旦有信号触发,立马处理信号,比如我们刚刚说过的SIGQUIT,结束了之后发现,sleep执行完了。

因为sleep的说明写了啊:

sleep() makes the calling thread sleep until seconds seconds have
       elapsed or a signal arrives which is not ignored.

需要注意的是,php的sleep没有return系统调用sleep的返回值,所以即使信号打断了sleep,也仅仅是跳过sleep继续执行而已。

composer的自动加载机制解读

按照composer文档的说法,如果是composer项目,只需要在开始的时候require 'vendor/autoload.php'即可享受类的自动加载特性。可是这是如何实现的呢?

vendor/autoload.php

以Laravel 5.1项目为例,vendor/autoload.php文件只做了两件事情:

  1. include vendor/composer/autoload_real.php
  2. 调用ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()

vendor/composer/autoload_real.php仅仅定义了ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b类和composerRequireb6d254015e39cf5090fb84fdb1ed664b函数。(b6d254015e39cf5090fb84fdb1ed664b应该是类似id一样的东西,确保每次不同)

接下来我们关注下ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()做了哪些事情。

ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b::getLoader()

首先,这个类的loader只会初始化一次,第二次是直接返回已经存在的loader了:

if (null !== self::$loader) {
    return self::$loader;
}

如果是第一次调用,先注册['ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b', 'loadClassLoader'],然后new一个\Composer\Autoload\ClassLoader 作为loader,然后立马取消注册loadClassLoader

接下来就一步一步处理各种autoload了。

autoload_namespaces.php

直接从vendor/composer/autoload_namespaces.php中返回的就是$namespace$path的一个数组。通过\Composer\Autoload\ClassLoader::set方法来记录这些信息:

if (!$prefix) {
    // 如果namespace/prefix为空,则在其他PSR-0规则中查不到,才会到这个规则中
    $this->fallbackDirsPsr0 = (array) $paths;
} else {
    // 以后根据对应的namespace/prefix查找类的时候,第一个字母用来加速
    // 使用第一个字母作为一级key,可以减少二级key的冲突
    $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}

autoload_psr4.php

直接从该文件的返回值是$namespace$path的数组。通过\Composer\Autoload\ClassLoader::setPsr4来记录此信息:

if (!$prefix) {
    // 如果namespace/prefix为空,则其他PSR-4规则查找不到的时候,会到此规则中查找
    $this->fallbackDirsPsr4 = (array) $paths;
} else {
    // TODO
    $length = strlen($prefix);
    // 略去错误检查

    // TODO
    $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
    // 用来根据类前缀找path
    $this->prefixDirsPsr4[$prefix] = (array) $paths;
}

autoload_classmap.php

直接从该文件的返回值是$classname$path的数组。通过\Composer\Autoload\ClassLoader::addClassMap来记录此信息:

if ($this->classMap) {
    $this->classMap = array_merge($this->classMap, $classMap);
} else {
    $this->classMap = $classMap;
}

这些都处理完之后,\Composer\Autoload\ClassLoader::register[$this, 'loadClass']注册为autoload函数(通过spl_autoload_register)。

autoload_files.php

直接从该文件的返回值是$fileIdentifier到文件路径的映射,通过之前定义的composerRequireb6d254015e39cf5090fb84fdb1ed664b函数来require每个文件。

这个函数在require文件的时候同时也设置了$GLOBALS['__composer_autoload_files'][$fileIdentifier]的值为true。

\Composer\Autoload\ClassLoader::loadClass

这个函数的作用是装载一个类。

if ($file = $this->findFile($class)) {
    includeFile($file);

    return true;
}

\Composer\Autoload\ClassLoader::findFile

这个函数是composer装载类的重点。

  1. 首先是从classMap里面找$class对应的文件,如果有,直接返回。
  2. 然后从prefixDirsPsr4找到前缀符合的文件,如果找到,直接返回。(那个prefixLengthsPsr4就是用来判断需要从$class去掉的前缀长度)
  3. 接下来,直接从fallbackDirsPsr4对应的目录中查找文件。
  4. PSR-0加载
    • namespaced class name变换规则:\A_namespace\a_class=>/A_namespace/a/class.php 
    • PEAR-like class name变换规则:\A_namespace\a_class=>/A/namespace/a/class.php 

    prefixesPsr0中查找(和prefixDirsPsr4类似),直接找到对应的文件,返回。

  5. 如果没有,直接从fallbackDirsPsr0中尝试加载。