0%

Android 加密方案整理

1. 加密方案整理

1.1. 简介

Android上加密方案分为全磁盘加密(FDE)/文件加密(FBE)/元数据加密(METADATA)
均是针对userdata分区进行加密, 与其他分区并无关系.
本篇主要介绍下这三种加密方式硬件加密的实现框架

1.2. 判断机器采用何种加密方式

最直观的方式是可以通过分区表fstab来区分
未加密/fde加密/fbe加密/metadata加密

1
2
3
/dev/block/bootdevice/by-name/userdata     /data           ext4    noatime,nosuid,nodev,barrier=1,data=ordered,noauto_da_alloc     wait,check,encryptable=footer
/dev/block/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=1,noauto_da_alloc,discard latemount,wait,check,fileencryption=ice,wrappedkey,quota,reservedsize=128M
/dev/block/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=1,noauto_da_alloc,discard latemount,wait,check,fileencryption=ice,wrappedkey,keydirectory=/metadata/vold/metadata_encryption,quota,reservedsize=128M

当前版本中最常见的是fbe加密的机型.

代码中判断是否加密:

1
2
3
StorageManager.isEncrypted()
StorageManager.isFileEncryptedNativeOnly()
StorageManager.isBlockEncrypted()

native 层多是通过FstabEntry来判断是何种加密方式

1
2
3
4
static bool should_use_metadata_encryption(const FstabEntry& entry) {
return !entry.key_dir.empty() &&
(entry.fs_mgr_flags.file_encryption || entry.fs_mgr_flags.force_fde_or_fbe);
}

硬件加密的启用, 其中fbe机型和metadata机型是用fileencryption=ice控制的(限于高通产品), 而fbe和metadata加密方式的不同是看keydirectory=是否为空

1.3. 硬件加密实现策略

首先跟着fileencryption=ice往下看

1
2
3
4
5
6
7
8
9
@startuml
autoactivate on
init->vold: initUser0
vold->vold: fscrypt_init_user0
vold->vold: fscrypt_prepare_user_storage
vold->vold: get_data_file_encryption_modes(&de_ref|&ce_ref);
note down: key_ref->contents_mode = entry->file_contents_mode;
vold-->init: return init_user0
@enduml
1
2
3
4
5
6
7
8
@startuml
autoactivate on
StorageManagerService->vold: prepareUserStorage
vold->vold: fscrypt_prepare_user_storage
vold->vold: get_data_file_encryption_modes(&de_ref|&ce_ref);
note right: key_ref->contents_mode = entry->file_contents_mode;
vold-->StorageManagerService: return prepareUserStorage
@enduml

对于FBE方案来说, Android标准方案使用ice来控制是否是硬件加密

文件加密 android developer
metadata加密 android developer

为了支持当前的元数据加密,您的硬件必须支持使用内嵌加密引擎进行文件级加密。fstab.hardware 中的用户数据分区的 fileencryption=ice 指令指明了这一点。

通过将 fileencryption=contents_encryption_mode[:filenames_encryption_mode] 标记添加到 userdata 分区最后一列的 fstab 行中,可以启用 FBE。contents_encryption_mode 参数定义用于文件内容加密的算法,filenames_encryption_mode 参数定义用于文件名加密的算法。 contents_encryption_mode 只能是 aes-256-xts。 filenames_encryption_mode 有两个可能的值:aes-256-cts 和 aes-256-heh。如果未指定 filenames_encryption_mode,则使用 aes-256-cts 值。

^61db89

1.4. 硬件加密底层实现

1.4.1. qcom实现

先看下metadata加密, metadata加密方案相对FBE来说, 在fbe的基础上又对文件系统的元数据进行了加密

1
2
3
4
5
#define DEFAULT_KEY_TARGET_TYPE "default-key"
if (!read_key(*data_rec, needs_encrypt, &key)) return false;
if (!get_number_of_sectors(data_rec->blk_device, &nr_sec)) return false;
if (!create_crypto_blk_dev(kDmNameUserdata, nr_sec, DEFAULT_KEY_TARGET_TYPE,
default_key_params(blk_device, key), &crypto_blkdev))

上述创建dm设备时, 传入的tgt->target_type是default-key, 对应于device-mapper框架来说, 最终找到的插件实现是dm-default-key.c

该层的代码在kernel md层
kernel/msm-4.14/drivers/md/dm-default-key.c

1
2
3
4
5
6
7
8
9
10
11
static struct target_type default_key_target = {
.name = "default-key",
.version = {1, 0, 0},
.module = THIS_MODULE,
.ctr = default_key_ctr,
.dtr = default_key_dtr,
.map = default_key_map,
.status = default_key_status,
.prepare_ioctl = default_key_prepare_ioctl,
.iterate_devices = default_key_iterate_devices,
};

关于dm设备的转发, 这里不再详细分析. 感兴趣的可以再跟着流程看下.
这里重点说下map函数, 在设置好dm规则后, 用户空间命令通过ioctl调用table_load函数,该函数根据用户空间传来的参数构建指定mapped device的映射表和所映射的target device。该函数先构建相应的dm_table、dm_target结构,再调用dm-table.c中的dm_table_add_target(populate_table—>dm_table_add_target)函数根据用户传入的参数初始化这些结构,并且根据参数所指定的target类型,调用相应的target类型的构建函数ctr在内存中构建target device对应的结构,然后再根据所建立的dm_target结构更新dm_table中维护的B树。上述过程完毕后,再将建立好的dm_table添加到mapped device的全局hash表对应的hash_cell结构中。

设置io转发, 一个是通过dm create 时初始设置的dm_wq_work后台线程执行的

1
2
3
ioctl(dm_fd.get(), DM_DEV_CREATE, io)
dev_create -> dm_create(minor, **result) -> alloc_dev(minor) ->
INIT_WORK(&md->work, dm_wq_work);

write_back时, 调用queue_io, 或dm设备处于suspend时

1
2
3
4
5
6
7
8
9
static void queue_io(struct mapped_device *md, struct bio *bio)
{
unsigned long flags;

spin_lock_irqsave(&md->deferred_lock, flags);
bio_list_add(&md->deferred, bio);
spin_unlock_irqrestore(&md->deferred_lock, flags);
queue_work(md->wq, &md->work);
}

还有一条路径是通过 dm_setup_md_queue

1
2
3
4
5
6
    case DM_TYPE_BIO_BASED:
case DM_TYPE_DAX_BIO_BASED:
dm_init_normal_md_queue(md);
blk_queue_make_request(md->queue, dm_make_request);
dm_make_request-> __dm_make_request(q, bio, __split_and_process_bio); -> __send_empty_flush | __split_and_process_non_flush
-> __clone_and_map_data_bio | __send_duplicate_bios -> __map_bio

