Android 双u盘方案【转】
本文转载自:https://blog.csdn.net/kc58236582/article/details/49618445
1 L1813系统上双U盘设计方案——系统设计
1.1 方案背景
Android原生的磁盘管理方案,设计的思想是将EMMC上的空间通过sdcard的server模拟成一个StorageVolume,供用户作为外部存储空间使用,而该部分空间是不能通过切换大容量存储在PC侧供用户使用,只能通过MTP协议,将手机和PC侧互联,从而在PC侧读取手机侧的外部存储空间的内容。Android原生也是留出了给外置SD卡的设计接口,允许外置SD卡通过大容量存储的方式同PC侧相连接。
图2.1.1 Android原生设计存储方案
这里需要分清一个概念,Android原生设计的外部存储空间,和我们常规意义上理解是不同的,Android原生将所有用户可见的存储空间都定义为外部存储空间,而本文以下所指的外部存储空间,特指外置SD卡所表示的存储空间。而本文以下所指的内部存储空间(内置U盘),特指从EMMC上独立划分出来的一个分区所表示的手机EMMC上的一块存储空间。本文所指的双U盘方案,即指的是在Android系统上,同时存在外部存储空间和内部存储空间,并且在切换大容量存储的时候,可以在PC侧同时出现两个可移动磁盘。
图2.1.2 Android双U盘生设计存储方案
双U盘方案涉及到Android系统磁盘存储的整体框架,从APP,Framework,Vold,Kernel几个层次进行整体修改和设计。从EMMC上缩小userdata分区大小,将新增加空间做为一块固定大小的分区,作为内置U盘分区,而外置的SD卡作为外置U盘分区,从而形成双U盘的方案。系统运行时,需要保证能在不重启的前提下,动态的切换默认存储的分区,并保证上层应用的稳定性和安装第三方应用的正确性。本文将从底向上的层次顺序描述该方案。
1.2 Bootloader层对EMMC的分区调整
2.2.1背景知识
在嵌入式操作系统中,分区只是内核的概念,就是说A~B[p1] 地址放内核,C~D地址放文件系统,(也就是规定哪个地址区间放内核或者文件系统)等等,而对于bootloader中只要能将内核下载到A~B区的A地址开始处就可以,C~D区的C起始地址下载文件系统。。。这些起始地址在kernel的分区信息中能找到。所以bootloader对分区的概念不重要,只要它能把内核烧到A位置,把文件系统烧到C位置。
所以,在bootloader对Flash进行操作时,哪块区域放什么是以内核为主,而为了方便操作,bootloader类似也引入分区的概念。所以,如果你是通过uboot的内核命令行给kernel层传递分区信息,这种情况下,内核读取到的分区信息始终和u-boot中的保持一致(推荐的做法);而如果你是把分区信息写在内核源代码里定义好的方法,那最好保证它和u-boot中的保持一致,即同步修改uboot及内核的相关部分。
目前L1813的kernel版本已经不支持通过uboot的内核命令行给kernel传递分区信息的方式,目前采用的是一种在uboot里就配置好所有分区信息,从而直接使用。
2.2.2 L1813针对双U盘的分区调整方法
目前,L1813方案的分区设置是在bootable/bootloader/uboot/include/configs/下针对不同项目有不同的.h的配置文件,以ust802手机的P0版手机为例,需要查看并修改comip_ust802_v1_0.h的相关分区信息。
2.2.2.1分区结构体
struct partition {
ulong sector_size;
ulong start;
ulong length;
const char *name;
};
成员名 |
描述 |
sector_size |
每个扇区大小 |
start |
分区起始地址 |
length |
分区长度 |
char *name |
分区名称 |
2.2.2.2 分区详细信息
每个扇区大小(单位:字节) |
分区起始位置 |
分区所占扇区数目(分区长度) |
分区名称 |
512(0x200) |
0x00000000 |
0x00000100 |
Uboot |
512(0x200) |
0x00000400 |
0x00000400 |
lcboot |
512(0x200) |
0x00000800 |
0x00002000 |
logo |
512(0x200) |
0x00002800 |
0x00000800 |
fota |
512(0x200) |
0x00003000 |
0x00000800 |
panic |
512(0x200) |
0x00003800 |
0x00001000 |
amt |
512(0x200) |
0x00004800 |
0x00008000 |
modemarm |
512(0x200) |
0x0000C800 |
0x00001620 |
modemdsp0 |
512(0x200) |
0x0000DE20 |
0x000009E0 |
modemdsp1 |
512(0x200) |
0x0000E800 |
0x00002000 |
kernel |
512(0x200) |
0x00010800 |
0x00000800 |
ramdisk |
512(0x200) |
0x00011000 |
0x00000800 |
ramdisk_amt1 |
512(0x200) |
0x00011800 |
0x00000800 |
ramdisk_amt3 |
512(0x200) |
0x00012000 |
0x00000800 |
ramdisk_recovery |
512(0x200) |
0x00012800 |
0x00002000 |
kernel_recovery |
512(0x200) |
0x00018000 |
0x00001000 |
misc |
512(0x200) |
0x00020000 |
0x00040000 |
cache |
512(0x200) |
0x00060000 |
0x000c0000 |
system |
512(0x200) |
0x00120000 |
userdata |
图1原ust802_v1_0手机分区详表
2.2.2.3分区修改方法
双U盘方案需要将EMMC上缩小userdata[p2] 分区的1G空间,将该空间作为一个1G大小的新分区,称为udisk分区,作为内置存储空间。
调整后的分区信息如下标红部分(udisk分区从userdata分区头部划分,是为了方便后期调整EMMC大小时,可以直接从userdata分区尾部调整):
每个扇区大小(单位:字节) |
分区起始位置 |
分区所占扇区数目(分区长度) |
分区名称 |
0x00000000 |
0x00000100 |
uboot |
|
512(0x200) |
0x00000400 |
0x00000400 |
lcboot |
512(0x200) |
0x00000800 |
0x00002000 |
logo |
512(0x200) |
0x00002800 |
0x00000800 |
fota |
512(0x200) |
0x00003000 |
0x00000800 |
panic |
512(0x200) |
0x00003800 |
0x00001000 |
amt |
512(0x200) |
0x00004800 |
0x00008000 |
modemarm |
512(0x200) |
0x0000C800 |
0x00001620 |
modemdsp0 |
512(0x200) |
0x0000DE20 |
0x000009E0 |
modemdsp1 |
512(0x200) |
0x0000E800 |
0x00002000 |
kernel |
512(0x200) |
0x00010800 |
0x00000800 |
ramdisk |
512(0x200) |
0x00011000 |
0x00000800 |
ramdisk_amt1 |
512(0x200) |
0x00011800 |
0x00000800 |
ramdisk_amt3 |
512(0x200) |
0x00012000 |
0x00000800 |
ramdisk_recovery |
512(0x200) |
0x00012800 |
0x00002000 |
kernel_recovery |
512(0x200) |
0x00018000 |
0x00001000 |
misc |
512(0x200) |
0x00020000 |
0x00040000 |
cache |
512(0x200) |
0x00060000 |
0x000c0000 |
system |
512(0x200) |
0x00120000 |
0x00200000 |
udisk |
512(0x200) |
0x00320000 |
0x003e0800 |
userdata |
图2 ust802_v1_0手机双U盘分区详表
将userdata分区原大小0x005e0800*0x200=2.94G缩小为0x003e0800*0x512=1.94G,
将该1G空间的大小分给udisk分区。
同时在BoardConfig.mk下,需要调整BOARD_USERDATAIMAGE_PARTITION_SIZE,由于USERDATAIMAGE有1M空间需要进行加密,因而在此处的userdata分区的空间调整需要比实际userdata分区小1M。[p3]
修改前,BOARD_USERDATAIMAGE_PARTITION_SIZE为3154116608字节,
修改后,BOARD_USERDATAIMAGE_PARTITION_SIZE为2080374784字节。[p4]
2.2.3 Bootloader下对本功能宏的控制
//TODO
1.3 修改SML烧录工具
由于修改了EMMC的分区表,对应在烧录版本的时候就需要修改SML工具,在userdata分区之前,新增加一个udisk分区,并将分区大小对应修改。
在SML工具根目录下,进入/Inifiles文件夹,找到对应项目的.pks配置文件(pks文件实际为zip压缩文件),L1813项目为LC1813-2.0.00-FEATURE.pks,解压后,找到DTL1813MicronUSB_HS_large.ini配置文件,修改相应分区配置项:
原配置项:
[DOWNLOADTYPE18]
DownloadTypeName=userdata
StartAddress=0x24000000
Type=0
Size=3221225472 #0xC0000000
修改为:
[DOWNLOADTYPE18]
DownloadTypeName=udisk
StartAddress=0x24000000
Type=0
Size=1073741824#0x40000000
[DOWNLOADTYPE19]
DownloadTypeName=userdata
StartAddress=0x64000000
Type=0
Size=2147483648 #0x80000000
完成后,压缩成zip文件,更改后缀名为pks文件,替换元Inifiles目录下的对应pks文件。
1.4 Linux Kernel层对USB驱动的调整
2.4.1背景知识
Linux kernel2.6以上的版本中,USB设备驱动的接口改为了gadget,在kernel/driver/usb/gadget目录下主要包含了平台USBUDC驱动和gadget接口驱动。Linux支持连接各种USB从设备,同时也支持自己作为设备插入到其他主机当中。最典型的例子就是AndroidOS的手机,插入电脑可以被识别为U盘之类的设备。
为了避免与作为主机时支持的"设备驱动(USB Device Driver)"一词混淆,Linux给这部分的实现取名为"Gadget",小玩具。内核源码的目录为\drivers\usb\gadget,里面包含了内核所支持的不同类型的USBDevice Controller (UDC)驱动的实现,以及框架和不同gadget的实现。
2.4.2 USB Gadget的三层架构
Linux USB Gadget分三层架构,层次关系从上到下:
一层:USB Gadget功能层。BSP/Driver开发者通常是要实现这一层,从而实现一个具体的设备驱动,如Anddroid在此层实现了adb,mtp,mass_storage等。浏览参考关注此层代码时,会发现“composite”是此层的关键字,此层中关键的数据结构是:struct usb_composite_driver。这一层的驱动文件一般为:driver/usb/gadget/android.c(android实现的)或driver/usb/gadget/serial.c(传统Linux实现的USB转串口)。
二层:USB设备层。这一层是Linux内核开发维护者实现的,与我们没太大关系,不用我们操心,我们只关心其的一些接口就行。浏览参考关注此层时,会发现“gadget”是此层的关键字,此层的关键数据结构是:usb_gadget_driver,usb_composite_dev。这层主要的一个驱动文件为:driver/usb/gadget/composite.c
三层:USB设备控制器驱动层。这一层主要是与CPU、CPU USB控制器有关,与硬件紧密相关,这一层也比较头痛,主要它和USB控制器牵扯在一起,涉及有寄存器、时钟、DMA等等。但是这一层往往是由芯片厂商去实现。我们一般仅需在板级文件中处理好所需要的USB接口即可。这层的关键字就是“UDC”,主要驱动文件命名含“udc”关键字,一般与CPU或芯片厂商有关,如driver/usb/gadget/xxx_udc.c。
可以用一句简单的话去概括三层的关系:USB Gadget功能层调用USB设备层的接口,USB设备层调用USB设备控制器驱动层的接口,然后USB设备控制器驱动层回调USB设备层,USB设备层回调USB Gadget功能层。
2.4.3 调整USB gadget的f_mass_storage
如上所述,我们主要关注的是三层里面的第一层:USB Gadget功能层。在这一层里,重点关注Android自己实现了的一个驱动文件driver/usb/gadget/android.c。
2.4.3.1 USBFunction结构体
目前在Android4.2上支持的所有USB Function如下:
static struct android_usb_function*supported_functions[] = {
&ffs_function,
&adb_function,
&serial_function,
//&acm_function,
&mtp_function,
&ptp_function,
&rndis_function,
&mass_storage_function,
&accessory_function,
&audio_source_function,
NULL
};
对于本文所关注的内置U盘功能,主要是涉及到mass_storage_function:
static struct android_usb_functionmass_storage_function = {
.name ="mass_storage",
.init =mass_storage_function_init,
.cleanup = mass_storage_function_cleanup,
.bind_config = mass_storage_function_bind_config,
.attributes = mass_storage_function_attributes,
};
默认情况下,mass_storage_function只设定了一个大容量存储空间,鉴于本功能的要求,需要将默认的config配置为可以使用两块大容量存储空间。
控制mass_storage_function的主要结构体是mass_storage_function_config,其成员变量fsg_config可以控制需要使用几块LUN的设备。
我们需要将其抽象出接口函数,可供动态控制。
//TODO
2.4.4 kernel层对本功能的宏控制
//TODO
1.5 格式化新分区
2.5.1需要格式化的原因
在bootloader对EMMC分区后,目前对分区的格式是使用EXT4文件系统:
/dev/block/platform/comip-mmc.1/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=1,data=ordered,noauto_da_alloc wait,check,encryptable=footer
而对内置U盘而言,由于需要切换到PC侧做大容量存储功能,EXT4的文件系统对Windows操作系统而言是不可识别的,因此需要对udisk分区做额外的格式化,将其格式化为FAT格式。
2.5.2添加格式化的时机
对EMMC分区的FAT格式化,常规考虑是在手机烧录完版本第一次开机后,进行FAT的格式化操作。但在实际生产过程中,有时会发现,某些EMMC的硬件特殊性,导致在生产线上,偶尔会出现手机烧录完版本第一次开机后,调用Linux的FAT格式化脚本失败的情况,导致手机无法使用内置U盘的功能。此类问题,需要在产线上就被过滤掉,因此,需要考虑在产线的校验功能中添加这一格式化检验功能。
另外,在手机正常升级版本的情况下,也需要添加格式化内置U盘的功能。而正常升级版本是不会操作AMT分区的数据的,因此还需要提供一种机制,能保证在擦除userdata分区数据的同时,对udisk分区进行格式化。
2.5.3在生产校验中添加格式化udisk操作
2.5.3.1快速AMT3介绍及其原理分析
//TODO
2.5.3.2在快速AMT3中添加脚本
//TODO
2.5.4添加格式化脚本
在本方案的设计中,添加脚本对该内置U盘空间进行格式化,将其格式化为FAT的文件系统。
2.5.4.1脚本启动方法
该脚本的启动方法为在手机烧录完版本后,第一次开机时,进行格式化操作。操作成功,在amt分区置一个标志位文件,以后每次开机时,先读取该标志位文件,如果之前格式化成功,就不在运行该脚本,否则将再次运行该脚本,直到格式化成功。
修改点为,在init.lc1813.rc里添加一个新的service用于启动格式化udisk分区的脚本:
service format_udisk /system/bin/sh/system/bin/mmc_format.sh udisk
class core
oneshot
编译时,在device.mk里将格式化udisk分区的脚本放置到手机system/bin:
#format internal sdcard.
PRODUCT_COPY_FILES += \
device/leadcore/ust802p0/mmc_format.sh:system/bin/mmc_format.sh
2.5.4.2格式化脚本分析
真正处理格式化的脚本是mmc_format.sh,其主要流程为,通过启动busybox的mkfs.vfat命令,格式化指定EMMC的分区,并在amt分区放置标志位文件,用以表示是否格式化成功。
脚本关键步骤命令:
MMC_DEV=/dev/block/mmcblk0 //指定EMMC分区
UDISK_PART=${MMC_DEV}p18//指定EMMC分区
FAT_MKFS="busyboxmkfs.vfat"//格式化命令
2.5.4.3正常升级能格式化udisk分区的方法
以上的方法只能保证手机第一次开机后格式化udisk分区,而在日常的使用中,由于手机软件版本的升级,往往需要对EMMC的软件分区都做大版本的升级,而做软件版本的升级时,是不会对AMT分区进行升级的,因为AMT分区保留的是手机的射频和Modem的相关数据。因此,在AMT的标志位文件也不会进行修改,而大版本的升级会对userdata分区进行擦除,因此可以根据userdata分区的数据的擦写情况,决定是否需要对udisk分区进行格式化操作。
在第一次烧录版本时,拷贝一个标志文件.first_flash到data分区:
PRODUCT_COPY_FILES += \
device/leadcore/ust802p0/first_flash:data/.first_flash
手机启动后,在mmc_format.sh脚本里对该标志文件进行控制。如果该标志文件存在,则表示擦写过userdata分区,需要格式化udisk分区,格式化后删除该标志文件。如果,该标志文件不存在,则表示是一次正常的开机,没有擦写过userdata分区,不需要格式化udisk分区。
udisk)
FILE=/amt/udisk_format.txt
FILE_FIRST_FLASH=/data/.first_flash
if [[ -e $FILE && ! -e$FILE_FIRST_FLASH ]] ; then
echo "start mount udisk "
#busybox mount -r -w -t vfat/dev/block/mmcblk0p18 /udisk
exit 2
fi
FORMAT_NUM_TOTAL=3
FORMAT_NUM=0
while [ "$FORMAT_NUM" !="$FORMAT_NUM_TOTAL" ]
do
$FAT_MKFS $UDISK_PART
covert_ret $?
ret=$?
FORMAT_NUM=`busybox expr$FORMAT_NUM + 1`
setprop $RESOULT_PROP end=$ret
if [ "$ret" ="1" ] ; then
echo "format udisk ok ! In$FORMAT_NUM" >> $FILE
if [ -e $FILE_FIRST_FLASH ];then
rm $FILE_FIRST_FLASH
fi
exit 2
fi
done
;;
1.6 启动脚本init.rc
2.6.1背景知识
init进程,它是一个由内核启动的用户级进程。内核自行启动(已经被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动一个用户级程序init的方式,完成引导进程。init始终是第一个进程.
2.6.2调整启动脚本init.rc
在init.rc里,有多个地方涉及到存储系统的路径和全局变量等,因此需要做一定的调整来适应双U盘的方案。
1.6.1.1 调整全局变量[p5]
全局变量名 |
全局变量值 |
修改说明 |
INTERNAL_STORAGE |
/storage/sdcard0 |
sdcard0对应内置U盘 |
EXTERNAL_STORAGE |
/storage/sdcard1 |
sdcard1对应外置SD卡 |
表2.6.2.1双U盘方案涉及的全局变量
1.6.1.2 调整存储空间路径和相关软链接
方法 |
路径和相关软链接 |
权限 |
修改说明 |
mkdir |
/storage/sdcard0 |
0555 root root |
新建内置U盘目录 |
mkdir |
/storage/sdcard1 |
0555 root root |
新建外置SD卡目录 |
symlink |
/storage/sdcard0 /sdcard |
NA |
添加软链接 |
symlink |
/storage/sdcard0 /mnt/sdcard |
NA |
添加软链接 |
symlink |
/storage/sdcard1/mnt/sdcard2 |
NA |
添加软链接 |
表2.6.2.2双U盘方案涉及的存储空间路径和相关软链接
1.6.1.3 取消原生的模拟sdcard服务
由于userdata分区的文件系统格式为EXT4,此类格式对文件的权限有严格的控制,而原生的Android设计中,是将userdata分区模拟成内置SD卡,对内置SD卡,是不能有如此严格的权限控制,因此Android原生设计了一套sdcard的service,通过该服务模拟userdata分区下的/data/media目录为/mnt/shell/emulated[p6] ,权限为media_rw(1023),将该目录模拟成fuse格式的文件系统,fuse文件系统可以提供普通sdcard一样的对文件的读写权限。
在本方案的设计中,不在需要该套sdcard的service,因此需要在init中,将该服务注释掉。
1.7 Vold层
2.7.1Vold磁盘管理框架介绍
1.7.1.1 Vold的产生和udev
udev是 Linux2.6内核里的一个功能,它替代了原来的 devfs,成为当前 Linux 预设的设备管理工具。udev以守护进程的形式运行,通过侦听内核发出来的 uevent来管理 /dev目录下的设备文件。不像之前的设备管理工具,udev在用户空间 (user space)运行,而不在内核空间 (kernel space)运行。
Vold的全称是Volume Daemon。在android中,取代udev的是vold, android一出生就没有遵守传统linux的许多标准,所以udev也不能很好的服务于android。android的的做法是定做一套udev,这就是vold。无论是udev还是vold,都是基于sysfs的,sysfs为内核与用户层的通讯提供了一种全新的方式,并将这种方式加以规范。kernel层能检测到有新的设备接入,并能为之加载相应的驱动,sysfs用于通知用户层,内核中的sysfs机制要求当有新的驱动加载时给用户层发送相应的event。Vold负责具体处理。对于用户层而言,无需关心sysfs的细节,只要知道sysfs可以向用户层提供信息即可。首先,我们要知道如何接收来自内核的event。这里就要用到Netlink socket,socket不仅能用于网络间的通讯, 也用能用于进程间的通讯,而这种内核态与用户沟通的活,也需要使用socket。
Vold是存储类的守护进程,是Android系统处理磁盘的核心部分。Vold服务由volumeManager统一管控,它将具体任务分别分派给netlinkManager,commandListener, directVolume, Volume去完成。Vold服务向下通过socket机制与底层驱动交互,向上通过JNI,intent, socket, doCommand等机制与Java Framework交互。
Android的volume服务主要是用来管理usb/sd卡等外部存储设备。平台可以对外部存储设备进行操作和轮询状态,当外部存储设备状态发生变化时,volume服务也会实时报告平台。
1.7.1.2 Vold启动
VolumeDaemon是在android init进程中启动的。在init进程中将解析init.rc文件。在该文件中有启动Vold的配置。如下:
service vold /system/bin/vold
class core
socket vold stream 0660 rootmount
ioprio be 2
在这里将启动Vold Daemon,并且创建一个socket。该socket主要是为了与framework层通信。
1.7.1.3 Vold内部结构
Vold(Volume Daemon)的内部框架如下图所示。Vold处理过程大致分为三步:创建连结、引导和事件处理。下面将结合下图对Vold工作流程进行分析。
Vold 内部架构
Vold作为一个守护进程,一方面接受驱动的信息,并把信息传给应用层;另一方面接受上层的命令并完成相应。所以这里的连结一共有两条:
1) vold socket:负责vold与framework层的信息传递;
2)接受sysfs uevent的socket:负责接受kernel的信息;
Vold socket是在init进程启动VolumeDaemon时创建的,这是一个用于和framework层通信的socket,在android系统中叫做Localsocket,framework层通过JNI机制调用C/C++空间函数与之通讯。
而与内核通信的socket是在vold main函数中创建的。在main函数中,vold将创建两个单例,VolumeManager和NetLinkManager,与内核通信的socket就在其中创建。这样Volume Daemon与Kernel、framework层的通信框架就建立。
1.7.1.4 Uevent和Netlink简介
uevent由内核发出,通过netlink sokect来传递给vold,在kobject被创建的时候,就会发生uevent的传递。对于未传递的uevent,会在kset下产生uevent文件,这是供用户态触发uevent使用的,通过向uevent档写入action(add,remove等),可以触发一个uevent,这些uevent可以被vold捕获,从而完成未完成的vold处理。在系统启动的时候,vold未启动的时候,这些uevent写入了uevent,vold启动后,会扫描sys目录查找uevent,然后触发它们,来完成之前未完成的事宜。uevent文件的内容,就是uevent事件的数据。
Netlink socket,socket不仅能用于网络间的通讯,也用能用于进程间的通讯,像这种内核态与用户沟通的活,自然也少不了它。在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程和使用者态进程的通信都是使用 netlink套接字实现的。netlink套接字的最大特点是对中断过程的支持,他在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。工作原理如图。
用户空间,用户态应用使用标准的socket和内核通讯,标准的socketAPI的函数,socket(),bind(), sendmsg(), recvmsg()和 close()非常容易地应用到 netlink socket。
为了创建一个 netlink socket,使用者需要使用如下参数调用 socket() socket(AF_NETLINK, SOCK_RAW, netlink_type) netlink对应的协议簇是AF_NETLINK,第二个参数必须是SOCK_RAW或SOCK_DGRAM,第三个参数指定netlink协议类型,他能是个自定义的类型,也能使用内核预定义的类型:
#defineNETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
同样地,socket函数返回的套接字,能交给bind等函数调用:
static int skfd;
skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
bind函数需要绑定协议地址,netlink的socket地址使用struct sockaddr_nl架构:
struct sockaddr_nl
{ sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups; };
成员 nl_family为协议簇 AF_NETLINK,成员 nl_pad当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的ID,如果希望内核处理消息或多播消息,就把该字段设置为0,否则设置为处理消息的进程ID。成员 nl_groups用于指定多播组,bind函数用于把调用进程加入到该字段指定的多播组,如果设置为0,表示调用者不加入所有多播组:
struct sockaddr_nl local;
memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid(); /*设置pid为自己的pid值*/
local.nl_groups = 0; /*绑定套接字*/
if(bind(skfd, (structsockaddr*)&local, sizeof(local)) != 0)
{
printf("bind() error\n"); return -1;
}
用户空间能调用send函数簇向内核发送消息,如sendto、sendmsg等,同样地,也能使用struct sockaddr_nl来描述一个对端地址,以待send函数来调用,和本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0:
struct sockaddr_nl kpeer;
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0; kpeer.nl_groups = 0;
rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
0, (struct sockaddr*)&kpeer, &kpeerlen);
/*处理接收到的数据*/ }
同样地,函数close用于关闭打开的netlinksocket.
1.7.1.5 处理来自framework层发出的command
class CommandListener主要收到上层 MountService通过doMountVolume发来的命令,分析后,转交给VolumeManager处理;VolumeManager处理信息后,或报告给上层MountService,或交给volume执行具体操作。
1.7.1.6 Vold.fstab分区表的介绍
Android系统上由于使用了Vold取代了udev来管理磁盘,自然也要有一个类似于udev的config来配置和管理磁盘的挂载,Android使用的是vold.fstab来实现这一目的。通过vold.fstab,Android可以读取预先配置好的sdcard或者多分区配置文件,该配置文件的格式和解析意义如下表:
格式 |
说明 |
示例 |
Format |
挂载格式 |
dev_mount |
Label |
挂载标签 |
sdcard |
Mount_point |
挂载点 |
storage/sdcard1 |
Part |
挂载分区(auto为自动挂载) |
auto |
<sys_path> |
设备实际路径 |
/devices/platform/comip-mmc.0/mmc_host/mmc1 |
表2.7.2 Vold.fstab分区表
需要注意的是:
1)子分区的数目可以为auto,表示只有一个子分区。子分区数目也可以为任意大于0的一个整数。
2)个参数间不能有空格,应该以tab制表符为参数的间隔,原因是android对vold.fstab的解析是以”\t”为标识,从而得到各个参数。
2.7.2 动态加载VOLD的分区表fstab
目前该文件位于device/leadcore/【项目名】/。该配置文件在Vold起到的配置外置存储器挂载点的作用[p7] 。调整vold.fstab,可以将内置U盘和外置SD卡分别挂载到vold需要解析的fstab上,让vold及以上层次能看到该FAT分区。
在Android原生设计里,只能读取一个配置好的fstab,无法实现动态加载。
在本双U盘的设计里,可以通过上层应用接口动态的调整当前的默认存储器。故在Vold层,需要可以根据动态的设定而决定当次加载的是那一个存储器。因此,本设计在Vold加载vold.fstab时,就动态的根据配置的默认存储器来决定当次加载的主存储器是内置U盘还是外置SD卡。
由于Android的传统习惯,将/storage/sdcard0/作为默认的存储器,因此,设计了两个fstab配置文件分别为:vold_internal.fstab,vold_external.fstab。分别表示内置U盘为默认存储器和外置SD卡为默认存储器的情况。再在Vold的process_config()函数里,解析fstab函数[p8] 时,根据当前配置的storage.config所指的默认存储器的值来决定本次加载的是哪一个fstab配置文件。
而当外置SD卡没有插入时的开机,需要做好特殊处理,强制设置为内置U盘为默认主存储器。
格式 |
说明 |
内置U盘 |
Format |
挂载格式 |
dev_mount |
Label |
挂载标签 |
Udisk |
Mount_point |
挂载点 |
storage/sdcard0 |
Part |
挂载分区(auto为自动挂载) |
18 |
<sys_path> |
设备实际路径 |
/devices/platform/comip-mmc.1/mmc_host/mmc0/mmc0 |
格式 |
说明 |
外置SD卡 |
Format |
挂载格式 |
dev_mount |
Label |
挂载标签 |
Sdcard |
Mount_point |
挂载点 |
storage/sdcard1 |
Part |
挂载分区(auto为自动挂载) |
Auto |
<sys_path> |
设备实际路径 |
/devices/platform/comip-mmc.0/mmc_host/mmc1 |
表2.7.3.1双U盘方案的vold_internal.fstab分区表
格式 |
说明 |
内置U盘 |
Format |
挂载格式 |
dev_mount |
Label |
挂载标签 |
Udisk |
Mount_point |
挂载点 |
storage/sdcard1 |
Part |
挂载分区(auto为自动挂载) |
18 |
<sys_path> |
设备实际路径 |
/devices/platform/comip-mmc.1/mmc_host/mmc0/mmc0 |
格式 |
说明 |
双U方案:外置SD卡挂载 |
Format |
挂载格式 |
dev_mount |
Label |
挂载标签 |
Sdcard |
Mount_point |
挂载点 |
storage/sdcard0 |
Part |
挂载分区(auto为自动挂载) |
Auto |
<sys_path> |
设备实际路径 |
/devices/platform/comip-mmc.0/mmc_host/mmc1 |
表2.7.3.2双U盘方案的vold_external.fstab分区表
2.7.3 调整MAX_PARTITIONS
在Vold的卷管理类DirectVolume里,由于一般的SD卡的分区不会超过4个分区,所以Android默认设置的对每个SD卡的最大分区为4。在本需求的修改中,由于在EMMC上目前的分区大大多于4,一般在15~20个分区(视项目而定),因此需要调整该参数设置。
static const int MAX_PARTITIONS = 32;
2.7.4 增加对多个大容量存储磁盘的适配
由于双U盘需要两个大容量存储设备,因而需要调整VolumeManager.cpp内对大容量存储设备的管理。
对应kernel的修改,添加内置U盘的大容量存储的路径:
MASS_STORAGE_FILE_PATH_1 "/sys/class/android_usb/android0/f_mass_storage/lun1/file"
在VolumeManager.cpp的shareVolume和unshareVolume函数里,需要增加一组设备的打开和关闭大容量存储的操作。
修改函数:int VolumeManager::shareVolume(const char *label, const char*method);
int VolumeManager::unshareVolume(const char *label, const char*method);
1.8 Framework层
2.8.1Framework层的磁盘管理框架
Android的原生设计,遵从软件工程的“高内聚,低耦合”的设计思想,在Framework层有一套独立的磁盘管理框架,起到承上启下的作用。承上,是指同APP层的交互,接受UI层发送过来的命令,或者将底层Vold上报的磁盘状态进行处理更新,上传给APP层;启下,是同Vold通讯,将APP层下发的对磁盘的操作指令传递给Vold,或者接受底层传来的磁盘状态更新的消息上报给APP层。而起到这一关键的作用的,就是Mountservice。
MountService作为Android的Java服务之一,在SystemServer进程启动的第二阶段创建并注册到ServiceManager中,同时长驻于SystemServer进程中。MountService各个类关系如下图:
直接查看4.4.4上a310p1上的代码,fstab.lc1860中每有Mount_point这一项
只有:voldmanaged=sdcard1::udisk,noemulatedsd
在开始构造MountService的时候,有一个非常重要的配置文件,storage_list.xml[p1] 文件,该文件以XML方式保存了所有存储设备的参数。Mountservice就是通过使用XML解析器读取该XML的文件内容,根据读取到的存储设备参数来构造StorageVolume对象,并将构造的所有StorageVolume对象存放到列表mVolumes中。同时,Mountservice注册了一个广播接收器,用于接收开机完成广播及USB状态广播,当开机完成时自动挂载存储设备,在大容量设备存储有效情况下,当USB状态变化也自动地挂载存储设备。
从上图可以清晰地看出SystemServer主线程启动MountService服务,该[p2] 服务启动时会创建一个MountService带有消息循环的工作线程,用于处理MountServiceHandle和ObbActionHandler分发过来的消息;同时创建一个用于连接Vold服务端socket的VoldConnector线程,该线程在进入闭环运行前会创建一个带有消息循环的VoldConnector.CallbackHandler线程,用于处理native层的Vold进程发送过来的uevent事件消息;然后向服务端Vold发送连接请求,得到socket连接后,从该socket中循环读取数据以接收来之服务端Vold的uevent消息,当读取的数据长度为0时,向服务端重新发起连接,如此循环,保证客户端MountService与服务端Vold一直保持正常连接。当成功连接到服务端Vold时,VoldConnector线程会创建一个MountService#onDaemonConnected线程,用于处理本次连接请求响应[p3] 。
MountService驻留在SystemServer进程中,和Vold作为两个不同的进程,它们之间的通信方式采用的是socket通信,Vold在CommandListener模块启动了一个socket监听线程,用于专门接收来之上层MountService的连接请求。而在MountService这端,同样启动了VoldConnector socket连接线程,用于循环连接服务端,保证连接不被中断,当成功连接Vold时,循环从服务端读取数据。MountService按照指定格式向Vold发送命令。下图表示了其中的mount命令的发送和接收流程。
2.8.2 双U盘策略下的Framework层的磁盘管理
2.8.2.1 storage_list.xml的动态配置
在双U盘策略下,由于需要动态的切换主存取器,就需要在对storage_list.xml的能相对应的做动态的解析。
storage_list.xml的配置参数说明如下:
参数 |
值类型 |
说明 |
mountPoint |
String |
[p4] 挂载点,同Vold.fstab里的挂载点对应 |
storageDescription |
String |
挂载点描述符 |
Primary |
Boolean |
该磁盘是否为主存储器 |
Removable |
Boolean |
该磁盘是否可移除,外置SD卡为可移除,内置空间不可移除 |
Emulated |
Boolean |
该磁盘是否为模拟磁盘,外置SD卡及独立udisk为非模拟磁盘,Android原生sdcard service启动的磁盘为模拟磁盘[p5] |
allowMassStorage |
Boolean |
该磁盘是否可用作大容量存储 |
MtpReserve |
Integer |
在MTP协议里保留的磁盘可用空间(MB单位) |
storage_list.xml的配置参数说明图
在原生的Mountservice里,对storage_list.xml为静态配置,即版本编译完后,storage_list.xml的值就决定了所有磁盘的形态,尤其是对主存储器挂载点是不可修改的。在双U盘设计里,就创新的使用了可配置的解析storage_list.xml,使用多组配置数据,来决定不同情况下,主存储器的挂载路径。目前使用的storage_list.xml配置如下:
参数 |
值类型 |
值 |
mountPoint |
String |
/storage/sdcard0 |
storageDescription |
String |
@string/storage_internal |
Primary |
Boolean |
true |
Removable |
Boolean |
false |
Emulated |
Integer |
false |
allowMassStorage |
Boolean |
true |
MtpReserve |
String |
100 |
主存储器为内置U盘时的内置U盘配置
参数 |
值类型 |
值 |
mountPoint |
String |
/storage/sdcard1 |
storageDescription |
String |
@string/storage_sd_card |
Primary |
Boolean |
false |
Removable |
Boolean |
true |
Emulated |
Integer |
false |
allowMassStorage |
Boolean |
true |
MtpReserve |
String |
100 |
主存储器为内置U盘时的外置SD卡配置
值类型 |
值 |
|
mountPoint |
String |
/storage/sdcard1 |
storageDescription |
String |
@string/storage_internal |
Primary |
Boolean |
false |
Removable |
Boolean |
false |
Emulated |
Integer |
false |
allowMassStorage |
Boolean |
|
MtpReserve |
String |
100 |
主存储器为外置SD卡时的内置U盘配置
参数 |
值类型 |
值 |
mountPoint |
String |
/storage/sdcard0 |
storageDescription |
String |
@string/storage_sd_card |
Primary |
Boolean |
|
Removable |
Boolean |
true |
Emulated |
Integer |
false |
allowMassStorage |
Boolean |
true |
MtpReserve |
String |
100 |
主存储器为外置SD卡时的外置SD卡配置
而在Mountservice里,就需要根据用户当前设置的主存储器的不同,而决定解析使用哪一组的storage_list.xml,从而实现了在Framework层的动态切换主存储器功能。
2.8.2.2 Mountservice添加对多个大容量存储设备的支持
Android Framework的原生设计里,只支持一块磁盘可以作为大容量存储,也就是说在storage_list.xml里只有一个磁盘卷设置了allowMassStorage为TRUE,而在双U盘的设计里,需要支持两个大容量存储器,同kernel的修改一样,Framework的Mountservice里,也需要增加对多个UMS的支持。
在原生的Mountservice里,获取UMS的方法为调用接口:
/**
* get the path of the ums device mounted at.
* there is a little hack, we always has onlyone device used ums.
*/
private String getUmsPath() {
synchronized (mVolumesLock) {
for (StorageVolume v : mVolumes) {
if ( v.allowMassStorage()) {
return v.getPath() ;
}
}
}
return null;
}
对应的需要修改该接口为返回一个String的List,可以包含多个UMS磁盘[p9] :
private List<String>getUmsPath() {
List<String> list = newArrayList<String>();
synchronized (mVolumesLock) {
for (StorageVolume v : mVolumes) {
if ( v.allowMassStorage()) {
list.add(v.getPath());
}
}
}
return list;
}
同时,在Mountservice里所有使用到UMS磁盘卷的地方,都需要相应的将原来只返回一个UMS卷的方法修改为返回一组StringList的UMS卷组。
而在切换大容量存储的操作时,Mountservice对上层和下层的指令切换不做更改,保持原生的切换大容量存储的指令流程。在双U盘方案里,每次切换都是整个UMS的list同时切换,保证了功能完整性和可维护性。
2.8.3双U盘功能在Framework的功能宏控制
//TODO
1.1 APP层
在应用层,Android SDK会提供Environment的一系列接口用于访问外部存储空间。其中最主要的接口getExternalStorageDirectory()和getPrimaryVolume()均为返回默认的主存储[p10] 器的挂载路径。在双U盘的设计里,能够实现默认的主存储器对上层是透明的,也就是说,所有底层的修改,双U盘的功能添加,都不会对上层应用调用SDK接口有影响,因此也保证了第三方应用能正常使用Android SDK的接口,保证平台的稳定性。
当然,由于添加了一个物理上的磁盘,在Settings应用里也会同步增加一个内部存储的磁盘卷,并且由于双U盘设计能动态切换主存储器挂载点,也需要在Settings应用做相应的功能添加。
在Setting的存储模块,添加对默认存储分区的控制和设置的UI界面。
默认存储器的设置值保存在/data/etc/storage.config下,在第一次烧录版本的时候,需要默认自动生成该文件并且保证有默认值,所以需要在编译时就默认生成该文件。Android编译框架下的device.mk里的全局PRODUCT_COPY_FILES控制编译时文件的复制,因此需要添加如下的文件:
#storageconfig
PRODUCT_COPY_FILES+= \
device/leadcore/ust802p0/storage.config:data/etc/storage.config
Settings需要对此文件可以读写,而默认生成的该文件由于是编译时生成,其文件的属性为root,而Settings应用的属性为System,对此文件无法读写,因此需要在系统启动后,对此文件的属性进行修改。修改文件属性的方法为在init.rc添加修改脚本:
chownsystem system /data/etc/storage.config
至此,Settings应用对默认存储器的读写控制可以实现,而/data/etc/storage.config也可以提供给vold,指明当前使用哪个挂载点为默认存储器。