0%

  1. 如果http请求中没有携带身份信息(AccessKey),则改该请求为匿名请求,会被认为是来自匿名用户的访问。
  2. 如果http请求中携带了身份信息(AccessKey),则认为访问来自该AccessKey所对应的用户。由于AccessKey是可以被他人获取到的, 为了防止其他人冒用您的AccessKey来访问服务,请求中还必须携带您的签名。在申请帐号以后,您将得到AccessKey和SecretKey, SecretKey是需要保密的。签名是由此次http请求的相关信息和您的SecretKey计算得到的,其他人因为不知道您的SecretKey,将不能 计算出正确的签名。
  3. 身份信息与签名可以放到请求头(Authorization)中,也可以放到请求参数中。
  4. 签名的方式与Amazon S3的签名方式兼容,支持signature version 2signature version 4

参考&鸣谢

s3 authorization v2

参数传递

通过Authorization请求头

1
Authorization: AWS AWSAccessKeyId:Signature
  • AWSAccessKeyId: 你的AccessKey
  • Signature: 计算得到的签名

eg:

1
Authorization: AWS ziw5dp1alvty9n47qksu:frJIUN8DYpKDtOLCwo//yllqDzg=

通过请求参数

HTTP请求中的参数:

  • AWSAccessKeyId: 你的AccessKey
  • Signature: 计算得到的签名
  • Expires: 签名的过期时间

eg:

1
GET /yourbucket/yourkey?AWSAccessKeyId=ziw5dp1alvty9n47qksu&Expires=1141889120&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D

签名计算

1
Signature = Base64(HMAC-SHA1(YourSecretKey, UTF-8-Encoding-Of(StringToSign)))
  • YourSecretKey: 你的SecretKey
  • StringToSign

StringToSign

1
2
3
4
5
6
StringToSign =  HTTP-Verb + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
Date|Expires + "\n" +
CanonicalizedAmzHeaders +
CanonicalizedResource
  • HTTP-Verb: 请求的方法,如:PUTGETDELETEPOST
  • Content-MD5: 请求头Content-MD5的内容,如果没有这个头,由空字符串代替
  • Content-Type: 请求头Content-Type的内容,如果没有这个头,由空字符串代替
  • Date|Expires: 如果使用Authorization头携带签名信息,为Date头的内容,如果没有Date头,由空字符串代替;如果使用请求参数携带签名信息,为参数Expires的内容
  • CanonicalizedAmzHeaders: 请求中所有以x-amz-开始的头所组成的字符串,如果没有这样的头,由空字符串代替
    eg:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    原始请求头:
    Date: Tue, 27 Mar 2007 19:36:42 +0000
    X-Amz-b: Bar
    x-amz-a: foob
    x-Amz-a: fooa
    Host: johnsmith.s3.amazonaws.com

    对应的CanonicalizedAmzHeaders为:
    x-amz-a:fooa,foob
    x-amz-b:Bar
  • CanonicalizedResource: 请求所对应的资源
    eg:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    GET /?foo=bar
    GET /yourbucket/yourkey?foo=bar
    GET /yourbucket/yourkey?acl&foo=bar

    对应的nicalizedResource分别为:


    /yourbucket/yourkey
    /yourbucket/yourkey?acl

参考&鸣谢

s3 authorization v4

参数传递

通过Authorization请求头

eg:

1
Authorization: AWS4-HMAC-SHA256 Credential=ziw5dp1alvty9n47qksu/20160830/us-east-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
  • Credential 由AccessKey,请求的日期,region,服务名,aws4_request五部分组成,各部分之间用斜线分隔
  • SignedHeaders 表示那些头参与了签名的计算,未包含在这里的头不会影响到签名的生成
  • Signature 计算得到的签名

通过请求参数

eg:

1
GET /yourbucket/test.mp4??X-Amz-Algorithm=AWS4-HMAC-SHA256&&X-Amz-Credential=ziw5dp1alvty9n47qksu/20160830/us-east-1/s3/aws4_request&X-Amz-Date=20160830T201207Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
  • X-Amz-Algorithm 计算签名时使用的Hash算法,指定为AWS4-HMAC-SHA256
  • X-Amz-Credential 包含了AccessKey,日期,region,服务名的信息
  • X-Amz-Date 请求的时间
  • X-Amz-Expires 指定签名在多长时间内有效
  • X-Amz-SignedHeaders 计算签名时用到的头
  • X-Amz-Signature 计算得的到签名

签名计算

1
signature = HexEncode(HMAC-SHA256(kSigning, StringToSign))

kSigning

1
2
3
4
5
kSecret = YourSecretKey
kDate = HMAC("AWS4" + kSecret, Date)
kRegion = HMAC(kDate, Region)
kService = HMAC(kRegion, Service)
kSigning = HMAC(kService, "aws4_request")
  • YourSecretKey 你的SecretKey
  • Date 8位数的日期,应与Credentail中的Date部分一样
  • Region 应与Credential中的region部分一样
  • Service 应与Credential中的服务名部分一样
  • kSigning 为用于计算签名的signing key

StringToSign