其中 __split_and_process_bio可以了解一下

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// mapped_device指向 映射的dm设备, map为映射规则
// split a bio into clones and submit them to the targets
static blk_qc_t __split_and_process_bio(struct mapped_device *md,
struct dm_table *map, struct bio *bio)
{
struct clone_info ci;
blk_qc_t ret = BLK_QC_T_NONE;
int error = 0;

blk_queue_split(md->queue, &bio);

init_clone_info(&ci, md, map, bio);

if (bio->bi_opf & REQ_PREFLUSH) {
ci.bio = &ci.io->md->flush_bio;
ci.sector_count = 0;
error = __send_empty_flush(&ci);
/* dec_pending submits any data associated with flush */
} else if (bio_op(bio) == REQ_OP_ZONE_RESET) {
ci.bio = bio;
ci.sector_count = 0;
error = __split_and_process_non_flush(&ci);
} else {
ci.bio = bio;
ci.sector_count = bio_sectors(bio);
while (ci.sector_count && !error) {
error = __split_and_process_non_flush(&ci);
if (current->bio_list && ci.sector_count && !error) {
struct bio *b = bio_split(bio, bio_sectors(bio) - ci.sector_count,
GFP_NOIO, &md->queue->bio_split);
ci.io->orig_bio = b;
bio_chain(b, bio);
ret = generic_make_request(bio);
break;
}
}
}
/* drop the extra reference count */
dec_pending(ci.io, errno_to_blk_status(error));
return ret;
}

/*
* Select the correct strategy for processing a non-flush bio.
*/
static int __split_and_process_non_flush(struct clone_info *ci)
{
struct bio *bio = ci->bio;
struct dm_target *ti;
unsigned len;
int r;
// 找到target_device
ti = dm_table_find_target(ci->map, ci->sector);
if (!dm_target_is_valid(ti))
return -EIO;

if (unlikely(__process_abnormal_io(ci, ti, &r)))
return r;

if (bio_op(bio) == REQ_OP_ZONE_REPORT)
len = ci->sector_count;
else
len = min_t(sector_t, max_io_len(ci->sector, ti),
ci->sector_count);

r = __clone_and_map_data_bio(ci, ti, ci->sector, &len);
if (r < 0)
return r;

ci->sector += len;
ci->sector_count -= len;

return 0;
}

static int __clone_and_map_data_bio(struct clone_info *ci, struct dm_target *ti,
sector_t sector, unsigned *len)
{
struct bio *bio = ci->bio;
struct dm_target_io *tio;
int r;

tio = alloc_tio(ci, ti, 0, GFP_NOIO);
tio->len_ptr = len;
r = clone_bio(tio, bio, sector, *len);
if (r < 0) {
free_tio(tio);
return r;
}
(void) __map_bio(tio);

return 0;
}

static blk_qc_t __map_bio(struct dm_target_io *tio)
{
int r;
sector_t sector;
struct bio *clone = &tio->clone;
struct dm_io *io = tio->io;
struct mapped_device *md = io->md;
// 取出上面封装的target_device的结构体
struct dm_target *ti = tio->ti;
blk_qc_t ret = BLK_QC_T_NONE;

clone->bi_end_io = clone_endio;

/*
* Map the clone. If r == 0 we don't need to do
* anything, the target has assumed ownership of
* this io.
*/
atomic_inc(&io->io_count);
sector = clone->bi_iter.bi_sector;
// 调用插件的map函数. ti为dm_target, clone为复制的bio
r = ti->type->map(ti, clone);
switch (r) {
...
// map正常执行的话, 返回DM_MAPIO_REMAPPED
case DM_MAPIO_REMAPPED:
/* the bio has been remapped so dispatch it */
trace_block_bio_remap(clone->bi_disk->queue, clone,
bio_dev(io->orig_bio), sector);
if (md->type == DM_TYPE_NVME_BIO_BASED)
ret = direct_make_request(clone);
else
// 往下层转发
ret = generic_make_request(clone);
break;
...
}

return ret;
}

由上述调用, 最终起到封装bio的目的. 复制了一份bio到clone中, 找到dm-target的type(插件), 用其注册的map函数封装clone, clone为指针, map函数会修改其值.

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
// ti封装的private为其初始化时填入的值
// Construct a default-key mapping: <mode> <key> <dev_path> <start>
static int default_key_ctr(struct dm_target *ti, unsigned int argc, char **argv)
{
ti->private = dkc;
hex2bin(dkc->key.raw, argv[1], key_size);
// 根据传入的path 找到其 target_device, 就是往dkc的dev里填入 bdev, 最后ti->private准备完成
dm_get_device(ti, argv[2], dm_table_get_mode(ti->table),
&dkc->dev);
sscanf(argv[3], "%llu%c", &tmp, &dummy)
dkc->start = tmp;
// 必须支持硬件加密?
if (!blk_queue_inlinecrypt(bdev_get_queue(dkc->dev->bdev))) {
ti->error = "Device does not support inline encryption";
err = -EINVAL;
goto bad;
}

}
static int default_key_map(struct dm_target *ti, struct bio *bio)
{
const struct default_key_c *dkc = ti->private;
// 设置clone的block_device(target_device), 最后通过generic_make_request 往下层转发
bio_set_dev(bio, dkc->dev->bdev);
if (bio_sectors(bio)) {
// bio来自于mapped_device需要根据映射关系转换成target_device的真实的扇区号
bio->bi_iter.bi_sector = dkc->start +
dm_target_offset(ti, bio->bi_iter.bi_sector);
}

if (!bio->bi_crypt_key && !bio->bi_crypt_skip)
// 最重要的信息, 为bio带上了密钥信息
bio->bi_crypt_key = &dkc->key;

return DM_MAPIO_REMAPPED;
}

可见metadata加密使用dm设备的目的, 就是为bio加上密钥信息, 那这个密钥信息是在哪里用的呢, 而且dm-default-key.c里的实现里明确提示了需要硬件支持.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int default_key_iterate_devices(struct dm_target *ti,
iterate_devices_callout_fn fn,
void *data)
{
struct default_key_c *dkc = ti->private;
// fn为queue_supports_inline_encryption函数, 所以最终是通过找到target_device查看是否支持硬件加密
return fn(ti, dkc->dev, dkc->start, ti->len, data);
}
static int queue_supports_inline_encryption(struct dm_target *ti,
struct dm_dev *dev,
sector_t start, sector_t len,
void *data)
{
struct request_queue *q = bdev_get_queue(dev->bdev);
return q && blk_queue_inlinecrypt(q);
}
// 是否支持硬件加密是在初始化时指定的, 绕到block层了 对应于emmc和ufs, 分别走的mmc framework和scsi架构
// mmc core的实现 是在mmc_blk_alloc_req时做的
if (host->inlinecrypt_support)
queue_flag_set_unlocked(QUEUE_FLAG_INLINECRYPT, mq->queue);
// scsi中的实现
if (shost->inlinecrypt_support)
queue_flag_set_unlocked(QUEUE_FLAG_INLINECRYPT, q);

