Android lmkd 机制从R到T

Posted 私房菜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android lmkd 机制从R到T相关的知识,希望对你有一定的参考价值。

源码基于:Android T

相关博文:

Android lmkd 机制详解(一)

Android lmkd 机制详解(二)

0. 前言

之前有粉丝在问笔者,如上面详解的两篇博文都是基于 android 11,现在都使用 Android 13了,是否有很大的区别呢?笔者特地去看了下 Android T,本文简单地总结下 R 与 T 的区别。

1. watchdog

在 Android T 中引入了 watchdog 机制,这个机制的引入是为了防止 lmkd 会在syscall 的时候长时间的卡住。当lmkd 在处理 events 的时候会通过watchdog 线程进行延迟的监听,在 events 处理完成后重置 watchdog。默认的延迟时长为 2s,如果在处理某个event 的时候超过了 timeout,就会唤醒 watchdog 并且会 kill 最近的重要的进程来防止内存压力。

下面结合代码对 watchdog 进行简单的分析,详细的代码逻辑请自行查看 Android T 源码。

step1,初始化watchdog

在 lmkd 的main 函数中首先会对 watchdog 进行初始化:

        if (!watchdog.init()) 
            ALOGE("Failed to initialize the watchdog");
        

来看下 watchdog 的初始化:

bool Watchdog::init() 
    pthread_t thread;

    if (pthread_create(&thread, NULL, watchdog_main, this)) 
        ALOGE("pthread_create failed: %s", strerror(errno));
        return false;
    
    if (pthread_setname_np(thread, "lmkd_watchdog")) 
        ALOGW("pthread_setname_np failed: %s", strerror(errno));
    

    return true;

正如上面所述,会在 lmkd 中新起一个线程,命名为 lmkd_watchdog。

step2,创建一个定时器

在 step1 中,初始化 watchdog 的时候会通过 watchdog_main() 创建一个定时器:

static void* watchdog_main(void* param) 
    Watchdog *watchdog = static_cast<Watchdog*>(param);
    sigset_t sigset;
    int signum;

    // Ensure the thread does not use little cores
    if (!SetTaskProfiles(gettid(), "CPUSET_SP_FOREGROUND", true)) 
        ALOGE("Failed to assign cpuset to the watchdog thread");
    

    if (!watchdog->create_timer(sigset)) 
        ALOGE("Watchdog timer creation failed!");
        return NULL;
    

    while (true) 
        if (sigwait(&sigset, &signum) == -1) 
            ALOGE("sigwait failed: %s", strerror(errno));
        

        watchdog->bite();
    

    return NULL;

代码比较简单,通过 create_timer() 创建定时器,同时初始化信号集;通过 sigwait() 等待捕捉信号,并最终通过 bite() 进行超时处理。

create_timer() 这里就不过多剖析了,主要是通过 timer_create() 创建一个 POSIX 的定时器,详细的定时器信息可以查看:《Linux中的几种定时器》一文。

step3,lmkd处理event时启动watchdog

static void call_handler(struct event_handler_info* handler_info,
                         struct polling_params *poll_params, uint32_t events) 
    struct timespec curr_tm;

    watchdog.start();
    ...

    watchdog.stop();

在 mainloop() 和这里的 call_handler() 处理 events 的时候,首先会通过 watchdog.start() 启动定时器,在处理完成之后通过 watchdog.stop() 停止定时器。

下面来看下 start():

bool Watchdog::start() 
    // Start the timer and keep it active until it's disarmed
    struct itimerspec new_timer;

    if (!timer_created_) 
        return false;
    

    new_timer.it_value.tv_sec = timeout_;
    new_timer.it_value.tv_nsec = 0;
    new_timer.it_interval.tv_sec = timeout_;
    new_timer.it_interval.tv_nsec = 0;

    if (timer_settime(timer_, 0, &new_timer, NULL)) 
        ALOGE("timer_settime failed: %s", strerror(errno));
        return false;
    

    return true;

代码中在 timer_created 之后通过 timer_settime() 设置一个timeout 的定时器,详细的 timer_settime() 接口可以查看:《Linux中的几种定时器》一文。

step4,超时,开咬

我们在上面 step2 中说到,当定时器超时时,会通过 watchdog 的bite() 接口开咬,而这个 bite() 是在watchdog 实例化的时候传入:

static Watchdog watchdog(WATCHDOG_TIMEOUT_SEC, watchdog_callback);

