timerslack 与 sleep()/usleep()

一、简介

1. timerslack 是 Linux 系统为了降低系统功耗,避免 timer 时间参差不齐,过于的频繁的唤醒 cpu,而设置的一种对齐策略。内核中将相近的定时器到期时间汇聚在一起,这会导致到期时间比定时时间晚一些(但不会提前)。

2. 会被timer slack到期时间影响的函数集有 select(2), pselect(2), poll(2), ppoll(2), epoll_wait(2), epoll_pwait(2), clock_nanosleep(2), nanosleep(2), futex(2), sigtimedwait(2)。还有通过futexes实现的库函数,包括 pthread_cond_timedwait(3), pthread_mutex_timedlock(3), pthread_rwlock_timedrdlock(3), pthread_rwlock_timedwrlock(3), sem_timedwait(3).

3. timerslack 是per-task的文件,其通过 /proc/<pid>/timerslack_ns 导出到用户空间, 定时时,定时到期时间若设置为time,则实际的到期时间点在区间 (time, time + p->timer_slack_ns) 中。


二、sleep()/usleep() 函数实现

timerslack 影响的函数众多,这里我们以常用的休眠函数为例进行讲解。

1. Android 中的 usleep() 实现

//bionic/libc/bionic/usleep.cpp
int usleep(useconds_t us) {
    timespec ts;
    ts.tv_sec = us / 1000000;
    ts.tv_nsec = (us % 1000000) * 1000;
    return nanosleep(&ts, nullptr);
}

2. Android 中的 sleep() 实现

//bionic/libc/bionic/sleep.cpp
unsigned sleep(unsigned s) {
    #if !defined(__LP64__)
    // `s` is `unsigned`, but tv_sec is `int` on LP32.
    if (s > INT_MAX) return s - INT_MAX + sleep(INT_MAX);
    #endif

    timespec ts = {.tv_sec = static_cast<time_t>(s)};
    return (nanosleep(&ts, &ts) == -1) ? ts.tv_sec : 0;
}

可见这两个函数都是通过 nanosleep() 系统调用进入与内核进行交互的。


三、nanosleep()系统调用与实现

1. nanosleep()函数

#include <time.h>

int nanosleep(const struct timespec *req, struct timespec *rem);

struct timespec {
    time_t tv_sec;    /* seconds */
    long   tv_nsec;    /* nanoseconds */
};

nanosleep() 是一个高精度休眠函数,它会休眠至少 req 时间,若被信号打断返回-1并将errno设置为EINTR,然后将剩余时间写到rem指针中(rem若不为NULL)。若成功休眠了请求的时间,返回0,若失败或被信号打断返回-1并将错误码设置进errno。和 sleep(3)、usleep(3) 比较起来 nanosleep() 为指定睡眠间隔提供了更高的分辨率。

 

2. nanosleep系统调用

//kernel/time/hrtimer.c
SYSCALL_DEFINE2(nanosleep, struct __kernel_timespec __user *, rqtp,
        struct __kernel_timespec __user *, rmtp)
{
    struct timespec64 tu;

    if (get_timespec64(&tu, rqtp))
        return -EFAULT;

    if (!timespec64_valid(&tu))
        return -EINVAL;

    current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;
    current->restart_block.nanosleep.rmtp = rmtp;
    return hrtimer_nanosleep(&tu, HRTIMER_MODE_REL, CLOCK_MONOTONIC);
}

 

3. nanosleep内核实现

nanosleep 在内核中的实现函数为 hrtimer_nanosleep()

/* nanosleep: (&tu, HRTIMER_MODE_REL, CLOCK_MONOTONIC) */
//kernel/time/hrtimer.c
long hrtimer_nanosleep(const struct timespec64 *rqtp, const enum hrtimer_mode mode, const clockid_t clockid)
{
    struct restart_block *restart;
    struct hrtimer_sleeper t;
    int ret = 0;
    u64 slack;

    /* 注意这里使用到了 timer_slack_ns */
    slack = current->timer_slack_ns;
    /* rt和dl任务不受timer_slack影响 */
    if (dl_task(current) || rt_task(current))
        slack = 0;

    /* 将 t->timer.function = hrtimer_wakeup, t->task = current; */
    hrtimer_init_sleeper_on_stack(&t, clockid, mode);
    /* 超时时间点在区间(rqtp, rqtp+slack)中  */
    hrtimer_set_expires_range_ns(&t.timer, timespec64_to_ktime(*rqtp), slack);

    /* 将当前进程切走,唤醒后从这里继续往下执行,正常返回0 */
    ret = do_nanosleep(&t, mode);
    if (ret != -ERESTART_RESTARTBLOCK)
        goto out;

    /* Absolute timers do not update the rmtp value and restart: */
    if (mode == HRTIMER_MODE_ABS) {
        ret = -ERESTARTNOHAND;
        goto out;
    }

    /* 下面应该是重复定时机制的实现 */
    restart = &current->restart_block;
    restart->nanosleep.clockid = t.timer.base->clockid;
    restart->nanosleep.expires = hrtimer_get_expires_tv64(&t.timer);
    set_restart_fn(restart, hrtimer_nanosleep_restart);
out:
    destroy_hrtimer_on_stack(&t.timer);
    return ret;
}

定时器到期回调函数:

//kernel/time/hrtimer.c
static enum hrtimer_restart hrtimer_wakeup(struct hrtimer *timer)
{
    struct hrtimer_sleeper *t = container_of(timer, struct hrtimer_sleeper, timer);
    struct task_struct *task = t->task;