所以上述硬件加密的信息应该是做在mmc框架层, 通过mmc框架与存储器件通信, 进行硬件加密

1.4.2. scsi架构探寻

1.4.2.1. 软件架构

Linux kernel的驱动框架有两个要点:

  • 抽象硬件(硬件架构是什么样子,驱动框架就应该是什么样子)。
  • 向“客户”提供使用该硬件的API(之前我们提到最多的客户是“用户空间的Application”,不过也有其它“客户”,例如内核空间的其它driver、其它framework)。

先看kconfig

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
config SCSI_UFSHCD
tristate "Universal Flash Storage Controller Driver Core"
depends on SCSI && SCSI_DMA
select PM_DEVFREQ
select DEVFREQ_GOV_SIMPLE_ONDEMAND
select NLS
---help---
This selects the support for UFS devices in Linux, say Y and make
sure that you know the name of your UFS host adapter (the card
inside your computer that "speaks" the UFS protocol, also
called UFS Host Controller), because you will be asked for it.
The module will be called ufshcd.

To compile this driver as a module, choose M here and read
<file:Documentation/scsi/ufs.txt>.
However, do not compile this as a module if your root file system
(the one containing the directory /) is located on a UFS device.

config SCSI_UFSHCD_PLATFORM
tristate "Platform bus based UFS Controller support"
depends on SCSI_UFSHCD
---help---
This selects the UFS host controller support. Select this if
you have an UFS controller on Platform bus

config CRYPTO_DEV_QCOM_ICE
tristate "Inline Crypto Module"
default n
depends on BLK_DEV_DM
help
This driver supports Inline Crypto Engine for QTI chipsets, MSM8994
and later, to accelerate crypto operations for storage needs.
To compile this driver as a module, choose M here: the
module will be called ice.

config SCSI_UFS_QCOM
tristate "QCOM specific hooks to UFS controller platform driver"
depends on SCSI_UFSHCD_PLATFORM && ARCH_QCOM
select PHY_QCOM_UFS
select EXTCON
select EXTCON_STORAGE_CD_GPIO
help
This selects the QCOM specific additions to UFSHCD platform driver.
UFS host on QCOM needs some vendor specific configuration before
accessing the hardware which includes PHY configuration and vendor
specific registers.

Select this if you have UFS controller on QCOM chipset.
If unsure, say N.

config SCSI_UFS_QCOM_ICE
bool "QCOM specific hooks to Inline Crypto Engine for UFS driver"
depends on SCSI_UFS_QCOM && CRYPTO_DEV_QCOM_ICE
help
This selects the QCOM specific additions to support Inline Crypto
Engine (ICE).
ICE accelerates the crypto operations and maintains the high UFS
performance.

Select this if you have ICE supported for UFS on QCOM chipset.
If unsure, say N.
1
2
3
4
5
6
obj-$(CONFIG_ARCH_QCOM)            += pfe/
obj-$(CONFIG_SCSI_UFSHCD) += ufshcd-core.o
obj-$(CONFIG_SCSI_UFSHCD_PLATFORM) += ufshcd-pltfrm.o
obj-$(CONFIG_CRYPTO_DEV_QCOM_ICE) += ice.o
obj-$(CONFIG_SCSI_UFS_QCOM_ICE) += ufs-qcom-ice.o
obj-$(CONFIG_SCSI_UFS_QCOM) += ufs-qcom.o

从上述的依赖条件看, 主要是涉及到ufs-qcom.cufs-qcom-ice.c文件的分析

platform driver 是什么?
一个现实的Linux设备和驱动通常挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设等却不依附于此类总线。基于这个背景,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform device,驱动称为platform driver。
注意platform device并不是和字符设备,块设备和网络设备并列的概念,而是实现它们的一种方式。通过这种方式实现的就称为platform device。

platform 是一个虚拟的地址总线,相比 PCI、USB,它主要用于描述SOC上的片上资源。platform 所描述的资源有一个共同点:在CPU 的总线上直接取址。平台设备会分到一个名称(用在驱动绑定中)以及一系列诸如地址和中断请求号(IRQ)之类的资源。 ^39c29c

platform_driver开发

1
2
3
4
5
6
7
8
9
10
11
12
static struct platform_driver ufs_qcom_pltform = {
.probe = ufs_qcom_probe,
.remove = ufs_qcom_remove,
.shutdown = ufshcd_pltfrm_shutdown,
.driver = {
.name = "ufshcd-qcom",
.pm = &ufs_qcom_pm_ops,
// 从设备数获取 { .compatible = "qcom,ufshc"},
.of_match_table = of_match_ptr(ufs_qcom_of_match),
},
};
module_platform_driver(ufs_qcom_pltform);

platform device的.prob函数中执行的是大致为四步:

  1. 申请设备号
  2. 实体设备(如果是字符设备,就是cdev)初始化注册,添加填充好的file_operation
  3. 从pdev读出硬件资源
  4. 对硬件资源初始化,ioremap() ,request_irq()等操作

platform device的.remove函数实现的是注销分配的各种资源。

driver的绑定是通过driver core自动完成的,完成driver和device的匹配后以后会自动执行probe()函数,如果函数执行成功,则driver和device就绑定在一起了,drvier和device匹配的方法有3种: ^a36448

  • 当一个设备注册的时候,他会在总线上寻找匹配的driver,platform device一般在系统启动很早的时候就注册了

  • 当一个驱动注册[platform_driver_register()]的时候,他会遍历所有总线上的设备来寻找匹配,在启动的过程驱动的注册一般比较晚,或者在模块载入的时候
    ^2bbbb2

  • 当一个驱动注册[platform_driver_probe()]的时候, 功能上和使用platform_driver_register()是一样的,唯一的区别是它不能被以后其他的device probe了,也就是说这个driver只能和一个device绑定。

    Platform device 和 Platform driver实际上是cpu总线可以直接寻址的设备和驱动,他们挂载在一个虚拟的总线platform_bus_type上,是一种bus-specific设备和驱动。与其他bus-specific驱动比如pci是一样的。他们都是将device和device_driver加了一个warpper产生,仔细看看platform_device就可以看到它必然包含一个device dev,而platform_driver也一样,它必然包含一个device_driver driver。
    所有的设备通过bus_id挂在总线上,多个device可以共用一个driver,但是一个device不可以对应多个driver。驱动去注册时候会根据设备名寻找设备,没有设备会注册失败,注册的过程会通过probe来进行相应资源的申请,以及硬件的初始化,如果probe执行成功,则device和driver的绑定就成功了。设备注册的时候同样会在总线上寻找相应的驱动,如果找到他也会试图绑定,绑定的过程同样是执行probe。