1
2
3
4
5
StringToSign  =
Algorithm + '\n' +
RequestDate + '\n' +
CredentialScope + '\n' +
HashedCanonicalRequest
  • Algorithm 指定为AWS4-HMAC-SHA256
  • RequestDate ISO8601 basic 格式的请求时间,如:20160830T123600Z
  • CredentialScope 日期,region,服务名等组成的字符串, 如:20160830/us-east-1/s3/aws4_request
  • HashedCanonicalRequest Hex(SHA256Hash(CanonicalRequest)),即CanonicalRequest的hash的16进制编码

CanonicalRequest

1
2
3
4
5
6
7
CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HexEncode(Hash(RequestPayload))
  • HTTPRequestMethod 如: PUT, GET, DELETE, POST
  • CanonicalURI 请求的uri
  • CanonicalQueryString 请求参数排序后组成的字符串
  • CanonicalHeaders 需要加入签名计算的头排序后组成的字符串
  • SignedHeaders 加入到签名计算的头的名字的列表,各个名字之间用逗号分隔
  • HexEncode(Hash(RequestPayload)) 请求body的hash的16进制编码,如果通过请求参数携带签名,此处应由字符串UNSIGNED-PAYLOAD代替

参考&鸣谢

PG的全称为Placement Group(放置组),放置顾名思义放置Object的载体。PG的创建是在创建Pool的时候根据指定的数量进行创建。PG的数量与副本数也有关系,比如是3副本的则会有3个相同的pg存在于3个不同的osd上,以filestore为例pg其实在osd的存在形式就是一个目录。其目录的命名规则为 {pool-id}.{pg-id}_head{pool-id}.{pg-id}.TEMP。如果你想找出一个pg对应哪些个osd,可以使用ceph pg map {pgid}的命令。

eg:

1
2
[root@ceph71 ~]# ceph pg map 1.5f
osdmap e30 pg 1.5f (1.5f) -> up [2,0,1] acting [2,0,1]

背景就是介绍这么多吧,接下来说说PG状态。

状态

状态 描述
active 当前拥有最新状态数据的pg正在工作中,能正常处理来自客户端的读写请求。
inactive 正在等待具有最新数据的OSD出现,即当前具有最新数据的pg不在工作中,不能正常处理来自客户端的读写请求。
activating Peering 已经完成,PG 正在等待所有 PG 实例同步并固化 Peering 的结果 (Info、Log 等)
clean pg所包含的object达到指定的副本数量,即object副本数量正常
unclean PG所包含的object没有达到指定的副本数量,比如一个PG没在工作,另一个PG在工作,object没有复制到另外一个PG中。
peering PG所在的OSD对PG中的对象的状态达成一个共识(维持对象一致性)
peered peering已经完成,但pg当前acting set规模小于存储池规定的最小副本数(min_size)
degraded 主osd没有收到副osd的写完成应答,比如某个osd处于down状态
stale 主osd未在规定时间内向mon报告其pg状态,或者其它osd向mon报告该主osd无法通信
inconsistent PG中存在某些对象的各个副本的数据不一致,原因可能是数据被修改
incomplete peering过程中,由于无法选出权威日志,通过choose_acting选出的acting set不足以完成数据修复,导致peering无法正常完成
repair pg在scrub过程中发现某些对象不一致,尝试自动修复
undersized pg的副本数少于pg所在池所指定的副本数量,一般是由于osd down的缘故
scrubbing pg对对象meta的一致性进行扫描
deep pg对对象数据的一致性进行扫描
creating pg正在被创建
recovering pg间peering完成后,对pg中不一致的对象执行同步或修复,一般是osd down了或新加入了osd
recovering-wait 等待 Recovery 资源预留
backfilling 一般是当新的osd加入或移除掉了某个osd后,pg进行迁移或进行全量同步
down 包含必备数据的副本挂了,pg此时处理离线状态,不能正常处理来自客户端的读写请求
remapped 重新映射态。PG 活动集任何的一个改变,数据发生从老活动集到新活动集的迁移。在迁移期间还是用老的活动集中的主 OSD 处理客户端请求,一旦迁移完成新活动集中的主 OSD 开始处理
misplaced 有一些回填的场景:PG被临时映射到一个OSD上。而这种情况实际上不应太久,PG可能仍然处于临时位置而不是正确的位置。这种情况下个PG就是misplaced。这是因为正确的副本数存在但是有个别副本保存在错误的位置上。

异常

active+undersized+degraded

若发现有osd挂掉,先尝试将osd重新拉起来,拉起来后集群会自动重新恢复健康状态。
但是也有可能出现这个osd再也起不来了,比如硬盘损坏了,这时多副本就发挥作用了,因为还有其它副本在其它osd上,这时我们可以通过均衡数据的方法来将集群恢复并将该osd踢出集群。

** 解决 **

  1. 将osd reweight权重置0,将数据分散到其他osd上 ceph osd reweight {osd-id} 0
  2. 待集群rebalance后,开始删除osd

unfound objects

ceph集群知道该对象存在,但无法定位该object在哪时会报这个错误。

** 解决 **

  1. 尝试让失败的osd起来,如果起来后集群恢复正常,则结束
  2. 试将该pg的unfound对象回滚到上一个版本,ceph pg {pgid} mark_unfound_lost revert如果恢复正常,则结束
  3. 如果还是不行,那只有将该object删除掉了,注意这会导致丢失数据,执行ceph pg {pgid} mark_unfound_lost delete删除unfound对象

inconsistent objects

pg中保存的object中有些副本数据不一致,有些事伴随着scrub errors错误

