分类 技术 下的文章

人工智能结合开源硬件工具能够提升严重传染病疟疾的诊断。

人工智能(AI)和开源工具、技术和框架是促进社会进步的强有力的结合。“健康就是财富”可能有点陈词滥调,但它却是非常准确的!在本篇文章,我们将测试 AI 是如何与低成本、有效、精确的开源深度学习方法结合起来一起用来检测致死的传染病疟疾。

我既不是一个医生,也不是一个医疗保健研究者,我也绝不像他们那样合格,我只是对将 AI 应用到医疗保健研究感兴趣。在这片文章中我的想法是展示 AI 和开源解决方案如何帮助疟疾检测和减少人工劳动的方法。

 title=

Python 和 TensorFlow: 一个构建开源深度学习方法的很棒的结合

感谢 Python 的强大和像 TensorFlow 这样的深度学习框架,我们能够构建健壮的、大规模的、有效的深度学习方法。因为这些工具是自由和开源的,我们能够构建非常经济且易于被任何人采纳和使用的解决方案。让我们开始吧!

项目动机

疟疾是由疟原虫造成的致死的、有传染性的、蚊子传播的疾病,主要通过受感染的雌性按蚊叮咬传播。共有五种寄生虫能够引起疟疾,但是大多数病例是这两种类型造成的:恶性疟原虫和间日疟原虫。

 title=

这个地图显示了疟疾在全球传播分布形势,尤其在热带地区,但疾病的性质和致命性是该项目的主要动机。

如果一只受感染雌性蚊子叮咬了你,蚊子携带的寄生虫进入你的血液,并且开始破坏携带氧气的红细胞(RBC)。通常,疟疾的最初症状类似于流感病毒,在蚊子叮咬后,他们通常在几天或几周内发作。然而,这些致死的寄生虫可以在你的身体里生存长达一年并且不会造成任何症状,延迟治疗可能造成并发症甚至死亡。因此,早期的检查能够挽救生命。

世界健康组织(WHO)的疟疾实情表明,世界近乎一半的人口面临疟疾的风险,有超过 2 亿的疟疾病例,每年由于疟疾造成的死亡将近 40 万。这是使疟疾检测和诊断快速、简单和有效的一个动机。

检测疟疾的方法

有几种方法能够用来检测和诊断疟疾。该文中的项目就是基于 Rajaraman, et al. 的论文:“预先训练的卷积神经网络作为特征提取器,用于改善薄血涂片图像中的疟疾寄生虫检测”介绍的一些方法,包含聚合酶链反应(PCR)和快速诊断测试(RDT)。这两种测试通常用于无法提供高质量显微镜服务的地方。

标准的疟疾诊断通常是基于血液涂片工作流程的,根据 Carlos Ariza 的文章“Malaria Hero:一个更快诊断疟原虫的网络应用”,我从中了解到 Adrian Rosebrock 的“使用 Keras 的深度学习和医学图像分析”。我感激这些优秀的资源的作者,让我在疟原虫预防、诊断和治疗方面有了更多的想法。

 title=

一个疟原虫检测的血涂片工作流程

根据 WHO 方案,诊断通常包括对放大 100 倍的血涂片的集中检测。受过训练的人们手工计算在 5000 个细胞中有多少红细胞中包含疟原虫。正如上述解释中引用的 Rajaraman, et al. 的论文:

厚血涂片有助于检测寄生虫的存在,而薄血涂片有助于识别引起感染的寄生虫种类(疾病控制和预防中心, 2012)。诊断准确性在很大程度上取决于诊断人的专业知识,并且可能受到观察者间差异和疾病流行/资源受限区域大规模诊断所造成的不利影响(Mitiku, Mengistu 和 Gelaw, 2003)。可替代的技术是使用聚合酶链反应(PCR)和快速诊断测试(RDT);然而,PCR 分析受限于它的性能(Hommelsheim, et al., 2014),RDT 在疾病流行的地区成本效益低(Hawkes, Katsuva 和 Masumbuko, 2009)。

因此,疟疾检测可能受益于使用机器学习的自动化。

疟疾检测的深度学习

人工诊断血涂片是一个繁重的手工过程,需要专业知识来分类和计数被寄生虫感染的和未感染的细胞。这个过程可能不能很好的规模化,尤其在那些专业人士不足的地区。在利用最先进的图像处理和分析技术提取人工选取特征和构建基于机器学习的分类模型方面取得了一些进展。然而,这些模型不能大规模推广,因为没有更多的数据用来训练,并且人工选取特征需要花费很长时间。

深度学习模型,或者更具体地讲,卷积神经网络(CNN),已经被证明在各种计算机视觉任务中非常有效。(如果你想更多的了解关于 CNN 的背景知识,我推荐你阅读视觉识别的 CS2331n 卷积神经网络。)简单地讲,CNN 模型的关键层包含卷积和池化层,正如下图所示。

 title=

一个典型的 CNN 架构

卷积层从数据中学习空间层级模式,它是平移不变的,因此它们能够学习图像的不同方面。例如,第一个卷积层将学习小的和局部图案,例如边缘和角落,第二个卷积层将基于第一层的特征学习更大的图案,等等。这允许 CNN 自动化提取特征并且学习对于新数据点通用的有效的特征。池化层有助于下采样和减少尺寸。

因此,CNN 有助于自动化和规模化的特征工程。同样,在模型末尾加上密集层允许我们执行像图像分类这样的任务。使用像 CNN 这样的深度学习模型自动的疟疾检测可能非常有效、便宜和具有规模性,尤其是迁移学习和预训练模型效果非常好,甚至在少量数据的约束下。

Rajaraman, et al. 的论文在一个数据集上利用六个预训练模型在检测疟疾对比无感染样本获取到令人吃惊的 95.9% 的准确率。我们的重点是从头开始尝试一些简单的 CNN 模型和用一个预训练的训练模型使用迁移学习来查看我们能够从相同的数据集中得到什么。我们将使用开源工具和框架,包括 Python 和 TensorFlow,来构建我们的模型。

数据集

我们分析的数据来自 Lister Hill 国家生物医学交流中心(LHNCBC)的研究人员,该中心是国家医学图书馆(NLM)的一部分,他们细心收集和标记了公开可用的健康和受感染的血涂片图像的数据集。这些研究者已经开发了一个运行在 Android 智能手机的疟疾检测手机应用,连接到一个传统的光学显微镜。它们使用吉姆萨染液将 150 个受恶性疟原虫感染的和 50 个健康病人的薄血涂片染色,这些薄血涂片是在孟加拉的吉大港医学院附属医院收集和照相的。使用智能手机的内置相机获取每个显微镜视窗内的图像。这些图片由在泰国曼谷的马希多-牛津热带医学研究所的一个专家使用幻灯片阅读器标记的。

让我们简要地查看一下数据集的结构。首先,我将安装一些基础的依赖(基于使用的操作系统)。

 title=

我使用的是云上的带有一个 GPU 的基于 Debian 的操作系统,这样我能更快的运行我的模型。为了查看目录结构,我们必须使用 sudo apt install tree 安装 tree 及其依赖(如果我们没有安装的话)。

 title=

我们有两个文件夹包含血细胞的图像,包括受感染的和健康的。我们通过输入可以获取关于图像总数更多的细节:

import os
import glob

base_dir = os.path.join('./cell_images')
infected_dir = os.path.join(base_dir,'Parasitized')
healthy_dir = os.path.join(base_dir,'Uninfected')

infected_files = glob.glob(infected_dir+'/*.png')
healthy_files = glob.glob(healthy_dir+'/*.png')
len(infected_files), len(healthy_files)

# Output
(13779, 13779)

看起来我们有一个平衡的数据集,包含 13,779 张疟疾的和 13,779 张非疟疾的(健康的)血细胞图像。让我们根据这些构建数据帧,我们将用这些数据帧来构建我们的数据集。

import numpy as np
import pandas as pd

np.random.seed(42)

files_df = pd.DataFrame({
    'filename': infected_files + healthy_files,
    'label': ['malaria'] * len(infected_files) + ['healthy'] * len(healthy_files)
}).sample(frac=1, random_state=42).reset_index(drop=True)

files_df.head()

 title=

构建和了解图像数据集

为了构建深度学习模型,我们需要训练数据,但是我们还需要使用不可见的数据测试模型的性能。相应的,我们将使用 60:10:30 的比例来划分用于训练、验证和测试的数据集。我们将在训练期间应用训练和验证数据集,并用测试数据集来检查模型的性能。

from sklearn.model_selection import train_test_split
from collections import Counter

train_files, test_files, train_labels, test_labels = train_test_split(files_df['filename'].values,
                                                                      files_df['label'].values, 
                                                                      test_size=0.3, random_state=42)
train_files, val_files, train_labels, val_labels = train_test_split(train_files,
                                                                    train_labels, 
                                                                    test_size=0.1, random_state=42)

print(train_files.shape, val_files.shape, test_files.shape)
print('Train:', Counter(train_labels), '\nVal:', Counter(val_labels), '\nTest:', Counter(test_labels))

# Output
(17361,) (1929,) (8268,)
Train: Counter({'healthy': 8734, 'malaria': 8627}) 
Val: Counter({'healthy': 970, 'malaria': 959}) 
Test: Counter({'malaria': 4193, 'healthy': 4075})

这些图片尺寸并不相同,因为血涂片和细胞图像是基于人、测试方法、图片方向不同而不同的。让我们总结我们的训练数据集的统计信息来决定最佳的图像尺寸(牢记,我们根本不会碰测试数据集)。

import cv2
from concurrent import futures
import threading

