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

本文是 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程序是错的。


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

ssh卡在debug1: SSH2_MSG_KEXINIT sent的解决办法

在公司通过kerberos登录ssh,完全没有问题。但是在家里通过l2tp连接vpn后,就无法登录了。

通过ssh -v查看日志,发现卡在debug1: SSH2_MSG_KEXINIT sent这一步。

查了下,发现是因为mtu设置太大


但是由于系统的网络是NetworkManager管理的,如果需要自动设置mtu,connection的配置中并没有vpn对应的mtu设置。后来,看了有人使用dispatcher脚本来设置。

创建/etc/NetworkManager/dispatcher.d/vpn-up文件:

#!/bin/sh

if [ "$2" = "vpn-up" ]; then
        ip link set "$1" mtu 1350
fi

然后vpn连接的时候,就会自动设置mtu了。

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

本文是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年年终募款

How to use mysql-connector-python 2.1.5 on Django 1.10?

MySQL Connector/Python currently not available from PyPI

When you want use mysql-connector-python as db backend in Django, the first thing you noticed is that the latest version of mysql-connector-python on PyPI is 2.0.4, but official version is 2.1.5.

The official developer explained this.(But why not host mysql-connector-python on PyPI?!).

so, you can bypass this problem. Add follow line into requirements.txt:

https://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-2.1.5.tar.gz#egg=mysql-connector-python

when you configured Connector/Python Django Backend, and run python manage.py migrate , you will meet error:

DatabaseError: Data truncated for column ‘applied’ at row 1

This is because mysql.connector.django cannot handle datetime with timezone.

You need patch 93c7b38.

when you fix this, you must drop all table, and re-run python manage.py migrate, you will meet another error:

  File "/path/to/venv/lib/python3.5/site-packages/mysql/connector/django/operations.py", line 223, in bulk_insert_sql
    return "VALUES " + ", ".join([items_sql] * num_values)
TypeError: can't multiply sequence by non-int of type 'tuple'

can’t multiply sequence by non-int of type ‘tuple’

This is because bulk_insert_sql ‘s third argument changed from int to list of placeholder rows, since Django 1.9.

You need another patch, patch 8dab00b. (Of cause, we doesn’t drop support of Django 1.6)


Now you can use mysql-connector-python 2.1.5 on Django 1.10, if you meet another problem, post the comment here!

Django的MySQL Driver配置

之前写过一篇文章《[email protected]添加MySQL/MariaDB支持》,现在Django对MySQL的支持已经很好了,现在就说一说最佳实践吧。

PEP 249规定了Python的数据库API。MySQL主要有三种API实现:

  • MySQLdb 是Andy Dustman开发的、使用原生代码/C语言绑定的驱动,它已经开发了数十年。
  • mysqlclient 是MySQLdb的一个支持Python3的fork,并且可以无缝替换调MySQLdb。mysqlclient目前是MySQL在Django下的推荐选择。
  • MySQL Connector/Python 是Oracle写的,纯Python实现的客户端库。

以上所有的驱动都是线程安全的,且提供了连接池。MySQLdb 是唯一一个不支持Python3的。

如果你使用mysqlclient

settings.py中的配置如下:

# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', #数据库引擎
        'NAME': 'test',                       #数据库名
        'USER': 'root',                       #用户名
        'PASSWORD': 'root',                   #密码
        'HOST': '',                           #数据库主机,默认为localhost
        'PORT': '',                           #数据库端口,MySQL默认为3306
        'OPTIONS': {
            'autocommit': True,
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        },
    }
}

第14行主要是为了防止警告:

(mysql.W002) MySQL Strict Mode is not set for database connection 'default'

当然,要在requirements.txt中添加对mysqlclient的依赖:

mysqlclient==1.3.10

然后运行python manage.py migrate :

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  。。。。。。

如果你使用MySQL Connector/Python

这么长时间了,Oracle官方的connector还是不太好用。

首先,一些补丁需要打:patch 93c7b38, patch 8dab00b要打上。

其次,这个包无法从PyPI安装了,只能从下载链接安装。

如果你要使用MySQL Connector/Python,首先,在requirements.txt中声明依赖:

https://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-2.1.5.tar.gz#egg=mysql-connector-python

其次,在settings.py里面关于数据库的配置如下:

# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'mysql.connector.django',   #数据库引擎
        'NAME': 'test',                       #数据库名
        'USER': 'root',                       #用户名
        'PASSWORD': 'root',                   #密码
        'HOST': 'localhost',
        'OPTIONS': {
            'autocommit': True,
        },
    }
}