下面来看下这个 callback:

static void watchdog_callback() 
    int prev_pid = 0;

    ALOGW("lmkd watchdog timed out!");
    for (int oom_score = OOM_SCORE_ADJ_MAX; oom_score >= 0;) 
        struct proc target;

        if (!find_victim(oom_score, prev_pid, target)) 
            oom_score--;
            prev_pid = 0;
            continue;
        

        if (reaper.kill( target.pidfd, target.pid, target.uid , true) == 0) 
            ALOGW("lmkd watchdog killed process %d, oom_score_adj %d", target.pid, oom_score);
            killinfo_log(&target, 0, 0, 0, NULL, NULL, NULL, NULL, NULL);
            break;
        
        prev_pid = target.pid;
    

这个代码是 watchdog 的核心处理函数,首先是通过函数 find_victim() 轮询查找最终被牺牲的进程,如果找到了会调用 reaper.kill() 进行kill 和 reap 操作。这里的reaper 是 Android T 的另一个特点,下一节会进行简单的总结,需要注意的是这里最后一个参数为 true,即立即进行处理,而不会放入队列等待 repaer 线程处理。

至此,Android T 中第一个差异点 watchdog 分析完成。主要是 lmkd 的主线程太重要了,不希望出现严重超时的锁住状态,在内存压力紧张的情况采用 watchdog 措施是很有必要的。

2. reaper

下面来看下 Android T 中第二个差异点 reaper。

原则上,在 kill 某个进程之后,其内存应该立即得到回收并交给其他用户使用。然而在现实世界中情况并不那么简单。被 kill 的进程本身要负责清理和释放它的资源,这个工作是在内核上下文中进行的。然而,如果被 kill 的进程发现自己被一个 uninterruptible sleep 给阻塞住的话,那么这个清理工作就会被推迟,没人知道会被推后多久。还有其他一些因素也会导致内存释放的速度变慢,比如相关的 CPU 是否非常繁忙、该 CPU 是否在慢速、低功率的状态下运行。

在内核中有OOM killer 的机制,采用 reaper 的内核线程来进行内存的清理工作。这使得 OOM kill 的效果很明显,哪怕被选中的进程不能立即退出,也仍然能快速释放内存。但 OOM killer 的另外一个问题是,它选择的 badness 进程并不一定是用户希望,它只是为了保证系统的继续运行。

对于Android 系统来说,希望将这种 OOM killer 放在用户空间,例如这里的 lmkd。不过用户空间的 OOM killer 在希望释放内存的时候,必须要依靠 kill() 或者是 pidfd_send_signal(),这种方式杀死一个进程并不能使 OOM reaper 接受来发挥作用,所以用户空间的守护进程又回到了不能等待目标进程自己来释放资源的情况。

Suren Baghdasaryan 针对这个问题提出了一个新的系统调用:

int process_mrelease(int pidfd, unsigned int flags);

该系统调用能够在一个进程被kill 之后,用来加快内存释放。这样允许内存被释放,无需目标进程被调度,因此不依赖于目标进程的优先级或它所运行的CPU。

然而,process_mrelease() 系统调用需要相当长时间。当 lmkd 正忙于收割前一个目标进程的内存时,这段时间阻塞 lmkd 主线程,可能会引起 PSI events 的丢失。因为这个原因,收割工作需要在一个独立的线程中完成。这样,lmkd 主线程可以在内存被释放的同时保持监听 PSI。

lmkd 中引入 Reaper 类,该类包含一个线程池用来处理 kill 工作和 reap 工作。主线程给 Reaper 提交请求对目标进程进行 kill 和 reap ,只需要将目标进程放入队列中,唤醒线程进行处理即可,无需阻塞收割。当然,当线程池中所有的线程处于忙碌时,下一个 kill 将直接在主线程中处理。

step1,初始化reaper

在 lmkd 的main 函数中首先会对 reaper 进行初始化:

        if (init_reaper()) 
            ALOGI("Process reaper initialized with %d threads in the pool",
                reaper.thread_cnt());
        

来看下 reaper 的初始化:

