Android-PSI 详解:libpsi 源码解析——116 行代码架起 lmkd 与内核之间的桥

Android PSI 详解:libpsi 源码解析------116 行代码架起 lmkd 与内核之间的桥

系列上一篇《Android 高版本 LMKD 源码解析》里,init_mp_psi() 里两句 init_psi_monitor() / register_psi_monitor() 一笔带过。本篇把它们挖到底:libpsi 是什么、每行代码在做什么、内核 PSI 接口的协议细节,以及那些不读源码绝对不会注意到的"坑"(比如 EPOLLPRI 而不是 EPOLLIN)。

本文基于 system/memory/libpsi/psi.c(本地 psi.cpp,共 116 行),行号均已标注。

0. libpsi 在整个体系中的位置

bash 复制代码
┌───────────────────── lmkd 进程 ─────────────────────┐
│  lmkd.cpp (3847 行)                                 │
│     │  调用                                          │
│     ▼                                               │
│  libpsi (psi.cpp, 116 行)  ←── 本篇主角              │
│     │  open/write /proc/pressure/memory             │
│     │  epoll_ctl(EPOLLPRI)                          │
└─────┼───────────────────────────────────────────────┘
      ▼
┌───────────────────── 内核 ──────────────────────────┐
│  PSI 子系统 (kernel/sched/psi.c, CONFIG_PSI=y)      │
│   /proc/pressure/{memory,io,cpu}                    │
│   - 读:输出 avg10/avg60/avg300/total 统计          │
│   - 写:注册 trigger,返回可 poll 的事件 fd          │
└─────────────────────────────────────────────────────┘

libpsi 的定位:对内核 PSI 接口的极薄封装------5 个函数、116 行,不含任何策略。它把"内核协议细节"(写入格式、事件语义、输出解析)集中在一处,让 lmkd 专注于查杀策略。

1. 公共头文件:psi.h 的数据结构

psi.cpp 用到的 PSI_SOME/PSI_FULLPSI_PATH_MEMORY 来自 <psi/psi.h>(libpsi 的公共头,路径宏经由内核 uapi 头 <linux/psi.h> 提供):

arduino 复制代码
enum psi_stall_type {
    PSI_SOME,   /* 部分停顿:至少一个任务在等内存 */
    PSI_FULL,   /* 完全停顿:所有非 idle 任务都在等内存 */
};
​
struct psi_stats {
    float avg10;        /* 过去 10s 停顿时间占比(百分比) */
    float avg60;        /* 过去 60s */
    float avg300;       /* 过去 300s */
    unsigned long total;/* 系统启动以来累计停顿微秒数 */
};
​
int  init_psi_monitor(enum psi_stall_type stall_type, int threshold_us, int window_us);
int  register_psi_monitor(int epollfd, int fd, void* data);
int  unregister_psi_monitor(int epollfd, int fd);
void destroy_psi_monitor(int fd);
int  parse_psi_line(char *line, enum psi_stall_type stall_type, struct psi_stats stats[]);

enum psi_stall_type 的顺序(SOME=0, FULL=1)不是随意的------psi.cpp 里用 stall_type_name[stall_type] 直接做数组下标,lmkd 里也用 mem_stats[PSI_FULL] 下标访问。

2. init_psi_monitor():向内核注册一个触发器

libpsi 的第一个、也是最重要的函数:

c 复制代码
/* psi.cpp:31-34 */
static const char* stall_type_name[] = {
        "some",
        "full",
};
​
/* psi.cpp:36-79 */
int init_psi_monitor(enum psi_stall_type stall_type,
             int threshold_us, int window_us) {
    int fd;
    int res;
    char buf[256];
​
    fd = TEMP_FAILURE_RETRY(open(PSI_PATH_MEMORY, O_WRONLY | O_CLOEXEC));
    if (fd < 0) {
        ALOGE("No kernel psi monitor support (errno=%d)", errno);
        return -1;
    }
​
    switch (stall_type) {
    case (PSI_SOME):
    case (PSI_FULL):
        res = snprintf(buf, sizeof(buf), "%s %d %d",
            stall_type_name[stall_type], threshold_us, window_us);
        break;
    default:
        ALOGE("Invalid psi stall type: %d", stall_type);
        errno = EINVAL;
        goto err;
    }
​
    if (res >= (ssize_t)sizeof(buf)) {
        ALOGE("%s line overflow for psi stall type '%s'",
            PSI_PATH_MEMORY, stall_type_name[stall_type]);
        errno = EINVAL;
        goto err;
    }
​
    res = TEMP_FAILURE_RETRY(write(fd, buf, strlen(buf) + 1));
    if (res < 0) {
        ALOGE("%s write failed for psi stall type '%s'; errno=%d",
            PSI_PATH_MEMORY, stall_type_name[stall_type], errno);
        goto err;
    }
​
    return fd;
​
err:
    close(fd);
    return -1;
}