def get_img_shape_parallel(idx, img, total_imgs):
    if idx % 5000 == 0 or idx == (total_imgs - 1):
        print('{}: working on img num: {}'.format(threading.current_thread().name,
                                                  idx))
    return cv2.imread(img).shape
  
ex = futures.ThreadPoolExecutor(max_workers=None)
data_inp = [(idx, img, len(train_files)) for idx, img in enumerate(train_files)]
print('Starting Img shape computation:')
train_img_dims_map = ex.map(get_img_shape_parallel, 
                            [record[0] for record in data_inp],
                            [record[1] for record in data_inp],
                            [record[2] for record in data_inp])
train_img_dims = list(train_img_dims_map)
print('Min Dimensions:', np.min(train_img_dims, axis=0)) 
print('Avg Dimensions:', np.mean(train_img_dims, axis=0))
print('Median Dimensions:', np.median(train_img_dims, axis=0))
print('Max Dimensions:', np.max(train_img_dims, axis=0))


# Output
Starting Img shape computation:
ThreadPoolExecutor-0_0: working on img num: 0
ThreadPoolExecutor-0_17: working on img num: 5000
ThreadPoolExecutor-0_15: working on img num: 10000
ThreadPoolExecutor-0_1: working on img num: 15000
ThreadPoolExecutor-0_7: working on img num: 17360
Min Dimensions: [46 46  3]
Avg Dimensions: [132.77311215 132.45757733   3.]
Median Dimensions: [130. 130.   3.]
Max Dimensions: [385 394   3]

我们应用并行处理来加速图像读取,并且基于汇总统计结果,我们将每幅图片的尺寸重新调整到 125x125 像素。让我们载入我们所有的图像并重新调整它们为这些固定尺寸。

IMG_DIMS = (125, 125)

def get_img_data_parallel(idx, img, total_imgs):
    if idx % 5000 == 0 or idx == (total_imgs - 1):
        print('{}: working on img num: {}'.format(threading.current_thread().name,
                                                  idx))
    img = cv2.imread(img)
    img = cv2.resize(img, dsize=IMG_DIMS, 
                     interpolation=cv2.INTER_CUBIC)
    img = np.array(img, dtype=np.float32)
    return img

ex = futures.ThreadPoolExecutor(max_workers=None)
train_data_inp = [(idx, img, len(train_files)) for idx, img in enumerate(train_files)]
val_data_inp = [(idx, img, len(val_files)) for idx, img in enumerate(val_files)]
test_data_inp = [(idx, img, len(test_files)) for idx, img in enumerate(test_files)]

print('Loading Train Images:')
train_data_map = ex.map(get_img_data_parallel, 
                        [record[0] for record in train_data_inp],
                        [record[1] for record in train_data_inp],
                        [record[2] for record in train_data_inp])
train_data = np.array(list(train_data_map))

print('\nLoading Validation Images:')
val_data_map = ex.map(get_img_data_parallel, 
                        [record[0] for record in val_data_inp],
                        [record[1] for record in val_data_inp],
                        [record[2] for record in val_data_inp])
val_data = np.array(list(val_data_map))

print('\nLoading Test Images:')
test_data_map = ex.map(get_img_data_parallel, 
                        [record[0] for record in test_data_inp],
                        [record[1] for record in test_data_inp],
                        [record[2] for record in test_data_inp])
test_data = np.array(list(test_data_map))

train_data.shape, val_data.shape, test_data.shape  


# Output
Loading Train Images:
ThreadPoolExecutor-1_0: working on img num: 0
ThreadPoolExecutor-1_12: working on img num: 5000
ThreadPoolExecutor-1_6: working on img num: 10000
ThreadPoolExecutor-1_10: working on img num: 15000
ThreadPoolExecutor-1_3: working on img num: 17360

Loading Validation Images:
ThreadPoolExecutor-1_13: working on img num: 0
ThreadPoolExecutor-1_18: working on img num: 1928

Loading Test Images:
ThreadPoolExecutor-1_5: working on img num: 0
ThreadPoolExecutor-1_19: working on img num: 5000
ThreadPoolExecutor-1_8: working on img num: 8267
((17361, 125, 125, 3), (1929, 125, 125, 3), (8268, 125, 125, 3))

我们再次应用并行处理来加速有关图像载入和重新调整大小的计算。最终,我们获得了所需尺寸的图片张量,正如前面的输出所示。我们现在查看一些血细胞图像样本,以对我们的数据有个印象。

import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(1 , figsize = (8 , 8))
n = 0 
for i in range(16):
    n += 1 
    r = np.random.randint(0 , train_data.shape[0] , 1)
    plt.subplot(4 , 4 , n)
    plt.subplots_adjust(hspace = 0.5 , wspace = 0.5)
    plt.imshow(train_data[r[0]]/255.)
    plt.title('{}'.format(train_labels[r[0]]))
    plt.xticks([]) , plt.yticks([])

 title=

基于这些样本图像,我们看到一些疟疾和健康细胞图像的细微不同。我们将使我们的深度学习模型试图在模型训练中学习这些模式。

开始我们的模型训练前,我们必须建立一些基础的配置设置。

BATCH_SIZE = 64
NUM_CLASSES = 2
EPOCHS = 25
INPUT_SHAPE = (125, 125, 3)

train_imgs_scaled = train_data / 255.
val_imgs_scaled = val_data / 255.

# encode text category labels
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
le.fit(train_labels)
train_labels_enc = le.transform(train_labels)
val_labels_enc = le.transform(val_labels)

print(train_labels[:6], train_labels_enc[:6])


# Output
['malaria' 'malaria' 'malaria' 'healthy' 'healthy' 'malaria'] [1 1 1 0 0 1]

我们修复我们的图像尺寸、批量大小,和纪元,并编码我们的分类的类标签。TensorFlow 2.0 于 2019 年三月发布,这个练习是尝试它的完美理由。

import tensorflow as tf

# Load the TensorBoard notebook extension (optional)
%load_ext tensorboard.notebook

tf.random.set_seed(42)
tf.__version__

# Output
'2.0.0-alpha0'

深度学习训练

在模型训练阶段,我们将构建三个深度训练模型,使用我们的训练集训练,使用验证数据比较它们的性能。然后,我们保存这些模型并在之后的模型评估阶段使用它们。

模型 1:从头开始的 CNN

我们的第一个疟疾检测模型将从头开始构建和训练一个基础的 CNN。首先,让我们定义我们的模型架构,

inp = tf.keras.layers.Input(shape=INPUT_SHAPE)

conv1 = tf.keras.layers.Conv2D(32, kernel_size=(3, 3), 
                               activation='relu', padding='same')(inp)
pool1 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv1)
conv2 = tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
                               activation='relu', padding='same')(pool1)
pool2 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv2)
conv3 = tf.keras.layers.Conv2D(128, kernel_size=(3, 3), 
                               activation='relu', padding='same')(pool2)
pool3 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv3)

flat = tf.keras.layers.Flatten()(pool3)

hidden1 = tf.keras.layers.Dense(512, activation='relu')(flat)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=inp, outputs=out)
model.compile(optimizer='adam',
                loss='binary_crossentropy',
                metrics=['accuracy'])
model.summary()


# Output
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 125, 125, 3)]     0         
_________________________________________________________________
conv2d (Conv2D)              (None, 125, 125, 32)      896       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 62, 62, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 62, 62, 64)        18496     
_________________________________________________________________
...
...
_________________________________________________________________
dense_1 (Dense)              (None, 512)               262656    
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 513       
=================================================================
Total params: 15,102,529
Trainable params: 15,102,529
Non-trainable params: 0
_________________________________________________________________

基于这些代码的架构,我们的 CNN 模型有三个卷积和一个池化层,其后是两个致密层,以及用于正则化的失活。让我们训练我们的模型。

import datetime

logdir = os.path.join('/home/dipanzan_sarkar/projects/tensorboard_logs', 
                      datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=2, min_lr=0.000001)
callbacks = [reduce_lr, tensorboard_callback]

history = model.fit(x=train_imgs_scaled, y=train_labels_enc, 
                    batch_size=BATCH_SIZE,
                    epochs=EPOCHS, 
                    validation_data=(val_imgs_scaled, val_labels_enc), 
                    callbacks=callbacks,
                    verbose=1)
                    

# Output
Train on 17361 samples, validate on 1929 samples
Epoch 1/25
17361/17361 [====] - 32s 2ms/sample - loss: 0.4373 - accuracy: 0.7814 - val_loss: 0.1834 - val_accuracy: 0.9393
Epoch 2/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.1725 - accuracy: 0.9434 - val_loss: 0.1567 - val_accuracy: 0.9513
...
...
Epoch 24/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.0036 - accuracy: 0.9993 - val_loss: 0.3693 - val_accuracy: 0.9565
Epoch 25/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.0034 - accuracy: 0.9994 - val_loss: 0.3699 - val_accuracy: 0.9559

我们获得了 95.6% 的验证精确率,这很好,尽管我们的模型看起来有些过拟合(通过查看我们的训练精确度,是 99.9%)。通过绘制训练和验证的精度和损失曲线,我们可以清楚地看到这一点。

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
t = f.suptitle('Basic CNN Performance', fontsize=12)
f.subplots_adjust(top=0.85, wspace=0.3)

max_epoch = len(history.history['accuracy'])+1
epoch_list = list(range(1,max_epoch))
ax1.plot(epoch_list, history.history['accuracy'], label='Train Accuracy')
ax1.plot(epoch_list, history.history['val_accuracy'], label='Validation Accuracy')
ax1.set_xticks(np.arange(1, max_epoch, 5))
ax1.set_ylabel('Accuracy Value')
ax1.set_xlabel('Epoch')
ax1.set_title('Accuracy')
l1 = ax1.legend(loc="best")

