0%

字符设备驱动的Makefile,驱动的插入和卸载

1 字符设备驱动的Makefile

1
2
3
4
5
6
7
8
9
10
ifneq ($(KERNELRELEASE),)
obj-m := hello_dev.o
else
PWD := $(shell pwd)
KDIR := /lib/modules/`uname -r`/build
all:
make -C $(KDIR) M=$(PWD)
clean:
rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~
endif
  • 首先判断 KERNELRELEASE 变量是否为空,刚开始这个变量肯定没有被定义,于是控制流跳转到else 分支
  • 在 else 分支中,定义变量 PWD 表示当前所在目录,定义变量 KDIR 表示内核所在目录,make -C $(KDIR) M=$(PWD)这一行的规则是:先进入 -C 指定的内核所在目录执行Makefile文件,在这个Makefile文件中会设置变量 KERNELRELEASE;“M=”选项的作用是,当用户需要以某个内核为基础编译1个外部模块,程序会自动到 “M=” 指定的 dir 目录中查找模块源码,将其编译,生成 .ko 文件
  • 这里的 M 是内核根目录下 Makefile 中使用的变量,
1
2
3
4
5
6
7
8
9
10
# Use make M=dir to specify directory of external module to build
# Old syntax make ... SUBDIRS=$PWD is still supported
# Setting the environment variable KBUILD_EXTMOD take precedence
ifdef SUBDIRS
KBUILD_EXTMOD ?= $(SUBDIRS)
endif

ifeq ("$(origin M)", "command line")
KBUILD_EXTMOD := $(M)
endif
  • obj-m 指定目标文件,表示将由 c 代码编译成 .o 文件,然后生成独立的 .ko 模块,不会编译到 zImage
  • 在驱动源码目录下执行 make 命令,
1
2
3
4
5
6
7
8
9
10
$ make 
make -C /lib/modules/`uname -r`/build M=/home/lnhoo/workspace/c/src/hello_dev
make[1]: Entering directory `/usr/src/linux-headers-4.4.0-142-generic'
LD /home/lnhoo/workspace/c/src/hello_dev/built-in.o
CC [M] /home/lnhoo/workspace/c/src/hello_dev/hello_dev.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/lnhoo/workspace/c/src/hello_dev/hello_dev.mod.o
LD [M] /home/lnhoo/workspace/c/src/hello_dev/hello_dev.ko
make[1]: Leaving directory `/usr/src/linux-headers-4.4.0-142-generic'

2 驱动的插入和卸载

2.1 插入

  • 执行 insmod 命令加载驱动模块到内核中,
1
$ sudo insmod hello_dev.ko

此时查看内核模块列表,

1
2
3
4
5
6
7
$ lsmod
Module Size Used by
hello_dev 16384 0
crct10dif_pclmul 16384 0
crc32_pclmul 16384 0
ghash_clmulni_intel 16384 0
......

hello_dev 驱动出现在列表中

  • 清空内核日志,
1
$ sudo dmesg -c

此时加载驱动模块,然后查看内核日志,

1
2
3
4
5
$ dmesg
[ 331.039816] hello_dev: loading out-of-tree module taints kernel.
[ 331.039869] hello_dev: module verification failed: signature and/or required key missing - tainting kernel
[ 331.040488] register_chrdev_region ok
[ 331.040490] hello driver init

hello_dev 字符设备驱动程序入口函数中的 printk 成功将消息写入内核日志

2.2 卸载

  • 执行 rmmod 命令卸载驱动程序,
1
$ sudo rmmod hello_dev

执行 lsmod 命令查看内核模块列表,此时 hello_dev 驱动已不在列表中

实现最简单的字符设备驱动

1 驱动程序的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int hello_init(void) {
devNum = MKDEV(reg_major, reg_minor);
if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")) {
printk(KERN_INFO "register_chrdev_region ok \n");
} else {
printk(KERN_INFO "register_chrdev_region error n");
return ERROR;
}
printk(KERN_INFO " hello driver init \n");
gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
gFile->open = hello_open;
gFile->read = hello_read;
gFile->write = hello_write;
gFile->owner = THIS_MODULE;
cdev_init(gDev, gFile);
cdev_add(gDev, devNum, 1);
return 0;
}
  • MKDEV 根据主、次设备号生成设备编号,通常主设备号表示某一类设备,次设备号表示这类设备里不同的设备。MKDEV 的实现,
1
2
#define MINORBITS   20
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

将主设备号左移20位,然后或上从设备号

  • 注册字符设备,
