我们为什么使用Linux内核的TCP栈

本文是 Why we use the Linux kernel’s TCP stack 的翻译。

最近,有一篇文章提出了一个非常有趣的问题,我们为什么使用Linux内核的TCP栈? 这在Hacker News上引发了非常有趣的讨论。

在CloudFlare工作的时候,我也一直在想这个问题。我的经验主要来自于和数千台生产机器打交道,我也会从这个角度来尝试回答这个问题。

CC BY 2.0 图片 来自 John Vetterli

让我们从一个更加宽泛的问题开始——跑起来一个操作系统是为了啥?如果你仅仅打算运行一个应用程序,那么运行数百万行代码的内核听起来绝对是一个负担。

但,我们通常都决定跑一个操作系统,有两个原因。第一,操作系统层提供了硬件独立性,以及很容易使用的API。这样,我们就可以为任何机器写代码了——不仅仅是当前运行代码的这种机器。第二,操作系统提供了时分复用层。这让我们能同时运行多个程序。不管它是另一个http服务,还是仅仅是一个bash会话,这种不同进程共享资源的能力是非常重要的。所有由内核暴露出来的资源都是能够被多个进程共享的!

用户态网络

对于网络栈,这也没什么不同。运行通用操作系统的网络栈,我们能够运行多个网络程序。如果为了运行用户态网络栈,而让单个应用程序独享网卡硬件,那就会丢失这种能力。将一个网卡分配给另一个程序,那么很可能就没法同时与服务器进行ssh会话了。

这听起来很疯狂,但这正是很多用户态网络栈所建议的。通用术语叫“全内核旁路”(full kernel bypass)。即绕过内核,用户态进程直接使用网络硬件 。

阅读更多

Docker for Mac with Kubernetes初次尝试

首先,2018-01-09日Docker公司宣布了Docker for Mac支持Kubernetes。 后来陆续尝试了几次,今天终于成功了,所以记录下。

安装Docker for Mac with Kubernetes

首先,安装Docker for Mac Edge版本:

brew cask install docker-edge

设置代理(我用的是https://github.com/netheril96/MEOW): 开启Kubernetes:   等待安装: OK了: kubectl version也能看到客户端版本和服务端版本了:

$ kubectl version
Client Version: version.Info{Major:”1”, Minor:”9”, GitVersion:”v1.9.2”, GitCommit:”5fa2db2bd46ac79e5e00a4e6ed24191080aa463b”, GitTreeState:”clean”, BuildDate:”2018-01-18T10:09:24Z”, GoVersion:”go1.9.2”, Compiler:”gc”, Platform:”darwin/amd64”}
Server Version: version.Info{Major:”1”, Minor:”9”, GitVersion:”v1.9.2”, GitCommit:”5fa2db2bd46ac79e5e00a4e6ed24191080aa463b”, GitTreeState:”clean”, BuildDate:”2018-01-18T09:42:01Z”, GoVersion:”go1.9.2”, Compiler:”gc”, Platform:”linux/amd64”}

创建kubernetes-dashboard 服务

$ kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml
secret “kubernetes-dashboard-certs” created
serviceaccount “kubernetes-dashboard” created
role “kubernetes-dashboard-minimal” created
rolebinding “kubernetes-dashboard-minimal” created
deployment “kubernetes-dashboard” created
service “kubernetes-dashboard” created

等一会就可以看到创建好了:

$ kubectl get pods –namespace kube-system
NAME READY STATUS RESTARTS AGE
etcd-docker-for-desktop 1/1 Running 0 6m
kube-apiserver-docker-for-desktop 1/1 Running 2 6m
kube-controller-manager-docker-for-desktop 1/1 Running 0 6m
kube-dns-6f4fd4bdf-zl9dh 3/3 Running 0 7m
kube-proxy-xsx8n 1/1 Running 0 7m
kube-scheduler-docker-for-desktop 1/1 Running 0 5m
kubernetes-dashboard-5bd6f767c7-6szsl 1/1 Running 0 1m
$ kubectl get deployments –namespace kube-system
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kube-dns 1 1 1 1 7m
kubernetes-dashboard 1 1 1 1 1m

阅读更多

[评论]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,很让我吃惊。

资源管理

阅读更多

如何调试iptables [CentOS 7]

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

基础知识

iptables可以jump到TRACE目标,开启_包追踪_(packet tracing)选项。 iptables有一个raw表,基本上是iptables最先处理包的地方。

开始调试

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

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

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

1
modprobe nf_log_ipv4

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

阅读更多

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!

阅读更多

如何在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

编译:

阅读更多

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

本文是 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)。它知道上一次按键的符号,当前活跃的修饰符和快捷键相关的修饰符。

阅读更多

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

本文是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中的设备

阅读更多

[译]什么是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允许更多的细节和沉浸体验:屏幕渐渐消失,成为电影故事的一个窗口。

半个像素是不存在的

阅读更多

Linux 3.4中acpi_pad的一个Bug分析

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


模块注册

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

acpi_pad_driver的定义如下:

1
2
3
4
5
6
7
8
9
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 的定义

1
2
3
4
5
6
7
8
9
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中):

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

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

acpi_bus_type的定义如下:

1
2
3
4
5
6
7
8
9
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) 函数是被内核调用的,相当于回调:

1
2
3
4
5
6
7
8
9
10
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的地址(就是根据子结构体地址,获取父级结构体地址):

1
2
3
#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:

1
2
3
4
5
6
7
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的定义:

1
2
3
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函数:

1
2
3
4
5
6
7
8
9
10
11
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数置空:

1
2
3
4
5
6
7
8
9
10
11
12
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结束:

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的代码:

1
2
3
4
5
6
7
8
9
10
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互相等待,成了死锁。

阅读更多