这里先带着问题看一下注册的过程: (前面说到metadata加密必须支持硬件加密才可以)

shost->inlinecrypt_support 这个赋值是哪来的呢?

搜了下代码, 是在这里

1
2
3
4
static int ufs_qcom_init(struct ufs_hba *hba) {
// err为0, 即支持硬件加密
err = ufs_qcom_ice_get_dev(host);
}

顺着ufs_qcom_init的初始化过程, 看下ufs_hba来自哪里
ufs_qcom_probe -> ufshcd_pltfrm_init(pdev, &ufs_hba_qcom_variant) -> ufshcd_alloc_host(dev, &hba) -> host = scsi_host_alloc(&ufshcd_driver_template, sizeof(struct ufs_hba));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int ufshcd_alloc_host(struct device *dev, struct ufs_hba **hba_handle)
{
struct Scsi_Host *host;
struct ufs_hba *hba;
int err = 0;
host = scsi_host_alloc(&ufshcd_driver_template,
sizeof(struct ufs_hba));
hba = shost_priv(host);
hba->host = host;
hba->dev = dev;
*hba_handle = hba;

INIT_LIST_HEAD(&hba->clk_list_head);
}

这个调用直接跑到了scsi/hosts.c中的scsi_host_alloc函数中. register a scsi host adapter instance. 这个函数的作用就是分配host端.
在scsi架构中, 系统初始化时会扫描platform总线,因此挂载其上的SCSI host adapter会被扫描到,并生成一个platform device。
扫描软件会为该platform device加载相应的驱动程序。加载SCSI host驱动时,其探测函数会初始化SCSI host,注册中断处理函数,最后调用scsi_scan_host函数扫描scsi host adapter所管理的所有scsi总线。
通常情况下,HBA驱动在系统中以模块形式加载。从而允许模块被卸载并重新加载,在该过程中SCSI扫描函数得以调用。通常,在卸载HBA驱动之前,SCSI设备的所有I/O都应该停止,卸载文件系统,多路径服务应用也需停止。如果有代理或HBA应用帮助模块,也应当中止。
/proc文件系统提供了可用SCSI设备的列表。如果系统中SCSI设备重新配置,那么所有这些改变通过echo /proc接口反映到SCSI设备中。添加一个设备,主机,channel,target ID,以及磁盘设备的LUN编号会被添加到/proc/scsi/,需指定scsi编号。

可见这里是将host纳入了scsi架构中, 后续只要访问到对应的host adapter, 可以通过host adapter与其匹配的driver进行通信.

这里列举一个加密过程的路径:
ufshcd_resume->ufshcd_reset_and_restore->ufshcd_detect_device->ufshcd_host_reset_and_restore->ufshcd_complete_requests->ufshcd_transfer_req_compl->__ufshcd_transfer_req_compl->ufshcd_vops_crypto_engine_cfg_end
ufshcd_pltfrm_init->ufshcd_init->INIT_DELAYED_WORK(&hba->card_detect_work, ufshcd_card_detect_handler)->ufshcd_detect_device…

v2-33d57d1aac13ec6a4e7f917e0a47bb69_1200x500

从之前的调度上看, 下发bio经过了device_mapper层, 通用块层即block层进行io调度, 使用各类调度算法合并bio到rq中, 最后下发bio到达scsi层, 在这一层有single-queue layermulti-queue layer的区分.
其中single-queue layer是使用的scsi_request_fn进行io下发的, 而支持multi-queue layer的则是通过blk_mq_ops结构实现了11个函数, 提供了支持超时,轮询完成,请求初始化的命令, 而io的下发是通过queue_rq函数执行.
而ufs是支持multi-queue layer的, 这里可以大致了解下mq_ops的结构的使用过程

io流程

对于多队列结果, rq需要提前分配, 可以看下scsi_alloc_sdev函数, 这里涉及到一个Disk Array Controller, gdth看起来是负责这块的.

A disk array controller is a device which manages the physical disk drives and presents them to the computer as logical units.

The disk array controller is made of up 3 important parts which play a key role in the controllers functioning and also show us indicators of storage I/O bottlenecks. These are:

CPU that processes the data sent to the controller
I/O port that includes:

  • Back-end interface to establish communication with the storage disks
  • Front-end interface to communicate with a computer’s host adapter
  • Software executed by the controller’s processor which also consumes the processor resources
1
2
3
4
5
6
7
8
9
static int gdth_open(struct inode *inode, struct file *filep)
{
gdth_ha_str *ha;
list_for_each_entry(ha, &gdth_instances, list) {
if (!ha->sdev)
ha->sdev = scsi_get_host_dev(ha->shost);
}
return 0;
}

还有ufshcd_init过程,
ufshcd_pltfrm_init -> ufshcd_init -> async_schedule(ufshcd_async_scan, hba); -> ufshcd_async_scan -> ufshcd_probe_hba(hba); -> scsi_scan_host(hba->host); -> async_schedule(do_scan_async, data); -> do_scsi_scan_host -> scsi_scan_host_selected -> scsi_scan_channel -> __scsi_scan_target -> scsi_probe_and_add_lun -> sdev = scsi_alloc_sdev(starget, lun, hostdata); -> sdev->request_queue = scsi_mq_alloc_queue(sdev); | sdev->request_queue = scsi_old_alloc_queue(sdev);

定义了 CONFIG_SCSI_MQ_DEFAULT才走多队列模式

这里还是看单队列模式 sdev->request_queue = scsi_old_alloc_queue(sdev);
scsi_old_alloc_queue中绑定了rq相关的处理函数.

1
q->request_fn = scsi_request_fn;

读取一个文件的时候,陷入系统调用,先检查数据是否在缓存中,如果没有则触发一次读盘操作,然后等待磁盘上的数据被更新到缓存中。