** 解决 **

  1. ceph health detail找出问题pg
  2. 尝试ceph pg repair {pgid},若成功,则结束;不成功进行如下操作。
  3. 使用ceph pg map {pgid}找出主osd,打开日志查看哪个object不一致
  4. 找出所有该objects所有副本存放的位置,用摘要算法(md5sum,sha256)等计算出其hash值,如果是3副本,删除与其他副本不一致的;如果是2副本,则可能会误删。
  5. 再次执行ceph pg repair {pgid}

stale pg

pg出现stale状态,也就是pg处于僵死状态,该状态是无法处理新的请求了的,新的请求过来只会block,这种情况一般是由于所有副本pg的osd都挂了。
Ceph使用心跳来确保主机和进程都在运行,OSD进程如果不能周期性的发送心跳包,那么PG就会变成stuck状态。默认情况下,OSD每半秒钟汇汇报一次PG,up thru,boot, failure statistics等信息,要比心跳包更会频繁一点。如果主OSD不能汇报给MON或者其他OSD汇报主OSD挂了,Monitor会将主OSD上的PG标记为stale。当启动集群后,直到peer过程完成,PG都会处于stale状态。而当集群运行了一段时间后,如果PG卡在stale状态,说明主OSD上的PG挂了或者不能给MON发送信息。

要模拟其实也很简单,比如设置2副本,然后将2个不同故障域的osd挂掉即可出现,最好的恢复方法当然是重新拉起这两个osd,但有时可能出现这样的情况,两个osd永远也拉不起来了,然后你把这两个osd清理出去了,清理完后这些pg当然就是stale的状态,这时的恢复方法只能是丢掉这个pg里的数据了,重新创建pg。

** 解决 **

  1. 使用命令ceph pg dump |grep stale找出所有的stale的pg,也可以ceph health detail | grep stale
  2. 执行ceph pg force_create_pg {pgid}命令强制重新创建pg,会看到pg转为creating状态
  3. 重启ceph中所有osd服务

Peered

Peering 已经完成,但是 PG 当前 Acting Set 规模小于存储池规定的最小副本数 (min_size)。
如果pool的副本数为3、min_size=2,停掉两个副本所在的osd,此时访问集群的客户端处于blocked状态。

** 解决 **

  1. 先尝试将down掉的两个osd恢复,或至少恢复一个。
  2. 若不能正常恢复,可修改min_size=1,解除客户端blocked状态

鸣谢&参考

Backfill和Recovery都是用来恢复数据的,但二者的区别是Backfill是全量恢复,Recovery是增量恢复。而且在恢复过程中当出现Client访问恢复数据现象时,Backfill不会block client i/o,但Recovery会block client i/o。但二者如果占用带宽过大,还是会影响client i/o的。

环境

Ceph Version : Jewel

优化

  • 默认情况下将Backfill和Recovery对Client I/O的影响降到最小。
  • 当需要Backfill或Recovery时,要根据集群负载动态调整。若集群负载低,可通过ceph tell {osd.id} injectargs "--config value"的放方式动态调整恢复速度,使其快速完成恢复操作。
  • 当恢复完成后,再次将Backfill和Recovery调整回默认值,以降低对Client I/O的影响。

Recovery 参数说明

参数 描述 默认值
osd recovery max active 每个 OSD 一次处理的活跃恢复请求数量,增大此值能加速恢复,但它们会增加集群负载。 15
osd recovery max chunk 一次推送的数据块的最大尺寸。 8 << 20
osd recovery threads 数据恢复时的线程数。 1
osd recovery thread timeout 恢复线程最大死亡时值。 30

Backfill 参数说明

参数 描述 默认值
osd max backfills 单个 OSD 允许的最大回填操作数。 1
osd backfill full ratio OSD 的占满率达到多少时拒绝接受回填请求 0.85
osd backfill retry interval 重试回填请求前等待秒数 10.0

原理

Todo…

问题

  • 增加了RBD Journal的写入操作,是否会产生写放大现象。
  • Ceph-RBD-Mirror回放日志的的时间间隔
  • RBD Journal的容量大小如何配置
  • RBD Journal存储介质是否可以优化

使用

环境

要求

  • Ceph版本必须为Jewel及之后版本
    RBD-Mirror功能从Jewel版本引入,之前版本不支持该feature。本文以jewel版本为例进行演示。
  • RBD feature必须包含journaling
    RBD-Mirror依赖journal完成工作,类似Mysql的主从同步机制。使用日志回放方法备份数据。

准备

  • 准备两个集群分别为cls17和cls26
    cls17为primary集群,cls26为non-primary集群,将cls17内的RBD数据备份到cls26集群内

  • 在这两个集群中创建同名pool——rmp(RBD Mirror Pool)

ceph-rbd-mirror服务

根据RBD-Mirror的原理,需要在non-primery集群上启动ceph-rbd-mirror服务

** 安装 **

1
$ yum install -y rbd-mirror

** 启动 **

1
2
$ systemctl enable ceph-rbd-mirror@admin.service
$ systemctl start ceph-rbd-mirror@admin.service

@admin是ceph中的client.admin用户,只是这里省略了client.。关于Ceph用户管理相关操作可以使用ceph auth命令。

Copy Config & keyring

将cls17集群中的ceph.confceph.client.admin.keyringcopy到cls26集群的/etc/ceph目录中,并重命名成cls17.confcls17.client.admin.keyring。然后在cls26集群中可通过指定--cluster cls17来指定访问集群。

