Android Lmkd 低内存终止守护程序

news2024/11/26 1:59:36

一、低内存终止守护程序

Android 低内存终止守护程序 (lmkd) 进程可监控运行中的 Android 系统的内存状态,并通过终止最不必要的进程来应对内存压力大的问题,使系统以可接受的性能水平运行。

所有应用进程都是从zygote孵化出来的,记录在AMS中mLruProcesses列表中,由AMS进行统一管理,AMS中会根据进程的状态更新进程对应的oom_adj值,这个值会通过socket传递给lmkd。lmdk根据内核的版本情况,或传递给kernel或自身处理低内存回收机制。为腾出更多的内存空间,在内存达到一定阀值时会触发清理oom_adj值高的进程。

1. 内存压力简介

并行运行多个进程的 Android 系统可能会遇到系统内存耗尽,需要更多内存的进程出现明显延迟的情况。内存压力是系统内存不足的一种状态,它需要 Android 通过限制或终止不必要的进程、请求进程释放非关键缓存资源等方式来释放内存(以缓解这种压力)。

过去,Android 使用内核中的低内存终止守护程序 (LMK) 驱动程序来监控系统内存压力,该驱动程序是一种依赖于硬编码值的严格机制。从内核 4.12 开始,LMK 驱动程序已从上游内核中移除,改由用户空间 lmkd 来执行内存监控和进程终止任务。

2. 压力失速信息

Android 10 及更高版本支持新的 lmkd 模式,它使用内核压力失速信息 (PSI) 监视器来检测内存压力。上游内核中的 PSI 补丁程序集(已向后移植到 4.9 和 4.14 内核)可测量由于内存不足导致任务延迟的时间。由于这些延迟会直接影响用户体验,因此它们代表了确定内存压力严重性的便捷指标。上游内核还包括 PSI 监视器,这类监视器允许特权用户空间进程(例如 lmkd)指定这些延迟的阈值,并在突破阈值时从内核订阅事件。

① PSI 监视器与 vmpressure 信号

由于 vmpressure 信号(由内核生成,用于检测内存压力并由 lmkd 使用)通常包含大量误报,因此 lmkd 必须执行过滤以确定是否真的存在内存压力。这会导致不必要的 lmkd 唤醒并使用额外的计算资源。使用 PSI 监视器可以实现更精确的内存压力检测,并最大限度地减少过滤开销。

②使用 PSI 监视器

如需使用 PSI 监视器(而不是 vmpressure 事件),请配置 ro.lmk.use_psi 属性。默认值为 true,这会以 PSI 监视器作为 lmkd 内存压力检测的默认机制。由于 PSI 监视器需要内核支持,因此内核必须包含 PSI 向后移植补丁程序,并在启用 PSI 支持 (CONFIG_PSI=y) 的情况下进行编译。

3. 内核中 LMK 驱动程序的缺点

由于存在大量问题,Android 弃用了 LMK 驱动程序,问题包括:

  • 对于低内存设备,必须主动进行调整,即便如此,在处理涉及支持大文件的活跃页面缓存的工作负载时,其性能也较差。性能不良会导致出现抖动,但不会终止。
  • LMK 内核驱动程序依赖于可用内存限制,不会根据内存压力进行扩缩。
  • 由于设计的严格性,合作伙伴通常会自定义该驱动程序,使其可以在自己的设备上使用。
  • LMK 驱动程序已挂接到 Slab Shrinker API,该 API 并非为了执行繁重操作(例如搜索并终止目标)而设计,这类操作会导致 vmscan 进程变慢。

4. 用户空间 lmkd

用户空间 lmkd 可实现与内核中的驱动程序相同的功能,但它使用现有的内核机制检测和评估内存压力。这些机制包括使用内核生成的 vmpressure 事件或压力失速信息 (PSI) 监视器来获取关于内存压力水平的通知,以及使用内存 cgroup 功能限制根据进程的重要性分配给每个进程的内存资源。

在 Android 10 中使用用户空间 lmkd

在 Android 9 及更高版本中,用户空间 lmkd 会在未检测到内核中的 LMK 驱动程序时激活。由于用户空间 lmkd 要求内核支持内存 cgroup,因此必须使用以下配置设置编译内核:

CONFIG_ANDROID_LOW_MEMORY_KILLER=n
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y

终止策略

用户空间 lmkd 支持基于以下各项的终止策略:vmpressure 事件或 PSI 监视器、其严重性以及交换利用率等其他提示。低内存设备和高性能设备的终止策略有所不同:

  • 对于内存不足的设备,一般情况下,系统会选择承受较大的内存压力。
  • 对于高性能设备,如果出现内存压力,则会视为异常情况,应及时修复,以免影响整体性能。

您可以使用 ro.config.low_ram 属性配置终止策略。如需了解详情,请参阅低 RAM 配置。

用户空间 lmkd 还支持一种旧模式,在该模式下,它使用与内核中的 LMK 驱动程序相同的策略(即可用内存和文件缓存阈值)做出终止决策。要启用旧模式,请将 ro.lmk.use_minfree_levels 属性设置为 true。

5. 图解

5-1. LMK/LMKD

5-2. lmkd kill process flow

lmkd can use minfree table to adjust which adj process to kill (ro.lmk.use_minfree_levels=1)

or use medium/critical pressure to adjust which adj process to kill (ro.lmk.use_minfree_levels=0).

the midum/citical pressure adjust was configured by ro.lmk.medium/ro.lmk.critical , which default value was 800/0 .

lmkd log:(in main log)

ro.lmk.use_minfree_levels=1

ro.lmk.use_minfree_levels=0

5-3. lmk kill process flow

lmk log: (in kernel log)

6 .minfree table & oom adj

6.1 minfree table

adb shell cat /sys/module/lowmemorykiller/parameters/minfree

或者

adb root

adb shell getprop |grep minfree

[sys.lmk.minfree_levels]: [18432:0,23040:100,27648:200,32256:250,36864:900,46080:950]