逐个拆解:

2.1 open(O_WRONLY | O_CLOEXEC)

三个细节:

  1. O_WRONLY :注册 trigger 是一次"写"操作,不需要读权限。lmkd 读统计数据用的是另一个 自己 reread_file() 打开的只读 fd(见第 5 节),同一个文件两条通道;
  2. O_CLOEXEC:lmkd 将来 fork/exec 其他东西时不会泄露这个 fd;
  3. open 失败的兜底 :如果内核没编译 PSI(CONFIG_PSI=n)或 cmdline 用 psi=0 关闭了,这里 open 会失败,打出日志 "No kernel psi monitor support",返回 -1------这正是上篇博客里 lmkd 三级 fallback(PSI → vmpressure → 启动失败)的第一级出口:init_psi_monitors() 返回 false,init_monitors()(lmkd.cpp:3354)转入 init_mp_common() 老 vmpressure 路径。

2.2 写入格式:"some 70000 1000000"

lmkd 调用它时(lmkd.cpp:3161-3163):

css 复制代码
fd = init_psi_monitor(psi_thresholds[level].stall_type,
    psi_thresholds[level].threshold_ms * US_PER_MS,     /* 70ms  → 70000us  */
    PSI_WINDOW_SIZE_MS * US_PER_MS);                    /* 1000ms→ 1000000us */

拼出来的字符串就是:

sql 复制代码
some 70000 1000000      ← medium 级:1 秒窗口内 some 停顿 ≥ 70ms 触发
full 700000 1000000     ← critical 级:1 秒窗口内 full 停顿 ≥ 700ms 触发

含义:"请内核帮我盯着:在 1 秒的滑窗内,一旦 some(或 full)停顿累计达到阈值,就叫醒我"。

内核侧对参数有硬约束(kernel/sched/psi.c):

约束
窗口下限 500ms(PSI_WINDOW_MIN_SIZE_US
窗口上限 10s(PSI_WINDOW_MAX_SIZE_US
阈值 必须大于 0 且不超过窗口
每个打开的 fd 只能注册一个 trigger

满足 500ms ≤ window ≤ 10s,lmkd 选的 1s 窗口正中间。另外注意 write(fd, buf, strlen(buf) + 1) 连结尾 \0 一起写------内核解析时按 NUL 结尾字符串处理,多写一个字节无伤大雅。

2.3 返回值的巧妙之处

成功时直接把 open 出来的 fd 返回 ------内核在 write() 成功的那一刻,已经把这个打开的文件与新建的 trigger 绑定,fd 瞬间变成了"事件 fd"。libpsi 不需要额外的 ioctl 或二次 open。这也是为什么函数名叫 init_*_monitor 而返回的是个 fd。

错误路径统一 goto err; close(fd); return -1;,并且每个分支都先设好 errno(EINVAL 或 write 的 errno),调用方可以直接 strerror(errno)

3. register_psi_monitor():EPOLLPRI 的坑

ini 复制代码
/* psi.cpp:81-92 */
int register_psi_monitor(int epollfd, int fd, void* data) {
    int res;
    struct epoll_event epev;
​
    epev.events = EPOLLPRI;
    epev.data.ptr = data;
    res = epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &epev);
    if (res < 0) {
        ALOGE("epoll_ctl for psi monitor failed; errno=%d", errno);
    }
    return res;
}

短短 12 行,藏着两个值得写进博客的点:

3.1 为什么是 EPOLLPRI 而不是 EPOLLIN?

PSI trigger fd 上没有任何可读数据 。内核通过 poll 队列的"优先级事件"(POLLPRI,select 语义里的 exceptional condition)通知用户态------和 inotify 的事件 fd 同一套语义。如果你按肌肉记忆写 EPOLLIN,epoll 会永远不唤醒,这是接入 PSI 时最经典的翻车现场。

3.2 事件是"一次性派发"的

内核 psi 的 poll 回调里,事件标志是用一次消费一次 的(内核在 .poll 里对 trigger 的 event 标志做 cmpxchg 清零,返回 EPOLLPRI)。效果是:每次 trigger 命中,恰好产生一次 epoll 唤醒。所以:

  • lmkd 从不 read 这个 fd,也无需 read;
  • 事件处理函数靠参数 events 区分"真事件"和"轮询"------上篇博客里 mp_event_psi() 的关键判断:
arduino 复制代码
/* lmkd.cpp:2770 */
} else if (level == VMPRESS_LEVEL_CRITICAL && events != 0) {
    /* 只有真·critical 事件才按 NOT_RESPONDING 杀,轮询进来的不算 */

call_handler 轮询路径调用 handler 时传的 events=0,lmkd.cpp:3639。)