备份Pool中所有RBD

配置

Pool Mirror Mode

需要将两个集群的的rmppool的mirror mode设置成pool

集群cls26

1
$ rbd mirror pool enable rmp pool

集群cls17

1
$ rbd mirror pool enable rmp pool --cluster cls17

Add Peer

将Primary集群信息加入到rmppool中。

集群cls26

1
$ rbd mirror pool peer add rmp client.admin@cls17

查看两个集群的peer信息

1
2
3
4
5
6
7
8
9
$ rbd mirror pool info rmp
Mode: pool
Peers:
UUID NAME CLIENT
2c2c1293-0e97-4c57-9bf1-e5f934274758 cls17 client.admin
$
$ rbd mirror pool info rmp --cluster cls17
Mode: pool
Peers: none

至此备份Pool中所有RBD的配置操作已经完成,接下来进行验证操作。

测试

在集群cls17中rmppool创建RBD,并查看集群cls26中rmppool是否有回放操作(RBD是否备份到cls26集群)。由于整个RBD-Mirror依赖于journalingfeature,所以创建RBD时需要指定该feature。若对当前rmppool中存在RBD进行备份,需要先增加jouranlingfeature,使用rbd feature enable rmp/rd1 journaling命令。

集群cls17

1
$ rbd create -p rmp --image-format 2 --image-feature layering,exclusive-lock,object-map,fast-diff,deep-flatten,journaling -s 1G rd1

集群cls26

1
2
$ rbd -p rmp ls
rd1

备份单个RBD

配置

Pool Mirror Mode

将两个集群的rbdpool的mirror mode设置成image

1
$ rbd mirror pool enable rbd image

Add Peer

将Primary集群信息加入到rbdpool中。具体操作方法,同上。

Enable Image Mirror

集群cls17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ rbd info rd1
rbd image 'rd1':
size 1024 MB in 256 objects
order 22 (4096 kB objects)
block_name_prefix: rbd_data.ad616b8b4567
format: 2
features: exclusive-lock, journaling
flags:
journal: ad616b8b4567
mirroring state: disabled
$ rbd mirror image enable rbd/rd1
Mirroring enabled
$ rbd info rd1
rbd image 'rd1':
size 1024 MB in 256 objects
order 22 (4096 kB objects)
block_name_prefix: rbd_data.ad616b8b4567
format: 2
features: exclusive-lock, journaling
flags:
journal: ad616b8b4567
mirroring state: enabled
mirroring global id: f0c9972d-7585-4bda-a486-baa9a6f96eb5
mirroring primary: true

集群cls26

1
2
3
4
5
6
7
8
9
10
11
12
$ rbd info rd1
rbd image 'rd1':
size 1024 MB in 256 objects
order 22 (4096 kB objects)
block_name_prefix: rbd_data.8587327b23c6
format: 2
features: exclusive-lock, journaling
flags:
journal: 8587327b23c6
mirroring state: enabled
mirroring global id: f0c9972d-7585-4bda-a486-baa9a6f96eb5
mirroring primary: false

测试

(同上)

*** 无论是全Pool备份,还是单个RBD备份,只要建立mirror关系,只有primary rbd才能处理读写操作 ***

  • Ceph中的RBD备份分为全量备份和增量备份,恢复也有增全量之分。
  • 备份和恢复的操作都是基于快照来操作的。
  • RBD的format必须为2 (–image-format 2)

备份

备份操作流程,先做一个全量备份,然后每隔一段时间做一个快照,并将快照导出。

准备

创建一个RBD image

1
$ rbd create --image-format 2 --size 1G crbd_cls17_1

全量

创建一个快照,然后备份从image创建一直到创建快照之前的全量数据

1
2
$ rbd snap create crbd_cls17_1@s1
$ rbd export-diff crbd_cls17_1@s1 ./crbd_base

增量

继续写入一段数据后,再次做快照,并导出增量数据

1
2
$ rbd snap create crbd_cls17_1@s2
$ rbd export-diff crbd_cls17_1@s2 --from-snap s1 ./crbd_s1_s2

至此,全量和增量备份数据都以导出到文件crbd_basecrbd_s1_s2两个文件中。

恢复

准备

将之前导出的增全量备份数据crbd_basecrbd_s1_s2拷贝到需要导入的ceph集群上。创建一个rbd image用于导入备份数据。

1
$ rbd create --image-format 2 --size 1G crbd_cls26_1

全量

恢复全量数据

1
$ rbd import-diff ./crbd_base crbd_cls26_1

增量

恢复增量数据

1
$ rbd import-diff ./crbd_s1_s2 crbd_cls26_1

Over!

Todo…

Journal存储格式

  1. header
  2. Ring buffer

journal_1

header格式

journal_1

Ring buffer中entry header格式

journal_1

块设备驱动程序,linux内核中的一类驱动程序。通过传输固定大小的随机数据来访问设备。

初始化

注册块设备驱动