1
2
/*指定设备编号来静态注册一个字符设备*/
int register_chrdev_region(dev_t from, unsigned count, const char *name); 
参数 说明
from 设备号
count 需要连续注册的次设备编号个数
name 字符设备名称

当返回值小于0时,表示注册失败

  • 为字符设备分配内存,
1
gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);

kzalloc等价于先调用kmalloc分配一块内存空间,然后初始化为0

1
2
3
4
5
6
7
8
9
/**
* kzalloc - allocate memory. The memory is set to zero.
* @size: how many bytes of memory are required.
* @flags: the type of memory to allocate (see kmalloc).
*/
static inline void *kzalloc(size_t size, gfp_t flags)
{
return kmalloc(size, flags | __GFP_ZERO);
}
参数 说明
size 要分配内存空间的字节数
flags 分配方式

对于 gfp_t (gfp 是 get free page的缩写)类型,有以下几个常量,

gfp_t 常量 说明
GFP_ATOMIC 内存分配过程是原子的,不会被(高优先级进程或中断)打断
GFP_KERNEL 正常分配内存
GFP_DMA 给 DMA 控制器分配内存

分配内存后如果不释放会造成内存泄漏,在内存中可能导致系统崩溃,

1
void kfree(const void *objp);

可以调用 kfree 函数释放动态分配的内存

  • file_operations 结构体中声明了一组对文件的操作,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};

包括应用开发常见的 open、read、write、flush 等。用户进程在对设备文件(linux 中一切皆为文件)进行诸如 read/write 操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,读取 file_operations 结构体中对应的函数指针,接着把控制权交给该函数,这是 linux 的设备驱动程序工作的基本原理

  • 以回调函数的形式指定驱动程序对文件的各种操作,
1
2
3
4
gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
gFile->open = hello_open;
gFile->read = hello_read;
gFile->write = hello_write;

实践中,file_operations 结构体的 owner 字段通常被初始化为宏 THIS_MODULE,

1
gFile->owner = THIS_MODULE;
  • 建立字符设备和 file_operations 对象的联系,
1
cdev_init(gDev, gFile);

cdev_init 函数声明,

1
void cdev_init(struct cdev *, const struct file_operations *);

传入字符设备、file_operations指针,建立二者之间的联系

  • 建立字符设备和设备号的联系,
1
cdev_add(gDev, devNum, 1);

cdev_add 函数声明,

1
int cdev_add(struct cdev *, dev_t, unsigned);

传入字符设备指针、设备号、添加设备的个数,建立字符设备和设备号之间的联系

2 驱动程序的出口

1
2
3
4
5
6
7
8
void __exit hello_exit(void) {
printk(KERN_INFO " hello driver exit \n");
cdev_del(gDev);
kfree(gFile);
kfree(gDev);
unregister_chrdev_region(devNum, subDevNum);
return;
}
  • 移除字符设备,
1
void cdev_del(struct cdev *);
  • 释放动态分配的内存空间,
1
void kfree(const void *);
  • 注销设备号,
1
extern void unregister_chrdev_region(dev_t, unsigned);

3 驱动程序的文件操作逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int hello_open(struct inode *p, struct file *f) {
printk(KERN_INFO "hello_open\r\n");
return 0;
}


ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) {
printk(KERN_INFO "hello_write\r\n");
return 0;
}


ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) {
printk(KERN_INFO "hello_read\r\n");
return 0;
}

作为实例程序,只输出操作对应的提示信息

4 注册驱动程序的入口、出口函数

1
2
3
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
  • 分别调用 module_init 和 module_exit 函数,注册驱动程序的入口、出口函数
  • 在 linux 系统,执行 insmod 命令会调用驱动程序入口函数,执行 rmmod 命令会调用驱动程序出口函数

linux 内核源码各目录的功能

1 arch

  • arch 目录下主要存放用于支持不同体系结构的代码
  • 不同的子目录存放对应平台的初始化代码,例如
1
2
3
4
$ ls arch/
Kconfig arm blackfin frv ia64 metag mn10300 parisc score tile x86
alpha arm64 c6x h8300 m32r microblaze nios2 powerpc sh um x86_64
arc avr32 cris hexagon m68k mips openrisc s390 sparc unicore32 xtensa

2 Documentation

  • 内核参数、配置、特性的技术说明文档

3 firmware

  • 固件相关的代码,比如 wifi、flash 等硬件的固件代码可能就存放于此

4 init

  • 内核启动相关的代码
  • 在 init/main.c 文件中,

img