3.3 epev.data.ptr = data

把 lmkd 的 struct event_handler_info*(内含函数指针 mp_event_psi 和级别 data)原样挂到 epoll 事件上,mainloop() 第二遍循环里 evt->data.ptr 取回直接调用(lmkd.cpp:3695-3698)。libpsi 对 void* data 语义不做任何假设,纯粹透传------库和调用方解耦的干净写法。

4. 反注册与销毁

arduino 复制代码
/* psi.cpp:94-102 */
int unregister_psi_monitor(int epollfd, int fd) {
    return epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
}
​
void destroy_psi_monitor(int fd) {
    if (fd >= 0) {
        close(fd);
    }
}

close(fd) 的同时,内核在文件 release 回调里销毁绑定的 trigger,不需要额外的"注销"写入。

lmkd 在两条路径上用到它们:

属性热更新LMK_UPDATE_PROPS 命令,lmkd.cpp:1524-1551)------阈值可能变了,必须拆掉旧 monitor 换新的:

scss 复制代码
/* lmkd.cpp:1528-1534 */
if (update_props()) {
    if (!use_inkernel_interface) {
        /* Reinitialize monitors to apply new settings */
        destroy_monitors();          /* → unregister_psi_monitor + destroy_psi_monitor */
        if (init_monitors()) { ... } /* → init_psi_monitor + register_psi_monitor */
    }
    ...
}

init_psi_monitors() 的逐级回滚 (lmkd.cpp:3243-3254):MEDIUM 注册成功、CRITICAL 失败时,把已注册的 LOW/MEDIUM 逐一 destroy_mp_psi(),不留半成品。

5. parse_psi_line():解析统计输出

arduino 复制代码
/* psi.cpp:104-116 */
int parse_psi_line(char *line, enum psi_stall_type stall_type, struct psi_stats stats[]) {
    char type_name[5];
    struct psi_stats *stat = &stats[stall_type];
​
    if (!line || sscanf(line, "%4s avg10=%f avg60=%f avg300=%f total=%lu",
        type_name, &stat->avg10, &stat->avg60, &stat->avg300, &stat->total) != 5) {
        return -1;
    }
    if (strcmp(type_name, stall_type_name[stall_type])) {
        return -1;
    }
    return 0;
}

它解析的对象长这样(cat /proc/pressure/memory):

ini 复制代码
some avg10=0.25 avg60=0.10 avg300=0.02 total=123456789
full avg10=0.00 avg60=0.00 avg300=0.00 total=9876543

四层防御:

  1. %4s :type_name 缓冲区只有 5 字节,%4s 最多读 4 个字符 + \0("some"/"full" 恰好 4 个字母),即使遇到畸形输入也不会溢出;
  2. != 5 :sscanf 必须恰好解析出 5 个字段,缺一不可(%f 收到非数字会少匹配);
  3. strcmp(type_name, ...) :确认这行真的是请求的类型------防止把 "full" 行的数字填进 stats[PSI_SOME]
  4. NULL 检查!line,因为 lmkd 传进来的是 strtok_r 的返回值,文件读空时可能是 NULL。

5.1 lmkd 侧的三个封装:读统计用的是另一个 fd

arduino 复制代码
/* lmkd.cpp:1946-1991 */
static int psi_parse(struct reread_data *file_data, struct psi_stats stats[], bool full) {
    char *buf;
    char *save_ptr;
    char *line;
​
    if ((buf = reread_file(file_data)) == NULL) {
        return -1;
    }
​
    line = strtok_r(buf, "\n", &save_ptr);
    if (parse_psi_line(line, PSI_SOME, stats)) {
        return -1;
    }
    if (full) {
        line = strtok_r(NULL, "\n", &save_ptr);
        if (parse_psi_line(line, PSI_FULL, stats)) {
            return -1;
        }
    }
​
    return 0;
}
​
static int psi_parse_mem(struct psi_data *psi_data) {
    static struct reread_data file_data = {
        .filename = PSI_PATH_MEMORY,
        .fd = -1,
    };
    return psi_parse(&file_data, psi_data->mem_stats, true);
}
​
static int psi_parse_io(struct psi_data *psi_data) { ... /* PSI_PATH_IO, true */ }
​
static int psi_parse_cpu(struct psi_data *psi_data) { ... /* PSI_PATH_CPU, false */ }