向内核注册设备驱动程序使用函数int register_blkdev(unsigned int major, const char *name);(linux/fs.h),参数是该设备使用的主设备号和名称(名字在/proc/devices中显示)。若传入的主设备号为0,内核将分配一个新的主设备号,并将该设备号返回给调用者;失败则返回负值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int __init sbull_init(void)
{
int i;
sbull_major = register_blkdev(sbull_major,"sbull"); // 注册块设备,第一个是设备号,0为动态
if(sbull_major <= 0){ //分配,第二个是设备名
printk(KERN_WARNING "sbull:unable to get major number\n");
return -EBUSY;
}
/* 为块核心数据结构 sbull_dev 分配空间*/
Devices = kmalloc(ndevices *sizeof(struct sbull_dev),GFP_KERNEL);
if(Devices == NULL)
goto out_unregister;
for(i = 0;i < ndevices;i++) /* 初始化 sbull_dev 核心数据结构 */
setup_device(Devices + i,i);
return 0;
out_unregister:
unregister_blkdev(sbull_major,"sbd");
return -ENOMEM;
}

对于register_blkdev的调用是可选的,该接口所做的事情是:如果需要的话分配一个动态的主设备号;在/proc/devices中创建一个入口项。

注册磁盘

内核使用gendisk结构(linux/genhd.h)来表示一个独立的磁盘设备。内核还使用该结构体表示分区。该结构中的许多成员必须由驱动程序进行初始化。

  • major
    int major;主设备号
  • first_minor
    int first_minor;一个驱动器至少使用一个次设备号,如果驱动器是可被分区的用户将要为每个可能的分区都分配一个次设备号。
  • minors
    int minors;常取16,一个完整的磁盘可以包含15个分区。
  • disk_name
    char disk_name[32];磁盘设备名字,该名字显示在/proc/partitionssysfs中。
  • fops
    struct block_device_operations *fops;块设备操作
  • queue
    struct request_queue *queue;设备I/O请求队列
  • flags
    int flags;驱动器状态标志。可移动介质将被设置为GENHD_FL_REMOVABLE;CD-ROM设备被设置为GENHD_FL_CD;若不希望在/proc/partitions中显示分区信息可设置为GENHD_FL_SUPPRESS_PARTITION_INFO
  • capacity
    sector_t capacity;以512字节为一个扇区,该驱动器可以包含的扇区数。驱动程序不能直接设置该成员,而要将扇区数传给set_capacity
  • private_data
    void *private_data;块设备驱动可以使用该成员保存指向其内部数据的指针

gendisk是一个动态分配的结构,驱动程序不能自己动态分配该结构,必须使用alloc_disk分配,使用del_gendisk回收。

  • alloc_disk
    struct gendisk *alloc_disk(int minors);参数minors是该磁盘使用的次设备号的数目。为了能正常工作,minors传入后就不能更改了。
  • add_disk
    void add_disk(struct gendisk *gd);使用alloc_disk分配的gendisk不能使磁盘对系统可用,还需要add_disk将磁盘设备激活,并随时准备调用它提供的方法。在驱动程序完全被初始化并且能够响应对磁盘的请求前,请不要调用add_disk
  • get_disk and put_disk
    gendisk是一个引用技术结构,get_diskput_disk函数负责处理引用计数。
  • del_gendisk
    void del_gendisk(struct gendisk *gd);调用del_gendisk会删除gendisk中的最终计数。当没有用户继续使用时,将真正删除该结构。此后,在系统中不会找到该设备信息。
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static void setup_device(struct sbull_dev *dev,int which)
{
memset(dev,0,sizeof(struct sbull_dev)); /* 初始化 dev 所指内容为0*/
dev->size = nsectors * hardsect_size;
dev->data = vmalloc(dev->size);
if(dev->data == NULL)
{
printk(KERN_NOTICE "vmalloc failure.\n");
return ;
}
spin_lock_init(&dev->lock); /* 初始化自旋锁*/
/* 在分配请求队列前要先初始化自旋锁*/
/* The timer which "invalidates the device给内核定时器初始化 "*/
init_timer(&dev->timer); /*初始化定时器,实际将结构中的list成员初始化为空*/
dev->timer.data = (unsigned long)dev; /*被用作function函数的调用参数*/
dev->timer.function = sbull_invalidate; /* 当定时器到期时,就执行function指定的函数*/

/*
* The I/O queue, depending on whether we are using our own
* make_request function or not.
*/
switch(request_mode)
{
case RM_NOQUEUE:
dev->queue = blk_alloc_queue(GFP_KERNEL); /* 分配“请求队列” */
if(dev->queue == NULL)
goto out_vfree;
blk_queue_make_request(dev->queue,sbull_make_request); /*绑定"制造请求"函数 */
break;
case RM_FULL:
dev->queue = blk_init_queue(sbull_full_request,&dev->lock); /*请求队列初始化*/
if(dev->queue == NULL)
goto out_vfree;
break;
case RM_SIMPLE:
dev->queue = blk_init_queue(sbull_request,&dev->lock); /*请求队列初始化*/
if(dev->queue == NULL)
goto out_vfree;
break;
default:
printk(KERN_NOTICE "Bad request mode %d,using simple\n",request_mode);
}
blk_queue_hardsect_size(dev->queue,hardsect_size); /* 硬件扇区尺寸设置 */
dev->queue->queuedata = dev;
dev->gd = alloc_disk(SBULL_MINORS); /* 动态分配 gendisk 结构体*/
if(!dev->gd)
{
printk(KERN_NOTICE "alloc_disk failure\n");
goto out_vfree;
}
/* 初始化 gendisk */
dev->gd->major = sbull_major; /* 主设备号 */
dev->gd->first_minor = which * SBULL_MINORS; /* 第一个次设备号 */
dev->gd->fops = &sbull_ops; /* 块设备操作结构体 */
dev->gd->queue = dev->queue; /* 请求队列 */
dev->gd->private_data = dev; /* 私有数据 */
snprintf(dev->gd->disk_name,32,"sbull%c",which + 'a');
/* 每个请求的大小都是扇区大小的整数倍,内核总是认为扇区大小是512字节,因此必须进行转换*/
set_capacity(dev->gd,nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
add_disk(dev->gd); /* 完成以上初始化后,调用 add_disk 函数来注册这个磁盘设备 */
return ;

out_vfree:
if(dev->data)
vfree(dev->data); /* 释放用 vmalloc 申请的不连续空间*/
}

逆初始化

注销块设备驱动

register_blkdev对应的注销函数为int unregister_blkdev(unsigned int major, const char *name);,传入的参数与传递给register_blkdev的参数必须匹配。

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
static void sbull_exit(void)
{
int i;
for(i = 0; i < ndevices;i++)
{
struct sbull_dev *dev = Devices + i;
del_timer_sync(&dev->timer); /* 去掉 "介质移除" 定时器*/
if(dev->gd)
{
del_gendisk(dev->gd); /* 释放 gendisk 结构体*/
put_disk(dev->gd); /* 释放对 gendisk 的引用 */
}
if(dev->queue)
{
if(request_mode == RM_NOQUEUE)
blk_put_queue(dev->queue);
else
blk_cleanup_queue(dev->queue); // 清除请求队列
}
if(dev->data)
vfree(dev->data);
}
unregister_blkdev(sbull_major,"sbull");
kfree(Devices);
}

块设备操作

字符设备使用file_operations结构,块设备使用类似的结构block_device_operations(linux2.6在linux/fs.h中,linux4.4在linux/blkdev.h中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t, void __pmem **, unsigned long *pfn);
unsigned int (*check_events) (struct gendisk *disk, unsigned int clearing);
/* ->media_changed() is DEPRECATED, use ->check_events() instead */
int (*media_changed) (struct gendisk *);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
/* this callback is with swap_lock and sometimes page table lock held */
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
struct module *owner;
const struct pr_ops *pr_ops;
};
  • open
    int (*open) (struct inode *inode, struct file *filp);当设备被打开时调用它。
  • release
    int (*release) (struct inode *inode, struct file *filp);当设备被关闭时调用它。
  • ioctl
    int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);实现ioctl系统调用的函数,块设备层会先截取大量的标准请求,因此大多数块设备的ioctl函数都十分短小。
  • media_changed
    int (*media_changed) (struct gendisk *gd);内核调用该函数检查用户是否更换了驱动器内的介质,如果更换返回一个非零值。该函数只适用于那些支持可移动介质。
  • revalidate_disk
    int (*revalidate_disk) (struct gendisk *gd);当介质被更换时被调用。
  • owner
    struct module *owner;指向拥有该结构的模块指针,通常被初始化为THIS_MODULE

