测试环境
设备:Sony Xperia XZ Premium (docomo版本)
版本:官方Android 9.0
内核版本:4.4.302
版本号:47.2.E.3.29
前言
With CONFIG_ZRAM_WRITEBACK, zram can write idle/incompressible page
to backing storage rather than keeping it in memory.
(抄自kernel中Documentation/blockdev/zram.txt的介绍)
通俗解答,就是相当于swap那样把zram中空闲与不可压缩的玩意写进存储设备里,需要时再读回去。
那么为啥搞这玩意呢,因为4G内存不够了,zram再怎么压也确实缺,就算sony有z3fold加成,开个微信开个qq后android lmk还是乱杀,simple lmk也没能改善。
按照你google的意思就是不用内核里的android lmk,改用用户空间的lmkd,通过压力失速信息 (PSI) 监视器来获取关于内存压力水平的通知。然后PSI在4.9内核中才有,由于本人菜鸡没能backport(
所以,我还是顶着就算是ufs2.1,我也要搞writeback,就为了多一些后台存活能力(
说的就是你微信,国内不走fcm不走mipush非要进程在后台才给我推消息
需要做什么
-
有源码,有root的设备
-
内核中的zram需要支持writeback,这个特性在4.4的android内核上应该是没有的(起码索官方没看到)
-
需要系统支持(Android10开始引入支持,google的Android9源码上没有)
-
以及需要一个好歹会点基础中的皮毛的你(
关于系统支持这个,确实也有dalao做模块去使用,因为这玩意也就是在合适的时候(例如设备空闲,锁屏后)把值写进/sys/block/zramX/下的接口里,但是直接从系统框架里走JobScheduler不是更香么(
内核部分
需要pick的commit有些多,故我就直接找了别人压缩了一堆upstream和backport的squash版本,可参考
zram: squash partial IO refactoring
zram: introduce ZRAM_IDLE flag
pick上去后自行修正冲突,然后defconfig里将writeback打开,编译刷入即可,具体不做累述。
系统部分(系统干了啥)
如题,我们得先知道它系统干了啥,才能做对应的移植工作。
在高版本中,zram的配置直接写在fstab中,包括zram大小,writeback的位置和大小。系统会读取fstab中的参数并且进行配置。这一部分的东西由fs_mgr来完成。鉴于这玩意是c++编译出来的lib,不方便移植进系统内(毕竟大法原生)。故这一块手动完成并且写个自启脚本即可代替。
(最简单的,写个sh,延迟一下,然后写上要做的命令,再扔magisk的service.d下)。
那么,fs_mgr.cpp干了啥。
我们可以看到,android现在把writeback的文件位置写死为/data/per_boot下的zram_swap文件。在PrepareZramBackingDevice函数中创建writeback的文件并且用DirectIO的方式挂载为loop设备。
在fs_mgr_swapon_all根据从fstab中读到的参数进行调用创建。
然后就是Framework部分了,从google在android10的commit Create a Zram writeback job 中可以看到,在zramwriteback.java部分中,先判断zram下的backing_dev是否为none,为none就没开zramwriteback,则不执行,开了就用onStartJob执行任务。
private static final String IDLE_SYS = "/sys/block/zram%d/idle";
private static final String IDLE_SYS_ALL_PAGES = "all";
private static final String WB_SYS = "/sys/block/zram%d/writeback";
private static final String WB_SYS_IDLE_PAGES = "idle";
private static final String WB_STATS_SYS = "/sys/block/zram%d/bd_stat";
private static final int WB_STATS_MAX_FILE_SIZE = 128;
private void markPagesAsIdle() {
String idlePath = String.format(IDLE_SYS, sZramDeviceId);
try {
FileUtils.stringToFile(new File(idlePath), IDLE_SYS_ALL_PAGES);
} catch (IOException e) {
Slog.e(TAG, "Failed to write to " + idlePath);
}
}
private void flushIdlePages() {
if (DEBUG) Slog.d(TAG, "Start writing back idle pages to disk");
String wbPath = String.format(WB_SYS, sZramDeviceId);
try {
FileUtils.stringToFile(new File(wbPath), WB_SYS_IDLE_PAGES);
} catch (IOException e) {
Slog.e(TAG, "Failed to write to " + wbPath);
}
if (DEBUG) Slog.d(TAG, "Finished writeback back idle pages");
}
private int getWrittenPageCount() {
String wbStatsPath = String.format(WB_STATS_SYS, sZramDeviceId);
try {
String wbStats = FileUtils
.readTextFile(new File(wbStatsPath), WB_STATS_MAX_FILE_SIZE, "");
return Integer.parseInt(wbStats.trim().split("\\s+")[2], 10);
} catch (IOException e) {
Slog.e(TAG, "Failed to read writeback stats from " + wbStatsPath);
}
return -1;
}
private void markAndFlushPages() {
int pageCount = getWrittenPageCount();
flushIdlePages();
markPagesAsIdle();
if (pageCount != -1) {
Slog.i(TAG, "Total pages written to disk is " + (getWrittenPageCount() - pageCount));
}
}
上面这串就是主要部分了(懒得截图了),原理与
echo "all" > /sys/block/zram0/idle
echo "idle" > /sys/block/zram0/writeback
是基本一样的。
声明zram页空闲,然后在合适的时间请求空闲页写回。
那么什么是合适的时间?
// Schedule a one time job to flush idle pages to disk.
// After the initial writeback, subsequent writebacks are done at interval set
// by ro.zram.periodic_wb_delay_hours.
js.schedule(new JobInfo.Builder(WRITEBACK_IDLE_JOB_ID, sZramWriteback)
.setMinimumLatency(TimeUnit.MINUTES.toMillis(firstWbDelay))
.setRequiresDeviceIdle(true)
.build());
其中setRequireDeviceIdle,判断设备进入idle状态后才会执行。任务执行时间可由prop定义,如prop无定义则由默认值
int markIdleDelay = SystemProperties.getInt(MARK_IDLE_DELAY_PROP, 20);
int firstWbDelay = SystemProperties.getInt(FIRST_WB_DELAY_PROP, 180);`
默认是20分钟后进行一次标志,180分钟后写回操作。
然后就是StorageManagerService类部分
在storagemanagerservice中判断persist.sys.zram_enabled值是否为1,然后framework-res.apk中的config_zramWriteback的值是否为true,才会去调用执行zrambackwrite.java中的任务。
系统部分(需要做的)
我们需要手动替代fs_mgr做的事
(位置,大小均可以自定义,framework只通过检测是否为none判断是否启用了writeback,这里以官方地址为例)
同样,也在/data下创建一个per_boot文件夹,在手机终端/adb下
mkdir -p /data/per_boot
然后我们创一个1G的writeback文件
dd if=/dev/zero of=/data/per_boot/zram_swap bs=1024 count=1048576
创建后我们将这个设备挂载到loop设备上
我这边loop0-7都是空的,那么我就挂上loop0
losetup /dev/block/loop0 /data/per_boot/zram_swap
如果没有报错,即正常挂上了。
我们测试一下
先关闭原有zram
swapoff /dev/block/zram0
然后触发zram初始化
echo 1 > /sys/block/zram/reset
将挂载的zram_swap的地址写到writeback里
(这一步执行后可以通过dmesg |grep zram来查看有没有正常挂上)
echo "/dev/block/loop0" > /sys/block/zram0/backing_dev
那么接着配置zram大小,这以2g为例
echo 2147483648 > /sys/block/zram0/disksize
最后开启zram
mkswap /dev/block/zram0
swapon /dev/block/zram0
可以通过cat命令或者用scene5去查看writeback的情况
以上我们需要的步骤可以全部做成自启脚本,在系统启动时就完成
我们测试一下回读情况
echo all > /sys/block/zram0/idle
echo idle > /sys/block/zram0/writeback
然后在scene5里可以看到zram压缩后的大小变小了,writeback有回写数据了,即正常。
将代码注入到framework里
在此之前,由于需要修改framework-res的AndroidManifest,故需要对services.jar修改支持核心破解(或者幸运破解器),反编译修改请参考去除签名验证
这部分我是用jadx换回java对着看改的(我菜炸了)
ZramWriteback.smali和ZramWriteback$1.smali可以直接扔进去,放在com/android/server下。
我们需要修改的就是StorageManagerService.smali了,但是这里我们先不改这个,我们先去把framework-res.apk改了,原因后面会提到。
那么反编译framework-res后,打开AndroidManifest.xml,找到
<service android:exported="true" android:name="com.android.server.MountServiceIdler" android:permission="android.permission.BIND_JOB_SERVICE"/>
那么我们在下面新加一句
<service android:exported="false" android:name="com.android.server.ZramWriteback" android:permission="android.permission.BIND_JOB_SERVICE"/>
这是给zramwriteback任务使其拥有BIND_JOB_SERVICE权限的,必须要加,不然会因为没有权限而在启动时报错。然后我们进入res/values里,在最后一行
</resources>
的上面一行,添加
<bool name="config_zramWriteback">true</bool>
然后修改完成,回编译
回编译后再次对修改后的framework-res.apk进行反编译。
一是检查AndroidManifest里是否修改成功,我这边有时候apktool抽风并没有改成。二是需要找一个值。
在res/values文件夹打开public.xml,搜索config_zramWriteback
记住后面这个id,例如我这边就是0x01120113,这个id是需要用的。现在反编译的这个不需要回编译,上面修改的回编译就是修改好的。
再去处理services.jar的storagemanagerservice.smali
需要修改两个method,先找到
.method private handleSystemReady()V
在
invoke-direct {p0}, Lcom/android/server/StorageManagerService;->refreshZramSettings()V
的后面到return-void的前面加上
const-string/jumbo v0, "persist.sys.zram_enabled"
invoke-static {v0}, Landroid/os/SystemProperties;->get(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
.local v0, "zramPropValue":Ljava/lang/String;
const-string v1, "0"
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-nez v1, :cond_0
iget-object v1, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;
invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v1
const v2, 0x01120113
invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getBoolean(I)Z
move-result v1
if-eqz v1, :cond_0
iget-object v1, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;
invoke-static {v1}, Lcom/android/server/ZramWriteback;->scheduleZramWriteback(Landroid/content/Context;)V
:cond_0
在这个最后的cond_0后是return-void
这里由于我Android9上并没有refreshIsolatedStorageSettings函数,故去除了。
注意:寄存器要自己调整!
TIP:想偷懒的话,拿jadx转换下自己原版的smali,和google的对比下,然后把整个method换过去,少了补上,多的去掉即可(
然后是.method private refreshZramSettings()V在这块由于我设备上的和google的基本一样,就直接替换过来了(
当然,要补的话就是在
.local v2, "desiredPropertyValue":Ljava/lang/String;
:goto_26
invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v4
if-nez v4, :cond_49
的后面,:cond_xx return-void的前面插入
invoke-static {v0, v2}, Landroid/os/SystemProperties;->set(Ljava/lang/String;Ljava/lang/String;)V
invoke-virtual {v2, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_49
iget-object v0, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;
invoke-virtual {v0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v0
const v3, 0x01120113
invoke-virtual {v0, v3}, Landroid/content/res/Resources;->getBoolean(I)Z
move-result v0
if-eqz v0, :cond_49
iget-object v0, p0, Lcom/android/server/StorageManagerService;->mContext:Landroid/content/Context;
invoke-static {v0}, Lcom/android/server/ZramWriteback;->scheduleZramWriteback(Landroid/content/Context;)V
同样,自行修改寄存器
插入进去后没结束,还记得前面记得id的值么,要改,上面两部分中都有(不包括const v数字)
const v2, 0x01120113
这个0x啥的就是对应com.android.internal.R.bool.config_zramWriteback的id,修改成我们上面记录的id
至于为什么上面framework-res的时候先回编译,因为apktool会自己在public里生成我们新加布尔值的id,生成后再反编译查看
这里提供下这三文件,放在我的E盘里了,在Blog_Files目录下,自行参考,文件为大法markII官方rom提取。
后续&调试
把回编译的文件自行放入system里替换掉,做好备份,然后开机
小贴士:测试前开好adb,授权好,一旦炸了还能根据logcat来查问题
注意:/sys/block/zram/下的接口如果是root所有者与用户组的话,框架层是没有权限写入的!
根据logcat我们可以看到“Failed to write to /sys/block/zram0/idle”
我们需要在前面搞得自启脚本里加上
chown system:system /sys/block/zram0/idle
chown system:system /sys/block/zram0/writeback
chown system:system /sys/block/zram0/bd_stat
chown system:system /sys/block/zram0/backing_dev
使得系统有权限写入。
这里说个调试小技巧,在最早给我报写入错误时是不知道原因的,因为google的这个代码里只用Slog.e打印出,并没有抛出异常。没有具体异常我们肯定看不到什么的。以markpageasidle为例,在
invoke-static {v3, v2}, Landroid/util/Slog;->e(Ljava/lang/String;Ljava/lang/String;)I·
也就是slog.e的后面一行我们加上
invoke-virtual {v1}, Ljava/io/IOException;->printStackTrace()V
也就是加上了e.printStackTrace() ;
最后的最后,在build.prop中加上(或者在自启脚本中使用setprop)
ro.zram.mark_idle_delay_mins = 60
ro.zram.first_wb_delay_mins = 1440
ro.zram.periodic_wb_delay_hours = 24
以上值来自pixel官方,其中,个人建议将first_wb_delay_mins改小,不然1440分钟就是24小时,60分钟标记一次,24小时后才进行写回,为了
保 护 闪 存 寿 命
也不至于这么久(
参考文章
除文中引用的
Android zram writeback
zram writeback 代码分析
常用android的smali注入代码
zram.txt翻译版
Special Thanks
@zobbz (提供了一份合并了odex,android10的services.jar)
@cramfs28 (提供官方writeback和storagemanagerservice的smali文件以及反编译上的一些技术支持)
本文部分来源于参考文章,其他均为原创
转载请注明
0 条评论