两个耐人寻味的设计:

双 fd 架构 。此刻 lmkd 进程里对 /proc/pressure/memory 有两个打开的文件:

css 复制代码
fd A(O_WRONLY,libpsi 注册的 trigger fd)→ epoll 监听 EPOLLPRI → 事件驱动
fd B(O_RDONLY,lmkd 的 reread_file 缓存)  → 每次事件后 pread  → 读 avg10/total

trigger fd 负责"何时看",stats fd 负责"看什么"。fd B 走 reread_file()(lmkd.cpp:623)------首次 open 后永久缓存 fd 和缓冲区,因为内存高压时连 open() 都可能卡住。

full=false 的 CPUpsi_parse_cpu 只解析第一行(some)------CPU 压力文件历史格式上就没有 full 统计行(CPU 上永远有任务在跑,不存在"全员停顿"),libpsi 用一个 bool 参数天然兼容了三种资源的格式差异。

5.2 统计数据在 lmkd 里的实际用途

scss 复制代码
/* lmkd.cpp:2755-2757 */
if (!psi_parse_mem(&psi_data)) {
    critical_stall = psi_data.mem_stats[PSI_FULL].avg10 > (float)stall_limit_critical;
}

决策树执行前读一次 /proc/pressure/memory,若 full avg10 (过去 10 秒完全停顿占比)超过 ro.lmk.stall_limit_critical(默认 100%),则 critical_stall=truemin_score_adj 直接降为 0,允许杀前台可感知应用 (lmkd.cpp:2857-2859)。此外 psi_parse_io/psi_parse_cpu 的结果会随 killinfo_log() 写进 event log(tag 10195355),供事后分析停顿时 IO/CPU 是否同病相怜。

6. 内核侧:一页纸讲清 PSI 原理

配合 libpsi 读,内核(kernel/sched/psi.c)的行为可以浓缩成四条:

  1. 记账 :调度器和内存回收路径上,任务因缺内存而停顿时,内核按 CPU 维度累计三种状态的时间:SOME(至少一个任务停)、FULL(所有非 idle 任务停)、NONIDLE。聚合出 avg10/60/300(各时间窗停顿百分比)和 total(累计微秒)------就是 /proc/pressure/* 的输出,也是 parse_psi_line 解析的内容;
  2. 触发判定 :对每个注册的 trigger,在窗口内累计对应类型的停顿时间,达到阈值即触发
  3. 限速两次事件之间至少间隔一个窗口。这是内核文档明确的语义,也是 lmkd 那套"事件后轮询 1 秒"设计的根本原因------事件每秒最多来一次,窗口内的后续恶化只能靠 lmkd 自己以 10/100ms 轮询补盲(上篇博客 6.8 节);
  4. 通知 :触发时唤醒该 fd 的 poll 等待队列,poll/epoll 返回 POLLPRI;事件标志被消费掉,每个触发点恰好一次唤醒。

一句话:PSI 把"内存压力"从"还剩多少"(meminfo)变成"卡了多久"(stall time),libpsi 把这套语义翻译成三个 fd 操作:write 注册、poll 等待、read 统计。

7. libpsi × lmkd 完整协作时序

把两篇博客串起来(行号均为 lmkd.cpp):

scss 复制代码
[启动] init_mp_psi() (3153)
  │  init_psi_monitor(PSI_SOME, 70000, 1000000)     ← psi.cpp:36  open+write
  │  init_psi_monitor(PSI_FULL, 700000, 1000000)    ← psi.cpp:36  open+write
  │  register_psi_monitor(epollfd, fd, &hinfo)      ← psi.cpp:81  epoll_ctl(EPOLLPRI)
  ▼
[待机] mainloop(): epoll_wait(-1)                   ← 零开销
  ▼
[事件] 内核窗口内停顿 ≥ 阈值 → POLLPRI 唤醒
  │  mp_event_psi(data=level, events=EPOLLPRI)      ← 2583
  │    ├─ reread_file + parse_psi_line (fd B)       ← 1946 读 mem/vmstat
  │    ├─ psi_parse_mem → mem_stats[PSI_FULL].avg10 ← 1969/2755 判 critical_stall
  │    ├─ 决策树 → find_and_kill_process()          ← 2421
  │    └─ 10/100ms 轮询持续 1s(PSI_WINDOW_SIZE_MS) ← 135/2890
  ▼
[销毁] LMK_UPDATE_PROPS 热更新或退出
     destroy_mp_psi() (3181)
       ├─ unregister_psi_monitor(epollfd, fd)       ← psi.cpp:94
       └─ destroy_psi_monitor(fd)                   ← psi.cpp:98 close → 内核销毁 trigger

8. 动手实验

8.1 观察统计

ini 复制代码
$ adb shell cat /proc/pressure/memory
some avg10=0.15 avg60=0.31 avg300=0.12 total=1819288761
full avg10=0.00 avg60=0.01 avg300=0.00 total=57236773

开几个大应用再 cat,看 some/total 飙升;在内存压力下 full 非 0 就说明出现了"全员停顿"。

8.2 最小 trigger demo(需 root)

把下面的小程序推到设备上跑,等 150ms/1s 的部分停顿出现:

c 复制代码
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
​
int main(void) {
    int fd = open("/proc/pressure/memory", O_WRONLY | O_CLOEXEC);
    if (fd < 0) { perror("open"); return 1; }
​
    char buf[64];
    int n = snprintf(buf, sizeof(buf), "some 150000 1000000");
    if (write(fd, buf, n + 1) < 0) { perror("write"); return 1; }
​
    struct pollfd pfd = { .fd = fd, .events = POLLPRI };
    while (poll(&pfd, 1, -1) > 0) {
        printf("memory pressure event! (revents=%d)\n", pfd.revents);
        sleep(1);  /* 不 sleep 的话,可以观察到事件最多 1s 一次(限速) */
    }
    return 0;
}