open

1
2
3
4
5
6
7
8
9
10
11
12
static int sbull_open(struct inode *inode,struct file *filp)
{
struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
del_timer_sync(&dev->timer); //去掉"介质移除"定时器
filp->private_data = dev;
spin_lock(&dev->lock);
if(!dev->users)
check_disk_change(inode->i_bdev);
dev->users++; // 使用计数加 1
spin_unlock(&dev->lock);
return 0;
}

release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int sbull_release(struct inode *inode,struct file *filp)
{
struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
spin_lock(&dev->lock);
dev->users--; // 使用计数减 1
if(!dev->users)
{
//30秒的定时器,如果这个时段内设备没有被打开则移除设备
dev->timer.expires = jiffies + INVALIDATE_DELAY;
add_timer(&dev->timer); //将定时器添加到定时器队列中
}
spin_unlock(&dev->lock);
return 0;
}

media_changed

open中调用check_disk_change函数触发media_changed检查介质是否被改变。如果介质改变则返回非零值。

1
2
3
4
5
int sbull_media_changed(struct gendisk *gd)
{
struct sbull_dev *dev = gd->private_data;
return dev->media_change;
}

revalidate_disk

介质改变后,内核会调用revalidate_disk,调用完成后,内核将重新读取设备的分区表。

1
2
3
4
5
6
7
8
9
10
int sbull_revalidate(struct gendisk *gd)
{
struct sbull_dev *dev = gd->private_data;
if(dev->media_change)
{
dev->media_change = 0;
memset(dev->data,0,dev->size);
}
return 0;
}

ioctl

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
int sbull_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
{
long size;
struct hd_geometry geo;
struct sbull_dev *dev = filp->private_data; // 通过 file->private 获得设备结构体
switch(cmd)
{
case HDIO_GETGEO:
/*
* Get geometry: since we are a virtual device, we have to make
* up something plausible. So we claim 16 sectors, four heads,
* and calculate the corresponding number of cylinders. We set the
* start of data at sector four.
*/
size = dev->size *(hardsect_size/KERNEL_SECTOR_SIZE);
/* 获得几何信息 */
geo.cylinders = (size & ~0x3f) >> 6;
geo.heads = 4;
geo.sectors = 16;
geo.start = 4;
if(copy_to_user((void __user *)arg,&geo,sizeof(geo)))
return -EFAULT;
return 0;
}
return -ENOTTY; // 不知道的命令
}

