- 如果http请求中没有携带身份信息(AccessKey),则改该请求为匿名请求,会被认为是来自匿名用户的访问。
- 如果http请求中携带了身份信息(AccessKey),则认为访问来自该AccessKey所对应的用户。由于AccessKey是可以被他人获取到的, 为了防止其他人冒用您的AccessKey来访问服务,请求中还必须携带您的签名。在申请帐号以后,您将得到AccessKey和SecretKey, SecretKey是需要保密的。签名是由此次http请求的相关信息和您的SecretKey计算得到的,其他人因为不知道您的SecretKey,将不能 计算出正确的签名。
- 身份信息与签名可以放到请求头(Authorization)中,也可以放到请求参数中。
- 签名的方式与Amazon S3的签名方式兼容,支持signature version 2 和 signature version 4。
s3 authorization v2
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 | StringToSign = HTTP-Verb + "\n" + |
- HTTP-Verb: 请求的方法,如:
PUT
,GET
,DELETE
,POST
- 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
9GET /?foo=bar
GET /yourbucket/yourkey?foo=bar
GET /yourbucket/yourkey?acl&foo=bar
对应的nicalizedResource分别为:
/
/yourbucket/yourkey
/yourbucket/yourkey?acl
参考&鸣谢
s3 authorization v4
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 | kSecret = YourSecretKey |
- YourSecretKey 你的
SecretKey
- Date 8位数的日期,应与Credentail中的Date部分一样
- Region 应与Credential中的region部分一样
- Service 应与Credential中的服务名部分一样
- kSigning 为用于计算签名的signing key
StringToSign
1 | StringToSign = |
- 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 | CanonicalRequest = |
- HTTPRequestMethod 如:
PUT
,GET
,DELETE
,POST
- CanonicalURI 请求的uri
- CanonicalQueryString 请求参数排序后组成的字符串
- CanonicalHeaders 需要加入签名计算的头排序后组成的字符串
- SignedHeaders 加入到签名计算的头的名字的列表,各个名字之间用逗号分隔
- HexEncode(Hash(RequestPayload)) 请求body的hash的16进制编码,如果通过请求参数携带签名,此处应由字符串
UNSIGNED-PAYLOAD
代替
参考&鸣谢
异常PG处理套路
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 | [root@ceph71 ~]# ceph pg map 1.5f |
背景就是介绍这么多吧,接下来说说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踢出集群。
** 解决 **
- 将osd reweight权重置0,将数据分散到其他osd上
ceph osd reweight {osd-id} 0
- 待集群rebalance后,开始删除osd
unfound objects
ceph集群知道该对象存在,但无法定位该object在哪时会报这个错误。
** 解决 **
- 尝试让失败的osd起来,如果起来后集群恢复正常,则结束
- 试将该pg的unfound对象回滚到上一个版本,
ceph pg {pgid} mark_unfound_lost revert
如果恢复正常,则结束 - 如果还是不行,那只有将该object删除掉了,注意这会导致丢失数据,执行
ceph pg {pgid} mark_unfound_lost delete
删除unfound对象
inconsistent objects
pg中保存的object中有些副本数据不一致,有些事伴随着scrub errors错误
** 解决 **
ceph health detail
找出问题pg- 尝试
ceph pg repair {pgid}
,若成功,则结束;不成功进行如下操作。 - 使用
ceph pg map {pgid}
找出主osd,打开日志查看哪个object不一致 - 找出所有该objects所有副本存放的位置,用摘要算法(md5sum,sha256)等计算出其hash值,如果是3副本,删除与其他副本不一致的;如果是2副本,则可能会误删。
- 再次执行
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。
** 解决 **
- 使用命令
ceph pg dump |grep stale
找出所有的stale的pg,也可以ceph health detail | grep stale
- 执行
ceph pg force_create_pg {pgid}
命令强制重新创建pg,会看到pg转为creating状态 - 重启ceph中所有osd服务
Peered
Peering 已经完成,但是 PG 当前 Acting Set 规模小于存储池规定的最小副本数 (min_size)。
如果pool的副本数为3、min_size=2,停掉两个副本所在的osd,此时访问集群的客户端处于blocked状态。
** 解决 **
- 先尝试将down掉的两个osd恢复,或至少恢复一个。
- 若不能正常恢复,可修改
min_size=1
,解除客户端blocked状态
鸣谢&参考
Backfill & Recovery 恢复优化
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 |
rbd-mirror 跨集群备份
原理
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 | $ systemctl enable ceph-rbd-mirror@admin.service |
@admin
是ceph中的client.admin
用户,只是这里省略了client.
。关于Ceph用户管理相关操作可以使用ceph auth
命令。
Copy Config & keyring
将cls17集群中的ceph.conf
和ceph.client.admin.keyring
copy到cls26集群的/etc/ceph
目录中,并重命名成cls17.conf
和cls17.client.admin.keyring
。然后在cls26集群中可通过指定--cluster cls17
来指定访问集群。
备份Pool中所有RBD
配置
Pool Mirror Mode
需要将两个集群的的rmp
pool的mirror mode设置成pool
。
集群cls26
1 | $ rbd mirror pool enable rmp pool |
集群cls17
1 | $ rbd mirror pool enable rmp pool --cluster cls17 |
Add Peer
将Primary集群信息加入到rmp
pool中。
集群cls26
1 | $ rbd mirror pool peer add rmp client.admin@cls17 |
查看两个集群的peer信息
1 | $ rbd mirror pool info rmp |
至此备份Pool中所有RBD的配置操作已经完成,接下来进行验证操作。
测试
在集群cls17中rmp
pool创建RBD,并查看集群cls26中rmp
pool是否有回放操作(RBD是否备份到cls26集群)。由于整个RBD-Mirror依赖于journaling
feature,所以创建RBD时需要指定该feature。若对当前rmp
pool中存在RBD进行备份,需要先增加jouranling
feature,使用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 | $ rbd -p rmp ls |
备份单个RBD
配置
Pool Mirror Mode
将两个集群的rbd
pool的mirror mode设置成image
。
1 | $ rbd mirror pool enable rbd image |
Add Peer
将Primary集群信息加入到rbd
pool中。具体操作方法,同上。
Enable Image Mirror
集群cls17
1 | $ rbd info rd1 |
集群cls26
1 | $ rbd info rd1 |
测试
(同上)
*** 无论是全Pool备份,还是单个RBD备份,只要建立mirror关系,只有primary rbd才能处理读写操作 ***
RBD备份
- Ceph中的RBD备份分为全量备份和增量备份,恢复也有增全量之分。
- 备份和恢复的操作都是基于快照来操作的。
- RBD的format必须为2 (–image-format 2)
备份
备份操作流程,先做一个全量备份,然后每隔一段时间做一个快照,并将快照导出。
准备
创建一个RBD image
1 | $ rbd create --image-format 2 --size 1G crbd_cls17_1 |
全量
创建一个快照,然后备份从image创建一直到创建快照之前的全量数据
1 | $ rbd snap create crbd_cls17_1@s1 |
增量
继续写入一段数据后,再次做快照,并导出增量数据
1 | $ rbd snap create crbd_cls17_1@s2 |
至此,全量和增量备份数据都以导出到文件crbd_base
和crbd_s1_s2
两个文件中。
恢复
准备
将之前导出的增全量备份数据crbd_base
、crbd_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!
Journal Format
Linux块设备驱动——sbull
块设备驱动程序,linux内核中的一类驱动程序。通过传输固定大小的随机数据来访问设备。
初始化
注册块设备驱动
向内核注册设备驱动程序使用函数int register_blkdev(unsigned int major, const char *name);
(linux/fs.h),参数是该设备使用的主设备号和名称(名字在/proc/devices
中显示)。若传入的主设备号为0,内核将分配一个新的主设备号,并将该设备号返回给调用者;失败则返回负值。
1 | static int __init sbull_init(void) |
对于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/partitions
和sysfs
中。 - 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_disk
和put_disk
函数负责处理引用计数。 - del_gendisk
void del_gendisk(struct gendisk *gd);
调用del_gendisk
会删除gendisk
中的最终计数。当没有用户继续使用时,将真正删除该结构。此后,在系统中不会找到该设备信息。
1 | static void setup_device(struct sbull_dev *dev,int which) |
逆初始化
注销块设备驱动
与register_blkdev
对应的注销函数为int unregister_blkdev(unsigned int major, const char *name);
,传入的参数与传递给register_blkdev
的参数必须匹配。
1 | static void sbull_exit(void) |
块设备操作
字符设备使用file_operations
结构,块设备使用类似的结构block_device_operations
(linux2.6在linux/fs.h
中,linux4.4在linux/blkdev.h
中)
1 | struct block_device_operations { |
- 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 | static int sbull_open(struct inode *inode,struct file *filp) |
release
1 | static int sbull_release(struct inode *inode,struct file *filp) |
media_changed
open
中调用check_disk_change
函数触发media_changed
检查介质是否被改变。如果介质改变则返回非零值。
1 | int sbull_media_changed(struct gendisk *gd) |
revalidate_disk
介质改变后,内核会调用revalidate_disk
,调用完成后,内核将重新读取设备的分区表。
1 | int sbull_revalidate(struct gendisk *gd) |
ioctl
1 | int sbull_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg) |
内核对块设备的物理信息并不感兴趣,它只把设备看成是线性的扇区数组。但一些用户空间的应用程序依然需要查询磁盘的物理信息。特别是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_request
和elv_requeue_request
request
一个块请求队列可以包含那些实际并不向磁盘读写数据的请求,生产商信息、底层诊断操作、与特殊设备模式相关指令、介质写模式设定等。每个request结构都代表一个块设备的I/O请求,这个I/O请求可以通过对多个独立请求的合并而来。
1 | static void sbull_request(request_queue_t *q) |
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结构包含了驱动程序执行请求的全部信息,而不必与初始化这个请求的用户空间的进程相关联。
1 | struct bio { |
bio的核心是一个名为bi_io_vec
的数组
1 | struct bio_vec { |
遍历request中的bio
1 | static int sbull_xfer_request(struct sbull_dev *dev,struct request *req) |
遍历bio中的segment
遍历bio中的segment,也就是遍历bio结构中的bi_io_vec
。
1 | static int sbull_xfer_bio(struct sbull_dev *dev,struct bio *bio) |
参考&鸣谢
- 《设备驱动程序》
perf-tools
Perf tools是由内核维护人员Ingo Molnar等人开发的linux内核的综合性能该要分析工具。通过它,应用程序可以利用 PMU,tracepoint 和内核中的特殊计数器来进行性能统计。它不但可以分析指定应用程序的性能问题 (per thread),也可以用来分析内核的性能问题,当然也可以同时分析应用代码和内核,从而全面理解应用程序中的性能瓶颈。
环境
** OS **
ubuntu 16.04.5 LTS
** Kernel **
1 | $ uname -a |
安装
perf的安装可以分成两种,一种是package安装法;
1 | sudo apt install linux-tools-common linux-tools-4.4.0-138-generic |
另一种安装方法是源码安装,这种方法需要先下载当前内核对应的源代码,
1 | # 下载 4.4.0-138 源码 |
然后进入/usr/src
目录中的源代码目录下,找到tools/perf
目录,并在该目录下执行make
编译perf源码,再执行make install
安装perf
perf使用姿势
姿势一
选择分析的事件
通过perf list
列出可以选择的分析事件
概要分析数据
使用perf top
和perf stat
进行该要分析系统性能,perf top
类似于linux的top
命令,按进程显示分析数据,可以使用-e
参数指定分析事件,通过perf top
找出系统中的问题进程。(更多关于perf top
的使用精请见perf top -h
)
perf stat
用于汇总综合性能,有人用此方法比较性能改善前后的结果。
1 | Performance counter stats for 'ls /': |
(由于我使用的是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脚本) orperf script -g python
(python脚本)生成自定义脚本,根据需要修改生成的脚本内容 - 执行自定义脚本
使用perf script
+-s
参数指定用户自定义的脚本执行追踪处理
姿势三
Todo…
收
perf tools这把瑞士军刀可以说功能齐全威力惊人,但能将其发挥几层还要看使用者的功力如何。