ax2.plot(epoch_list, history.history['loss'], label='Train Loss')
ax2.plot(epoch_list, history.history['val_loss'], label='Validation Loss')
ax2.set_xticks(np.arange(1, max_epoch, 5))
ax2.set_ylabel('Loss Value')
ax2.set_xlabel('Epoch')
ax2.set_title('Loss')
l2 = ax2.legend(loc="best")

 title=

基础 CNN 学习曲线

我们可以看在在第五个纪元,情况并没有改善很多。让我们保存这个模型用于将来的评估。

model.save('basic_cnn.h5')

深度迁移学习

就像人类有与生俱来在不同任务间传输知识的能力一样,迁移学习允许我们利用从以前任务学到的知识用到新的相关的任务,即使在机器学习或深度学习的情况下也是如此。如果想深入探究迁移学习,你应该看我的文章“一个易于理解与现实应用一起学习深度学习中的迁移学习的指导实践”和我的书《Python 迁移学习实践》。

 title=

在这篇实践中我们想要探索的想法是:

在我们的问题背景下,我们能够利用一个预训练深度学习模型(在大数据集上训练的,像 ImageNet)通过应用和迁移知识来解决疟疾检测的问题吗?

我们将应用两个最流行的深度迁移学习策略。

  • 预训练模型作为特征提取器
  • 微调的预训练模型

我们将使用预训练的 VGG-19 深度训练模型(由剑桥大学的视觉几何组(VGG)开发)进行我们的实验。像 VGG-19 这样的预训练模型是在一个大的数据集(Imagenet)上使用了很多不同的图像分类训练的。因此,这个模型应该已经学习到了健壮的特征层级结构,相对于你的 CNN 模型学到的特征,是空间不变的、转动不变的、平移不变的。因此,这个模型,已经从百万幅图片中学习到了一个好的特征显示,对于像疟疾检测这样的计算机视觉问题,可以作为一个好的合适新图像的特征提取器。在我们的问题中发挥迁移学习的能力之前,让我们先讨论 VGG-19 模型。

理解 VGG-19 模型

VGG-19 模型是一个构建在 ImageNet 数据库之上的 19 层(卷积和全连接的)的深度学习网络,ImageNet 数据库为了图像识别和分类的目的而开发。该模型是由 Karen Simonyan 和 Andrew Zisserman 构建的,在他们的论文“大规模图像识别的非常深的卷积网络”中进行了描述。VGG-19 的架构模型是:

 title=

你可以看到我们总共有 16 个使用 3x3 卷积过滤器的卷积层,与最大的池化层来下采样,和由 4096 个单元组成的两个全连接的隐藏层,每个隐藏层之后跟随一个由 1000 个单元组成的致密层,每个单元代表 ImageNet 数据库中的一个分类。我们不需要最后三层,因为我们将使用我们自己的全连接致密层来预测疟疾。我们更关心前五个块,因此我们可以利用 VGG 模型作为一个有效的特征提取器。

我们将使用模型之一作为一个简单的特征提取器,通过冻结五个卷积块的方式来确保它们的位权在每个纪元后不会更新。对于最后一个模型,我们会对 VGG 模型进行微调,我们会解冻最后两个块(第 4 和第 5)因此当我们训练我们的模型时,它们的位权在每个时期(每批数据)被更新。

模型 2:预训练的模型作为一个特征提取器

为了构建这个模型,我们将利用 TensorFlow 载入 VGG-19 模型并冻结卷积块,因此我们能够将它们用作特征提取器。我们在末尾插入我们自己的致密层来执行分类任务。

vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet', 
                                        input_shape=INPUT_SHAPE)
vgg.trainable = False
# Freeze the layers
for layer in vgg.layers:
    layer.trainable = False
    
base_vgg = vgg
base_out = base_vgg.output
pool_out = tf.keras.layers.Flatten()(base_out)
hidden1 = tf.keras.layers.Dense(512, activation='relu')(pool_out)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=base_vgg.input, outputs=out)
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=1e-4),
                loss='binary_crossentropy',
                metrics=['accuracy'])
model.summary()


# Output
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         [(None, 125, 125, 3)]     0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 125, 125, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 125, 125, 64)      36928     
_________________________________________________________________
...
...
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 3, 3, 512)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 4608)              0         
_________________________________________________________________
dense_3 (Dense)              (None, 512)               2359808   
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_4 (Dense)              (None, 512)               262656    
_________________________________________________________________
dropout_3 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 513       
=================================================================
Total params: 22,647,361
Trainable params: 2,622,977
Non-trainable params: 20,024,384
_________________________________________________________________

从整个输出可以明显看出,在我们的模型中我们有了很多层,我们将只利用 VGG-19 模型的冻结层作为特征提取器。你可以使用下列代码来验证我们的模型有多少层是实际可训练的,以及我们的网络中总共存在多少层。

print("Total Layers:", len(model.layers))
print("Total trainable layers:", 
      sum([1 for l in model.layers if l.trainable]))

# Output
Total Layers: 28
Total trainable layers: 6

我们将使用和我们之前的模型相似的配置和回调来训练我们的模型。参考我的 GitHub 仓库以获取训练模型的完整代码。我们观察下列图表,以显示模型精确度和损失曲线。

 title=

冻结的预训练的 CNN 的学习曲线

这表明我们的模型没有像我们的基础 CNN 模型那样过拟合,但是性能有点不如我们的基础的 CNN 模型。让我们保存这个模型,以备将来的评估。

model.save('vgg_frozen.h5')

模型 3:使用图像增强来微调预训练的模型

在我们的最后一个模型中,我们将在预定义好的 VGG-19 模型的最后两个块中微调层的位权。我们同样引入了图像增强的概念。图像增强背后的想法和其名字一样。我们从训练数据集中载入现有图像,并且应用转换操作,例如旋转、裁剪、转换、放大缩小等等,来产生新的、改变过的版本。由于这些随机转换,我们每次获取到的图像不一样。我们将应用 tf.keras 中的一个名为 ImageDataGenerator 的优秀工具来帮助构建图像增强器。

train_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255,
                                                                zoom_range=0.05, 
                                                                rotation_range=25,
                                                                width_shift_range=0.05, 
                                                                height_shift_range=0.05, 
                                                                shear_range=0.05, horizontal_flip=True, 
                                                                fill_mode='nearest')

val_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)

# build image augmentation generators
train_generator = train_datagen.flow(train_data, train_labels_enc, batch_size=BATCH_SIZE, shuffle=True)
val_generator = val_datagen.flow(val_data, val_labels_enc, batch_size=BATCH_SIZE, shuffle=False)

我们不会对我们的验证数据集应用任何转换(除非是调整大小,因为这是必须的),因为我们将使用它评估每个纪元的模型性能。对于在传输学习环境中的图像增强的详细解释,请随时查看我上面引用的文章。让我们从一批图像增强转换中查看一些样本结果。

img_id = 0
sample_generator = train_datagen.flow(train_data[img_id:img_id+1], train_labels[img_id:img_id+1],
                                      batch_size=1)
sample = [next(sample_generator) for i in range(0,5)]
fig, ax = plt.subplots(1,5, figsize=(16, 6))
print('Labels:', [item[1][0] for item in sample])
l = [ax[i].imshow(sample[i][0][0]) for i in range(0,5)]

 title=

你可以清晰的看到与之前的输出的我们图像的轻微变化。我们现在构建我们的学习模型,确保 VGG-19 模型的最后两块是可以训练的。

vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet', 
                                        input_shape=INPUT_SHAPE)
# Freeze the layers
vgg.trainable = True

set_trainable = False
for layer in vgg.layers:
    if layer.name in ['block5_conv1', 'block4_conv1']:
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False
    
base_vgg = vgg
base_out = base_vgg.output
pool_out = tf.keras.layers.Flatten()(base_out)
hidden1 = tf.keras.layers.Dense(512, activation='relu')(pool_out)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=base_vgg.input, outputs=out)
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=1e-5),
                loss='binary_crossentropy',
                metrics=['accuracy'])

print("Total Layers:", len(model.layers))
print("Total trainable layers:", sum([1 for l in model.layers if l.trainable]))


# Output
Total Layers: 28
Total trainable layers: 16

在我们的模型中我们降低了学习率,因为我们不想在微调的时候对预训练的层做大的位权更新。模型的训练过程可能有轻微的不同,因为我们使用了数据生成器,因此我们将应用 fit_generator(...) 函数。

tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=2, min_lr=0.000001)

callbacks = [reduce_lr, tensorboard_callback]
train_steps_per_epoch = train_generator.n // train_generator.batch_size
val_steps_per_epoch = val_generator.n // val_generator.batch_size
history = model.fit_generator(train_generator, steps_per_epoch=train_steps_per_epoch, epochs=EPOCHS,
                              validation_data=val_generator, validation_steps=val_steps_per_epoch, 
                              verbose=1)


# Output
Epoch 1/25
271/271 [====] - 133s 489ms/step - loss: 0.2267 - accuracy: 0.9117 - val_loss: 0.1414 - val_accuracy: 0.9531
Epoch 2/25
271/271 [====] - 129s 475ms/step - loss: 0.1399 - accuracy: 0.9552 - val_loss: 0.1292 - val_accuracy: 0.9589
...
...
Epoch 24/25
271/271 [====] - 128s 473ms/step - loss: 0.0815 - accuracy: 0.9727 - val_loss: 0.1466 - val_accuracy: 0.9682
Epoch 25/25
271/271 [====] - 128s 473ms/step - loss: 0.0792 - accuracy: 0.9729 - val_loss: 0.1127 - val_accuracy: 0.9641