    /* 定时到期将 t->task 设置为NULL, 这个在对端作为退出等待的判断条件 */
    t->task = NULL;
    if (task)
        wake_up_process(task);

    return HRTIMER_NORESTART;
}

 

四、timerslack 的设置方法

1. 通过proc文件进行设置,会设置到任务 p->timer_slack_ns 中。

echo slack_ns > /proc/pod/timerslack_ns
    timerslack_ns_write
        p->timer_slack_ns = slack_ns

 

2. 在Android下,Process.java中封装的cgroup分组切换函数会进行设置,

Process.java 中的 setProcessGroup(pid, THREAD_GROUP_TOP_APP) 在 task_profiles.json 中对应的集合属性 SCHED_SP_TOP_APP 中的 TimerSlackNormal 属性就会根据任务分组重新配置任务的
timerslack 值。


3. Android java 中 java.lang.Thread 类封装的 setPriority() 也会进行调用。

Thread t = new Thread(); t.start(); t.setPriority(3); 这是个错误用法,这样设置由于线程pid还是0导致设置的是调用线程的优先级,但是作为讲解的例子足够了。

t.setPriority() 最终会调用到 SetNativePriority():

//art/runtime/thread.cc
void Thread::SetNativePriority(int new_priority) {
    palette_status_t status = PaletteSchedSetPriority(GetTid(), new_priority);
}

/* 若Java中设置 Thread t = new Thread(); t.setPriority(3); 就是设置nice=13 */
palette_status_t PaletteSchedSetPriority(int32_t tid, int32_t managed_priority) {
    /* Thread::setPriority(X),这层的取值范围是 [1, 10] */
    if (managed_priority < art::palette::kMinManagedThreadPriority ||
        managed_priority > art::palette::kMaxManagedThreadPriority) {
        return PALETTE_STATUS_INVALID_ARGUMENT;
    }

    /* 若传3,就是3-1,对应 ANDROID_PRIORITY_BACKGROUND + 3,nice=13 */
    int new_nice = kNiceValues[managed_priority - art::palette::kMinManagedThreadPriority];
    int curr_nice = getpriority(PRIO_PROCESS, tid);

    if (curr_nice == new_nice) {
        return PALETTE_STATUS_OK;
    }

    /* 还与cgroup挂钩,切cgroup */
    if (new_nice >= ANDROID_PRIORITY_BACKGROUND) { //10
        /* libprocessgroup 中的函数,其会设置任务的 timerslack */
        SetTaskProfiles(tid, {"SCHED_SP_SYSTEM"}, true);
    } else if (curr_nice >= ANDROID_PRIORITY_BACKGROUND) {
        SetTaskProfiles(tid, {"SCHED_SP_FOREGROUND"}, true);
    }

    /* 设当前进程nice值,new_nice取值[-20, 19] */
    if (setpriority(PRIO_PROCESS, tid, new_nice) != 0) {
        return PALETTE_STATUS_CHECK_ERRNO;
    }

    return PALETTE_STATUS_OK;
}

t.setPriority() 是怎么调用下来的,向上逐层查找:

//art/runtime/thread.h 
pid_t GetTid() const {    return tls32_.tid;}

//art/runtime/thread.cc tls32_.tid 的赋值位置:
void Thread::InitTid() {
    tls32_.tid = ::art::GetTid();
}

bool Thread::Init(ThreadList* thread_list, JavaVMExt* java_vm, JNIEnvExt* jni_env_ext) { ...  InitTid(); ...}

void* Thread::CreateCallback(void* arg) { ... self->Init(runtime->GetThreadList(), runtime->GetJavaVM(), self->tlsPtr_.tmp_jni_env); ...}

/* Thread::CreateCallback 就是线程的执行函数体,它先做一些初始化后再调用的用户的回调函数 */
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { ... pthread_create(&new_pthread, &attr, Thread::CreateCallback, child_thread); ...}

/* art/openjdkjvm/OpenjdkJvm.cc */
JNIEXPORT void JVM_StartThread(JNIEnv* env, jobject jthread, jlong stack_size, jboolean daemon) { ... art::Thread::CreateNativeThread(env, jthread, stack_size, daemon == JNI_TRUE); }

/* external/oj-libjdwp/src/share/javavm/export/jvm.h  java.lang.Thread 类的 */
JNIEXPORT void JNICALL JVM_StartThread(JNIEnv *env, jobject thread);

上面设置方法是错误的原因是:在回调函数,也就是创建的线程执行后,GetTid()的值才是新建线程的。在调用设置优先级函数时,线程回调函数还没来得及调用,因此此时 GetTid() 的值还是0。对于 setpriority() 系统调用,若是0就表示设置的是当前进程。正确设置方法:

Thread thread = new Thread( new Runnable(){
    public void run() {
        Thread.currentThread().setPriority(3); 
        Process.setThreadPriority(13);
    }});
thread.start();

 

五、timerslack 相关问题

1. 一个微信卡音相关分享:https://cloud.tencent.com/developer/article/1836285

 

六、总结

1. timerslack 是一个per-task的可调的参数,它会影响CFS任务(不会影响RT任务)sleep、wait等众多函数的唤醒及时性。

2. timerslack 在上层cgroup分组切换中,和java代码的 Thread 类实现函数中都有调用。

 

七、补充

1. futex中对hrtime的使用函数是 futex_setup_timer(), 其里面也使用到了 current->timer_slack_ns 将超时设定在一个区间中。

 

posted on 2022-10-19 22:34  Hello-World3  阅读(855)  评论(0编辑  收藏  举报

导航