内核对块设备的物理信息并不感兴趣,它只把设备看成是线性的扇区数组。但一些用户空间的应用程序依然需要查询磁盘的物理信息。特别是fdisk工具。

请求处理

块设备中没有字符设备中的read、write函数,那么块设备是如何处理I/O请求的呢?早在sbull_init中初始化queue时需要传入一个queue处理函数,待内核接收到磁盘I/O时,会调用queue处理函数来处理。

request queue

一个请求队列就是一个动态的数据结构,该结构必须由块设备的I/O子系统创建。

创建删除

  • 创建初始化
    request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);参数request是处理这个队列的函数指针,lock是控制访问队列权限的自旋锁。由于创建过程会分配内存,因此会有失败的可能,所以在使用队列前一定要检查返回值。
  • 删除
    void blk_cleanup_queue(request_queue_t *);调用该函数后,驱动程序将不会再得到这个队列中的请求,也不能再引用这个队列了。

队列元素

  • 从队列中获取请求
    struct request *elv_next_request(request_queue_t *queue);返回一个需要处理的请求指针,该指针由I/O调度器决定,如果没有请求需要处理返回NULL。该函数被调用后,请求依然保存在队列中,但是为其做了活动标记,该标记保证了当开始执行该请求时I/O调度器不再将该请求与其他请求合并。
  • 从队列中删除请求
    void blkdev_dequeue_request(struct request *req);将请求从队列中删除。
  • 请求返回队列
    void elv_requeue_request(request_queue_t *queue, struct request *req);将拿出队列的请求再返回给队列。当驱动需要同时处理同一队列中的多个请求时,一般多用blkdev_dequeue_requestelv_requeue_request

request

一个块请求队列可以包含那些实际并不向磁盘读写数据的请求,生产商信息、底层诊断操作、与特殊设备模式相关指令、介质写模式设定等。每个request结构都代表一个块设备的I/O请求,这个I/O请求可以通过对多个独立请求的合并而来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void sbull_request(request_queue_t *q)
{
struct request *req; //定义请求结构体
while((req = elv_next_request(q)) != NULL)//elv_next_request()获得队列中第一个未完成请求
{
struct sbull_dev *dev = req->rq_disk->private_data;
if(!blk_fs_request(req)) //判断是否为文件系统请求
{
printk(KERN_NOTICE "Skip non-fs request\n");
end_request(req,0); //通知请求处理失败,0为失败,1为成功
continue;
}
sbull_transfer(dev,req->sector,req->current_nr_sectors,req->buffer,rq_data_dir(req));
end_request(req,1);
}
}
  • elv_next_request
    用来获取队列中第一个未完成的请求,当没有请求需要处理时,返回NULL。请求被获取后并不从队列中删除。
  • blk_fs_request
    用来判断该请求是否是一个文件系统请求。
  • end_request
    void end_request(struct request *req, int succeeded);传递当前请求的指针和完成结果(0表示不成功,非0表示成功)。
  • req->sector
    在设备上开始扇区的索引号
  • req->buffer
    传输或者接收数据的缓冲区指针
  • rq_data_dir
    传输方向,返回0表示从设备读数据,非0表示向设备写入数据

如果多个请求都是对磁盘中相邻扇区进行操作,则内核将合并它们,内核不会合并在单独request结构中的读写操作,如果合并的结果会打破对请求队列的限制,则内核也不会对请求进行合并。

bio

一个request结构是作为一个bio结构的链表实现的,保证在执行请求的时候驱动程序能知道执行的位置。当内核以文件系统、虚拟内存子系统或者系统调用的形式决定从块I/O设备输入、输出块数据时,它将再结合一个bio结构,用来描述这个操作。该结构被传递给I/O代码,代码会把它合并到一个已存在的request结构中,或者根据需要再创建一个新的request结构。bio结构包含了驱动程序执行请求的全部信息,而不必与初始化这个请求的用户空间的进程相关联。

block

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned int bi_flags; /* status, command, etc */
int bi_error;
unsigned long bi_rw; /* bottom bits READ/WRITE, top bits priority */
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
unsigned short bi_vcnt; /* how many bio_vec's */
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t __bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};

bio的核心是一个名为bi_io_vec的数组

1
2
3
4
5
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};

遍历request中的bio

1
2
3
4
5
6
7
8
9
10
11
static int sbull_xfer_request(struct sbull_dev *dev,struct request *req)
{
struct bio *bio;
int nsect = 0;
rq_for_each_bio(bio,req)//此宏遍历请求中的每个bio,传递用于sbull_xfer_bio()传输的指针
{
sbull_xfer_bio(dev,bio); //调用 bio 处理函数
nsect += bio->bi_size/KERNEL_SECTOR_SIZE; //传递的字节数/扇区大小等于扇区数
}
return nsect;
}

遍历bio中的segment