然后运行python manage.py migrate

[译]什么是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或者在这儿评论。

[译]UUID和Linux:你需要知道的一切

本文为 UUIDs and Linux: Everything you ever need to know [Update] 的翻译。

UUID在Linux中为何如此特别?我[原作者]不知道!但是这儿有你需要知道的、关于UUID在Linux下的一切!

背景

UUID是128位长的数字,表示为32位的16进制数字,被用于在软件开发中、在没有上下文的情况下唯一地标识信息。RFC 4122描述了UUID的规范,一个UUID的例子是:

13152fae-d25a-4d78-b318-74397eb08184

UUID在Linux最熟悉的场景应该是块设备的标识符了吧。Windows世界中的UUID以Microsoft的全局唯一标识符GUID的形式出现,这些标识符在Microsoft的组件对象模型[COM]中使用。

UUID以各种变体生成:最初大多数是从计算机的MAC派生的,随后使用了基于name的散列值。关于这个问题,有多少UUID,有多大概率会生成一个已有的UUID,这是来自维基百科上 UUID 的文章的一些数字:

在100年里,每秒生成十亿个UUID,生成重复的UUID的概率约为50%。如果每个人都有6亿个UUID,下一个UUID重复的概率约为50%。

fstab中的用法

就像之前提到的,UUID在Linux下被经常用于标识块设备。想象一下,你有两个硬盘通过USB连接上,它们不是持久存储,只能依赖两个设备的名字来区分:有时第一个USB硬盘是“sda”,有时它是“sdb”。所以要唯一寻址正确的磁盘,比如在 /etc/fstab 中,你必须添加如下条目:

UUID=9043278a-1817-4ff5-8145-c79d8e24ea79 /boot ext3 defaults 0 2

对于块设备,uuid是存储在超级块中的。
但请注意,当您使用LVM快照时,不应在fstab中使用UUID。 有关更多详细信息,请参阅下面的“何时不使用它们”。

Linux下的实现和生成

在Linux下UUID在file中生成,你可以通过 /drivers/char/random.c 来生成新的UUID:

$ cat /proc/sys/kernel/random/uuid
eaf3a162-d770-4ec9-a819-ec96d429ea9f

uuidgen,以及ext 2/3/4 工具 E2fsprogs 使用程序库 libuuid 来生成UUID:

$ uuidgen 
f81cc383-aa75-4714-aa8a-3ce39e8ad33c

Bash中如何得到UUID

UUID中最有趣的部分很可能是如何获取硬盘的当前UUID。 如前面已经提到的,有两种主要的方法来获取它们:在特殊目录中的 ls 命令和工具 blkid。

第一,在 /dev/disk/by-uuid 中执行 ls 命令。这个目录包含了名字为UUID,链接到“真正的”块设备文件。对于你很难辨别安装了什么磁盘的情况下,这是很方便的。

$ ls -l /dev/disk/by-uuid
lrwxrwxrwx 1 root root 10 11. Oct 18:02 53cdad3b-4b01-4a6c-a099-be1cdf1acf6d -> ../../sda2

第二,blkid 工具是util-linux软件包的一部分。它提供了一个界面来查询特定设备,并且还支持搜索标签。

$ blkid /dev/sda1
/dev/sda1: LABEL="/" UUID="ee7cf0a0-1922-401b-a1ae-6ec9261484c0" SEC_TYPE="ext2" TYPE="ext3"

这儿还有更多方法!让我们安装 hwinfo:

$ hwinfo --block
[...]
  UDI: /org/freedesktop/Hal/devices/volume_uuid_3e953ee0_79f2_4d94_98b3_5f49ad652b7c
[...]
  Device Files: [...] /dev/disk/by-uuid/3e953ee0-79f2-4d94-98b3-5f49ad652b7c
[...]

就像你看到的,hwinfo列出了大量有关硬件的数据——包括设备的UUID。 当你获取更多关于块设备的信息时使用它。

或者 udevadm?它是udev提供的、从udev数据库查询数据的工具。udev数据库包含了udev系统的所有信息,因此UUID信息只是一部分。 如果你正在写一个“现代”的脚本,它很好地与Linux标准工具集成,我想我会使用udev。 但是对于快速直接的、一次性的命令行工具,和hwinfo一样,它包含了太多的信息。

$ udevadm info -q all -n /dev/sda1|grep uuid
S: disk/by-uuid/9043278a-1817-4ff5-8145-c79d8e24ea79
E: [...] /dev/disk/by-uuid/9043278a-1817-4ff5-8145-c79d8e24ea79
E: ID_FS_UUID=9043278a-1817-4ff5-8145-c79d8e24ea79
E: ID_FS_UUID_ENC=9043278a-1817-4ff5-8145-c79d8e24ea79