读取磁盘过程:调用文件系统层的readpages函数,使用各种文件系统层的get_block函数获取磁盘物理地址,存放到bh里(即buffer_head),使用bh构造bio,然后提交bio(一般使用submit_bio函数将数据bio提交到io的块设备层)。函数generic_make_request转发bio,generic_make_request是一个循环,通过generic_make_request提交请求给I/O调度层,这个函数最后调用到q->make_request_fn(q, bio),那么对于这个函数的调用就是I/O调度层的入口点(ps: Generic_make_request的执行上下文可能有两种,一种是用户上下文,另一种为pdflush.)

q->make_request_fn调用的是blk_queue_bio函数(早期版本是__make_request),在blk_init_allocated_queue里注册。blk_queue_bio函数是Linux提供的块设备请求处理函数,实现IO Schedule。在该函数中试图将转发过来的bio merge到一个已经存在的request中,如果可以合并,那么将新的bio请求挂载到一个已经存在request中。如果不能合并,那么分配一个新的request,然后将bio添加到其中。

blk_queue_bio里分为plug和unplug机制,在plug下就直接把request存到plug list(例如dio就是依赖于plug机制)。unplug下就直接调用queue的request_fn方法把request提交给磁盘驱动进行真正的处理了。当然,我们现在使用的是unplug机制。

然后q->request_fn调用queue队列的request_fn方法scsi_request_fn函数(我们这里选择sda,sdb之类scsi设备),
在scsi_request_fn函数中会扫描request队列,通过blk_peek_request(原先版本是:elv_next_request)函数从队列中获取一个request。在blk_peek_request函数中通过scsi总线层注册的q->prep_rq_fn(scsi层注册为scsi_prep_fn``blk_queue_prep_rq(q, scsi_prep_fn);)函数将具体的request转换成scsi驱动所能认识的scsi command。获取一个request之后,scsi_request_fn函数直接调用scsi_dispatch_cmd函数将scsi command发送给一个具体的scsi host。

到这一步,在scsi_dispatch_cmd函数中,通过scsi host的接口方法queuecommand(.queuecommand= ufshcd_queuecommand,)将scsi command发送给scsi host。通常scsi host的queuecommand方法会将接收到的scsi command挂到自己维护的队列中,然后再启动DMA过程将scsi command中的数据发送给具体的磁盘。DMA完毕之后,DMA控制器中断CPU,告诉CPU DMA过程结束,并且在中断上下文中设置DMA结束的中断下半部。DMA中断服务程序返回之后触发软中断,执行SCSI中断下半部。

在SCSi中断下半部中,调用scsi command结束的回调函数,这个函数往往为scsi_done,在scsi_done函数调用blk_complete_request函数结束请求request,每个请求维护了一个bio链,所以在结束请求过程中回调每个请求中的bio回调函数,结束具体的bio。Bio又有文件系统的buffer head生成,所以在结束bio时,回调buffer_head的回调处理函数bio->bi_end_io(注册为end_bio_bh_io_sync)。自此,由中断引发的一系列回调过程结束,总结一下回调过程如下:scsi_done->end_request->end_bio->end_bufferhead。

那什么时候知道数据已经在缓存里了呢?
do_generic_file_read –> PageUptodate(page)即检查PG_uptodate标志位。

注意点是上述queuecommand ufshcd_queuecommand下发磁盘时, qcom的硬件加密流程正处于这个位置, 而scsi中断下半部中, 在scsi done前, 触发了ufshcd_vops_crypto_engine_cfg_end
devm_request_irq(dev, irq, ufshcd_intr, IRQF_SHARED,
dev_name(dev), hba)->ufshcd_intr->ufshcd_sl_intr->ufshcd_transfer_req_compl->__ufshcd_transfer_req_compl->ufshcd_vops_crypto_engine_cfg_end

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
err = ufshcd_vops_crypto_engine_cfg_start(hba, tag);
static inline int ufshcd_vops_crypto_engine_cfg_start(struct ufs_hba *hba,
unsigned int task_tag)
{
if (hba->var && hba->var->crypto_vops &&
hba->var->crypto_vops->crypto_engine_cfg_start)
return hba->var->crypto_vops->crypto_engine_cfg_start
(hba, task_tag);
return 0;
}
qcom_host->ice.vops->config_start(qcom_host->ice.pdev, req, &ice_set, true)
// qcom硬件加密的方案全在这个函数中
static int qcom_ice_config_start(struct platform_device *pdev,
struct request *req,
struct ice_data_setting *setting, bool async)
{
struct ice_crypto_setting pfk_crypto_data = {0};
struct ice_crypto_setting ice_data = {0};
int ret = 0;
bool is_pfe = false;
unsigned long sec_end = 0;
sector_t data_size;
struct ice_device *ice_dev;

ice_dev = platform_get_drvdata(pdev);
/*
* It is not an error to have a request with no bio
* Such requests must bypass ICE. So first set bypass and then
* return if bio is not available in request
*/
if (setting) {
setting->encr_bypass = true;
setting->decr_bypass = true;
}
// 判断是否是pfe模式, fbe加密和metadata加密的实现都是pfe的.
ret = pfk_load_key_start(req->bio, ice_dev, &pfk_crypto_data,
&is_pfe, async);
if (is_pfe) {
if (ret) {
if (ret != -EBUSY && ret != -EAGAIN)
pr_err("%s error %d while configuring ice key for PFE\n",
__func__, ret);
return ret;
}

return qti_ice_setting_config(req, ice_dev,
&pfk_crypto_data, setting, ICE_CRYPTO_CXT_FBE);
}
// 是否是fde模式且是userdata分区
if (ice_fde_flag && req->part && req->part->info
&& req->part->info->volname[0]) {
if (!strcmp(req->part->info->volname, "userdata")) {
sec_end = req->part->start_sect + req->part->nr_sects -
QCOM_UD_FOOTER_SECS;
if ((req->__sector >= req->part->start_sect) &&
(req->__sector < sec_end)) {
data_size = req->__data_len /
QCOM_SECT_LEN_IN_BYTE;

if ((req->__sector + data_size) > sec_end)
return 0;
else
return qti_ice_setting_config(req,
ice_dev, &ice_data, setting,
ICE_CRYPTO_CXT_FDE);
}
}
}

return 0;
}