6.2 如何修改?

•可以通过修改修改minfree表

(frameworks/base/core/res/res/values/config.xml)

config_lowMemoryKillerMinFreeKbytesAbsolute
config_lowMemoryKillerMinFreeKbytesAdjust

(frameworks/base/services/core/java/com/android/server/am/ProcessList.java)
updateOomLevels() function's calculation formula or
default value table(mOomMinFreeLow/mOomMinFreeHigh)

6.3 oom adj

ADJ优先级>

OOMADJ

对应场景

UNKNOWN_ADJ

1001

一般指将要会缓存进程,无法获取确定值

CACHED_APP_MAX_ADJ

906

不可见进程的adj最大值(不可见进程可能在任何时候被杀死)

CACHED_APP_MIN_ADJ

900

不可见进程的adj最小值(不可见进程可能在任何时候被杀死)

SERVICE_B_ADJ

800

B List中的Service(较老的、使用可能性更小)

PREVIOUS_APP_ADJ

700

上一个App的进程(比如APP_A跳转APP_B,APP_A不可见的时候,A就是属于PREVIOUS_APP_ADJ)

HOME_APP_ADJ

600

Home进程

SERVICE_ADJ

500

服务进程(Service process)

HEAVY_WEIGHT_APP_ADJ

400

后台的重量级进程,system/rootdir/init.rc文件中设置

BACKUP_APP_ADJ

300

备份进程

PERCEPTIBLE_APP_ADJ

200

可感知进程,比如后台音乐播放

VISIBLE_APP_ADJ

100

可见进程(可见,但是没能获取焦点,比如新进程仅有一个悬浮Activity,Visible process)

FOREGROUND_APP_ADJ

0

前台进程(正在展示是APP,存在交互界面,Foreground process)

PERSISTENT_SERVICE_ADJ

-700

关联着系统或persistent进程

PERSISTENT_PROC_ADJ

-800

系统persistent进程,比如telephony

SYSTEM_ADJ

-900

系统进程

NATIVE_ADJ

-1000

native进程(不被系统管理)

7. lmkd parameters

Parameter

Description

Default
value

LowRam
value

ro.lmk.debug

debug 开关, 除了killing log以外的debug 讯息需要打开这个才能看的到

false

ro.lmk.kill_heaviest_task

默认false - 每次需要杀进程时,则从高oom_adj开始遍历,同oom_adj时从最后加入列表的开始杀,直到释放出足够内存为止;

true - 每次需要杀进程时,则从高oom_adj开始遍历,同oom_adj时从rss最高的进程开始杀(参考节点/proc/<$pid>/statm中的第二个数值),直到释放出足够内存为止;

false

ro.config.low_ram

一般ago device定义为low ram device , 目前是1GB ram 以下的device , 有两个特点

1.依据不同oomadj 限制内存 , 2.一次只会kill 一个process

false

ro.lmk.kill_timeout_ms

kill process 后下次kill 中间间隔的 timeout 时间

0

ro.lmk.use_minfree_levels

采用kernel lowmemory killer 的 cache /minfree 参考机制来kill process , 而非参考memory pressure

false

Mem Pressure relative

mp_event_common 使用的prop,和PSI 参数不同时生效

ro.lmk.low

memory pressure 为low 时kill 的最低 adj

1001

ro.lmk.medium

memory pressure 为medium时kill 的最低 adj

800

ro.lmk.critical

memory pressure 为high时kill 的最低 adj

0

ro.lmk.critical_upgrade

允许 memory pressure 从medium 被上升到critical , 条件是mem_pressure计算低于upgrade_pressure临界值

false

ro.lmk.upgrade_pressure

critical pressure 的参考值 , 以上为medium , 以下为critical

100

ro.lmk.downgrade_pressure

medium pressure 的参考值 , 以上为low ,以下为medium

100

PSI relative (>=AndroidQ)

mp_event_psi使用的参数,和Pressuure参数不同时生效

ro.lmk.use_psi

kernel 使用psi event上发lmkd

1

1

ro.lmk.use_new_strategy

1: use mp_event_psi , 0: use mp_event_common to kill process

0

1

ro.lmk.swap_free_low_percentage

判定swap low的百分比 ex : swap free < 10/100

20

10

ro.lmk.swap_util_max

最大内存交换量:占可交换内存的百分比。(默认值实际上会停用此功能)

100

100

ro.lmk.thrashing_limit

判定 thrashing 的标准值

100

30

ro.lmk.thrashing_limit_decay

thrashing limit衰减百分比 , 每次衰减
thrashing_limit = (thrashing_limit * (100 - thrashing_limit_decay_pct)) / 100

10

50

ro.lmk.psi_partial_stall_ms

内存失速阈值。用于触发内存不足的通知。 Default for low-RAM devices = 200, for high-end devices = 70 (PSI_SOME)

70

200

ro.lmk.psi_complete_stall_ms

完全PSI失速阈值。用于触发关键内存通知。 Default =700 (PSI_FULL)

700

700

ro.lmk.thrashing_min_score_adj

发生thrashing 时kill 的 min score adj

200

200

二、低内存的数据特征和行为特征

1、Meminfo 信息

最简单的方法是使用 Android 系统自带的 Dumpsys meminfo 工具

1
2
3
4
5
6
7
8

adb shell dumpsys meminfo
......
Total RAM: 7,658,060K (status moderate)
Free RAM: 550,200K ( 78,760K cached pss + 156,ba480K cached kernel + 314,960K free)
Used RAM: 7,718,091K (6,118,703K used pss + 1,599,388K kernel)
Lost RAM: -319,863K
ZRAM: 2,608K physical used for 301,256K in swap (4,247,544K total swap)
Tuning: 256 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)

如果系统处于低内存的话 , 会有如下特征:

  1. FreeRam 的值非常少 , Used RAM 的值非常大
  2. ZRAM 使用率非常高(如果开了 Zram 的话)

