如何在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中尝试加载。

为何不可使用Redis的KEYS命令

在Redis KEYS命令的文档中,有如下一句话:

consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases.

简而言之,KEYS命令性能很差,尽量不要在生产环境中使用。

不过官方文档也提到了,可以使用SCAN命令

那么,KEYS命令为什么性能差呢?

server.c中可以看到,KEYS命令被keysCommand函数处理

void keysCommand(client *c) {
    dictIterator *di;
    dictEntry *de;
    sds pattern = c->argv[1]->ptr;
    int plen = sdslen(pattern), allkeys;
    unsigned long numkeys = 0;
    void *replylen = addDeferredMultiBulkLength(c);

    di = dictGetSafeIterator(c->db->dict);
    allkeys = (pattern[0] == '*' && pattern[1] == '\0');
    while((de = dictNext(di)) != NULL) {
        sds key = dictGetKey(de);
        robj *keyobj;

        if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
            keyobj = createStringObject(key,sdslen(key));
            if (expireIfNeeded(c->db,keyobj) == 0) {
                addReplyBulk(c,keyobj);
                numkeys++;
            }
            decrRefCount(keyobj);
        }
    }
    dictReleaseIterator(di);
    setDeferredMultiBulkLength(c,replylen,numkeys);
}

db本来就是一个dict,所以直接拿到一个dictIterator di,然后依次遍历,整个KEYS操作的复杂度是O(n),n为redis中key的数量。

这个操作,会阻塞到redis进程的。

可以看到,redis对于KEYS *这种情况做了优化。

strace 速查表

此文是 strace cheat sheet 的翻译。

TL;DR

strace 可以追踪系统调用、信号,在调试中,它还是收集上下文环境的非常好的工具。

此速查表将会展示一些技巧来说明 strace 的使用方法、如何过滤输出,并且介绍了一些 strace 的实用参数。

strace PTRACE_TRACEME EPERM (Operation not permitted)

你应该以 root 身份来运行 strace。如果你以 root 用户运行,也得到这个提示,这说明在你的系统中,strace 不允许附加到进程上。

你可以修改 /proc 中的设置,来开启 strace 附加到进程的功能:

 sudo bash -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'

为了让这个修改永久生效,请首先检查你的系统有没有 /etc/sysctl.d/10-ptrace.conf  文件。

如果你的系统有这个文件,修改该文件:

kernel.yama.ptrace_scope = 0

如果你的系统没有这个文件:

  1. 打开 /etc/sysctl.conf 文件
  2. 找到设置 kernel.yama.ptrace_scope
  3. 如果该设置存在,将其修改为 0
  4. 如果不存在,添加一行 kernel.yama.ptrace_scope = 0`

strace pid

strace 一个运行中的进程

strace -p [pid]

strace 一个运行中的进程和线程

strace -fp [pid]

strace 一个运行中的进程并且输出字符串

strace -s 80 -fp [pid]

此命令会输出每一个字符串的前 80 个字符。

strace 一个程序

strace 一个程序

strace ./program

strace 一个程序和线程

strace -f ./program

strace 一个程序并且输出字符串

strace -s 80 -f ./program

此命令会输出每一个字符串的前 80 个字符。

strace futex

使用 strace 的时候,经常会看到很多 futex 系统调用,比如:

[pid  2082] futex(0x7f4c4d3bff30, FUTEX_WAKE_PRIVATE, 1) = 0 <0.000003>
[pid  2081] futex(0x7f4c4d3bff60, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid  2082] futex(0x7f4c4d3bff60, FUTEX_WAKE_PRIVATE, 1) = 0 <0.000003>
[pid  2082] futex(0x7f4c4d3bff64, FUTEX_WAIT_PRIVATE, 7, NULL <unfinished ...>

`futex`系统调用被很多线程库广泛使用,比如 libpthread ,用来实现更高层次的锁原语,比如 mutexes(互斥器)、semaphore(信号量)等。

过滤掉 futex 调用

如果你的程序使用了大量的 futex 调用,而且你也不希望看到它们,你可以:

使用 grep 过滤掉 futex 调用:

strace strace -Tf ./program 2>&1 | grep -v futex

或者,追踪一些你真正关注的系统调用。比如,追踪此程序和其线程调用的 open、read和write调用:

strace -Tfe trace=open,read,write ./program

其他有用的选项

strace 有很多实用的选项。一些常用的选项如下:

  • -f 追踪创建的线程和子进程。许多程序会派生出额外的进程或者线程来完成一些工作,所以此选项很有用。
  • -T 输出系统调用花费的时间。当你需要确认特定的系统调用是否花费了很多时间的时候,此选项会有帮助。
  • -t 在每行输出当前时间(没有日期)。
  • -s [size] 每个字符串输出 size 个字符。如果程序在写入文件描述符,此选项会有帮助。
  • -c 输出系统调用次数、系统调用花费的时间的直方图。
  • -e trace=open,close 仅仅追踪 open 和 close 系统调用。

如果需要查看全部的命令行参数,请查看 strace 的文档。

总结

在 Linux 上调试的时候, strace 是必不可少的工具。当你需要了解一个程序在做什么的时候,你应该使用 strace。作为防御式调试的第一条命令,strace 是收集程序上下文信息的不错的方法。

strace 接收许多命令行参数,读者最好阅读 strace 文档来了解全部参数。

将OVA格式的虚拟机导入KVM

首先,用file命令查看OVA文件,即可看到OVA文件实际就是tar文件:

$file Hortonworks+Sandbox+2.0+VirtualBox.ova 
Hortonworks+Sandbox+2.0+VirtualBox.ova: POSIX tar archive (GNU)

使用tar命令可以看到,其中包含了两个文件:

$tar tf Hortonworks+Sandbox+2.0+VirtualBox.ova 
Hortonworks Sandbox 2.0 VirtualBox.ovf
Hortonworks Sandbox 2.0 VirtualBox-disk1.vmdk

用tar解压:

$tar xvf Hortonworks+Sandbox+2.0+VirtualBox.ova 
Hortonworks Sandbox 2.0 VirtualBox.ovf
Hortonworks Sandbox 2.0 VirtualBox-disk1.vmdk

ovf文件保存的是配置信息,没法直接导入;但是一般能导入vmdk格式的镜像就可以了。

把vmdk格式的镜像转化为qcow2格式的:

$qemu-img convert -O qcow2 'Hortonworks Sandbox 2.0 VirtualBox-disk1.vmdk' 'Hortonworks Sandbox 2.0 VirtualBox-disk1.qcow2'

转换完成后,用 Hortonworks Sandbox 2.0 VirtualBox-disk1.qcow2文件创建虚拟机即可。