在这种情况下,有时也提到udevinfo。 但是,它已被弃用,大多数发行版不再包含。 另外,另一种常提到的检索UUID的方法是从程序库 /lib/vol/vol_id。 但是如bug #476379所述,vol_id只是一个私有的udev函数。因为此接口不稳定它不应该被外部程序(或人)使用。此外,整个库可能会在将来删除——实际上已在某些发行版中删除了。

如何在GUI程序中获取UUID

如果你不善于使用shell,KDE-GUI工具也可以用来查找UUID:/usr/bin/kcmshell4 devinfo

设置UUID

就像评论中提到的,设置UUID有时也是必要的。因为UUID是超级块的一部分,所以根据文件系统的不同,可能要使用不同的工具。比如ext文件系统可以使用tune2fs:

# tune2fs -U new_uuid /dev/sdaX

什么时候不使用UUID

Zhenech 提到,并不是所有的地方都适合使用UUID。

因为挂载两个具有相同的UUID的文件系统是不可行的,所以需要额外关注LVM快照(或者克隆磁盘):因为具有同样的UUID,它们可能会挂载失败。

XFS:dm-2文件系统也会有重复的UUID,也无法挂载。

处理该问题的一种方法是更改UUID,另一种办法是挂载的时候带上nouuid选项。

[译]为了更具有创造力,还是不要那么高效吧

本文为 To Get More Creative, Become Less Productive 的翻译。

高效和创造力之间存在着根本性的矛盾,在意识到这一点之前,管理者无法获取更多的创意。

高效的人们以一种系统化的方式来完成一系列必须完成的任务。他们以稳定的、可衡量的方式来完成目标。他们非常有效率、并且能够有效地利用自己的时间。

创造力——可不是这样的。

创造性的发展需要时间和空间。虽然我们可以系统地从事与创造力有关的活动,但是很难系统化创造力本身。特别是,创造力是知识的基础。 几乎所有的创意想法都涉及人们寻找现有知识的新用途——一些旧想法的新组合。James Dyson通过类比锯木厂发明了他的真空吸尘器。 Fiona Fairhurst通过研究鲨鱼皮肤设计了更快的泳装。 George de Mestral通过了解苍耳发明了魔术贴。

这意味着从他们需要创造力的开始,人们就需要时间来学习与他们的工作无关的东西,以便他们有广泛而深入的知识库。

此外,有创造力的企业很少涉及稳定和可衡量的进步。 相反,创造力包括尝试许多不同的可能性,以及在找到正确的解决方案之前努力尝试几个盲目的岔路。

但这些活动——建立一个知识库并探索它——需要时间。 很难简单地安排几个小时在这里和那里进行创造性的追求。相反,有些时候,需要花时间学习一个新的知识领域,或者与同事进行漫长的谈话,来理清楚一个新的想法。 所以很多创意活动可能看起来像是一个可疑的,直到突破来临。

许多公司想要他们的员工获得更多创造力,这其中的主要原因就是生产力和创造力之间的这种差异。

公司通常基于生产率来评估员工。 更重要的是,他们根据假设他们将雇佣高效的人来制定他们的招聘计划。 他们希望组织中的人员能够取得明显的进步。 他们致力于发展尽责的人——能够完成任务的人。

如果一个组织真正想要创造力,它必须开始招聘更多的人,而不是完成任务、仅仅让公司保持运作所需的那么点人。Google的20%的时间,即鼓励员工将20%的时间花在新想法上的行动,已经执行了很多时间了。对于如何在公司实施这项政策,已经有了很多的讨论,我的观点是,如果你要给每个人一个机会来发展他们的创造力,你需要多雇用10-20%的人。

当要开发一个新的想法的时候,管理者还需要为员工提供一些时间安排上的灵活性。 给予某人10-20%的自由时间来发展他们的创造力,这并不一定意味着他们每周将花费4-8小时来发展创造力。 相反,可能有几个星期,一些人专注于他们需要完成的任务,还有几个星期或者几天来验证一个“兔子洞”的想法。

但是,仅仅为员工提供创造性所需的时间和灵活性是不够的。 管理者必须奖励员工从事的、提供创造性解决方案的任务,如学习新事物,发展新技能,与同事广泛的对话,以及尝试不成功的想法。

发展创造力的管理工作是可行的,但是它需要那些对效率痴迷的管理者们放松对工作时间的约束。

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继续执行而已。