2、LMK && kswapd 线程活跃

低内存的时候, LKMD 会非常活跃, 在 Kernel Log 里面可以看到 LMK 杀进程的信息:

1
2
3
4
5
6
7
8
9

[kswapd0] lowmemorykiller: Killing 'u.mzsyncservice' (15609) (tgid 15609), adj 906,
to free 28864kB on behalf of 'kswapd0' (91) because
cache 258652kB is below limit 261272kB for oom score 906
Free memory is -5540kB above reserved.
Free CMA is 3172kB
Total reserve is 227288kB
Total free pages is 271748kB
Total file cache is 345384kB
GFP mask is 0x14000c0

上面这段 Log 的意思是说, 由于 mem 低于我们设定的 900 的水位线 (261272kB),所以把 pid 为 15609 的 mzsyncservice 这个进程杀掉(这个进程的 adj 是 906 )

3、proc/meminfo

这里是 Linux Kernel 展示 meminfo 的地方 , 关于 meminfo 的解读,可以参考这篇文章:/PROC/MEMINFO之谜

从结果来 , 当系统处于低内存的情况时候 , MemFree 和 MemAvailable 的值都很小

shell cat proc/meminfo

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

MemTotal: 5630104 kB
MemFree: 148928 kB
MemAvailable: 864172 kB
Buffers: 28464 kB
Cached: 1003144 kB
SwapCached: 19844 kB
Active: 1607512 kB
Inactive: 969208 kB
Active(anon): 1187828 kB
Inactive(anon): 426192 kB
Active(file): 419684 kB
Inactive(file): 543016 kB
Unevictable: 62152 kB
Mlocked: 62152 kB
SwapTotal: 2097148 kB
SwapFree: 42576 kB
Dirty: 3604 kB
Writeback: 0 kB
AnonPages: 1602928 kB
Mapped: 996768 kB
Shmem: 7284 kB
Slab: 306440 kB
SReclaimable: 72320 kB
SUnreclaim: 234120 kB
KernelStack: 89776 kB
PageTables: 107572 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 4912200 kB
Committed_AS: 118487976 kB
VmallocTotal: 263061440 kB
VmallocUsed: 0 kB
VmallocChunk: 0 kB
CmaTotal: 303104 kB
CmaFree: 3924 kB

4、整机卡顿 && 响应慢

低内存的时候,整机使用的时候要比非低内存的时候要卡很多,点击应用或者启动 App 都会有不顺畅或者响应慢的感觉

三、低内存对性能的具体影响

1、LMK 频繁工作抢占 cpu

LMK 工作时, 会占用 cpu 资源 , 其表现主要有下面几点

  1. CPU 资源 : 由于 LMK 杀掉的进程通常都是一些 Cache 或者 Service , 这些进程由于低内存被杀之后 , 通常会很快就被其主进程拉起来, 然后又被 LMK 杀掉, 从而进入了一种循环. 由于起进程是一件很消耗 cpu 的操作, 所以如果后台一直有进程被杀和重启, 那么前台的进程很容易出现卡顿
  2. Memory : 由于低内存的原因, 很容易触发各个进程的 GC , 如下图的 CPU 状态可以看到, 用于内存回收的 HeapTaskDeamon 出现非常频繁
  3. IO : 低内存会导致磁盘 IO 变多, 如果频繁进行磁盘 IO , 由于磁盘IO 很慢, 那么主线程会有很多进程处于等 IO 的状态, 也就是我们经常看到的 Uninterruptible Sleep

2、影响主线程 IO 操作

主线程出现大量的 IO 相关的问题 ,

  1. 反馈到 Trace 上就是有大量的黄色 Trace State 出现 , 例如 : Uninterruptible Sleep | WakeKill - Block I/O .
  2. 查看其 Block 信息 (kernel callsite when blocked:: “wait_on_page_bit_killable+0x78/0x88)

Linux 系统的 page cache 链表中有时会出现一些还没准备好的 page ( 即还没把磁盘中的内容完全地读出来 ) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.

当出现大量的 IO 操作的时候,应用主线程的 Uninterruptible Sleep 也会变多,此时涉及到 io 操作(比如 view ,读文件,读配置文件、读 odex 文件),都会触发 Uninterruptible Sleep , 导致整个操作的时间变长

3、出现 CPU 竞争

低内存会触发 Low Memory Killer 进程频繁进行扫描和杀进程,kswapd0 是一个内核工作线程,内存不足时会被唤醒,做内存回收的工作。 当内存频繁在低水位的时候,kswapd0 会被频繁唤醒,占用 cpu ,造成卡顿和耗电。

比如下面这个情况, kswapd0 占用了 855 的超大核 cpu7 ,而且是满频在跑,耗电可想而知,如果此时前台应用的主线程跑到了 cpu7 上,很大可能会出现 cpu 竞争,导致调度不到而丢帧。

HeapTaskDaemon 通常也会在低内存的时候跑的很高

, 来做内存相关的操作

4、进程频繁查杀和重启

对 AMS 的影响主要集中在进程的查杀上面 , 由于 LMK 的介入 , 处于 Cache 状态的进程很容易被杀掉 , 然后又被他们的父进程或者其他的应用所拉起来 , 导致陷入了一种死循环 . 对系统 CPU \ Memory \ IO 等资源的影响非常大.

比如下面就是一次 Monkey 之后的结果 , QQ 在短时间内频繁被杀和重启 .

14:32:16.932 1435 1510 I am_proc_start: [0,30387,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]

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