start_kernel函数在汇编指令之后执行启动内核

5 security

  • 安全相关的代码

6 block

  • 块设备相关的代码
  • 在 linux 内核中,块设备子系统是很重要的一部分
  • 如果是做存储设备相关的开发,可能会比较关注这部分代码

7 driver

  • 外部设备驱动相关的代码,比如 gpio、gpu、i2c、video 等
  • 如果是做驱动开发,可能会比较关注这部分代码

8 fs

  • 文件系统相关的代码

9 ipc

  • 进程间通信相关的代码

10 kernel

  • 进程管理、调度等核心代码

11 net

  • 网络协议栈相关的代码
  • 如果是做通讯设备开发,可能会比较关注这部分代码

12 sound

  • 声卡相关的代码

13 crypto

  • 加密、解密相关的代码
  • linux 内核不能依赖于外部的 c 库,所以其封装了一些加、解密代码存放在此目录下

14 mm

  • 内存管理相关的代码

15 include

  • 内核头文件相关的代码

16 lib

  • 内核的通用库,可被其他内核程序调用

17 scripts

  • 内核编译脚本

基于 busybox 打包、制作根文件系统并通过 qemu 启动内核和文件系统

1 busybox 打包、制作根文件系统

1.1 制作空镜像文件

1
$ dd if=/dev/zero of=./rootfs.ext3 bs=1M count=32 

1.2 将此镜像文件格式化为ext3格式

1
$ mkfs.ext3 rootfs.ext3 

1.3 挂载镜像文件,将根文件系统复制到挂载目录