int pfk_load_key_start(const struct bio *bio, struct ice_device *ice_dev,
struct ice_crypto_setting *ice_setting, bool *is_pfe,
bool async)
{
int ret = 0;
struct pfk_key_info key_info = {NULL, NULL, 0, 0};
enum ice_cryto_algo_mode algo_mode = ICE_CRYPTO_ALGO_MODE_AES_XTS;
enum ice_crpto_key_size key_size_type = 0;
unsigned int data_unit = 1 << ICE_CRYPTO_DATA_UNIT_512_B;
u32 key_index = 0;

/*
* only a few errors below can indicate that
* this function was not invoked within PFE context,
* otherwise we will consider it PFE
*/
// is_pfe默认为true
*is_pfe = true;

if (!pfk_is_ready())
return -ENODEV;

// 从bio中获取key的信息

ret = pfk_get_key_for_bio(bio, &key_info, &algo_mode, is_pfe,
&data_unit);

if (ret != 0)
return ret;

ret = pfk_key_size_to_key_type(key_info.key_size, &key_size_type);
if (ret != 0)
return ret;

ret = pfk_kc_load_key_start(key_info.key, key_info.key_size,
key_info.salt, key_info.salt_size, &key_index, async,
data_unit, ice_dev);

ice_setting->key_size = key_size_type;
ice_setting->algo_mode = algo_mode;
/* hardcoded for now */
ice_setting->key_mode = ICE_CRYPTO_USE_LUT_SW_KEY;
ice_setting->key_index = key_index;

return 0;
}

static int pfk_get_key_for_bio(const struct bio *bio,
struct pfk_key_info *key_info,
enum ice_cryto_algo_mode *algo_mode,
bool *is_pfe, unsigned int *data_unit)
{
const struct inode *inode;
enum pfe_type which_pfe;
const struct blk_encryption_key *key = NULL;
char *s_type = NULL;

// 从bio中获取inode信息
inode = pfk_bio_get_inode(bio);
which_pfe = pfk_get_pfe_type(inode);
// 判断inode文件系统类型
s_type = (char *)pfk_kc_get_storage_type();

/*
* Update dun based on storage type.
* 512 byte dun - For ext4 emmc
* 4K dun - For ext4 ufs, f2fs ufs and f2fs emmc
*/

if (data_unit) {
if (!bio_dun(bio) && !memcmp(s_type, "sdcc", strlen("sdcc")))
*data_unit = 1 << ICE_CRYPTO_DATA_UNIT_512_B;
else
*data_unit = 1 << ICE_CRYPTO_DATA_UNIT_4_KB;
}

// 对于metadata元数据信息来说, 并没有inode信息, 所以which_pfe是INVAILD_PFE.
if (which_pfe != INVALID_PFE) {
/* Encrypted file; override ->bi_crypt_key */
pr_debug("parsing inode %lu with PFE type %d\n",
inode->i_ino, which_pfe);
return (*(pfk_parse_inode_ftable[which_pfe]))
(bio, inode, key_info, algo_mode, is_pfe);
}

/*
* bio is not for an encrypted file. Use ->bi_crypt_key if it was set.
* Otherwise, don't encrypt/decrypt the bio.
*/
// metadata加密继续走这里, 这个bio是dm-default-key转发过来的, 所以是带有bi_crypt_key信息的, 能找到key, is_pfe即为true
#ifdef CONFIG_DM_DEFAULT_KEY
key = bio->bi_crypt_key;
#endif
if (!key) {
*is_pfe = false;
return -EINVAL;
}

key_info->key = &key->raw[0];
key_info->key_size = PFK_SUPPORTED_KEY_SIZE;
key_info->salt = &key->raw[PFK_SUPPORTED_KEY_SIZE];
key_info->salt_size = PFK_SUPPORTED_SALT_SIZE;
if (algo_mode)
*algo_mode = ICE_CRYPTO_ALGO_MODE_AES_XTS;
return 0;
}

1.4.3. 总结

qcom的硬件加密方案是按照google 规范写的, 先在fstab的userdata的表里加入了fileencryption=ice, 文件系统判断是硬件加密方案不再对page cache做额外的加解密处理.
而metadata加密方案通过dm-default-key的插件在文件系统层下为元数据的bio挂上了加密密钥信息,而非文件系统元数据对应的目录或文件的inode(以及data block)则使用ensure_policy时对目录层级采用的加密策略.
保证了me加密方案不会对元数据或文件inode单独的加密要求, 而在通用块层之下的scsi架构中插桩, 通用块层请求下来后, bio merge到requeset后, 对request下发到ufs device之前(queuecommand时)由scsi host adapter为request挂上了加密信息, 而加密信息来自于文件系统inode或元数据挂上的密钥信息. 保证了再下发到device后, 通过硬件器件对request进行加解密.

Linux scsi设备读写流程:
http://blog.chinaunix.net/uid-29634482-id-5127267.html
块设备读写流程:
http://blog.chinaunix.net/uid-25052030- id-58337.html
usb驱动学习笔记:
http://blog.chinaunix.net/uid-25627207-id-3341609.html
plug/unplug机制:
http://blog.chinaunix.net/xmlrpc.php?r= blog/article&uid=14528823&id=4778396

Linux SCSI回调IO的分析

scsi驱动
scsi三层驱动模型
在scsi middle level定义了scsi device的数据结构,用于描述一个scsi的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。
  在scsi host中可以存在多个channel,每个channel是一条完整的scsi总线,在scsi总线上可以连接多个scsi节点,每个节点采用id进行编号,编号的大小与具体的scsi specification相关,与总线层的驱动能力等因素相关。每个节点可以根据功能划分成多个lun,每个lun才是我们通常所说的scsi设备。
  channel lun
  scsi host的语义很清晰,其描述了一个scsi总线控制器。在很多实际的系统中,scsi host为一块基于PCI总线的HBA或者为一个SCSI控制器芯片。每个scsi host可以存在多个channel,一个channel实际扩展了一条SCSI总线。每个channel可以连接多个scsi节点,具体连接的数量与scsi总线带载能力有关.
  在scsi总线probe的过程中,scsi middle level会为每个lun抽象成scsi device,实现的核心函数为scsi_probe_and_add_lun()
  scsi target对scsi总线上的scsi 节点进行了抽象。每个scsi target可能拥有多个lun,即多个scsi devie   

1.4.3.1. low-level接口方法——scsi_host_template

  scsi middle level通过scsi_host_template接口调用scsi host的具体方法。在scsi host driver向middle level注册host对象的同时需要注册scsi_host_template方法,该方法被注册到scsi host对象中。
  scsi middle level层提供了scsi host扫描函数,在设备枚举过程中scsi host可以调用该函数对scsi总线适配器进行扫描,当然host驱动也可以调用更加底层的函数对scsi总线进行扫描。scsi_scsn_host函数实现流程如下:
  扫描channel

scsi host作为 platform 设备会被platform总线驱动层扫描到,扫描到scsi host之后,操作系统开始加载scsi host的驱动,scsi host driver就是上面说所的low level driver。scsi host driver初始化scsi控制器, 分配硬件资源,注册中断服务。最后开始扫描通过scsi控制器扩展出来的下一级总线——scsi bus。
  scsi bus的扫描通过scsi middle level提供的服务完成。scsi host driver可以调用scsi middle level提供的扫描算法完成scsi总线设备的扫描