static bool init_reaper() 
    if (!reaper.is_reaping_supported()) 
        ALOGI("Process reaping is not supported");
        return false;
    

    if (!setup_reaper_comm()) 
        ALOGE("Failed to create thread communication channel");
        return false;
    

    // Setup epoll handler
    struct epoll_event epev;
    static struct event_handler_info kill_failed_hinfo =  0, kill_fail_handler ;
    epev.events = EPOLLIN;
    epev.data.ptr = (void *)&kill_failed_hinfo;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, reaper_comm_fd[0], &epev)) 
        ALOGE("epoll_ctl failed: %s", strerror(errno));
        drop_reaper_comm();
        return false;
    

    if (!reaper.init(reaper_comm_fd[1])) 
        ALOGE("Failed to initialize reaper object");
        if (epoll_ctl(epollfd, EPOLL_CTL_DEL, reaper_comm_fd[0], &epev)) 
            ALOGE("epoll_ctl failed: %s", strerror(errno));
        
        drop_reaper_comm();
        return false;
    
    maxevents++;

    return true;

首先,通过 is_reaping_supported() 系统调用来确认内核是否支持 process_mrelease(),这个需要 Linux 5.15 之后版本支持;

接着,通过 setup_reaper_comm() 创建一个 pipe,reaper_comm_fd[0] 为读pipe 端,reaper_comm_fd[1] 为写 pipe 端,reaper_comm_fd[0] 为非阻塞方式;

接着,通过 epoll_ctl 将 reaper_comm_fd[0] 加入监听,当 kill 失败时会通过 reaper_comm_fd[1] 写入信息进行管道通信,处理函数为 kill_fail_handler()

最后通过 init() 函数,创建 Reaper 的线程池,线程处理函数为 reaper_main(),详细看 step2;

step2,init()

system/memory/lmkd/reaper.cpp

bool Reaper::init(int comm_fd) 
    char name[16];

    if (thread_cnt_ > 0) 
        // init should not be called multiple times
        return false;
    

    thread_pool_ = new pthread_t[THREAD_POOL_SIZE];
    for (int i = 0; i < THREAD_POOL_SIZE; i++) 
        if (pthread_create(&thread_pool_[thread_cnt_], NULL, reaper_main, this)) 
            ALOGE("pthread_create failed: %s", strerror(errno));
            continue;
        
        snprintf(name, sizeof(name), "lmkd_reaper%d", thread_cnt_);
        if (pthread_setname_np(thread_pool_[thread_cnt_], name)) 
            ALOGW("pthread_setname_np failed: %s", strerror(errno));
        
        thread_cnt_++;
    

    if (!thread_cnt_) 
        delete[] thread_pool_;
        return false;
    

    queue_.reserve(thread_cnt_);
    comm_fd_ = comm_fd;
    return true;
  • 线程池的个数通过宏 THREA_POLL_SIZE 控制,这里默认为 2,即reaper 线程池中创建两个线程;
  • for 循环创建线程,指定执行函数为 reaper_main(),并设定线程名为 lmkd_reaper0 和 lmkd_reaper1;
  • 将 reaper_comm_fd[1] 存到 reaper.comm_fd_ 中,用以写pipe;

step3,reaper_main()

system/memory/lmkd/reaper.cpp

static void* reaper_main(void* param) 
    Reaper *reaper = static_cast<Reaper*>(param);
    struct timespec start_tm, end_tm;
    struct Reaper::target_proc target;
    pid_t tid = gettid();

    // Ensure the thread does not use little cores
    if (!SetTaskProfiles(tid, "CPUSET_SP_FOREGROUND", true)) 
        ALOGE("Failed to assign cpuset to the reaper thread");
    

    for (;;) 
        target = reaper->dequeue_request();

        if (reaper->debug_enabled()) 
            clock_gettime(CLOCK_MONOTONIC_COARSE, &start_tm);
        

        if (pidfd_send_signal(target.pidfd, SIGKILL, NULL, 0)) 
            // Inform the main thread about failure to kill
            reaper->notify_kill_failure(target.pid);
            goto done;
        
        if (process_mrelease(target.pidfd, 0)) 
            ALOGE("process_mrelease %d failed: %s", target.pidfd, strerror(errno));
            goto done;
        
        if (reaper->debug_enabled()) 
            clock_gettime(CLOCK_MONOTONIC_COARSE, &end_tm);
            ALOGI("Process %d was reaped in %ldms", target.pid,
                  get_time_diff_ms(&start_tm, &end_tm));
        
