本文主要是介绍Android文件访问权限的管理机制以及SDCardFS,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Android文件访问权限的管理机制以及SDCardFS
1. 原生Linux文件访问权限控制
原生的Linux操作系统是通过拥有者 ID(uid) 和群组 ID(gid)对文件的访问权限进行管理,如:
user0@user0:testLinuxPermission$ ls -l访问权限 uid gid 文件名
-rw-rw---- 1 root root 0 Apr 24 22:12 123.txt
-rw-r--r-- 1 user0 user0 0 Apr 24 22:11 abc.txt
123.txt
是属于root
用户,且属于root
群组,且文件的权限设置为-rw-rw----
,即660权限,允许文件拥有者,与文件拥有者同一组的用户(具有相同gid)进行访问。由于user0
并不是123.txt
的拥有者,也不属于同一组,因此user0
无法访问123.txt
文件。
文件的访问权限可以在终端可以通过chmod
命令进行修改,文件拥有者通过chown
命令对进行修改,将某个用户加入某个组,可以通过useradd
命令。
内核层面上,文件的访问权限由inode->mode
指定,文件拥有者,以及所属群组分别通过inode->uid
和inode->gid
进行指定。
2. Android文件访问权限控制
2.1 内部存储以及外部存储
Android的存储区域分为内部存储和外部存储。由于现在一般手机已经不使用SD-Card、TF-Card等扩展存储器,因此内部存储和外部存储大部分情况下是一个逻辑上划分区域的概念。内部存储一般指的是/data/
目录,而外部存储指的是/sdcard/
目录。
/data/
目录: 主要包含两个子目录app
目录和data
目录,app
目录主要用于存放系统APP,以及用户APP的可执行文件,例如.apk,.dex,.so文件等。data
目录主要存放APP的私有数据,例如用户的信息,缓存文件等。随着APP的删除,app
目录和data
目录里面的文件也会随之删除。
/sdcard/
目录: 该目录作用很多。它既包含多个可以共用子目录,例如Download
目录、Music
目录、Pictures
目录等,也包含一些用于存放用户APP的体积较大的私有数据的目录(视频、图片等)。
内部存储以及外部存储的私有目录是APP默认就可以获取权限进行访问,但是外部存储的公有目录需要APP安装或者启动的时候进行权限请求。例如,APP会在AndroidManifest.xml
文件进行如下权限声明:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
然后系统在安装的过程或者启动的过程中,授予APP相应的外部存储公有目录的访问权限。
2.2 文件访问权限控制
私有数据和公有数据
从上一节可以知道,Android会将数据分为私有数据和公有数据,例如APP1的私有数据,不能被APP2去访问。那么是Android是如何实现私有数据和公有数据的控制呢?
Android沿用了第一节介绍的Linux对文件权限管理模式,通过uid和gid进行管理,分为如下步骤:
-
Android给每一个APP分配一个独特的
APP ID
,然后将这个APP ID
作为文件的uid(inode->uid
),写入到文件系统中。 -
下一步设置文件的gid(
inode->gid
)- 对于内部存储的文件,将文件的gid(
inode->gid
)设置为APP ID
- 对于外部存储的文件,将文件的gid(
inode->gid
)设置为sdcard_rw
,它们的差别后面继续讨论。
- 对于内部存储的文件,将文件的gid(
-
最后给每一个文件设置相应的权限(
inode->mode
),- 对于内部存储的文件,设置为
rwx------
(700),那么表示只允许APP本身(具有相同APP ID
)进行访问 - 对于外部存储的文件,设置为
rwxrwx---
(770),那么表示除了允许APP本身(具有相同APP ID
)进行访问以外,也允许属于同一组的APP(具有相同gid
)进行访问。因此,外部存储可以同时实现公有文件和私有文件的访问控制。一般情况下,外部存储的文件的gid
设置为sdcard_rw
。
- 对于内部存储的文件,设置为
读写权限控制
-
对于内部存储文件,目的是不让APP1访问APP2的私有文件,这个目的基于Linux的访问权限机制就可以实现(uid + 700权限)。
-
对于外部存储文件,可能会有一些问题:
从上面的描述,Android可以通过uid
和gid
的组合,控制每一个APP的文件访问权限。Android在此基础上更进一步,要求独立控制每一个APP对于外部存储的权限。例如外部存储一个典型场景:
Android可能要求外部存储对于APP1是只读的,但是对于APP2是读写均可。
即判断APP是否设置了READ_EXTERNAL_STORAGE以及WRITE_EXTERNAL_STORAGE,然后授予该APP对外部存储的相应的权限。
这个情况基于传统的Linux访问权限机制(uid
和gid
)是无法实现。因为外部存储内的每一个文件只有一个inode->gid
,它可以被分配去只读rwxr-x---
,或者读写rwxrwx---
,但是不能单独给每一个APP设置为只读和读写两种状态。因此,Android使用了SDCardFS
去处理单个文件对于不同的APP访问权限的问题。
2.3 SDCardFS的原理以及作用
从2.1节知道,Android从逻辑上分为内部存储和外部存储,且外部存储目录的名字为/sdcard/
。因此为了解决外部存储目录的权限访问控制问题,设计了SDCardFS
文件系统,作为/sdcard/
目录的文件系统,管理它的权限。SDCardFS
跟SD-Card、TF-Card那些没什么关系,是一个历史遗留命名方式。
2.3.1 SDCardFS设计
从2.1节知道,为了实现APP访问隔离,Android从逻辑上分为内部存储和外部存储,但是物理上都是属于同一段物理空间,同一个ext4文件系统。因此,SDCardFS
是一个包装文件系统,它给将其中一段ext4文件系统空间包装了起来,并在包装层实现了访问控制。
2.3.2 如何解决单个文件对不同APP具有不同的访问权限的问题
Android可以采用每一个APP动态挂载SDCardFS
方式去解决这个问题,每一个APP的在启动的时候,跟据该APP在AndroidManifest.xml
申请的访问权限,通过挂载参数选择将文件系统挂载成为只读、读写类型(仍然是那一段物理空间,只是挂载为不同访问类型)。
但是这个方案有一个明显的缺点: 显然如果每一个APP都需要动态挂载一个文件系统,会耗费大量内存,因为每一个挂载的文件系统都有各自独立的大量的dentry
、inode
等内存数据结构。
因此Android使用了一个改进方案: 为了减少内存使用,Android使用了bind remount的方式进行多次挂载,bind remount有几个特性:
- 将已经挂载好的
SDCardFS
,重新挂载到一个新目录 - 新挂载的
SDCardFS
与原始的SDCardFS
共用一个inode
,因此可以减少内存使用 - 由于共用一个
inode
,因此inode->gid
,inode->uid
,inode->mode
还是一样的,访问模式也是一样的
2.3.3 如何解决inode->gid
,inode->uid
,inode->mode
一样的问题
然而,inode->gid
,inode->uid
,inode->mode
如果跟原始的SDCardFS
一样的话,无法解决APP单独控制读写权限的问题。因此SDCardFS
作为包装文件系统最重要的功能出现了:
通过动态bind remount的挂载选项,通过对inode的包装动态设定inode的uid,gid,mode值。
实现方案的思路: 既然inode->gid
只有一个,那么就让inode->mode
可以动态变化。
内核实现:
- 每一次调用
open
函数,都会对inode
进行访问鉴权,一般是通过generic_permission
函数进行。 SDCardFS
在执行generic_permission
函数之前,先创建一个临时的inode
,然后给这个临时inode
赋予特定inode->uid
、inode->gid
、inode->mode
。其中inode->mode
的值与SDCardFS
挂载选项opt->mask
值相关,关系为inode->mode = 0775 & ~opt->mask
。- 使用这个临时
inode
代替原来的inode
进行鉴权。
那么会出现什么情形呢?假设SDCardFS0
被bind remount了2次,分别是SDCardFS1
和SDCardFS2
,其中挂载参数opt->mask
分别是23和7,则:
-
SDCardFS1, mask=23
: 挂载参数opt->mask
的值是23,那么临时inode->mode =750
,即只读权限,任何通过SDCardFS1
访问的inode都会被临时inode处理为只读权限。 -
SDCardFS2, mask=7
: 挂载参数opt->mask
的值是7,那么临时inode->mode =770
,即读写权限,任何通过SDCardFS2
访问的inode都会被临时inode处理为读写权限。
因此,对于一个原始SDCardFS
修改挂载参数进行bind remount,可以在节省内存的同时,让同一个目录具有多种访问权限。
实际例子:
/sdcard/
目录实际上是一个链接,链接到/storage/emulated/0
目录,这个目录通过各种挂载关系之后,链接到/data/media
目录,同时/data/media
目录也被挂载到/mnt/runtime/default/emulated
。因此/sdcard/
和/mnt/runtime/default/emulated
的数据是关联的,接下来主要讨论/mnt/runtime/default/emulated
目录。
a) 系统层面
Android最原始的SDCardFS
是在系统启动的过程中挂载的,目录是/mnt/runtime/default/emulated
,如下:
/data/media on /mnt/runtime/default/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,...)
然后系统通过bind remount的方式挂载了/mnt/runtime/read/emulated
目录以及/data/media on /mnt/runtime/write/emulated
目录,使其拥有了不同的访问权限,如下:
/data/media on /mnt/runtime/read/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=23,...)
/data/media on /mnt/runtime/write/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,...)
可以看到挂载参数gid
和mask
不同,他们有什么不同呢?
mask
的不同: 前面内核实现这一节提到inode->mode = 0775 & ~opt->mask
,因此mask
的值影响到了临时inode
的访问权限。通过简单计算可以得到mask=7 --> 771
、mask=23 --> 750
、mask=7 --> 770
,即/mnt/runtime/read/emulated
是读写权限,/mnt/runtime/read/emulated
是只读权限,/mnt/runtime/write/emulated
是读写权限。gid
的不同:gid=1015
其实就是之前提及到的sdcard_rw
组的id,不是该组的应用无法访问里面的数据。gid=9997
就是everybody
组的id,一般APP都属于这一组。因此,只要APP挂载了/mnt/runtime/read/emulated
目录或者/mnt/runtime/write/emulated
,就可以访问到里面的文件。
综上所述,对于同一个/sdcard/
目录,使用了三种挂载的方式,挂载到三个目录中,并基于bind remount,以及挂载选项+临时inode
的方法,实现了三个挂载目录具有不同的访问权限。
b) APP层面
现在回到最开始的问题,讨论APP是如何将其实现的:
Android可能要求外部存储对于APP1是只读的,但是对于APP2是读写均可。
由于APP1只申请了只读的权限,它在启动时,挂载/mnt/runtime/read/emulated
目录,因此APP1只能只读地访问/sdcard/
。(前面提及/mnt/runtime/read/emulated
和/sdcard/
是对应的)
由于APP2申请了读写的权限,它在启动时,挂载/mnt/runtime/write/emulated
目录,因此APP2可以读写模式访问/sdcard/
。
因此就实现了,对于同一个文件,对于APP1是只读的,对于APP2是读写的。
如果一个APP不申请任何权限,那么它会挂载/mnt/runtime/default/emulated
目录,同时由于该APP不属于sdcard_rw
组,因此对/sdcard/
文件没有读写权限。
这篇关于Android文件访问权限的管理机制以及SDCardFS的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!