07-23 14:32:16.969 1435 3420 I am_proc_bound: [0,30387,com.tencent.mobileqq]
07-23 14:32:16.979 1435 3420 I am_kill : [0,30387,com.tencent.mobileqq,901,empty #3]
07-23 14:32:16.996 1435 3420 I am_proc_died: [0,30387,com.tencent.mobileqq,901,18]
07-23 14:32:17.028 1435 1510 I am_proc_start: [0,30400,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.054 1435 3420 I am_proc_bound: [0,30400,com.tencent.mobileqq]
07-23 14:32:17.064 1435 3420 I am_kill : [0,30400,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.082 1435 3420 I am_proc_died: [0,30400,com.tencent.mobileqq,901,18]
07-23 14:32:17.114 1435 1510 I am_proc_start: [0,30413,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.139 1435 3420 I am_proc_bound: [0,30413,com.tencent.mobileqq]
07-23 14:32:17.149 1435 3420 I am_kill : [0,30413,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.166 1435 3420 I am_proc_died: [0,30413,com.tencent.mobileqq,901,18]
07-23 14:32:17.202 1435 1510 I am_proc_start: [0,30427,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.216 1435 3420 I am_proc_bound: [0,30427,com.tencent.mobileqq]
07-23 14:32:17.226 1435 3420 I am_kill : [0,30427,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.249 1435 3420 I am_proc_died: [0,30427,com.tencent.mobileqq,901,18]
07-23 14:32:17.278 1435 1510 I am_proc_start: [0,30440,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.299 1435 3420 I am_proc_bound: [0,30440,com.tencent.mobileqq]
07-23 14:32:17.309 1435 3420 I am_kill : [0,30440,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.329 1435 2116 I am_proc_died: [0,30440,com.tencent.mobileqq,901,18]
07-23 14:32:17.362 1435 1510 I am_proc_start: [0,30453,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.387 1435 2116 I am_proc_bound: [0,30453,com.tencent.mobileqq]
07-23 14:32:17.398 1435 2116 I am_kill : [0,30453,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.420 1435 2116 I am_proc_died: [0,30453,com.tencent.mobileqq,901,18]
07-23 14:32:17.447 1435 1510 I am_proc_start: [0,30466,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.474 1435 2116 I am_proc_bound: [0,30466,com.tencent.mobileqq]
07-23 14:32:17.484 1435 2116 I am_kill : [0,30466,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.507 1435 2116 I am_proc_died: [0,30466,com.tencent.mobileqq,901,18]
07-23 14:32:17.533 1435 1510 I am_proc_start: [0,30479,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.556 1435 2116 I am_proc_bound: [0,30479,com.tencent.mobileqq]
07-23 14:32:17.566 1435 2116 I am_kill : [0,30479,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.587 1435 2116 I am_proc_died: [0,30479,com.tencent.mobileqq,901,18]
07-23 14:32:17.613 1435 1510 I am_proc_start: [0,30492,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.636 1435 2116 I am_proc_bound: [0,30492,com.tencent.mobileqq]
07-23 14:32:17.646 1435 2116 I am_kill : [0,30492,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.667 1435 2116 I am_proc_died: [0,30492,com.tencent.mobileqq,901,18]

其对应的 Systrace - SystemServer 中可以看到 AM 在频繁杀 QQ 和起 QQ

此 Trace 对应的 Kernel 部分也可以看到繁忙的 cpu

5、影响内存分配和触发 IO

手机经过长时间老化使用整机卡顿一下 , 或者整体比刚刚开机的时候操作要慢 , 可能是因为触发了内存回收或者 block io , 而这两者又经常有关联 . 内存回收可能触发了 fast path 回收 \ kswapd 回收 \ direct reclaim 回收 \ LMK杀进程回收等。(fast path 回收不进行回写)

回收的内容是匿名页 swapout 或者 file-backed 页写回和清空。(假设手机都是 swap file 都是内存,不是 disk), 涉及到 file 的,都可能操作 io,增加 block io 的概率。

还有更常见的是打开之前打开过的应用,没有第一次打开的快,需要加载或者卡一段时间 . 可能发生了 do_page_fault,这条路径经常见到 block io 在 wait_on_page_bit_killable(),如果是 swapout 内存,就要 swapin 了。如果是普通文件,就要 read out in pagecache/disk.

do_page_fault —> lock_page_or_retry -> wait_on_page_bit_killable 里面会判断 page 是否置位 PG_locked, 如果置位就一直阻塞, 直到 PG_locked 被清除 , 而 PG_locked 标志位是在回写开始时和 I/O 读完成时才会被清除,而 readahead 到 pagecache 功能也对 block io 产生影响,太大了增加阻塞概率。

四、实例

下面这个 Trace 是低内存情况下 , 抓取的一个 App 的冷启动 , 我们只取应用启动到第一帧显示的部分 ,总耗时为2s 。
可以看到其 Running 的总时间是 682 ms ,

1、低内存的启动情况

低内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 共花费了 2s . 从下面的 Thread 信息那里可以看到

  1. Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共花费 750 ms 左右(对比下面正常情况才 130 ms)
  2. Running 的时间在 600 ms (对比下面正常情况才 624 ms , 相差不大)

从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDaemon 跑的比较多之外 , 其他的内存和 io 相关的进程也非常多 , 比如若干个 kworker 和 kswapd0.

2、正常内存情况下

正常内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 只需要 1.22s . 从下面的 Thread 信息那里可以看到

  1. Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共才 130 ms.
  2. Running 的时间是 624 ms

从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程非常少.

五、Low memory处理建议

1. 优化系统进程内存占用

参考

Quick Start > Advanced System Debug & Tuning Method > Memory > memory usage分析

排查内存占比高进程并优化

2. 减少reserved memory

2-1 获取reserved memory 讯息:

<=Android P , 参考FAQ21499reserve_memory 用量

>=Android Q, 请提e-service 申请 “memory-layout-parser” 工具

也可从lk log 搜mblock_reserve-R (但可能有缺漏)

Line 1920: [1604] mblock_reserve-R[3].start: 0x46000000, sz: 0x400000 map:0 name:lk_addr_mb
Line 1921: [1605] mblock_reserve-R[4].start: 0x46900000, sz: 0x8000000 map:0 name:scratch_addr_mb
Line 1922: [1606] mblock_reserve-R[5].start: 0x44000000, sz: 0x80000 map:1 name:dtb_kernel_addr_mb
Line 1923: [1607] mblock_reserve-R[6].start: 0x40008000, sz: 0x3200000 map:0 name:kernel_addr_mb
Line 1924: [1608] mblock_reserve-R[7].start: 0x45000000, sz: 0x1000000 map:0 name:ramdisk_addr_mb
Line 1925: [1609] mblock_reserve-R[8].start: 0x77370000, sz: 0xc90000 map:0 name:framebuffer
Line 1926: [1610] mblock_reserve-R[9].start: 0x7fa00000, sz: 0x400000 map:0 name:logo_db_addr_pa
Line 1927: [1611] mblock_reserve-R[10].start: 0x77360000, sz: 0x10000 map:0 name:SPM-reserved
Line 1928: [1612] mblock_reserve-R[11].start: 0x77350000, sz: 0x10000 map:0 name:MCUPM-reserved
Line 1929: [1613] mblock_reserve-R[12].start: 0x72000000, sz: 0x4000000 map:0 name:ccci

或是lk 代码搜

mblock_reserve 或 mblock_reserve_ext

ex:

230  logo_db_addr_pa = (void *)(u32)mblock_reserve_ext(&g_boot_arg->mblock_info,

231  LK_LOGO_MAX_SIZE, PAGE_SIZE, 0x80000000, 0, "logo_db_addr_pa");

或.dts 搜 reserved-memory

ex:

318 		reserve-memory-scp_share {
319 			compatible = "mediatek,reserve-memory-scp_share";
320 			no-map;
321 			size = <0 0x01400000>; /*20 MB share mem size */
322 			alignment = <0 0x1000000>;
323 			alloc-ranges = <0 0x40000000 0 0x50000000>; /*0x4000_0000~0x8FFF_FFFF*/
324 		};
325 		consys-reserve-memory {
326 			compatible = "mediatek,consys-reserve-memory";
327 			no-map;
328 			size = <0 0x200000>;
329 			alignment = <0 0x200000>;
330 			alloc-ranges = <0 0x40000000 0 0x80000000>;

3. 限制后台

3-1修改DEFAULT_MAX_CACHED_PROCESSES

/frameworks/base/services/core/java/com/android/server/am/ActivityManagerConstants.java or ProcessList.java

public int MAX_CACHED_PROCESSES = DEFAULT_MAX_CACHED_PROCESSES;
private static final int DEFAULT_MAX_CACHED_PROCESSES = 32; // 改为DEFAULT_MAX_CACHED_PROCESSES = 8 or 16 or ...

3-2修改mCachedRestoreLevel

/frameworks/base/services/core/java/com/android/server/am/ProcessList.java中

long getCachedRestoreThresholdKb() {

return mCachedRestoreLevel; //将mCachedRestoreLevel 改为 mCachedRestoreLevel/2

}

4. 调整lmk参数

4-1. 调整minfree table

<=kernel-4.9 non-ago project or kernel-4.14 (ro.lmk.use_minfree_levels=1)

minfree table后三项阀值 ,分别增大1.x倍 1.x倍,1.x倍 (ex: 1.2 , 1.5 ,...倍)

minfree table 参考

Quick Start > Advanced System Debug & Tuning Method > Memory > lmkd & lmk

4-2. 调整lmkd 参数

Ago project , or kernel-4.14 (ro.lmk.use_minfree_levels=0)

ro.lmk.medium 调小(mediaum pressure kill adj 减小, 更多进程可杀)
ro.lmk.downgrade_pressure 调大(更容易进到mediaum pressure状态)
ro.lmk.upgrade_pressure 调大(更容易进到critical pressure状态)

5. swap szie & swappiness

5-1.调大swap size, 使系统逻辑内存延伸加大

/device/mediatek/mt6xxx/
/device/mediatek/vendor/common/
fstab.enableswap
fstab.enableswap_gmo
fstab.enableswap_ago

/dev/block/zram0 none swap defaults zramsize=xx% 把值或百分比调大

可从/proc/zraminfo确认是否生效

5-2.调大swappiness, 使系统充分利用swap 分区

/proc/sys/vm/swappiness
/dev/memcg/memory.swappiness
/dev/memcg/apps/memory.swappiness
/dev/memcg/system/memory.swappiness

6. Duraspeed enable (or 做好后台管理)

duraspeed 可主动管理后台进程与内存, 避免进入内存恶劣情况

7. 其他优化方案

  1. 提高 extra_free_kbytes 值
  2. 提高 disk I/O 读写速率,如用 UFS3.0,用固态硬盘
  3. 避免设置太大的 read_ahead_kb 值
  4. 使用 cgroup 的 blkio 来限制后台进程的 io 读操作,缩短前台 io 响应时间
  5. 提前做内存回收的操作,避免在用户使用应用时碰到而感受到稍微卡顿
  6. 增加 LMK 效率,避免无效的 kill
  7. kswapd 周期性回收更多的 high 水位
  8. 调整 swappiness 来平衡 pagecache 和 swap
  9. 策略 : 针对低内存机器做特殊的策略 , 比如杀进程更加激进 (这会带来用户体验的降低 , 所以这个度需要兼顾性能和用户体验)
  10. 策略 : 在内存不足的时候提醒用户(或者不提醒用户) , 杀掉不必要的后台进程 .
  11. 策略 : 在内存严重不足且无法恢复的情况下 , 可以提示用户重启手机.

八.Slab内存占用以致Kill应用程序问题分析

一般的,都是有应用程序向系统申请内存,但是系统发现剩余的内存大小无法满足当前的申请,进行一系列的操作之后还是无法满足,将会选择最合适的程序将其kill,这样系统将可以回收它的内存,从而满足系统中其他进程的内存需求。所以,程序被kill掉,并不一定说该程序有内存泄露,只是说当系统内存被kill时,它最适合被kill。

在程序被kill之前,可以查看进程占用的内存信息,看看进程是否存在内存泄露:

其中部分信息如下:
VmPeak:     3068 kB
VmSize:     3068 kB
VmLck:        0 kB
VmPin:        0 kB
VmHWM:      612 kB
VmRSS:      612 kB

我们主要查看VmRSS的大小是否逐渐在增大,如果该值逐渐增大,很大可能是程序存在内存泄露。但是在test测试中,程序的该值并没有很明显的变化,所以转向系统内存信息。

每隔一段时间,查看系统内存的信息,操作如下:

root@Linux: /# cat /proc/meminfo 
MemTotal:         493184 kB
MemFree:          442572 kB
MemAvailable:     452300 kB
Buffers:            3424 kB
Cached:             3224 kB
SwapCached:            0 kB
Active:             8940 kB
Inactive:            284 kB
Active(anon):       2588 kB
Inactive(anon):      120 kB
Active(file):       6352 kB
Inactive(file):      164 kB
Unevictable:           0 kB
Mlocked:               0 kB
HighTotal:             0 kB
HighFree:              0 kB
LowTotal:         493184 kB
LowFree:          442572 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:                20 kB
Writeback:             0 kB
AnonPages:          2616 kB
Mapped:             2204 kB
Shmem:               124 kB
Slab:              30528 kB
SReclaimable:      13904 kB
SUnreclaim:        16624 kB
KernelStack:         704 kB
PageTables:          296 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:      246592 kB
Committed_AS:      55424 kB
VmallocTotal:     507904 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
CmaTotal:          65536 kB
CmaFree:           59280 kB

通过cat /proc/meminfo查看系统的内存信息,其中,Slab是slab占用的内存大小,SReclaimable是可回收的,而SUnreclaim是不可回收的。发现Slab占用了系统快30M的内存,留意这个信息。接着,再查看一下,slab的详细使用情况:

root@Linux: /# cat /proc/slabinfo 
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
ext4_groupinfo_4k     58     81    296   27    2 : tunables    0    0    0 : slabdata      3      3      0
ext4_groupinfo_1k      1     28    288   28    2 : tunables    0    0    0 : slabdata      1      1      0
jbd2_1k                0      0   3072   10    8 : tunables    0    0    0 : slabdata      0      0      0
bridge_fdb_cache       0      0    320   25    2 : tunables    0    0    0 : slabdata      0      0      0
sd_ext_cdb             2     18    216   18    1 : tunables    0    0    0 : slabdata      1      1      0
sgpool-128             2     14   2304   14    8 : tunables    0    0    0 : slabdata      1      1      0
sgpool-64              2     25   1280   25    8 : tunables    0    0    0 : slabdata      1      1      0
sgpool-32              2     21    768   21    4 : tunables    0    0    0 : slabdata      1      1      0
sgpool-16              2     16    512   16    2 : tunables    0    0    0 : slabdata      1      1      0
sgpool-8               2     21    384   21    2 : tunables    0    0    0 : slabdata      1      1      0
cfq_io_cq             10     31    264   31    2 : tunables    0    0    0 : slabdata      1      1      0
cfq_queue              9     22    360   22    2 : tunables    0    0    0 : slabdata      1      1      0
fat_inode_cache        3     26    616   26    4 : tunables    0    0    0 : slabdata      1      1      0
fat_cache              0      0    200   20    1 : tunables    0    0    0 : slabdata      0      0      0
squashfs_inode_cache     88    200    640   25    4 : tunables    0    0    0 : slabdata      8      8      0
jbd2_transaction_s      0     42    384   21    2 : tunables    0    0    0 : slabdata      2      2      0
jbd2_inode             1     76    208   19    1 : tunables    0    0    0 : slabdata      4      4      0
                             ......
kmalloc-128         1589   1596    384   21    2 : tunables    0    0    0 : slabdata     76     76      0
kmalloc-64         15937  16200    320   25    2 : tunables    0    0    0 : slabdata    648    648      0
kmem_cache_node      107    125    320   25    2 : tunables    0    0    0 : slabdata      5      5      0
kmem_cache           107    126    384   21    2 : tunables    0    0    0 : slabdata      6      6      0

从这里可以了解到slab的使用情况,记录下来。

slab是Linux操作系统的一种内存分配机制。其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内碎片,而且处理速度也太慢。而slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片。slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化。

接着将可以隔较长一段时间,重复的进行cat /proc/meminfo和cat /proc/slabinfo操作,对比几次的信息,检查问题。

最后发现,经过较长一段时间的测试之后,Slab占用的内存数量大大增加,如果是slab占用较大的内存,则是内核频繁分配结构体导致,导致系统可用内存减小。直到出现Out of memory导致kill程序。

1.解决

了解到是Slab导致的占用内存过高的问题之后,可以手动的刷Slab,操作如下:

echo 3 > /proc/sys/vm/drop_caches /* 回刷缓冲 */

其中drop_caches的4个值有如下含义:

  • 0:不做任何处理,由系统自己管理
  • 1:清空pagecache
  • 2:清空dentries和inodes
  • 3:清空pagecache、dentries和inodes

但是这样的办法不是最佳的,最好还是应该通过slabinfo信息,了解到应用程序进行什么操作,导致内核频繁申请结构体导致Slab占用大量内存,看能否避免这样的问题,同时,内核有自动回收机制,可修改触发自动回收的阀值,当slab空闲内存达到一定量的时候,进行有效的回收。

2.后续

后来在参考文章看到信息,概括如下:
文中开头的说到的老化测试程序test,就是大量的保存文件,频繁的文件io操作(open、write、close),导致了dentry_cache占用了系统太多的内存资源。
inode对应于物理磁盘上的具体对象,而dentry是一个内存实体,其中的d_inode成员指向对应的inode,故可以把dentry看成是Linux文件系统中某个索引节点(inode)的链接,这个索引节点可以是文件,也可以是目录。而dentry_cache是目录项高速缓存,是Linux为了提高目录项对象的处理效率而设计的,它记录了目录项到inode的映射关系。

3.系统的自动slab缓存回收

在slab缓存中,对象分为SReclaimable(可回收)和SUnreclaim(不可回收),而在系统中绝大多数对象都是可回收的。内核有一个参数,当系统内存使用到一定量的时候,会自动触动回收操作。

  • 内核参数:
    vm.min_free_kbytes = 836787
    代表系统所保留空闲内存的最低限。
    在系统初始化时会根据内存大小计算一个默认值,计算规则是:
    min_free_kbytes = sqrt(lowmem_kbytes * 16) = 4 * sqrt(lowmem_kbytes)(注:lowmem_kbytes即可认为是系统内存大小)
    另外,计算出来的值有最小最大限制,最小为128K,最大为64M。
    可以看出,min_free_kbytes随着系统内存的增大不是线性增长,因为随着内存的增大,没有必要也线性的预留出过多的内存,能保证紧急时刻的使用量便足矣。
  • min_free_kbytes的主要用途是计算影响内存回收的三个参数 watermark[min/low/high]
  1. watermark[high] > watermark [low] > watermark[min],各个zone各一套
  2. 在系统空闲内存低于 watermark[low]时,开始启动内核线程kswapd进行内存回收(每个zone一个),直到该zone的空闲内存数量达到watermark[high]后停止回收。如果上层申请内存的速度太快,导致空闲内存降至watermark[min]后,内核就会进行direct reclaim(直接回收),即直接在应用程序的进程上下文中进行回收,再用回收上来的空闲页满足内存申请,因此实际会阻塞应用程序,带来一定的响应延迟,而且可能会触发系统OOM。这是因为watermark[min]以下的内存属于系统的自留内存,用以满足特殊使用,所以不会给用户态的普通申请来用。
  3. 三个watermark的计算方法:
    watermark[min] = min_free_kbytes换算为page单位即可,假设为min_free_pages。(因为是每个zone各有一套watermark参数,实际计算效果是根据各个zone大小所占内存总大小的比例,而算出来的per zone min_free_pages)
    watermark[low] = watermark[min] * 5 / 4
    watermark[high] = watermark[min] * 3 / 2
    所以中间的buffer量为 high - low = low - min = per_zone_min_free_pages * 1/4。因为min_free_kbytes = 4* sqrt(lowmem_kbytes),也可以看出中间的buffer量也是跟内存的增长速度成开方关系。
  4. 可以通过/proc/zoneinfo查看每个zone的watermark
  • min_free_kbytes大小的影响
    min_free_kbytes设的越大,watermark的线越高,同时三个线之间的buffer量也相应会增加。这意味着会较早的启动kswapd进行回收,且会回收上来较多的内存(直至watermark[high]才会停止),这会使得系统预留过多的空闲内存,从而在一定程度上降低了应用程序可使用的内存量。极端情况下设置min_free_kbytes接近内存大小时,留给应用程序的内存就会太少而可能会频繁地导致OOM的发生。
    min_free_kbytes设的过小,则会导致系统预留内存过小。kswapd回收的过程中也会有少量的内存分配行为(会设上PF_MEMALLOC)标志,这个标志会允许kswapd使用预留内存;另外一种情况是被OOM选中杀死的进程在退出过程中,如果需要申请内存也可以使用预留部分。这两种情况下让他们使用预留内存可以避免系统进入deadlock状态。
    可测试,当调整完min_free_kbytes值大于系统空闲内存后,kswapd进程的确从休眠状态进入运行态,开始回收内存。

同时,还有一个参数vm.vfs_cache_pressure = 200
该文件表示内核回收用于directory和inode cache内存的倾向;缺省值100表示内核将根据pagecache和swapcache,把directory和inode cache保持在一个合理的百分比;降低该值低于100,将导致内核倾向于保留directory和inode cache;增加该值超过100,将导致内核倾向于回收directory和inode cache。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/377518.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android问题笔记 - 打开Android Studio先弹出项目选择框

专栏分享点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例 &#x1f449;关于作者 众所周知&#xff0c;人生是一个漫长的流程&#xff0c;不断克服困难&#xff0c;不断…

leetcode 427. Construct Quad Tree(构建四叉树)

刚看到题的时候是懵的&#xff0c;这也太长了。到底是要表达什么呢。 不妨把这个矩阵看成一个正方形的图片&#xff0c;想象你在处理图片&#xff0c;从整体逐步到局部。 刚开始看一整张图片&#xff0c;如果是全0或全1&#xff0c;这个就是叶子节点&#xff0c;怎么表达叶子节…

网络货运平台“降本提质引流增值”秘籍是什么?

2月24日&#xff0c;2022&#xff08;第五届&#xff09;中国网络货运平台年会在厦门举行&#xff0c;数据宝作为中物联副会长单位受邀参加峰会&#xff0c;数据宝轮值CEO肖斌发表题为“网络货运平台数字化创新应用实践分享”的主题分享。 据交通运输部统计&#xff0c;截止到2…

某建筑设计研究院“综合布线管理软件”应用实践

某建筑设计研究院有限公司&#xff08;简称“某院”&#xff09;隶属于国务院国资委直属的大型骨干科技型中央企业。“某院”前身为中央直属设计公司&#xff0c;创建于1952年。成立近70年来&#xff0c;始终秉承优良传统&#xff0c;致力于推进国内勘察设计产业的创新发展&…

CASENet中edge GT是如何产生的

1&#xff1a;首先下载cityscape数据集&#xff0c;包含两个大文件夹&#xff0c;具体的数据集介绍参考此链接。cityscape数据集解析 看一下gtFine子文件夹&#xff0c;另一个也是同理&#xff1a; 2&#xff1a;将下载好的数据集放到data_orig中&#xff0c;还有一个文件…

报错记录:element-admin框架打包后静态文件加载异常与登录异常解决方案

报错记录&#xff1a;element-admin框架打包后静态文件加载异常与登录异常解决方案 一、静态文件加载异常解决方案 二、登录异常解决方案 现象 element-admin如果用 electron 打包 是会有个问题&#xff1a; electron serve 运行没问题 electron build 打release 包 就会出错…

AST之path常用属性和方法总结笔记

文章目录1. path常用属性总结1.1 path.node1.2 path.scope1.3 path.parentPath1.4 path.parent1.5 path.container1.6 path.type1.7 path.key2. path常用方法总结2.1 path.toString2.2 path.replaceWith2.3 path.replaceWithMultiple2.4 path.remove2.5 path.insertBefore2.6 p…

TypeScript 常用知识

「 推荐一个学习 ts 基础的专栏&#xff0c;满满的干货&#xff1a;typeScript 」 1、为什么推荐使用 TypeScript 【】ts 是 js 的超集&#xff0c;包含 js 的所有元素 【】ts 通过对代码进行类型检查&#xff0c;可以帮助我们避免在编写 js 时经常遇到令人痛苦的错误 【】强…

第六节 方法

方法 方法是一种语法结构。 方法的作用&#xff1a; 1.提高代码的复用性 2.让程序逻辑更加清晰 方法定义的完整格式&#xff1a; 修饰符 返回值类型 方法名&#xff08;形参列表&#xff09;{ 方法体的代码&#xff08;需要执行的功能代码&#xff09; return 返回值&#xff…

Unity性能优化: 性能优化之内存篇

前言 本文和传统的内存优化不一样&#xff0c;不是讲如何降低内存占用&#xff0c;而是讲编程开发中要注意的内存问题以及一些内存技术的演变与原理。 对惹&#xff0c;这里有一个游戏开发交流小组&#xff0c;希望大家可以点击进来一起交流一下开发经验呀 1: Application进程…

maven项目无法解析插件

发现问题使用IDEA创建Maven项目时&#xff0c;报错无法解析插件 org.apache.maven.plugins:maven-clean-plugin这里使用的是IDEA捆绑的Maven插件解决方案查看Maven配置打开用户设置文件settings.xml&#xff0c;在其中加入如果该路径下没有此文件&#xff0c;可以自己创建一个。…

软件分析笔记02---Intermediate Representation

整体contents compiler &#xff08;source code ——> machine code&#xff09; non-trivial非平凡的 经过 语义分析->语法分析->类型检查等各种trivial的分析&#xff08;前端&#xff09;&#xff0c;生成中间代码IR->进行non-trivial的分析&#xff08;及静…

Linux 基础介绍-基础命令

文章目录01 学习目标02 Linux/Unix 操作系统简介2.1 Linux 操作系统的目标2.2 Linux 操作系统的作用2.3 Unix 家族历史2.4 Linux 家族历史2.5 Linux 和Unix 的联系2.6 Linux 内核介绍2.7 Linux 发行版本2.8 Unix/Linux 开发应用领域介绍03 Linux 目录结构3.1 Win 和Linux 文件系…

C++之入门之引用,内联函数

一、引用 1、引用的概念 在C中&#xff0c;引用的本质其实就是给一个已经存在的变量”起别名“。也就是说&#xff0c;引用与它所引用的对象共用一块空间。&#xff08;同一块空间的多个名字&#xff09; 就比如说&#xff0c;李逵又叫黑旋风&#xff0c;而黑旋风就是指李逵…

线程安全实例分析

一、变量的线程安全分析 成员变量和静态变量是否线程安全&#xff1f; ● 如果它们没有共享&#xff0c;则线程安全 ● 如果它们被共享了&#xff0c;根据它们的状态是否能够改变&#xff0c;又分两种情况 —— 如果只有读操作&#xff0c;则线程安全 —— 如果有读写操作&am…

STM32学习笔记-USART串口通信+与野火STM32F407板载ESP8266进行通信

文章目录STM32USART介绍STM32USART框图第一部分第二部分第三部分发送器时序图接收器第四部分软件部分&#xff1a;STM32通过USART与板载ESP8266通讯实验板载WIFI模块电路图实现方式&#xff1a;第一步&#xff1a;配置USART1和USART3的GPIO及其中断第二步&#xff1a;通过中断服…

(2023版)零基础入门网络安全/Web安全,收藏这一篇就够了

由于我之前写了不少网络安全技术相关的文章和回答&#xff0c;不少读者朋友知道我是从事网络安全相关的工作&#xff0c;于是经常有人私信问我&#xff1a; 我刚入门网络安全&#xff0c;该怎么学&#xff1f; 要学哪些东西&#xff1f; 有哪些方向&#xff1f; 怎么选&#x…

智慧渔业海洋鱼类捕捉系统

我国的水产捕捞业可分为海洋捕捞、远洋捕捞和淡水捕捞三类&#xff0c;其中淡水渔业是指在淡水水域进行捕捞、养殖和加工淡水水产品的社会生产领域。近年来&#xff0c;随着经济水平的提高和淡水渔业的快速发展&#xff0c;捕捞业规模不断壮大。尽管渔业已从单纯的捕捞发展为系…

卷麻了,00后测试用例写的比我还好,简直无地自容......

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 如何编写测试用例&#xff1f; 作为一个测试新人&#xff0c;刚开始接触测试&#xff0c;对于怎么写测试用例很是头疼&#xff0c;无法…

Apple主推的智能家居是什么、怎么用?一篇文章带你从零完全入门 HomeKit

如果你对智能家居有所了解&#xff0c;那应该或多或少听人聊起过 HomeKit。由 Apple 开发并主推的的 HomeKit 既因为产品选择少、价格高而难以成为主流&#xff0c;又因其独特的优秀体验和「出身名门」而成为智能家居领域的焦点。HomeKit 究竟是什么&#xff1f;能做什么&#…