1
2
3
$ mkdir fs
$ mount -o loop rootfs.ext3 ./fs
$ cp -rf ./_install/* ./fs

1.4 卸载镜像文件

1
$ umount ./fs 

1.5 打包 gzip 包

1
$ gzip --best -c rootfs.ext3 > rootfs.img.gz 

2 通过 qemu 启动内核和文件系统

2.1 安装 qemu

1
2
$ sudo apt install qemu
$ sudo apt install qemu-system-x86

2.2 启动内核和文件系统

1
2
3
4
5
$ qemu-system-x86_64 \
-kernel ./linux-4.9.229/arch/x86_64/boot/bzImage \
-initrd ./busybox-1.30.0/rootfs.img.gz \
-append "root=/dev/ram init=/linuxrc" \
-serial file:output.txt

img

2.3 执行一些命令作为测试,

img

编译 linux 内核和 busybox 文件系统

1 前期准备

1.1 下载、解压 linux 内核源码

1
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.9.229.tar.gz

压缩包体积较大,下载用时可能会很长,请耐心等待

  • 解压
1
tar -xzf ./linux-4.9.229.tar.gz

1.2 linux内核源码目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ tree linux-4.9.229 -L 1
linux-4.9.229
├── COPYING
├── CREDITS
├── Documentation
├── Kbuild
├── Kconfig
├── MAINTAINERS
├── Makefile
├── README
├── REPORTING-BUGS
├── arch
├── block
├── certs
├── crypto
├── drivers
├── firmware
├── fs
├── include
├── init
├── ipc
├── kernel
├── lib
├── mm
├── net
├── samples
├── scripts
├── security
├── sound
├── tools
├── usr
└── virt

22 directories, 8 files

1.2.1 arch

1
2
3
4
5
6
7
8
9
10
$ tree linux-4.9.229/arch/ -L 1
linux-4.9.229/arch/
├── Kconfig
├── alpha
├── arc
├── arm
├── arm64
├── avr32
├── blackfin
......
  • 在arch目录下存放着用于支持不同体系结构的代码,其中包括常见的x86、arm64等

1.2.2 Documentation

  • 内核参数、配置、特性的技术说明文档

1.2.3 init

  • 内核启动相关的代码

1.2.4 block

  • 块设备相关代码

1.2.5 drivers

  • 不同外部设备的驱动代码,占据内核代码的很大一部分

1.2.6 ipc

  • 进程间通信相关的代码

1.2.7 security

  • 安全相关的代码

1.2.8 net

  • 协议栈相关的代码

1.2.9 sound

  • 声音相关的代码

1.2.10 fs

  • 文件系统相关的代码

1.2.11 kernel

  • 进程管理、调度等核心代码

1.2.12 mm

  • 内存管理相关的代码

2 编译 linux 内核

2.1 指定硬件体系架构

1
$ export ARCH=x86

2.2 配置 board config

1
2
3
4
$ make x86_64_defconfig
#
# configuration written to .config
#
  • 内核的编译系统会根据 .config 文件中的配置去编译linux内核

2.3 配置 kernel

1
$ make menuconfig
  • 这一步如果出现报错:fatal error: curses.h: No such file or directory,需要安装依赖库,
1
$ sudo apt install libncurses5-dev
  • 选中如下配置,让内核支持ramdisk驱动:
1
2
3
4
5
6
7
8
9
10
11
General setup  --->

----> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

Device Drivers --->

[*] Block devices --->

<*> RAM block device support

(65536) Default RAM disk size (kbytes)

2.4 编译

1
$ make
  • 编译成功后内核位于:./arch/x86_64/boot/bzImage

3 编译 busybox

  • busybox 是一个集成了三百多个最常用Linux命令和工具的软件

3.1 下载、解压 busybox

1
$ wget https://busybox.net/downloads/busybox-1.30.0.tar.bz2
  • 解压
1
$ tar xf busybox-1.30.0.tar.bz2
  • 切换目录
1
$ cd busybox-1.30.0/

3.2 配置 busybox 源码

  • 将 busybox 配置为静态链接,这样 busybox 在运行的时候就不需要额外的动态链接库
1
2
3
4
5
$ make menuconfig

Busybox Settings --->
Build Options --->
[*] Build BusyBox as a static binary (no shared libs)

3.3 编译、安装

1
$ make && make install 

3.4 补充一些必要的文件或目录

3.4.1 切换目录

1
$ cd _install/

3.4.2 创建文件夹

1
2
3
$ mkdir etc dev mnt
$ mkdir -p proc sys tmp mnt
$ mkdir -p etc/init.d/
  • init.d 目录包含系统许多服务的启动和停止脚本

3.4.3 配置自动挂载

1
2
3
4
$ vim etc/fstab
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs   defaults 0 0
sysfs /sys sysfs defaults 0 0
  • 系统开机时会主动读取 /etc/fstab 这个文件中的内容,根据文件里面的配置挂载磁盘,这样我们只需要将磁盘的挂载信息写入这个文件中就不需要每次开机启动之后手动进行挂载了

3.4.4 配置开机启动脚本

1
2
3
4
5
6
7
8
9
$ vim etc/init.d/rcS
echo -e "Welcome to tinyLinux"
/bin/mount -a
echo -e "Remounting the root filesystem"
mount -o remount,rw /
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
  • 修改文件权限,
1
$ chmod 755 etc/init.d/rcS 

3.4.5 配置系统初始化脚本

1
2
3
4
5
$ vim etc/inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
  • 修改文件权限,
1
chmod 755 etc/inittab 

3.4.6 创建设备文件

1
2
3
4
$ cd dev
$ mknod console c 5 1
$ mknod null c 1 3
$ mknod tty1 c 4 1
  • 这样就实现了一个最小的、完整的,可以被内核启动的文件系统

grafana介绍和使用

1 简介

  • grafana是监控数据的可视化分析工具,可以将数据以非常美观直接的图形展示出来,可以配置多种数据源,包括:Graphite, InfluxDB, OpenTSDB, Prometheus, Elasticsearch, CloudWatch。

2 安装

2.1 下载压缩包

1
$ wget https://dl.grafana.com/enterprise/release/grafana-enterprise-8.2.0.linux-amd64.tar.gz

2.2 解压、移动

1
2
$ tar -zxf grafana-enterprise-8.2.0.linux-amd64.tar.gz
$ mv grafana-8.2.0/ /opt/grafana-8.2.0

2.3 设置环境变量

  • 在 ~/.bashrc 文件末尾添加如下几行,
1
2
export GRAFANA=/opt/grafana-8.2.0                                                                                         
export PATH=$PATH:$GRAFANA/bin
  • 使环境变量生效,
1
source ~/.bashrc

3 使用(以CPU使用率展示为例)

3.1 启动grafana服务

1
$ grafana-server -homepath $GRAFANA

img

3.2 打开grafana web页面

img

  • 输入用户名和密码,默认都为admin,然后可以修改用户名、密码,也可以选择跳过

3.3 添加InfluxDB数据源

  • 点击设置 -> 添加数据源,

img

  • 选择 InfluxDB,

img

  • 填写配置,

img

  • 保存并测试

img

3.4 打点

3.4.1 获取CPU使用率信息

1
2
3
4
5
6
7
func GetCPUPercentInfo() (info []float64, err error) {
info, err = cpu.Percent(time.Second, true)
if err != nil {
return info, fmt.Errorf("get cpu usage info error: %w", err)
}
return info, nil
}
  • 以slice的形式返回本机所有CPU的使用率信息

3.4.2 连接InfluxDB

1
2
3
4
5
6
7
8
9
import influxdb2 "github.com/influxdata/influxdb-client-go/v2"

var (
client influxdb2.Client
)

func Connect(userName, password string) {
client = influxdb2.NewClient("http://localhost:8086", fmt.Sprintf("%s:%s", userName, password))
}

3.4.3 写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func WritePoint(database string, point *write.Point) error {
writeAPI := client.WriteAPIBlocking("", database+"/autogen")
err := writeAPI.WritePoint(context.Background(), point)
if err != nil {
return fmt.Errorf("write to influxdb error: %w", err)
}
return nil
}

func WriteCPUPercentInfo(usage []float64) error {
for id, percent := range usage {
point := influxdb2.NewPoint("cpu_usage",
map[string]string{"cpu_id": strconv.FormatInt(int64(id), 10)},
map[string]interface{}{"usage_percent": percent},
time.Now())
err := WritePoint("monitor", point)
if err != nil {
return fmt.Errorf("write cpu usage percent to influxdb error: %w", err)
}
}
return nil
}

3.4.4 主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
// 连接 influxdb
influx.Connect("admin", "")
// 获取实时CPU利用率,存储到influxdb
ticker := time.Tick(time.Second)
for _ = range ticker {
info, err := gopsutil.GetCPUPercentInfo()
if err != nil {
logrus.Errorf("get cpu usage info error: %w", err)
os.Exit(1)
}
err = influx.WriteCPUPercentInfo(info)
if err != nil {
logrus.Errorf("write cpu usage info to influxdb error: %w", err)
os.Exit(1)
}
logrus.Infof("write success, cpu usage percent: %v", info)
}
}
  • 每秒采集1次CPU使用率数据,写入时序数据库InfluxDB中

img

3.5 前端展示

3.5.1 新建 Dashboard

img

3.5.2 添加 panel

img

3.5.3 获取时序数据

img

  • InfluxDB 1.x 兼容 sql 语句,上图表示从 cpu_usage 表中选取 usage_percent 字段展示,别名为 cpu0

MapperX: 基于DM-Cache混合存储设备的自适应元数据维护,实现快速故障恢复

背景

混合存储设备

  • 固态硬盘相比于传统的机械硬盘,在处理随机小I/O时具有更出色的表现
  • 但是现在固态硬盘的价格依然比机械硬盘昂贵得多
  • 近些年,提出了混合存储设备的概念,例如SSHD技术,将SSD作为HHD的高速缓存,以低成本的方式提供更快速的读写

DM-Cache 概览

  • DM-Cache是Linux内核设备映射器的一个组件,实现了物理块设备到虚拟块设备的映射,其将SSDs和HHDs映射到虚拟块设备,SSD作为HHD的高速缓存
  • Linux中I/O设备分为两类:块设备和字符设备,SSD和HHD都属于块设备

img

  • 虚拟块设备是一种抽象,应用程序将数据写入虚拟块设备、获得ACK确认,从虚拟块设备读取数据,底层的SSD、HHD操作对应用程序是透明的
  • DM-Cache元数据(metadata)存储:SSD块和对应HHD块之间的映射关系、“脏”位、当前采用策略相关的元数据
  • 若采用写回法(writeback)的模式,有两种可能的情况:

(1) 被修改的块在缓存中:

不需要更新元数据中SSD块和对应HHD块之间的映射关系,但是要在将“脏”位信息放到内存中后向调用方发送ACK确认,之后由异步任务将“脏”位信息持久化到元数据驱动器上

(2) 被修改的块不在缓存中:

需要将HHD块加载到SSD缓存中,同步修改元数据中SSD块和对应HHD块之间的映射关系,之后的步骤和情况(1)中一样

  • 如果写局部性较高,则缓存命中率高,不需要频繁修改元数据中的映射关系,这样“脏”位的同步会造成更大的影响
  • MapperX使用ABT(adaptive bit-tree)数据结构同步地以层次结构的形式维护”脏”位元数据;ABT用一个比特位表示某个连续范围内的数据块

DM-Cache 的缺陷

  • DM-Cache默认在发生写操作时先将“脏”位信息放入内存中,之后再由异步任务写入元数据驱动器,因为内存是易失性设备,断电后数据消失,这样当系统崩溃时,无法得知SSD缓存中哪些是“脏”数据,只好假定SSD缓存中全部都是“脏”数据(即使大部分数据块是一致的),全都要写入HHD,导致崩溃恢复时间长、系统可用性差

MapperX

CBT

  • 有1个在内存中的位图,记录着全部数据块的状态(1个bit代表对应数据块是否为“脏”,即不一致),首先把这个位图展开成度为d的树,称为CBT(Complete bit-tree)
  • 展开方式:数据块的个数最好为d的幂次,刚开始一分为d,分为d个子树,然后递归划分下去,最后叶子结点就代表位图中的某个bit

ABT

  • MapperX在元数据存储器中维护着CBT的摘要,称之为ABT(Adaptive bit-tree)
  • 对于每次写操作,ABT同步更新
  • ABT的结点表示某个连续范围内的数据块状态,初始状态是只有一个“干净”的根结点,表示SSD缓存和HHD数据完全一致

自适应算法

BitTreeUpdate

  1. 更新内存中的位图(每一bit表示某个数据块,0代表数据一致,1代表存在“脏”数据)
  2. 更新CBT:当某个结点任何一个子孙结点状态为“脏”,置这个结点状态为脏;如果所有子孙结点都“干净”,这个结点状态也为“干净”
  3. 如果写操作导致ABT的某个叶子结点变“脏”,根据CBT修改磁盘上的ABT

PeriodicAdjust

  • 参数列表:
参数 类型 含义
p 时间 异步任务执行周期(采样间隔,默认为1s)
n 整数 SLA(Service-Level Agreement)
abt 自适应树ABT
  • 算法:
  1. 置W为客户端在异步任务执行周期p内的写入操作次数
  2. 置N为元数据驱动器被写入的次数
  3. 如果 N/W ≥ 1/(10^n),认定元数据写入频率太高了,因为元数据修改是同步的,这会降低写操作的性能,在ABT中找到所有叶子结点的直接父结点,在这个范围内寻找在周期p中导致最多元数据写操作的结点,在ABT中将它的子结点移除
  4. 如果 N/W < 1/(10^n),认定还可以接受更高一点频率的元数据写入,于是在ABT中找到周期p内最少元数据写入的叶子结点,在它的下方生成d个子结点,状态根据CBT设置

快速崩溃恢复策略

  • 因为元数据是在每次写操作发生时同步更新的,所以在ABT中的叶子结点对“脏”状态的判断不存在假阴性,也就是说,如果一个叶子结点的状态不为“脏”,那么它所表示的连续数据块也就没有“脏”数据,在数据恢复时可以安全地跳过这些块
  • 对于ABT中每个状态为“脏”的叶子结点,其所表示范围的SSD缓存块全都写回HHD

实现

  • 重用DM-Cache原有的4字节元数据类型,最后两位分别是“脏”位和有效位,将所有4字节元数据的前30位组织成一个位数组,每一bit表示1个结点,以宽度优先的方式表示
  • V-ABT和CBT具有相同的层数和叶子结点数,但是只要状态为“脏”的叶子结点标记为1,其余结点标记为0
  • 例如:

img

上图是1个V-ABT,度为2,层数为4,有两个“脏”状态的叶子结点被标记为1,根据宽度优先,可以将这个V-ABT表示成1个15个bit的位数组:001010000000000

评估

写入时延

  • 根据实验数据,使用MapperX后,写入时延相比于原有的DM-Cache略微高一些,但是远低于使用同步写“脏”位元数据的策略
  • MapperX本质上也是同步写“脏”位元数据,但是利用了客户端写操作的局部性原理,减少了对元数据的写入次数
  • 随着预期β值(写元数据次数占所有客户端写操作次数的比例)的增加,客户端写入时延增加

IOPS(writes per sec)

  • 根据实验数据,使用MapperX后,IOPS低于原有的DM-Cache,远高于使用同步写“脏”位元数据的策略

恢复时间

  • 根据实验数据,使用MapperX后,崩溃恢复时间相比于原有的DM-Cache显著减少
  • 随着预期β值(写元数据次数占所有客户端写操作次数的比例)的增加,恢复时间减少,这是因为在PeriodicAdjust过程中,当N/W小于β时会在ABT中增加叶子结点,从而叶子结点表示范围粒度更细,在崩溃恢复的时候,需要写回HHD的缓存块个数更少

influxDB 介绍和使用

1 简介

  • InfluxDB 是一个开源的分布式时序、事件和指标数据库,使用go语言编写,无需外部依赖,其设计目标是实现分布式和水平伸缩拓展。

2 安装

2.1 下载压缩包、解压、移动

  • 下载压缩包,
1
$ wget https://dl.influxdata.com/influxdb/releases/influxdb-1.8.9_linux_amd64.tar.gz
  • 解压,
1
$ tar xvfz influxdb-1.8.9_linux_amd64.tar.gz
  • 移动文件夹,
1
$ mv influxdb-1.8.9-1/ /opt/influxdb-1.8.9

2.2 设置环境变量

  • 在 ~/.bashrc 文件末尾添加以下内容,
1
2
export INFLUXDB=/opt/influxdb-1.8.9
export PATH=$PATH:$INFLUXDB/usr/bin
  • 使环境变量生效,
1
$ source ~/.bashrc

3 概念说明

3.1 数据库对象

influxDB 名词 传统数据库概念
database 数据库
measurement 数据表
point 数据行

3.2 point

  • influxDB 中的 point 相当于传统数据库里的一行数据,由时间戳(time)、数据(field)、标签(tag)组成。
Point 属性 传统数据库概念
time 每个数据记录时间,是数据库中的主索引
field 各种记录值(没有索引的属性),例如温度、湿度
tags 各种有索引的属性,例如地区、海拔

3.3 Series

  • Series 相当于是 InfluxDB 中一些数据的集合,在同一个 database 中,retention policy、measurement、tag sets 完全相同的数据同属于一个 series,同一个 series 的数据在物理上会按照时间顺序排列存储在一起。

4 go语言操作influxDB

4.1 安装 client

1
go get github.com/influxdata/influxdb-client-go/v2

4.2 连接数据库

  • 使用 influxDB shell 客户端创建数据库 test,
1
2
3
4
5
$ influx                                                               
Connected to http://localhost:8086 version 1.8.9
InfluxDB shell version: 1.8.9
> create database test;
>
  • client 连接数据库
1
2
3
4
5
6
7
userName := "admin"
password := ""
// Create a new client using an InfluxDB server base URL and an authentication token
// For authentication token supply a string in the form: "username:password" as a token. Set empty value for an unauthenticated server
client := influxdb2.NewClient("http://localhost:8086", fmt.Sprintf("%s:%s", userName, password))
// 不要忘记关闭连接
defer client.Close()

4.3 写入一条 point 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Get the blocking write client
// Supply a string in the form database/retention-policy as a bucket. Skip retention policy for the default one, use just a database name (without the slash character)
// Org name is not used
writeAPI := client.WriteAPIBlocking("", "test/autogen")
// create point using full params constructor
p := influxdb2.NewPoint("stat",
map[string]string{"unit": "temperature"},
map[string]interface{}{"avg": 24.5, "max": 45},
time.Now())
// Write data
err := writeAPI.WritePoint(context.Background(), p)
if err != nil {
fmt.Printf("Write error: %s\n", err.Error())
}

4.4 查询刚被写入的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Get query client. Org name is not used
queryAPI := client.QueryAPI("")
// Supply string in a form database/retention-policy as a bucket. Skip retention policy for the default one, use just a database name (without the slash character)
result, err := queryAPI.Query(context.Background(), `from(bucket:"test")|> range(start: -1h) |> filter(fn: (r) => r._measurement == "stat")`)
if err == nil {
for result.Next() {
if result.TableChanged() {
fmt.Printf("table: %s\n", result.TableMetadata().String())
}
fmt.Printf("row: %s\n", result.Record().String())
}
if result.Err() != nil {
fmt.Printf("Query error: %s\n", result.Err().Error())
}
} else {
fmt.Printf("Query error: %s\n", err.Error())
}

4.5 influxDB shell 验证是否写入成功

1
2
3
4
5
6
7
8
9
10
$ influx                                                               
Connected to http://localhost:8086 version 1.8.9
InfluxDB shell version: 1.8.9
> use test;
Using database test
> select * from stat;
time avg max unit
---- --- --- ----
1633274191505313000 24.5 45 temperature
>
  • 1.x 版本的 influxDB shell 兼容 sql 语句,而 2.x 版本后默认使用 js 操作数据库

gopsutil 获取系统信息

1 简介

gopsutil 是 python 工具库 psutil 的 golang 移植版,可以方便地获取各种系统和硬件信息。gopsutil屏蔽了各个系统之间的差异,具有很好的移植性。

2 安装

1
go get github.com/shirou/gopsutil

3 获取系统和硬件信息

3.1 CPU

3.1.1 CPU 硬件信息

1
2
3
4
5
6
7
8
cpuInfos, err := cpu.Info()
if err != nil {
logrus.Errorf("get cpu info error: %w", err)
return
}
for _, info := range cpuInfos {
fmt.Println(info)
}

3.1.2 CPU 实时使用率

1
2
3
4
for {
precent, _ := cpu.Percent(time.Second, true)
fmt.Printf("cpu precent:%v\n", precent)
}
  • 上面的代码每秒获取一次各 CPU 核心的实时使用率

3.1.3 CPU 负载

1
2
3
4
func getCPULoad() {
info, _ := load.Avg()
fmt.Printf("%v\n", info)
}

程序输出,

1
{"load1":0.11,"load5":0.13,"load15":0.09}
  • load1、load5、load15 分别表示1分钟、5分钟、15分钟内系统的平均负荷
  • 当系统负荷持续大于0.7,你必须开始调查了,问题出在哪里,防止情况恶化
  • 当系统负荷持续大于1.0,你必须动手寻找解决办法,把这个值降下来
  • 当系统负荷达到5.0,就表明你的系统有很严重的问题,长时间没有响应,或者接近死机了
  • 以上指标都是基于单CPU的,但是现在很多电脑都是多核的。所以,对一般的系统来说,是根据cpu 数量去判断系统是否已经过载(Over Load)的。如果我们认为 0.7 算是单核机器负载的安全线的话,那么四核机器的负载最好保持在 3(4*0.7 = 2.8) 以下

3.2 内存

1
2
3
4
func getMemInfo() {
memInfo, _ := mem.VirtualMemory()
fmt.Printf("mem info: %v\n", memInfo)
}

3.3 主机

1
2
3
4
func getHostInfo() {
hInfo, _ := host.Info()
fmt.Printf("host info:%v uptime:%v boottime:%v\n", hInfo, hInfo.Uptime, hInfo.BootTime)
}

3.4 磁盘

3.4.1 分区信息

1
2
3
4
5
6
7
8
9
10
parts, err := disk.Partitions(true)
if err != nil {
fmt.Printf("get Partitions failed, err:%v\n", err)
return
}
for _, part := range parts {
fmt.Printf("part:%v\n", part.String())
diskInfo, _ := disk.Usage(part.Mountpoint)
fmt.Printf("disk info:used:%v free:%v\n", diskInfo.UsedPercent, diskInfo.Free)
}

3.4.2 磁盘IO

1
2
3
4
ioStat, _ := disk.IOCounters()
for k, v := range ioStat {
fmt.Printf("%v:%v\n", k, v)
}

3.5 网络 IO

1
2
3
4
5
6
func getNetInfo() {
info, _ := net.IOCounters(true)
for index, v := range info {
fmt.Printf("%v:%v send:%v recv:%v\n", index, v, v.BytesSent, v.BytesRecv)
}
}

logagent 根据 IP 获取配置

1 上一个版本的问题

  • 每台服务器上的 logagent 的收集项可能都不一致,我们需要让 logagent 去 etcd 中根据 IP 地址获取自己的配置

2 如何获取本机的 IP

2.1 net.InterfaceAddrs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func GetLocalIP() (ip string, err error) {
addrs, err := net.InterfaceAddrs() // 获取所有的网卡IP
if err != nil {
return "", err
}
for _, addr := range addrs {
ipAddr, ok := addr.(*net.IPNet)
if !ok {
continue
}

if ipAddr.IP.IsLoopback() {
continue
}

if !ipAddr.IP.IsGlobalUnicast() {
continue
}

return ipAddr.IP.String(), nil
}
return "", fmt.Errorf("empty ip addr list")
}

2.2 net.Dial

1
2
3
4
5
6
7
8
9
10
11
// Get preferred outbound ip of this machine
func GetOutboundIP() (string, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "", err
}
defer conn.Close()

localAddr := conn.LocalAddr().(*net.UDPAddr)
return strings.Split(localAddr.IP.String(), ":")[0], nil
}

3 logagent 中集成根据 ip 拉取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func GetCollectConfig(ip string) (config []common.CollectConfigEntry, err error) {
key := fmt.Sprintf(conf.DefaultConfig.ETCD.CollectConfKey, ip)
logrus.Info("etcd key:", key)
configStr, err := GetValue(key)
if err != nil {
logrus.Errorf("get collect config error: %w", err)
return nil, err
}
err = json.Unmarshal([]byte(configStr), &config)
if err != nil {
logrus.Errorf("unmarshal config str error: %w", err)
return nil, err
}
return config, nil
}
  • 根据本机 ip 计算出在 etcd 配置中心的 key,从而取出配置项