在scsi总线扫描过程中用到了scsi middle level层的如下重要函数:
  1、scsi_scan_host:对scsi host设备进行扫描。
2、scsi_add_device:探测具体的device,并且将其加入系统。
  3、scsi_probe_and_add_lun:探测具体指定的lun,并且将其加入系统。
  4、scsi_probe_lun:采用INQUIRY命令对lun节点进行探测。
  5、scsi_add_lun:加入lun节点并且初始化SCSI设备。
  scsi总线scan过程中的函数调用情况如下图所示:
  scsi扫描过程

1.5. scsi设备读写过程

在此给出一个scsi设备的读写数据流程,通过该例子,读者可以方便查找Linux
源代码,并且能够理清繁杂的代码结构。假设读写的scsi设备为scsi disk设备,数据首先通过文件系统,进入到文件系统的Cache。文件系统的pdflush daemon会将Cache住的数据刷新到磁盘,其根据buffer head的内容构造bio,然后调用块设备接口(submit_bio)将请求发送给块设备层。bio在块设备层多次转发,最后被merge到块设备的请求队列中。请求可能会在请求队列滞留一段时间,然后在软中断或者用户上下文中调用request_fn去处理请求队列。在scsi middle level驱动层,块设备的请求被转换成scsi command,然后通过queuecommand函数接口将scsi command提交给scsi host,通常scsi host会发起DMA操作将数据传输给具体的设备。至此,数据从应用程序转移到了scsi设备,当然上述过程还没有涉及到回调过程,实际的回调会在中断上下文、软中断上下文中完成,在请求发送的每一层都保存了相应的回调上下文。整个数据流的过程中,涉及到的函数如下:
读写过程
对scsi总线层有个提纲挈领的效果。在分析scsi middle level的过程中,有如下几点感想:
  1、 scsi驱动采用了规范的分层设计思想,其一共分为三层,分层之后使得设计分工更加明确,而且在逻辑上也更加清晰,设计工作也更加简单。
  2、 scsi驱动中比较固定的层次为scsi middle level,该层可以称之为scsi通用中间层,或者为总线驱动层。该层向上和向下都需要提供接口,所以上层驱动开发时需要注册相关接口函数,下层驱动工作时也需要注册接口函数,只有这样中间层才可以很灵活的进行上下层数据传输。
  scsi middle level主要实现了scsi总线扫描算法,scsi命令转换算法,scsi出错处理等机制,这些东西都是scsi的核心所在。

1.5.1. card层

设备接口层:
提供scsi设备的使用接口
scsi/sd.c:
实现scsi硬盘接口,通过硬盘注册接口,将scsi硬盘注册到块设备系统,使得系统可以通过通用块设备接口来使用scsi硬盘,在scsi/sd.c里面实现。

scsi host driver在初始化时,会调用scsi_scan_host来扫描host。扫描整个host以为着扫描host所对应的channel,target和lun。因此,它分别调用scsi_scan_channel,__scsi_scan_target,scsi_probe_and_add_lun来每个target及其lun。其中,在scsi_probe_and_add_lun中会分配代表每个lun即scsi设备的scsi_device结构:
scsi_scan_host->do_scsi_scan_host->scsi_scan_host_selected->scsi_scan_channel->__scsi_scan_target->scsi_probe_and_add_lun:
sdev = scsi_alloc_sdev(starget, lun, hostdata);
scsi_scan_host->do_scsi_scan_host->scsi_scan_host_selected->scsi_scan_channel->__scsi_scan_target->scsi_probe_and_add_lun->scsi_probe_lun

分配好scsi_device结构以后,调用scsi_probe_lun来发送INQUIRY命令,探测制定的lun。scsi设备返回的inquiry data将保存在result参数中,以备scsi_add_lun使用。其中包括了设备的信息,包括设备的种类type等。
scsi_scan_host->do_scsi_scan_host->scsi_scan_host_selected->scsi_scan_channel->__scsi_scan_target->scsi_probe_and_add_lun->scsi_add_lun
如果scsi_probe_lun成功,则调用scsi_add_lun来添加lun。在scsi_add_lun中,先根据inquiry data来初始化scsi_device中的一些属性,包括其type属性(在这儿为TYPE_DISK)。
scsi_scan_host->do_scsi_scan_host->scsi_scan_host_selected->scsi_scan_channel->__scsi_scan_target->scsi_probe_and_add_lun->scsi_add_lun->scsi_sysfs_add_sdev

之后调用scsi_sysfs_add_sdev来添加scsi_device。这儿与device_driver的注册类似,调用device_attach来对扫描bus上所有的driver,调用这些driver的probe方法来探测自身的device。如果此时,sd没有初始话,即bus上没有相应的驱动,则不会调用probe方法。即不会生成lun对应的gend。并且,sd_probe中,会检测scsi_device的具体类型,只有自己支持的才回去探测。

通过上述两个过程,device_driver和device联系在了一起。总之,对于每个lun的加入,sd_probe都会执行一次。只不过sd_probe的触发,要么是通过sd驱动scsi_register_driver,要么是通过LLD scsi_scan_host
sd_probe调用栈(由scsi_scan_host触发):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#0 sd_probe (dev=0xc714a4b0) at drivers/scsi/sd.c:1597
#1 0xc018df10 in driver_probe_device (drv=0xc0335df8, dev=0xc714a4b0) at drivers/base/dd.c:121
#2 0xc018dfd8 in __device_attach (drv=0xc714a4b0, data=0x0) at drivers/base/dd.c:207
#3 0xc018d110 in bus_for_each_drv (bus=, start=, data=0x0, fn=0xc018dfc8 <__device_attach>) at drivers/base/bus.c:349
#4 0xc018e078 in device_attach (dev=0x1) at drivers/base/dd.c:238
#5 0xc018d070 in bus_attach_device (dev=0xc714a4b0) at drivers/base/bus.c:492
#6 0xc018be64 in device_add (dev=0xc714a4b0) at drivers/base/core.c:781
#7 0xc019fc84 in scsi_sysfs_add_sdev (sdev=0xc714a400) at drivers/scsi/scsi_sysfs.c:783
#8 0xc019d9b8 in scsi_probe_and_add_lun (starget=0xc76d9000, lun=, bflagsp=, sdevp=0x0, rescan=0, hostdata=0x0) at drivers/scsi/scsi_scan.c:914
#9 0xc019e088 in __scsi_scan_target (parent=0xc715e8d8, channel=0, id=0, lun=4294967295, rescan=0) at drivers/scsi/scsi_scan.c:1550
#10 0xc019e538 in scsi_scan_channel (shost=0xc715e800, channel=0, id=0, lun=4294967295, rescan=0) at drivers/scsi/scsi_scan.c:1626
#11 0xc019e608 in scsi_scan_host_selected (shost=0xc714a4b0, channel=1, id=1, lun=3222995532, rescan=0) at drivers/scsi/scsi_scan.c:1654
#12 0xc019e6e8 in do_scsi_scan_host (shost=0xc715e800) ---Type to continue, or q to quit--- at drivers/scsi/scsi_scan.c:1786
#13 0xc019eb50 in scsi_scan_host (shost=0xc715e800) at drivers/scsi/scsi_scan.c:1813