这看起来是我们的最好的模型。它给了我们近乎 96.5% 的验证精确率,基于训练精度,它看起来不像我们的第一个模型那样过拟合。这可以通过下列的学习曲线验证。

 title=

微调过的预训练 CNN 的学习曲线

让我们保存这个模型,因此我们能够在测试集上使用。

model.save('vgg_finetuned.h5')

这就完成了我们的模型训练阶段。现在我们准备好了在测试集上测试我们模型的性能。

深度学习模型性能评估

我们将通过在我们的测试集上做预测来评估我们在训练阶段构建的三个模型,因为仅仅验证是不够的!我们同样构建了一个检测工具模块叫做 model_evaluation_utils,我们可以使用相关分类指标用来评估使用我们深度学习模型的性能。第一步是扩展我们的数据集。

test_imgs_scaled = test_data / 255.
test_imgs_scaled.shape, test_labels.shape

# Output
((8268, 125, 125, 3), (8268,))

下一步包括载入我们保存的深度学习模型,在测试集上预测。

# Load Saved Deep Learning Models
basic_cnn = tf.keras.models.load_model('./basic_cnn.h5')
vgg_frz = tf.keras.models.load_model('./vgg_frozen.h5')
vgg_ft = tf.keras.models.load_model('./vgg_finetuned.h5')

# Make Predictions on Test Data
basic_cnn_preds = basic_cnn.predict(test_imgs_scaled, batch_size=512)
vgg_frz_preds = vgg_frz.predict(test_imgs_scaled, batch_size=512)
vgg_ft_preds = vgg_ft.predict(test_imgs_scaled, batch_size=512)

basic_cnn_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0 
                                                  for pred in basic_cnn_preds.ravel()])
vgg_frz_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0 
                                                  for pred in vgg_frz_preds.ravel()])
vgg_ft_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0 
                                                  for pred in vgg_ft_preds.ravel()])

下一步是应用我们的 model_evaluation_utils 模块根据相应分类指标来检查每个模块的性能。

import model_evaluation_utils as meu
import pandas as pd

basic_cnn_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=basic_cnn_pred_labels)
vgg_frz_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=vgg_frz_pred_labels)
vgg_ft_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=vgg_ft_pred_labels)

pd.DataFrame([basic_cnn_metrics, vgg_frz_metrics, vgg_ft_metrics], 
             index=['Basic CNN', 'VGG-19 Frozen', 'VGG-19 Fine-tuned'])

 title=

看起来我们的第三个模型在我们的测试集上执行的最好,给出了一个模型精确性为 96% 的 F1 得分,这非常好,与我们之前提到的研究论文和文章中的更复杂的模型相当。

总结

疟疾检测不是一个简单的过程,全球的合格人员的不足在病例诊断和治疗当中是一个严重的问题。我们研究了一个关于疟疾的有趣的真实世界的医学影像案例。利用 AI 的、易于构建的、开源的技术在检测疟疾方面可以为我们提供最先进的精确性,因此使 AI 具有社会效益。

我鼓励你查看这篇文章中提到的文章和研究论文,没有它们,我就不能形成概念并写出来。如果你对运行和采纳这些技术感兴趣,本篇文章所有的代码都可以在我的 GitHub 仓库获得。记得从官方网站下载数据。

让我们希望在健康医疗方面更多的采纳开源的 AI 能力,使它在世界范围内变得更便宜、更易用。


via: https://opensource.com/article/19/4/detecting-malaria-deep-learning

作者:Dipanjan (DJ) Sarkar 选题:lujun9972 译者:warmfrog 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我们都已经知道 SOSReport。它用来收集可用于诊断的系统信息。Redhat 的支持服务建议我们在提交案例时提供 SOSReport 来分析当前的系统状态。

它会收集全部类型的报告,以帮助用户找出问题的根本原因。我们可以轻松地提取和阅读 SOSReport,但它很难阅读。因为它的每个部分都是一个单独的文件。

那么,在 Linux 中使用语法高亮显示阅读所有这些内容的最佳方法是什么。是的,这可以通过 xsos 工具做到。

sosreport

sosreport 命令是一个从运行中的系统(尤其是 RHEL 和 OEL 系统)收集大量配置细节、系统信息和诊断信息的工具。它可以帮助技术支持工程师在很多方面分析系统。

此报告包含有关系统的大量信息,例如引导信息、文件系统、内存、主机名、已安装的 RPM、系统 IP、网络详细信息、操作系统版本、已安装的内核、已加载的内核模块、打开的文件列表、PCI 设备列表、挂载点及其细节、运行中的进程信息、进程树输出、系统路由、位于 /etc 文件夹中的所有配置文件,以及位于 /var 文件夹中的所有日志文件。

这将需要一段时间来生成报告,这取决于你的系统安装和配置。

完成后,sosreport 将在 /tmp 目录下生成一个压缩的归档文件。

xsos

xsos 是一个帮助用户轻松读取 Linux 系统上的 sosreport 的工具。另一方面,我们可以说它是 sosreport 考官。

它可以立即从 sosreport 或正在运行的系统中汇总系统信息。

xsos 将尝试简化、解析、计算并格式化来自数十个文件(和命令)的数据,以便为你提供有关系统的详细概述。

你可以通过运行以下命令立即汇总系统信息。

# curl -Lo ./xsos bit.ly/xsos-direct; chmod +x ./xsos; ./xsos -ya

如何在 Linux 上安装 xsos

我们可以使用以下两种方法轻松安装 xsos

如果你正在寻找最新的前沿版本。使用以下步骤:

# curl -Lo /usr/local/bin/xsos bit.ly/xsos-direct
# chmod +x /usr/local/bin/xsos

下面是安装 xsos 的推荐方法。它将从 rpm 文件安装 xsos

# yum install http://people.redhat.com/rsawhill/rpms/latest-rsawaroha-release.rpm
# yum install xsos

如何在 Linux 上使用 xsos

一旦通过上述方法之一安装了 xsos。只需运行 xsos 命令,不带任何选项,它们会显示有关系统的基本信息。

# xsos

OS
  Hostname: CentOS7.2daygeek.com
  Distro:   [redhat-release] CentOS Linux release 7.6.1810 (Core)
            [centos-release] CentOS Linux release 7.6.1810 (Core)
            [os-release] CentOS Linux 7 (Core) 7 (Core)
  RHN:      (missing)
  RHSM:     (missing)
  YUM:      2 enabled plugins: fastestmirror, langpacks
  Runlevel: N 5  (default graphical)
  SELinux:  enforcing  (default enforcing)
  Arch:     mach=x86_64  cpu=x86_64  platform=x86_64
  Kernel:
    Booted kernel:  3.10.0-957.el7.x86_64
    GRUB default:   3.10.0-957.el7.x86_64
    Build version:
      Linux version 3.10.0-957.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red
      Hat 4.8.5-36) (GCC) ) #1 SMP Thu Nov 8 23:39:32 UTC 2018
    Booted kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    GRUB default kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    Taint-check: 0  (kernel untainted)
    - - - - - - - - - - - - - - - - - - -
  Sys time:  Sun May 12 10:05:21 CDT 2019
  Boot time: Sun May 12 09:50:20 CDT 2019  (epoch: 1557672620)
  Time Zone: America/Chicago
  Uptime:    15 min,  1 user
  LoadAvg:   [1 CPU] 0.00 (0%), 0.04 (4%), 0.09 (9%)
  /proc/stat:
    procs_running: 2   procs_blocked: 0    processes [Since boot]: 6423
    cpu [Utilization since boot]:
      us 1%, ni 0%, sys 1%, idle 99%, iowait 0%, irq 0%, sftirq 0%, steal 0%

如何使用 xsos 命令在 Linux 中查看生成的 SOSReport 输出?

我们需要份 SOSReport 以使用 xsos 命令进一步阅读。

是的,我已经生成了一个 SOSReport,文件如下。

# ls -lls -lh /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa.tar.xz
9.8M -rw-------. 1 root root 9.8M May 12 10:13 /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa.tar.xz

运行如下命令解开它。

# tar xf sosreport-CentOS7-01-1005-2019-05-12-pomeqsa.tar.xz

要查看全部信息,带上 -a--all 开关运行 xsos

# xsos --all /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa

要查看 BIOS 信息,带上 -b--bios 开关运行 xsos

# xsos --bios /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
DMIDECODE
  BIOS:
    Vend: innotek GmbH
    Vers: VirtualBox
    Date: 12/01/2006
    BIOS Rev:
    FW Rev:
  System:
    Mfr:  innotek GmbH
    Prod: VirtualBox
    Vers: 1.2
    Ser:  0
    UUID: 002f47b8-2af2-48f5-be1d-67b67e03514c
  CPU:
    0 of 0 CPU sockets populated, 0 cores/0 threads per CPU
    0 total cores, 0 total threads
    Mfr:
    Fam:
    Freq:
    Vers:
  Memory:
    Total: 0 MiB (0 GiB)
    DIMMs: 0 of 0 populated
    MaxCapacity: 0 MiB (0 GiB / 0.00 TiB)

要查看系统基本信息,如主机名、发行版、SELinux、内核信息、正常运行时间等,请使用 -o--os 开关运行 xsos