.events 改成 POLLIN 再跑一遍------永远不会醒,亲测 3.1 节的坑。

8.3 调节 lmkd 的 PSI 阈值

ruby 复制代码
$ adb root
$ adb shell setprop persist.device_config.lmkd_native.psi_partial_stall_ms 50
$ adb shell setprop persist.device_config.lmkd_native.psi_complete_stall_ms 500
$ adb shell stop lmkd && adb shell start lmkd   # 或发 LMK_UPDATE_PROPS

属性生效路径就是第 4 节的 LMK_UPDATE_PROPSdestroy_monitors()init_monitors(),libpsi 的 register/destroy 就是为这种热更新准备的。

9. 总结

libpsi 函数 一句话职责 踩坑点
init_psi_monitor open+write 注册内核 trigger,fd 即事件 fd 窗口 500ms~10s;一个 fd 一个 trigger;参数是微秒
register_psi_monitor 挂进 epoll EPOLLPRI,写 EPOLLIN 永远不醒
unregister/destroy_psi_monitor 摘除并 close close 即注销 trigger
parse_psi_line 解析 some/full avg10=... total=... %4s 防溢出;CPU 无 full 行

libpsi 的价值恰恰在于它把所有这些协议细节封进了 116 行代码,让 lmkd.cpp 可以用纯粹的策略语言写代码:init_psi_monitor(PSI_SOME, 70ms, 1s) 读起来就像一句需求。薄而正确的基础库,是上层系统软件能写简单的前提。

系列下一篇可以顺着往两头走:往下是内核 kernel/sched/psi.c 的记账与触发实现;往上是 AMS 侧 ProcessList.javaupdateOomAdjLocked()------看 oomadj 是怎么被算出来、又怎么通过 LMK_PROCPRIO 灌进 lmkd 的进程表。

相关推荐
用户36533043470941 小时前
深入理解 Java Unsafe:从底层魔法到被淘汰的命运
java
宸津-代码粉碎机2 小时前
FastUtil+AI多Agent实战:Java AI项目性能终极加速方案
java·服务器·开发语言·python·安全·php
choumou_M2 小时前
SpringBoot_5:商品秒杀场景的并发实践(初步)
java·spring boot·后端
suaizai_2 小时前
AI Agent如何懂你:四层能力拆解
java·前端·人工智能
javachen__2 小时前
Docker一键清理 + 全局限制,告别日志报满
java·docker·容器
早安试言2 小时前
maven安装
java·maven
攻城有术3 小时前
专项攻克-SpringBoot启动后 Bean的实例数量问题
java·spring boot·后端
步行cgn3 小时前
PageHelper 的用法
java·后端
Sam_Deep_Thinking4 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员