我们为什么使用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,很让我吃惊。

资源管理

资源管理用的是cgroup2: 结合systemd中的slice: 在docker的潮流之下,FaceBook还是比较 old school style 的。不过,slice确实很够解决这个问题,后面可以看看

服务监控

systemd也提供了监控接口,通过dbus暴露出来: 一个daemon收集数据,上报: 使用Cython包装的sd-bus来获取数据,IPython写原型:

案例学习

一般都是直接关闭rpm里面重启的脚本,FaceBook还做的一个工具。 journalctl可以看单机日志,不知道分布式日志有没有用journald。 嗯,一个小坑 systemd的这个转义很反人类,但是他们并没有打算改!

FaceBook的建议

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

评论

阅读更多

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

编译:

阅读更多

[译]什么是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互相等待,成了死锁。

阅读更多

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了:

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

如果是第一次调用,先注册['ComposerAutoloaderInitb6d254015e39cf5090fb84fdb1ed664b', 'loadClassLoader'],然后new一个\Composer\Autoload\ClassLoader 作为loader,然后立马取消注册loadClassLoader。 接下来就一步一步处理各种autoload了。

autoload_namespaces.php

阅读更多

给Eclipse添加pkg-config支持

原来用gedit+gcc来编写GTK程序(主要是学习例程),后来随着代码越写越多,需要一个IDE来辅助开发,于是想到了万能的Eclipse。 可是Eclipse可以支持C语言开发,却不支持pkg-config命令集成。搞得每次新建GTK工程都要把pkgconfig的输出改到Eclipse中。 后来烦了,自己google之,得解:http://marketplace.eclipse.org/content/pkg-config-support-eclipse-cdt

安装Eclipse

首先,安装jre和jdk等依赖。 接下来下载Eclipse。我原来下载了Eclipse 4.3 Classic ,但是最新版成了Eclipse 4.3 Standard ,都是可以的。 然后解压Eclipse。(双击eclipse文件即可运行Eclipse) 另外,Eclipse CDT和gtk开发的其他依赖包也要安装。

安装Eclipse Marketpalce Client(MPC)

我下载的是Eclipse 4.3 Classic,所以需要安装MPC;如果你下载的是Eclipse 4.3 Standard,可以跳过此步。 在Install New Software窗口中选上所有网站,然后搜索market即可看到MPC;点击下一步,然后同意许可协议等。 安装完成后重启Eclipse即可完成MPC的安装。

在market中安装Pkg-config support

在帮助菜单中打开Eclipse Marketpalce: 1 搜索pkg-config,如图: 2 点击安装,一直确认、同意许可协议。

Pkg-config support的使用

首先,创建一个Hello World C工程,贴入GTK程序代码(代码附后),此时Eclipse提示程序中有好几处错误/警告: 3 接下来,打开项目的设置菜单 -> C/C++ Build -> Settings -> Pkg-config 选项卡: 4 在其中的选项中勾选 GTK+-2.0 (我个人使用的是GTK 2.0,可根据自己的情况酌情选择): 5 然后点击确定。 稍等几秒后就会发现Eclipse识别出来了头文件包含、GTK类型定义等,如图: 6 点击运行(Run),可以看到GTK程序在Eclipse中正常运行: 7 GTK测试代码

#include <stdio.h>
#include <stdlib.h>
#include <gtk/gtk.h>

阅读更多

gcc的使用和编译时的符号确定

一般来说,我们编译c语言程序要经过编译(预处理)、和链接两步。当然教材上讲的时候还有预处理。 预处理

gcc -E code.c -o code.e.c

预处理主要处理文件包含和宏定义等等。 编译

gcc -c code.c -o code.o

编译主要把c语句翻译成二进制代码(一般此步骤包括了预处理)。 但是有一些函数的实现没有在预处理过的c语言文件中,类似的还有extern声明的变量,所以现在编译的文件还没有办法执行。这些无法决定位置的函数和变量,在.o文件中统称为未定义的符号。 链接

gcc code.o -o code

在这一步我们要把所有.o文件中的未定义符号给确定下来,确定的来源有两种,查看其他.o文件的导出表中查找,从其他.so(dll)文件的导出表中查找。 gcc还有一个重要的参数是-g,表示编译的时候保留调试信息(包括c语句和汇编语句的对应关系,变量的分配) gcc的-o参数指定输出文件的名字。 让我们通过一个例子来了解这个流程 test.c文件:

#include <stdio.h>

extern int a;

extern void test();

阅读更多