1. 加密方案整理
1.1. 简介
Android上加密方案分为全磁盘加密(FDE
)/文件加密(FBE
)/元数据加密(METADATA
)
均是针对userdata分区进行加密, 与其他分区并无关系.
本篇主要介绍下这三种加密方式硬件加密的实现框架
1.2. 判断机器采用何种加密方式
最直观的方式是可以通过分区表fstab
来区分
未加密/fde加密/fbe加密/metadata加密
1 | /dev/block/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=1,data=ordered,noauto_da_alloc wait,check,encryptable=footer |
当前版本中最常见的是fbe加密的机型.
代码中判断是否加密:
1 | StorageManager.isEncrypted() |
native 层多是通过FstabEntry来判断是何种加密方式
1 | static bool should_use_metadata_encryption(const FstabEntry& entry) { |
硬件加密的启用, 其中fbe机型和metadata机型是用fileencryption=ice
控制的(限于高通产品), 而fbe和metadata加密方式的不同是看keydirectory=
是否为空
1.3. 硬件加密实现策略
首先跟着fileencryption=ice
往下看
1 | @startuml |
1 | @startuml |
对于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 |
|
上述创建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 | static struct target_type default_key_target = { |
关于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 | ioctl(dm_fd.get(), DM_DEV_CREATE, io) |
write_back时, 调用queue_io, 或dm设备处于suspend时
1 | static void queue_io(struct mapped_device *md, struct bio *bio) |
还有一条路径是通过 dm_setup_md_queue
1 | case DM_TYPE_BIO_BASED: |
其中 __split_and_process_bio
可以了解一下
1 | // mapped_device指向 映射的dm设备, map为映射规则 |
由上述调用, 最终起到封装bio的目的. 复制了一份bio到clone中, 找到dm-target的type(插件), 用其注册的map函数封装clone, clone为指针, map函数会修改其值.
1 | // ti封装的private为其初始化时填入的值 |
可见metadata加密使用dm设备的目的, 就是为bio加上密钥信息, 那这个密钥信息是在哪里用的呢, 而且dm-default-key.c
里的实现里明确提示了需要硬件支持.
1 | static int default_key_iterate_devices(struct dm_target *ti, |
所以上述硬件加密的信息应该是做在mmc框架层, 通过mmc框架与存储器件通信, 进行硬件加密
1.4.2. scsi架构探寻
1.4.2.1. 软件架构
Linux kernel的驱动框架有两个要点:
- 抽象硬件(硬件架构是什么样子,驱动框架就应该是什么样子)。
- 向“客户”提供使用该硬件的API(之前我们提到最多的客户是“用户空间的Application”,不过也有其它“客户”,例如内核空间的其它driver、其它framework)。
先看kconfig
1 | config SCSI_UFSHCD |
1 | obj-$(CONFIG_ARCH_QCOM) += pfe/ |
从上述的依赖条件看, 主要是涉及到ufs-qcom.c
和 ufs-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
1 | static struct platform_driver ufs_qcom_pltform = { |
platform device的.prob函数中执行的是大致为四步:
- 申请设备号
- 实体设备(如果是字符设备,就是cdev)初始化注册,添加填充好的file_operation
- 从pdev读出硬件资源
- 对硬件资源初始化,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 | static int ufs_qcom_init(struct ufs_hba *hba) { |
顺着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 | int ufshcd_alloc_host(struct device *dev, struct ufs_hba **hba_handle) |
这个调用直接跑到了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…
从之前的调度上看, 下发bio经过了device_mapper层, 通用块层即block层进行io调度, 使用各类调度算法合并bio到rq中, 最后下发bio到达scsi层, 在这一层有single-queue layer
和multi-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
的结构的使用过程
对于多队列结果, 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 | static int gdth_open(struct inode *inode, struct file *filep) |
还有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 | err = ufshcd_vops_crypto_engine_cfg_start(hba, tag); |
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
在scsi middle level定义了scsi device的数据结构,用于描述一个scsi的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。
在scsi host中可以存在多个channel,每个channel是一条完整的scsi总线,在scsi总线上可以连接多个scsi节点,每个节点采用id进行编号,编号的大小与具体的scsi specification相关,与总线层的驱动能力等因素相关。每个节点可以根据功能划分成多个lun,每个lun才是我们通常所说的scsi设备。
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函数实现流程如下:
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过程中的函数调用情况如下图所示:
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 | 0 sd_probe (dev=0xc714a4b0) at drivers/scsi/sd.c:1597 |
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. 对总线的理解
所谓的桥,就是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上 总线协议
AHB总线的强大之处在于它可以将微控制器(CPU)、高带宽的片上RAM、高带宽的外部存储器接口、DMA总线master、各种拥有AHB接口的控制器等等连接起来构成一个独立的完整的SOC系统,不仅如此,还可以通过AHB-APB桥
来连接APB总线系统。AHB可以成为一个完整独立的SOC芯片的骨架。
AXI总线替代AHB APB总线
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。