done:
        close(target.pidfd);
        reaper->request_complete();
    

    return NULL;
  • 当线程被唤醒,会通过 dequeue_request() 将目标进程从队列中取出,并进行 pidfd_send_signal() 和 process_mrelease() 操作;
  • 如果 pidfd_send_signal() 失败,会通过 notify_kill_failure() 通知读端 pipe fd,最终触发 step1 中所述的 kill_fail_handler() 函数;

step4,reaper.kill()

如 kill_one_process() 中所调用,在kill 目标进程时,会通过 reaper.kill() 进行kill 流程:

system/memory/lmkd/reaper.cpp

int Reaper::kill(const struct target_proc& target, bool synchronous) 
    /* CAP_KILL required */
    if (target.pidfd < 0) 
        return ::kill(target.pid, SIGKILL);
    

    if (!synchronous && async_kill(target)) 
        // we assume the kill will be successful and if it fails we will be notified
        return 0;
    

    int result = pidfd_send_signal(target.pidfd, SIGKILL, NULL, 0);
    if (result) 
        return result;
    

    return is_reaping_supported() ? process_mrelease(target.pidfd, 0) : 0;

如果参数 synchronous 为false,即进行异步处理,会通过 async_kill() 将目标进程加入到队列中,唤醒reaper 线程池中的一个线程进行处理。当然,当线程池中的线程已经都被唤醒进行操作了,那么此次的 kill 将不会在放入队列异步执行,而是直接立即执行;

如果参数 synchronous 为true,即立即处理,如上面 watchdog_callback(),此时会通过 pidfd_send_signal() 发送 SIGKILL 信号,并通过 process_mrelease() 进行加速内存回收。

至此,Android T 中第二个差异点 reaper 分析完成。reaper 的引入主要考虑在主线程中进行kill 会引起阻塞,这个阻塞的时间段可能会引起 PSI events 的丢失。因为这个原因,收割工作需要在一个独立的线程中完成。这样,lmkd 主线程可以在内存被释放的同时保持监听 PSI。另外,reaper 中还引入 process_mrelease(),当内核支持 process_mrelease() 系统调用时,那么会加速内存回收。

3. 总结

每一个版本的改变都是又一次的性能提升,除了上面这两点,Android T 中还有其他的逻辑细节处理,但大致的方向都是没有变化的。如果还有其他细节是笔者没有注意到的,也欢迎留言提醒!

 相关博文:

Android lmkd 机制详解(一)

Android lmkd 机制详解(二)

IVI15.1.3 系统稳定性优化篇(LMKD Ⅲ)LMKD的设计原则

LMK(Low Memory Killer)是一个多层级内存溢出查杀工具,由Android基于OOM-Killer原则开发扩展而来。

当系统的可用内存很低时,lmkd则会进行选择性的进程查杀。相对于OOM-Killer,LMKD更具灵活性。LMKD的设计原则包括如下:

  • 定义优先级(首杀原则)
  • 优先级根据场景动态调整;
  • 不同进程区别对待,如应用程序,服务等;

一,Android应用程序进程优先级和OOM Adj

Android会尽可能地确保应用程序的运行,但为了创建和运行更重要或优先级更高的进程,它也需要移除一些低优先级或长时间不活动的进程或服务来回收内存。在Android选择性的查杀进程时,系统将会基于进程的状态来评估进程是否要被系统回收。

在Android中,进程被分为5个等级,分别是:

  1. 前台进程(Foreground Process)
  2. 可见进程(Visible Process)
  3. 服务进程(Service Process)
  4. 后台进程(Background Process)
  5. 空进程(Empty Process)

1.1 前台进程(Foreground Process)

这部分进程主要指当前正在与用户进行交互的进程,如下:

  • 包含正在交互的Activity的进程(resumed)
  • 包含Activity绑定的Service进程
  • 包含运行在“foreground”

以上是关于Android lmkd 机制从R到T的主要内容,如果未能解决你的问题,请参考以下文章

Android 进阶—— Framework 核心之 Low Memory Killer机制和进程优先级小结

IVI15.1.1 系统稳定性优化篇(LMKD Ⅰ)Android低内存查杀守护进程(Android12)

IVI15.1.2 系统稳定性优化篇(LMKD Ⅱ)PSI 压力失速信息

IVI15.1.3 系统稳定性优化篇(LMKD Ⅲ)LMKD的设计原则

IVI15.1.7 系统稳定性优化篇(LMKD 七)AMS与LMKD

LMKD十 有问有答 - FAQ