# xsos --os /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
OS
  Hostname: CentOS7.2daygeek.com
  Distro:   [redhat-release] CentOS Linux release 7.6.1810 (Core)
            [centos-release] CentOS Linux release 7.6.1810 (Core)
            [os-release] CentOS Linux 7 (Core) 7 (Core)
  RHN:      (missing)
  RHSM:     (missing)
  YUM:      2 enabled plugins: fastestmirror, langpacks
  SELinux:  enforcing  (default enforcing)
  Arch:     mach=x86_64  cpu=x86_64  platform=x86_64
  Kernel:
    Booted kernel:  3.10.0-957.el7.x86_64
    GRUB default:   3.10.0-957.el7.x86_64
    Build version:
      Linux version 3.10.0-957.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red
      Hat 4.8.5-36) (GCC) ) #1 SMP Thu Nov 8 23:39:32 UTC 2018
    Booted kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    GRUB default kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    Taint-check: 536870912  (see https://access.redhat.com/solutions/40594)
      29  TECH_PREVIEW: Technology Preview code is loaded
    - - - - - - - - - - - - - - - - - - -
  Sys time:  Sun May 12 10:12:22 CDT 2019
  Boot time: Sun May 12 09:50:20 CDT 2019  (epoch: 1557672620)
  Time Zone: America/Chicago
  Uptime:    22 min,  1 user
  LoadAvg:   [1 CPU] 1.19 (119%), 0.27 (27%), 0.14 (14%)
  /proc/stat:
    procs_running: 8   procs_blocked: 2    processes [Since boot]: 9005
    cpu [Utilization since boot]:
      us 1%, ni 0%, sys 1%, idle 99%, iowait 0%, irq 0%, sftirq 0%, steal 0%

要查看 kdump 配置,请使用 -k--kdump 开关运行 xsos

# xsos --kdump /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
KDUMP CONFIG
  kexec-tools rpm version:
    kexec-tools-2.0.15-21.el7.x86_64
  Service enablement:
    UNIT           STATE
    kdump.service  enabled
  kdump initrd/initramfs:
    13585734 Feb 19 05:51 initramfs-3.10.0-957.el7.x86_64kdump.img
  Memory reservation config:
    /proc/cmdline { crashkernel=auto }
    GRUB default  { crashkernel=auto }
  Actual memory reservation per /proc/iomem:
      2a000000-340fffff : Crash kernel
  kdump.conf:
    path /var/crash
    core_collector makedumpfile -l --message-level 1 -d 31
  kdump.conf "path" available space:
    System MemTotal (uncompressed core size) { 1.80 GiB }
    Available free space on target path's fs { 22.68 GiB }  (fs=/)
  Panic sysctls:
    kernel.sysrq [bitmask] =  "16"  (see proc man page)
    kernel.panic [secs] =  0  (no autoreboot on panic)
    kernel.hung_task_panic =  0
    kernel.panic_on_oops =  1
    kernel.panic_on_io_nmi =  0
    kernel.panic_on_unrecovered_nmi =  0
    kernel.panic_on_stackoverflow =  0
    kernel.softlockup_panic =  0
    kernel.unknown_nmi_panic =  0
    kernel.nmi_watchdog =  1
    vm.panic_on_oom [0-2] =  0  (no panic)

要查看有关 CPU 的信息,请使用 -c--cpu 开关运行 xsos

# xsos --cpu /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
CPU
  1 logical processors
  1 Intel Core i7-6700HQ CPU @ 2.60GHz (flags: aes,constant_tsc,ht,lm,nx,pae,rdrand)

要查看内存利用情况,请使用 -m--mem 开关运行 xsos

# xsos --mem /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
MEMORY
  Stats graphed as percent of MemTotal:
    MemUsed    ▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊.....................  58.8%
    Buffers    ..................................................   0.6%
    Cached     ▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊...................................  29.9%
    HugePages  ..................................................   0.0%
    Dirty      ..................................................   0.7%
  RAM:
    1.8 GiB total ram
    1.1 GiB (59%) used
    0.5 GiB (28%) used excluding Buffers/Cached
    0.01 GiB (1%) dirty
  HugePages:
    No ram pre-allocated to HugePages
  LowMem/Slab/PageTables/Shmem:
    0.09 GiB (5%) of total ram used for Slab
    0.02 GiB (1%) of total ram used for PageTables
    0.01 GiB (1%) of total ram used for Shmem
  Swap:
    0 GiB (0%) used of 2 GiB total

要查看添加的磁盘信息,请使用 -d-disks 开关运行 xsos

# xsos --disks /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
STORAGE
  Whole Disks from /proc/partitions:
    2 disks, totaling 40 GiB (0.04 TiB)
    - - - - - - - - - - - - - - - - - - - - -
    Disk    Size in GiB
    ----    -----------
    sda     30
    sdb     10

要查看网络接口配置,请使用 -e--ethtool 开关运行 xsos

# xsos --ethtool /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
ETHTOOL
  Interface Status:
    enp0s10     0000:00:0a.0  link=up 1000Mb/s full (autoneg=Y)  rx ring 256/4096  drv e1000 v7.3.21-k8-NAPI / fw UNKNOWN
    enp0s9      0000:00:09.0  link=up 1000Mb/s full (autoneg=Y)  rx ring 256/4096  drv e1000 v7.3.21-k8-NAPI / fw UNKNOWN
    virbr0      N/A           link=DOWN                          rx ring UNKNOWN   drv bridge v2.3 / fw N/A
    virbr0-nic  tap           link=DOWN                          rx ring UNKNOWN   drv tun v1.6 / fw UNKNOWN

要查看有关 IP 地址的信息,请使用 -i--ip 开关运行 xsos

# xsos --ip /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
IP4
  Interface   Master IF  MAC Address        MTU     State  IPv4 Address
  =========   =========  =================  ======  =====  ==================
  lo          -          -                  65536   up     127.0.0.1/8
  enp0s9      -          08:00:27:0b:bc:e9  1500    up     192.168.1.8/24
  enp0s10     -          08:00:27:b2:08:91  1500    up     192.168.1.9/24
  virbr0      -          52:54:00:ae:01:94  1500    up     192.168.122.1/24
  virbr0-nic  virbr0     52:54:00:ae:01:94  1500    DOWN   -

IP6
  Interface   Master IF  MAC Address        MTU     State  IPv6 Address                                 Scope
  =========   =========  =================  ======  =====  ===========================================  =====
  lo          -          -                  65536   up     ::1/128                                      host
  enp0s9      -          08:00:27:0b:bc:e9  1500    up     fe80::945b:8333:f4bc:9723/64                 link
  enp0s10     -          08:00:27:b2:08:91  1500    up     fe80::7ed4:1fab:23c3:3790/64                 link
  virbr0      -          52:54:00:ae:01:94  1500    up     -                                            -
  virbr0-nic  virbr0     52:54:00:ae:01:94  1500    DOWN   -                                            -

要通过 ps 查看正在运行的进程,请使用 -p--ps 开关运行 xsos

# xsos --ps /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
PS CHECK
  Total number of threads/processes:
    501 / 171
  Top users of CPU & MEM:
    USER     %CPU   %MEM   RSS
    root     20.6%  14.1%  0.30 GiB
    gdm      0.3%   16.8%  0.33 GiB
    postfix  0.0%   0.6%   0.01 GiB
    polkitd  0.0%   0.6%   0.01 GiB
    daygeek  0.0%   0.2%   0.00 GiB
    colord   0.0%   0.4%   0.01 GiB
  Uninteruptible sleep threads/processes (0/0):
    [None]
  Defunct zombie threads/processes (0/0):
    [None]
  Top CPU-using processes:
    USER      PID   %CPU  %MEM  VSZ-MiB  RSS-MiB  TTY    STAT  START  TIME  COMMAND
    root      6542  15.6  4.2   875      78       pts/0  Sl+   10:11  0:07  /usr/bin/python /sbin/sosreport
    root      7582  3.0   0.1   10       2        pts/0  S     10:12  0:00  /bin/bash /usr/sbin/dracut --print-cmdline
    root      7969  0.7   0.1   95       4        ?      Ss    10:12  0:00  /usr/sbin/certmonger -S -p
    root      7889  0.4   0.2   24       4        ?      Ss    10:12  0:00  /usr/lib/systemd/systemd-hostnamed
    gdm       3866  0.3   7.1   2856     131      ?      Sl    09:50  0:04  /usr/bin/gnome-shell
    root      8553  0.2   0.1   47       3        ?      S     10:12  0:00  /usr/lib/systemd/systemd-udevd
    root      6971  0.2   0.4   342      9        ?      Sl    10:12  0:00  /usr/sbin/abrt-dbus -t133
    root      3200  0.2   0.9   982      18       ?      Ssl   09:50  0:02  /usr/sbin/libvirtd
    root      2855  0.1   0.1   88       3        ?      Ss    09:50  0:01  /sbin/rngd -f
    rtkit     2826  0.0   0.0   194      2        ?      SNsl  09:50  0:00  /usr/libexec/rtkit-daemon
  Top MEM-using processes:
    USER      PID   %CPU  %MEM  VSZ-MiB  RSS-MiB  TTY    STAT  START  TIME  COMMAND
    gdm       3866  0.3   7.1   2856     131      ?      Sl    09:50  0:04  /usr/bin/gnome-shell
    root      6542  15.6  4.2   875      78       pts/0  Sl+   10:11  0:07  /usr/bin/python /sbin/sosreport
    root      3264  0.0   1.2   271      23       tty1   Ssl+  09:50  0:00  /usr/bin/X :0 -background
    root      3200  0.2   0.9   982      18       ?      Ssl   09:50  0:02  /usr/sbin/libvirtd
    root      3189  0.0   0.9   560      17       ?      Ssl   09:50  0:00  /usr/bin/python2 -Es /usr/sbin/tuned
    gdm       4072  0.0   0.9   988      17       ?      Sl    09:50  0:00  /usr/libexec/gsd-media-keys
    gdm       4076  0.0   0.8   625      16       ?      Sl    09:50  0:00  /usr/libexec/gsd-power
    gdm       4056  0.0   0.8   697      16       ?      Sl    09:50  0:00  /usr/libexec/gsd-color
    root      2853  0.0   0.7   622      14       ?      Ssl   09:50  0:00  /usr/sbin/NetworkManager --no-daemon
    gdm       4110  0.0   0.7   544      14       ?      Sl    09:50  0:00  /usr/libexec/gsd-wacom
  Top thread-spawning processes:
    #   USER      PID   %CPU  %MEM  VSZ-MiB  RSS-MiB  TTY    STAT  START  TIME  COMMAND
    17  root      3200  0.2   0.9   982      18       ?      -     09:50  0:02  /usr/sbin/libvirtd
    12  root      6542  16.1  4.5   876      83       pts/0  -     10:11  0:07  /usr/bin/python /sbin/sosreport
    10  gdm       3866  0.3   7.1   2856     131      ?      -     09:50  0:04  /usr/bin/gnome-shell
    7   polkitd   2864  0.0   0.6   602      13       ?      -     09:50  0:01  /usr/lib/polkit-1/polkitd --no-debug
    6   root      2865  0.0   0.0   203      1        ?      -     09:50  0:00  /usr/sbin/gssproxy -D
    5   root      3189  0.0   0.9   560      17       ?      -     09:50  0:00  /usr/bin/python2 -Es /usr/sbin/tuned
    5   root      2823  0.0   0.3   443      6        ?      -     09:50  0:00  /usr/libexec/udisks2/udisksd
    5   gdm       4102  0.0   0.2   461      5        ?      -     09:50  0:00  /usr/libexec/gsd-smartcard
    4   root      3215  0.0   0.2   470      4        ?      -     09:50  0:00  /usr/sbin/gdm
    4   gdm       4106  0.0   0.2   444      5        ?      -     09:50  0:00  /usr/libexec/gsd-sound

via: https://www.2daygeek.com/xsos-a-tool-to-read-sosreport-in-linux/

作者:Magesh Maruthamuthu 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在这篇树莓派文章中,你将学到如何在树莓派中启用 SSH 以及之后如何通过 SSH 进入树莓派。

在你可以用树莓派做的所有事情中,将其作为一个家庭网络的服务器是十分流行的做法。小体积与低功耗使它成为运行轻量级服务器的完美设备。

在这种情况下你做得到的事情之一是能够每次在树莓派上无须接上显示器、键盘、鼠标以及走到放置你的树莓派的地方就可以运行指令。

你可以从其它任意电脑、笔记本、台式机甚至你的手机通过 SSH(Secure Shell)登入你的树莓派来做到这一点。让我展示给你看:

如何 SSH 进入树莓派

我假设你已经在你的树莓派上运行 Raspbian 并已经成功通过有线或者无线网连进网络了。你的树莓派接入网络这点是很重要的,否则你无法通过 SSH 连接树莓派(抱歉说出这种显而易见的事实)。

步骤一:在树莓派上启用 SSH

SSH 在树莓派上是默认关闭的,因此在你安装好全新的 Raspbian 后打开树莓派时,你需要启用它。

首先通过菜单进入树莓派的配置界面。

树莓派菜单,树莓派配置

现在进入 接口 interfaces 标签,启动 SSH 并重启你的树莓派。

在树莓派上启动 SSH

你也可以通过终端直接启动 SSH。仅需输入命令 sudo raspi-config 然后进入高级设置以启用 SSH。

步骤二: 找到树莓派的 IP 地址

在大多数情况下,你的树莓派会被分配一个看起来长得像 192.168.x.x 或者 10.x.x.x 的本地 IP 地址。你可以使用多种 Linux 命令来找到 IP 地址

我在这使用古老而好用的 ifconfig 命令,但是你也可以使用 ip address

ifconfig

树莓派网络配置

这行命令展现了所有活跃中的网络适配器以及其配置的列表。第一个条目(eth0)展示了例如192.168.2.105 的有效 IP 地址。我用有线网将我的树莓派连入网络,因此这里显示的是 eth0。如果你用无线网的话在叫做 wlan0 的条目下查看。

你也可以用其他方法例如查看你的路由器或者调制解调器的网络设备表以找到 IP 地址。

步骤三:SSH 进你的树莓派

既然你已经启用了 SSH 功能并且找到了 IP 地址,你可以从任何电脑 SSH 进入你的树莓派。你同样需要树莓派的用户名和密码。

默认用户名和密码是:

  • 用户名:pi
  • 密码:raspberry

如果你已改变了默认的密码,那就使用新的而不是以上的密码。理想状态下你必须改变默认的密码。在过去,有一款恶意软件感染数千使用默认用户名和密码的树莓派设备

(在 Mac 或 Linux 上)从你想要 SSH 进树莓派的电脑上打开终端输入以下命令,在 Windows 上,你可以用类似 Putty 的 SSH 客户端。

这里,使用你在之前步骤中找到的 IP 地址。

ssh [受保护的邮件]
注意: 确保你的树莓派和你用来 SSH 进入树莓派的电脑接入了同一个网络。

通过命令行 SSH

第一次你会看到一个警告,输入 yes 并按下回车。

输入密码 (默认是 ‘raspberry‘)

现在,输入密码按下回车。

成功通过 SSH 登入

成功登入你将会看到树莓派的终端。现在你可以通过这个终端无需物理上访问你的树莓派就可以远程(在当前网络内)在它上面运行指令。

在此之上你也可以设置 SSH 密钥这样每次通过 SSH 登入时就可以无需输入密码,但那完全是另一个话题了。

我希望你通过跟着这个教程已能够 SSH 进入你的树莓派。在下方评论中让我知道你打算用你的树莓派做些什么!


via: https://itsfoss.com/ssh-into-raspberry/

作者:Chinmay 选题:lujun9972 译者:tomjlw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在我们覆盖 7 个 PyPI 库的系列文章中了解更多解决 Python 问题的信息。

Python 是当今使用最多流行的编程语言之一,因为:它是开源的,它具有广泛的用途(例如 Web 编程、业务应用、游戏、科学编程等等),它有一个充满活力和专注的社区支持它。这个社区是我们在 Python Package Index(PyPI)中提供如此庞大、多样化的软件包的原因,用以扩展和改进 Python。并解决不可避免的问题。

在本系列中,我们将介绍七个可以帮助你解决常见 Python 问题的 PyPI 库。今天,我们将研究 singledispatch,这是一个能让你追溯地向 Python 库添加方法的库。

singledispatch

想象一下,你有一个有 Circle、Square 等类的“形状”库。

Circle 类有半径、Square 有边、Rectangle 有高和宽。我们的库已经存在,我们不想改变它。

然而,我们想给库添加一个面积计算。如果我们不会和其他人共享这个库,我们只需添加 area 方法,这样我们就能调用 shape.area() 而无需关心是什么形状。

虽然可以进入类并添加一个方法,但这是一个坏主意:没有人希望他们的类会被添加新的方法,程序会因奇怪的方式出错。

相反,functools 中的 singledispatch 函数可以帮助我们。

@singledispatch
def get_area(shape):
    raise NotImplementedError("cannot calculate area for unknown shape",
                              shape)

get_area 函数的“基类”实现会报错。这保证了如果我们出现一个新的形状时,我们会明确地报错而不是返回一个无意义的结果。

@get_area.register(Square)
def _get_area_square(shape):
    return shape.side ** 2
@get_area.register(Circle)
def _get_area_circle(shape):
    return math.pi * (shape.radius ** 2)

这种方式的好处是如果某人写了一个匹配我们代码的形状,它们可以自己实现 get_area

from area_calculator import get_area

@attr.s(auto_attribs=True, frozen=True)
class Ellipse:
    horizontal_axis: float
    vertical_axis: float

@get_area.register(Ellipse)
def _get_area_ellipse(shape):
    return math.pi * shape.horizontal_axis * shape.vertical_axis

调用 get_area 很直接。

print(get_area(shape))

这意味着我们可以将大量的 if isintance()/elif isinstance() 的代码以这种方式修改,而无需修改接口。下一次你要修改 if isinstance,你试试 `singledispatch!

在本系列的下一篇文章中,我们将介绍 tox,一个用于自动化 Python 代码测试的工具。

回顾本系列的前几篇文章:


via: https://opensource.com/article/19/5/python-singledispatch

作者:Moshe Zadka 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linux 容器已经成为一个热门话题,保证容器镜像较小被认为是一个好习惯。本文提供了有关如何构建较小 Fedora 容器镜像的一些技巧。

microdnf

Fedora 的 DNF 是用 Python 编写的,因为它有各种各样的插件,因此它的设计是可扩展的。但是 有一个 Fedora 基本容器镜像替代品,它使用一个较小的名为 microdnf 的包管理器,使用 C 编写。要在 Dockerfile 中使用这个最小的镜像,FROM 行应该如下所示:

FROM registry.fedoraproject.org/fedora-minimal:30

如果你的镜像不需要像 Python 这样的典型 DNF 依赖项,例如,如果你在制作 NodeJS 镜像时,那么这是一个重要的节省项。

在一个层中安装和清理

为了节省空间,使用 dnf clean all 或其 microdnf 等效的 microdnf clean all 删除仓库元数据非常重要。但是你不应该分两步执行此操作,因为这实际上会将这些文件保存在容器镜像中,然后在另一层中将其标记为删除。要正确地执行此操作,你应该像这样一步完成安装和清理:

FROM registry.fedoraproject.org/fedora-minimal:30
RUN microdnf install nodejs && microdnf clean all

使用 microdnf 进行模块化

模块化是一种给你选择不同堆栈版本的方法。例如,你可能需要在项目中用非 LTS 的 NodeJS v11,旧的 LTS NodeJS v8 用于另一个,最新的 LTS NodeJS v10 用于另一个。你可以使用冒号指定流。

# dnf module list
# dnf module install nodejs:8

dnf module install 命令意味着两个命令,一个启用流,另一个是从它安装 nodejs。

# dnf module enable nodejs:8
# dnf install nodejs

尽管 microdnf 不提供与模块化相关的任何命令,但是可以启用带有配置文件的模块,并且 libdnf(被 microdnf 使用)似乎支持模块化流。该文件看起来像这样:

/etc/dnf/modules.d/nodejs.module
[nodejs]
name=nodejs
stream=8
profiles=
state=enabled

使用模块化的 microdnf 的完整 Dockerfile 如下所示:

FROM registry.fedoraproject.org/fedora-minimal:30
RUN \
   echo -e "[nodejs]\nname=nodejs\nstream=8\nprofiles=\nstate=enabled\n" > /etc/dnf/modules.d/nodejs.module && \
   microdnf install nodejs zopfli findutils busybox && \
   microdnf clean all

多阶段构建

在许多情况下,你可能需要大量的无需用于运行软件的构建时依赖项,例如构建一个静态链接依赖项的 Go 二进制文件。多阶段构建是分离应用构建和应用运行时的有效方法。

例如,下面的 Dockerfile 构建了一个 Go 应用 confd

# building container
FROM registry.fedoraproject.org/fedora-minimal AS build
RUN mkdir /go && microdnf install golang && microdnf clean all
WORKDIR /go
RUN export GOPATH=/go; CGO_ENABLED=0 go get github.com/kelseyhightower/confd

FROM registry.fedoraproject.org/fedora-minimal
WORKDIR /
COPY --from=build /go/bin/confd /usr/local/bin
CMD ["confd"]

通过在 FROM 指令之后添加 AS 并从基本容器镜像中添加另一个 FROM 然后使用 COPY --from= 指令将内容从构建的容器复制到第二个容器来完成多阶段构建。

可以使用 podman 构建并运行此 Dockerfile:

$ podman build -t myconfd .
$ podman run -it myconfd

via: https://fedoramagazine.org/building-smaller-container-images/

作者:Muayyad Alsadi 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

虚拟文件系统是一种神奇的抽象,它使得 “一切皆文件” 哲学在 Linux 中成为了可能。

什么是文件系统?根据早期的 Linux 贡献者和作家 Robert Love 所说,“文件系统是一个遵循特定结构的数据的分层存储。” 不过,这种描述也同样适用于 VFAT( 虚拟文件分配表 Virtual File Allocation Table )、Git 和Cassandra(一种 NoSQL 数据库)。那么如何区别文件系统呢?

文件系统基础概念

Linux 内核要求文件系统必须是实体,它还必须在持久对象上实现 open()read()write() 方法,并且这些实体需要有与之关联的名字。从 面向对象编程 的角度来看,内核将通用文件系统视为一个抽象接口,这三大函数是“虚拟”的,没有默认定义。因此,内核的默认文件系统实现被称为虚拟文件系统(VFS)。

 title=

如果我们能够 open()read()write(),它就是一个文件,如这个主控台会话所示。

VFS 是著名的类 Unix 系统中 “一切皆文件” 概念的基础。让我们看一下它有多奇怪,上面的小小演示体现了字符设备 /dev/console 实际的工作。该图显示了一个在虚拟电传打字控制台(tty)上的交互式 Bash 会话。将一个字符串发送到虚拟控制台设备会使其显示在虚拟屏幕上。而 VFS 甚至还有其它更奇怪的属性。例如,它可以在其中寻址

我们熟悉的文件系统如 ext4、NFS 和 /proc 都在名为 file\_operations 的 C 语言数据结构中提供了三大函数的定义。此外,个别的文件系统会以熟悉的面向对象的方式扩展和覆盖了 VFS 功能。正如 Robert Love 指出的那样,VFS 的抽象使 Linux 用户可以轻松地将文件复制到(或复制自)外部操作系统或抽象实体(如管道),而无需担心其内部数据格式。在用户空间这一侧,通过系统调用,进程可以使用文件系统方法之一 read() 从文件复制到内核的数据结构中,然后使用另一种文件系统的方法 write() 输出数据。

属于 VFS 基本类型的函数定义本身可以在内核源代码的 fs/*.c 文件 中找到,而 fs/ 的子目录中包含了特定的文件系统。内核还包含了类似文件系统的实体,例如 cgroup、/dev 和 tmpfs,在引导过程的早期需要它们,因此定义在内核的 init/ 子目录中。请注意,cgroup、/dev 和 tmpfs 不会调用 file_operations 的三大函数,而是直接读取和写入内存。

下图大致说明了用户空间如何访问通常挂载在 Linux 系统上的各种类型文件系统。像管道、dmesg 和 POSIX 时钟这样的结构在此图中未显示,它们也实现了 struct file_operations,而且其访问也要通过 VFS 层。

 title=

VFS 是个“垫片层”,位于系统调用和特定 file_operations 的实现(如 ext4 和 procfs)之间。然后,file_operations 函数可以与特定于设备的驱动程序或内存访问器进行通信。tmpfs、devtmpfs 和 cgroup 不使用 file_operations 而是直接访问内存。

VFS 的存在促进了代码重用,因为与文件系统相关的基本方法不需要由每种文件系统类型重新实现。代码重用是一种被广泛接受的软件工程最佳实践!唉,但是如果重用的代码引入了严重的错误,那么继承常用方法的所有实现都会受到影响。

/tmp:一个小提示

找出系统中存在的 VFS 的简单方法是键入 mount | grep -v sd | grep -v :/,在大多数计算机上,它将列出所有未驻留在磁盘上,同时也不是 NFS 的已挂载文件系统。其中一个列出的 VFS 挂载肯定是 /tmp,对吧?

 title=

谁都知道把 /tmp 放在物理存储设备上简直是疯了!图片:https://tinyurl.com/ybomxyfo

为什么把 /tmp 留在存储设备上是不可取的?因为 /tmp 中的文件是临时的(!),并且存储设备比内存慢,所以创建了 tmpfs 这种文件系统。此外,比起内存,物理设备频繁写入更容易磨损。最后,/tmp 中的文件可能包含敏感信息,因此在每次重新启动时让它们消失是一项功能。

不幸的是,默认情况下,某些 Linux 发行版的安装脚本仍会在存储设备上创建 /tmp。如果你的系统出现这种情况,请不要绝望。按照一直优秀的 Arch Wiki 上的简单说明来解决问题就行,记住分配给 tmpfs 的内存就不能用于其他目的了。换句话说,包含了大文件的庞大的 tmpfs 可能会让系统耗尽内存并崩溃。

另一个提示:编辑 /etc/fstab 文件时,请务必以换行符结束,否则系统将无法启动。(猜猜我怎么知道。)

/proc 和 /sys

除了 /tmp 之外,大多数 Linux 用户最熟悉的 VFS 是 /proc/sys。(/dev 依赖于共享内存,而没有 file_operations 结构)。为什么有两种呢?让我们来看看更多细节。

procfs 为用户空间提供了内核及其控制的进程的瞬时状态的快照。在 /proc 中,内核发布有关其提供的设施的信息,如中断、虚拟内存和调度程序。此外,/proc/sys 是存放可以通过 sysctl 命令配置的设置的地方,可供用户空间访问。单个进程的状态和统计信息在 /proc/<PID> 目录中报告。

 title=

/proc/meminfo 是一个空文件,但仍包含有价值的信息。

/proc 文件的行为说明了 VFS 可以与磁盘上的文件系统不同。一方面,/proc/meminfo 包含了可由命令 free 展现出来的信息。另一方面,它还是空的!怎么会这样?这种情况让人联想起康奈尔大学物理学家 N. David Mermin 在 1985 年写的一篇名为《没有人看见月亮的情况吗?现实和量子理论》。事实是当进程从 /proc 请求数据时内核再收集有关内存的统计信息,而且当没有人查看它时,/proc 中的文件实际上没有任何内容。正如 Mermin 所说,“这是一个基本的量子学说,一般来说,测量不会揭示被测属性的预先存在的价值。”(关于月球的问题的答案留作练习。)

 title=

当没有进程访问它们时,/proc 中的文件为空。(来源

procfs 的空文件是有道理的,因为那里可用的信息是动态的。sysfs 的情况则不同。让我们比较一下 /proc/sys 中不为空的文件数量。

procfs 只有一个不为空的文件,即导出的内核配置,这是一个例外,因为每次启动只需要生成一次。另一方面,/sys 有许多更大一些的文件,其中大多数由一页内存组成。通常,sysfs 文件只包含一个数字或字符串,与通过读取 /proc/meminfo 等文件生成的信息表格形成鲜明对比。

sysfs 的目的是将内核称为 “kobject” 的可读写属性公开给用户空间。kobject 的唯一目的是引用计数:当删除对 kobject 的最后一个引用时,系统将回收与之关联的资源。然而,/sys 构成了内核著名的“到用户空间的稳定 ABI”,它的大部分内容在任何情况下都没有人能“破坏”。但这并不意味着 sysfs 中的文件是静态,这与易失性对象的引用计数相反。

内核的稳定 ABI 限制了 /sys 中可能出现的内容,而不是任何给定时刻实际存在的内容。列出 sysfs 中文件的权限可以了解如何设置或读取设备、模块、文件系统等的可配置、可调参数。逻辑上强调 procfs 也是内核稳定 ABI 的一部分的结论,尽管内核的文档没有明确说明。

 title=

sysfs 中的文件确切地描述了实体的每个属性,并且可以是可读的、可写的,或两者兼而有之。文件中的“0”表示 SSD 不可移动的存储设备。

用 eBPF 和 bcc 工具一窥 VFS 内部

了解内核如何管理 sysfs 文件的最简单方法是观察它的运行情况,在 ARM64 或 x86\_64 上观看的最简单方法是使用 eBPF。eBPF( 扩展的伯克利数据包过滤器 extended Berkeley Packet Filter )由在内核中运行的虚拟机组成,特权用户可以从命令行进行查询。内核源代码告诉读者内核可以做什么;而在一个启动的系统上运行 eBPF 工具会显示内核实际上做了什么。

令人高兴的是,通过 bcc 工具入门使用 eBPF 非常容易,这些工具在主要 Linux 发行版的软件包 中都有,并且已经由 Brendan Gregg 给出了充分的文档说明。bcc 工具是带有小段嵌入式 C 语言片段的 Python 脚本,这意味着任何对这两种语言熟悉的人都可以轻松修改它们。据当前统计,bcc/tools 中有 80 个 Python 脚本,使得系统管理员或开发人员很有可能能够找到与她/他的需求相关的已有脚本。

要了解 VFS 在正在运行中的系统上的工作情况,请尝试使用简单的 vfscountvfsstat 脚本,这可以看到每秒都会发生数十次对 vfs_open() 及其相关的调用。

 title=

vfsstat.py 是一个带有嵌入式 C 片段的 Python 脚本,它只是计数 VFS 函数调用。

作为一个不太重要的例子,让我们看一下在运行的系统上插入 USB 记忆棒时 sysfs 中会发生什么。

 title=

用 eBPF 观察插入 USB 记忆棒时 /sys 中会发生什么,简单的和复杂的例子。

在上面的第一个简单示例中,只要 sysfs_create_files() 命令运行,trace.py bcc 工具脚本就会打印出一条消息。我们看到 sysfs_create_files() 由一个 kworker 线程启动,以响应 USB 棒的插入事件,但是它创建了什么文件?第二个例子说明了 eBPF 的强大能力。这里,trace.py 正在打印内核回溯(-K 选项)以及 sysfs_create_files() 创建的文件的名称。单引号内的代码段是一些 C 源代码,包括一个易于识别的格式字符串,所提供的 Python 脚本引入 LLVM 即时编译器(JIT) 来在内核虚拟机内编译和执行它。必须在第二个命令中重现完整的 sysfs_create_files() 函数签名,以便格式字符串可以引用其中一个参数。在此 C 片段中出错会导致可识别的 C 编译器错误。例如,如果省略 -I 参数,则结果为“无法编译 BPF 文本”。熟悉 C 或 Python 的开发人员会发现 bcc 工具易于扩展和修改。

插入 USB 记忆棒后,内核回溯显示 PID 7711 是一个 kworker 线程,它在 sysfs 中创建了一个名为 events 的文件。使用 sysfs_remove_files() 进行相应的调用表明,删除 USB 记忆棒会导致删除该 events 文件,这与引用计数的想法保持一致。在 USB 棒插入期间(未显示)在 eBPF 中观察 sysfs_create_link() 表明创建了不少于 48 个符号链接。

无论如何,events 文件的目的是什么?使用 cscope 查找函数 __device_add_disk() 显示它调用 disk_add_events(),并且可以将 “mediachange” 或 “ejectrequest” 写入到该文件。这里,内核的块层通知用户空间该 “磁盘” 的出现和消失。考虑一下这种检查 USB 棒的插入的工作原理的方法与试图仅从源头中找出该过程的速度有多快。

只读根文件系统使得嵌入式设备成为可能

确实,没有人通过拔出电源插头来关闭服务器或桌面系统。为什么?因为物理存储设备上挂载的文件系统可能有挂起的(未完成的)写入,并且记录其状态的数据结构可能与写入存储器的内容不同步。当发生这种情况时,系统所有者将不得不在下次启动时等待 fsck 文件系统恢复工具 运行完成,在最坏的情况下,实际上会丢失数据。

然而,狂热爱好者会听说许多物联网和嵌入式设备,如路由器、恒温器和汽车现在都运行着 Linux。许多这些设备几乎完全没有用户界面,并且没有办法干净地让它们“解除启动”。想一想启动电池耗尽的汽车,其中运行 Linux 的主机设备 的电源会不断加电断电。当引擎最终开始运行时,系统如何在没有长时间 fsck 的情况下启动呢?答案是嵌入式设备依赖于只读根文件系统(简称 ro-rootfs)。

 title=

ro-rootfs 是嵌入式系统不经常需要 fsck 的原因。 来源:https://tinyurl.com/yxoauoub

ro-rootfs 提供了许多优点,虽然这些优点不如耐用性那么显然。一个是,如果 Linux 进程不可以写入,那么恶意软件也无法写入 /usr/lib。另一个是,基本上不可变的文件系统对于远程设备的现场支持至关重要,因为支持人员拥有理论上与现场相同的本地系统。也许最重要(但也是最微妙)的优势是 ro-rootfs 迫使开发人员在项目的设计阶段就决定好哪些系统对象是不可变的。处理 ro-rootfs 可能经常是不方便甚至是痛苦的,编程语言中的常量变量经常就是这样,但带来的好处很容易偿还这种额外的开销。

对于嵌入式开发人员,创建只读根文件系统确实需要做一些额外的工作,而这正是 VFS 的用武之地。Linux 需要 /var 中的文件可写,此外,嵌入式系统运行的许多流行应用程序会尝试在 $HOME 中创建配置的点文件。放在家目录中的配置文件的一种解决方案通常是预生成它们并将它们构建到 rootfs 中。对于 /var,一种方法是将其挂载在单独的可写分区上,而 / 本身以只读方式挂载。使用绑定或叠加挂载是另一种流行的替代方案。

绑定和叠加挂载以及在容器中的使用

运行 man mount 是了解 绑定挂载 bind mount 叠加挂载 overlay mount 的最好办法,这种方法使得嵌入式开发人员和系统管理员能够在一个路径位置创建文件系统,然后以另外一个路径将其提供给应用程序。对于嵌入式系统,这代表着可以将文件存储在 /var 中的不可写闪存设备上,但是在启动时将 tmpfs 中的路径叠加挂载或绑定挂载到 /var 路径上,这样应用程序就可以在那里随意写它们的内容了。下次加电时,/var 中的变化将会消失。叠加挂载为 tmpfs 和底层文件系统提供了联合,允许对 ro-rootfs 中的现有文件进行直接修改,而绑定挂载可以使新的空 tmpfs 目录在 ro-rootfs 路径中显示为可写。虽然叠加文件系统是一种适当的文件系统类型,而绑定挂载由 VFS 命名空间工具 实现的。

根据叠加挂载和绑定挂载的描述,没有人会对 Linux 容器 中大量使用它们感到惊讶。让我们通过运行 bcc 的 mountsnoop 工具监视当使用 systemd-nspawn 启动容器时会发生什么:

 title=

在 mountsnoop.py 运行的同时,system-nspawn 调用启动容器。

让我们看看发生了什么:

 title=

在容器 “启动” 期间运行 mountsnoop 可以看到容器运行时很大程度上依赖于绑定挂载。(仅显示冗长输出的开头)

这里,systemd-nspawn 将主机的 procfs 和 sysfs 中的选定文件按其 rootfs 中的路径提供给容器。除了设置绑定挂载时的 MS_BIND 标志之外,mount 系统调用的一些其它标志用于确定主机命名空间和容器中的更改之间的关系。例如,绑定挂载可以将 /proc/sys 中的更改传播到容器,也可以隐藏它们,具体取决于调用。

总结

理解 Linux 内部结构看似是一项不可能完成的任务,因为除了 Linux 用户空间应用程序和 glibc 这样的 C 库中的系统调用接口,内核本身也包含大量代码。取得进展的一种方法是阅读一个内核子系统的源代码,重点是理解面向用户空间的系统调用和头文件以及主要的内核内部接口,这里以 file_operations 表为例。file_operations 使得“一切都是文件”得以可以实际工作,因此掌握它们收获特别大。顶级 fs/ 目录中的内核 C 源文件构成了虚拟文件系统的实现,虚拟文件​​系统是支持流行的文件系统和存储设备的广泛且相对简单的互操作性的垫片层。通过 Linux 命名空间进行绑定挂载和覆盖挂载是 VFS 魔术,它使容器和只读根文件系统成为可能。结合对源代码的研究,eBPF 内核工具及其 bcc 接口使得探测内核比以往任何时候都更简单。

非常感谢 Akkana PeckMichael Eager 的评论和指正。

Alison Chaiken 也于 3 月 7 日至 10 日在加利福尼亚州帕萨迪纳举行的第 17 届南加州 Linux 博览会(SCaLE 17x)上演讲了本主题


via: https://opensource.com/article/19/3/virtual-filesystems-linux

作者:Alison Chariken 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出