UL初始化时准备device_driver结构,其中包括probe方法。而LLD(准确地说,应该是middle layer)准备device结构。二者都把其bus初始化为scsi_bus_type,并挂入该总线。之后,如果是UL初始化,则调用自身的probe方法,去探测bus上的所有device,从而把device_driver绑定倒device。而如果是LLD初始化,则调用bus上所有的device_driver的probe方法来探测自身的device。因此,无论是UL先初始化还是LLD先初始化,二者都能取得联系。

这里推荐一个博客, 里面有关于scsi里许多流程的介绍
scsi介绍多篇

1.6. 对总线的理解

pc桥
所谓的桥,就是Bridge,桥接的意思。以前的老式电脑中,CPU是通过南桥和北桥连接其它设备的。其中北桥连接高速设备和南桥,南桥连接低速设备。
例如:
CPU——北桥——内存
CPU——北桥——显卡
CPU——北桥——南桥——硬盘
CPU——北桥——南桥——网卡
CPU——北桥——南桥——PS/2键鼠
CPU——北桥——南桥——USB设备
Intel从SandyBridge开始,CPU整合内存控制器和PCI-E控制器、DMI通道,相当于是把原来北桥的功能集成在CPU内部了,北桥自然就消失了。南桥换了个称呼叫IO Hub

1.6.1. ARM SOC上 总线协议

AMBA总线

AHB总线的强大之处在于它可以将微控制器(CPU)、高带宽的片上RAM、高带宽的外部存储器接口、DMA总线master、各种拥有AHB接口的控制器等等连接起来构成一个独立的完整的SOC系统,不仅如此,还可以通过AHB-APB桥来连接APB总线系统。AHB可以成为一个完整独立的SOC芯片的骨架。
AMBA

AXI总线替代AHB APB总线
ufs 2.1

The IP for UFS 2.1 Host Controller allows for highlysecured applications by employing AES encryption. The data encryption and decryption is done seamlessly by the
controller as data is written to or read from the UFS 2.1 device.

高通平台上UFS是通过AXI/AHB总线连接到cpu的.

回到platform driver上, 看高通的实现, ufs driver是注册在platform总线上的, 这个platform总线和AHB/AXI的关系应该怎样理解呢?

这里引用一篇问答:

链接:https://www.zhihu.com/question/33414159/answer/56558491

bus上有driver和device,一个driver可以对应多个device,一个device只能有一个driver。比如pcie是一种总线,intel的82574网卡是一种pcie设备,e1000e.c 是网卡驱动。因为操作系统要支持外设,必须先要知道当前计算机有哪些设备,以及这些设备的访问方式、访问地址、中断号、中断触发方式等。这些资源信息假如写死在driver代码里,普通用户就无法DIY PC了。比如把硬盘换一个sata口,显卡换一个pcie插槽,控制这些设备的地址和中断就变了,用户就得自己改改驱动代码编译一下,没法玩嘛。这样就需要把当前计算机有哪些设备,以及这些设备的访问方式、访问地址、中断号、中断触发方式等 这些资源信息单独剥离出来,封进一个一个xxxx_device这样的结构体里。这样xxx_driver也更容易实现 一次编写,管你x86、arm、powerpc 到处运行。运行时,每当注册一个新的xxx_device或者xxx_driver到一条xxx_bus上,linux就尝试在xxx_bus给这个新加入的xxx_device或者xxx_driver配对(probe),device和driver配对成功了,xxx_driver就能利用xxx_device的内容去控制xxx设备了。综上,bus, device 和driver是一个清晰的树状结构,一种好的设计。可以在/sys/bus目录下用ls 、tree命令列出来这个树。
终于能回到题目,platform 是一种虚拟的bus;而char driver 和 block driver,只是driver的细分。它们并不是并列的关系。那为什么要有platform 这条虚拟的总线呢?还是上面说的,操作系统要支持外设,必须先要知道当前计算机有哪些设备。要做到这一点,要么硬件本身能自动探测出来当前bus上有哪些device,要么就由程序员事先把device资源信息写在代码或者dts这类配置文件里。pci/pcie 总线是x86架构的脊椎,且拥有探测pci/pcie 设备的能力。PC上的usb总线控制器,对上是一个pcie设备,对下则是usb总线的控制器。等于pcie总线下扩展出了新的usb总线。usb总线也是有硬件探测能力的。个人电脑几乎所有重要的外设:硬盘、u盘、键盘、鼠标、声卡、显卡,都是pcie或者usb设备。再加上BIOS的帮助,普通pc的内核几乎不需要程序员手动注册一个设备信息,自己就能探测就出来当前计算机插了哪些设备。但是手机、平板等大量使用SOC的非X86架构计算机,它自己的usb控制器、i2c控制器、声卡控制器、lcd驱动器、存储控制器(一般是flash控制器,个别SOC也有sata控制器)等,都不再是X86架构下的pci设备了,靠硬件自己是无法探测的。此时就需要程序员自己手动写代码或者配置文件,来注册这些device的信息。假如是i2c 设备、spi设备,硬件也没有探测设备属性的能力,程序员手动注册device就注册了吧,起码知道是要注册到那条bus上去。。可偏偏这些SOC外设并没有一个统一的总线名称,ARM上叫AHB、 APB ,powerpc上叫CCB,甚至隔个几年又会发明新的叫法。platform 这条虚拟的bus,就是用来统一维护此类device的bus。

linux上把这种SOC内部bus下的设备(不一定全都是子总线控制器如usb控制器、i2c控制器,也可以是音频、视频控制器这种设备控制器),都注册到platform这条虚拟bus上。
软件对于这种bus下的设备的访问倒很简单统一,按照芯片手册给的物理地址去访问即可。因为是SOC内部的设备,自然是SOC的厂家自己写这些设备的driver。这种driver一般注册到platform bus。