遍历bio中的segment,也就是遍历bio结构中的bi_io_vec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int sbull_xfer_bio(struct sbull_dev *dev,struct bio *bio)
{
int i;
struct bio_vec *bvec; //定义实际的 vec 列表
sector_t sector = bio->bi_sector; //定义要传输的第一个扇区
//下面的宏遍历bio的每一段,获得一个内核虚拟地址来存取缓冲
bio_for_each_segment(bvec,bio,i)
{
char *buffer = __bio_kmap_atomic(bio,i,KM_USER0);//通过kmap_atomic()函数获得返
//回bio的第i个缓冲区的虚拟地址
sbull_transfer(dev,
sector, // 开始扇区的索引号
bio_cur_sectors(bio), // 需要传输的扇区数
buffer, // 传输数据的缓冲区指针
bio_data_dir(bio)== WRITE); // 传输方向,0表述从设备读,非0从设备写
sector += bio_cur_sectors(bio); //返回扇区数
__bio_kunmap_atomic(bio,KM_USER0); //返回由 __bio_kmap_atomic()获得的内核虚拟地址
}
return 0;
}

参考&鸣谢

  • 《设备驱动程序》

Perf tools是由内核维护人员Ingo Molnar等人开发的linux内核的综合性能该要分析工具。通过它,应用程序可以利用 PMU,tracepoint 和内核中的特殊计数器来进行性能统计。它不但可以分析指定应用程序的性能问题 (per thread),也可以用来分析内核的性能问题,当然也可以同时分析应用代码和内核,从而全面理解应用程序中的性能瓶颈。

环境

** OS **

ubuntu 16.04.5 LTS

** Kernel **

1
2
$ uname -a
Linux ubuntu-linux 4.4.0-138-generic #164-Ubuntu SMP Tue Oct 2 17:16:02 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

安装

perf的安装可以分成两种,一种是package安装法;

1
sudo apt install linux-tools-common linux-tools-4.4.0-138-generic

另一种安装方法是源码安装,这种方法需要先下载当前内核对应的源代码,

1
2
# 下载 4.4.0-138 源码
sudo apt-get install linux-source=4.4.0.138.144

然后进入/usr/src目录中的源代码目录下,找到tools/perf目录,并在该目录下执行make编译perf源码,再执行make install安装perf

perf使用姿势

姿势一

选择分析的事件

通过perf list列出可以选择的分析事件

概要分析数据

使用perf topperf stat进行该要分析系统性能,perf top类似于linux的top命令,按进程显示分析数据,可以使用-e参数指定分析事件,通过perf top找出系统中的问题进程。(更多关于perf top的使用精请见perf top -h

perf stat用于汇总综合性能,有人用此方法比较性能改善前后的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Performance counter stats for 'ls /':

0.827614 task-clock (msec) # 0.707 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
91 page-faults # 0.110 M/sec
<not supported> cycles
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
<not supported> instructions
<not supported> branches
<not supported> branch-misses

0.001169835 seconds time elapsed

(由于我使用的是VM,所有有很多不支持的)

  • task clock
    进程执行所花费的时间,CPU 利用率,该值高,说明程序的多数时间花费在 CPU 计算上而非 IO。
  • context switches
    进程切换次数,记录了程序运行过程中发生了多少次进程切换,频繁的进程切换是应该避免的。
  • cpu migrations
    进程运行过程中发生了多少次 CPU 迁移,即被调度器从一个 CPU 转移到另外一个 CPU 上运行。
  • page faults
    按需分页或交换等过程中使用的页面错误的发生次数和频率
  • cycles
    处理器时钟,一条机器指令可能需要多个 cycles
  • instructions
    机器指令数目
  • branches
    分支命令的数量和频率
  • branch misses
    分支预测错误的比例

分析记录的数据

记录并保存分析数据

通过perf record命令实现将分析数据保存到perf.data(默认)文件中,虽然perf top也能获取分析数据,但是perf top每隔2秒(默认)更新一次,而且对于一些特殊case不好抓取。perf record特别适合针对某个进程某个事件的分析非常好用。具体使用方法可以参考perf record -h

查看保存的分析数据

通过perf report查看perf.data(默认),perf report分析内容的查看与perf top的查看方式相同。

姿势二

perf script

一般情况下,我们可以通过perf record-e指定追踪事件方式来获取进程相关事件分析数据(perf.data),然后通过perf script(不加任何脚本情况下)只显示记录到的追踪事件数据。若perf script后指定脚本,那么会将记录到的追踪事件数据作为输入,传递给指定的脚本处理。

perf脚本处理数据分为在线处理和离线处理;

  • 在线处理
    在进行追踪的同时运行脚本,则不可避免地会增加负载
    eg: perf script sctop 统计系统调用次数
  • 离线处理
    进行离线处理时先追踪后处理,可以抑制处理对追踪对象的影响
    eg: perf script record syscall-counts ls / 先追踪,后处理;perf script report syscall-counts 查看统计系统调用次数结果

** 自定义脚本 **

用户可根据自己的需求编写自己的Python或Perl脚本处理追踪事件数据,二者分别需要安装libpython或libperl,并且需要重新build perf。

过程:

  • 先记录必要事件的数据文件
    使用perf record记录执行ls /过程中内存分配kmalloc()的相关事件数据
    eg: perf record -e kmem:kmalloc -e raw_syscalls:sys_enter ls /
  • 生成自定义脚本
    使用perf script -g perl(perl脚本) or perf script -g python(python脚本)生成自定义脚本,根据需要修改生成的脚本内容
  • 执行自定义脚本
    使用perf script+-s参数指定用户自定义的脚本执行追踪处理

姿势三

Todo…

perf tools这把瑞士军刀可以说功能齐全威力惊人,但能将其发挥几层还要看使用者的功力如何。

参考&鸣谢