본문 바로가기
IT 기술/리눅스 커널

Linux kernel CPU Frequency 변경(DVFS) 코드

by 땅뚱 2011. 2. 11.
아래부분은 CPU DVFS 코드중 governor 정책인 hotplug 에 대해서 분석한 내용이다.
이 내용은 omap 에서만 사용하는 정책으로 보인다. 리눅스 바닐라 커널에서는 보이지 않는다.
하지만, 전체적인 동작방식은 거의 동일하다.

해당 내용은 linux kernel 의 device driver 부분에 구현되어있다.(drivers/cpufreq/)

## drivers/cpufreq/cpufreq_hotplug.c
해당 파일은 크게 두부분으로 나뉘어있다. sysfs 를 구성하는 파일에 대한 내용을 구현한 부분과 governor 의 hotplug 정책에 맞게 cpu load를 계산해서 cpu 를 끄고 켜는 부분이다.

실제 cpu load를 계산해서 cpu up / down을 수행하는 코드의 핵심 함수는 dbs_check_cpu() 이다.

dbs_check_cpu() 함수는 아래 호출순서부분을 참조해보면, 최초 cpu frequency 관련 모듈이 초기화 되면서
등록되어 delayed workqueue timer 로 주기적으로 동작된다.
최종적으로 update_ts_time_stats() 를 호출하여 현재까지 idle_sleep_time 과 ts (tick_sched 구조체) 를 변경한 wall time을 얻어온다. (dbs_tuner_ins.sample_rate 값이 100000임)

dbs_check_cpu() 는 do_dbs_timer 에서 호출되며, do_dbs_timer 는 kondemand_wq 에 delayed work queue 와 함께 수행되며, kondemand workqueue thread 생성. 해당 thread 에서 처리한다.

다음은 cpu_dbs_info_s 구조체이다.

struct cpu_dbs_info_s {
    cputime64_t prev_cpu_idle;
    cputime64_t prev_cpu_wall;
    cputime64_t prev_cpu_nice;
    struct cpufreq_policy *cur_policy;
    struct delayed_work work;
    struct cpufreq_frequency_table *freq_table;
    int cpu;
    /*
     * percpu mutex that serializes governor limit change with
     * do_dbs_timer invocation. We do not want do_dbs_timer to run
     * when user is changing the governor or limits.
     */
    struct mutex timer_mutex;
};

dbs_check_cpu() 가 timer 로 부터 주기적으로 동작되어 cpu load를 계산하는데, 우선 현재 cur_idle_time / cur_wall_time을 얻어온다. cur_idle_time 은 현재 idle_sleep_time을 얻어오고, cur_wall_time은 tick_sched 구조체 정보를 수정한 시간을 얻어온다.

tick_sched 정보는 다음과 같다. 이 변수도 역시 per_cpu 변수로 선언되어있으며, 이 구조체 정보는 다음의 몇가지 함수 호출 경로를 따라 수정된다.

include/linux/tick.h

/**
 * struct tick_sched - sched tick emulation and no idle tick control/stats
 * @sched_timer:    hrtimer to schedule the periodic tick in high
 *          resolution mode
 * @idle_tick:      Store the last idle tick expiry time when the tick
 *          timer is modified for idle sleeps. This is necessary
 *          to resume the tick timer operation in the timeline
 *          when the CPU returns from idle
 * @tick_stopped:   Indicator that the idle tick has been stopped
 * @idle_jiffies:   jiffies at the entry to idle for idle time accounting
 * @idle_calls:     Total number of idle calls
 * @idle_sleeps:    Number of idle calls, where the sched tick was stopped
 * @idle_entrytime: Time when the idle call was entered
 * @idle_waketime:  Time when the idle was interrupted
 * @idle_exittime:  Time when the idle state was left
 * @idle_sleeptime: Sum of the time slept in idle with sched tick stopped
 * @iowait_sleeptime:   Sum of the time slept in idle with sched tick stopped, with IO outstanding
 * @sleep_length:   Duration of the current idle sleep
 * @do_timer_lst:   CPU was the last one doing do_timer before going idle
 */
struct tick_sched {
    struct hrtimer          sched_timer;
    unsigned long           check_clocks;
    enum tick_nohz_mode     nohz_mode;
    ktime_t             idle_tick;
    int             inidle;
    int             tick_stopped;
    unsigned long           idle_jiffies;
    unsigned long           idle_calls;
    unsigned long           idle_sleeps;
    int             idle_active;
    ktime_t             idle_entrytime;
    ktime_t             idle_waketime;
    ktime_t             idle_exittime;
    ktime_t             idle_sleeptime;
    ktime_t             iowait_sleeptime;
    ktime_t             sleep_length;
    unsigned long           last_jiffies;
    unsigned long           next_jiffies;
    ktime_t             idle_expires;
    int             do_timer_last;
};

1) irq_enter
tick_check_idle
tick_check_nohz
tick_nohz_stop_idle
update_ts_time_stats()
2) cpu_idle()
tick_nohz_restart_sched_tick
tick_nohz_stop_idle
update_ts_time_stats()

3) cpu_idle() / irq_exit()
tick_nohz_stop_sched_tick
tick_nohz_start_idle
update_ts_time_stats()

4) drivers/cpufreq/cpufreq_conservative.c
5) drivers/cpufreq/cpufreq_hotplug.c
6) drivers/cpufreq/cpufreq_ondemand.c
<공통>
store_ignore_nice_load() : sysfs 에서 ignore_nice_load 를 설정한 경우 호출된다.
dbs_check_cpu : 주기적으로 호출
cpufreq_governor_dbs : get_cpu_idle_time을 얻어와 prev_idle_time 에 넣은 후에 do_dbs_timer를 호출한다.
get_cpu_idle_time
get_cpu_iowait_time_us
update_ts_time_stats()

위 함수 호출정보중 4), 5), 6)을 종합해서 정리해보면, 최초 cpufreq_govoernor_dbs 함수가 호출되어 update_ts_time_stats을 처음 호출하여 ts 구조체를 채워넣고, do_dbs_timer 를 통하여 주기적으로 호출하도록 되어있다.

cpufreq_govoernor_dbs 함수의 호출 경로는 아래와 같다.(1~5번)

1. store_scaling_min_freq (store_one(scaling_min_freq)) : scaling_min_freq 변경시
2. store_scaling_max_freq (store_one(scaling_max_freq)) : scaling_max_freq 변경시
3. store_scaling_governor() : scaling_governor 변경시

여기까지는 모두 sysfs 의 CPU frequency 관련 내용을 root 사용자가 직접 바꾸는 경우이다.

4. cpufreq_cpu_callback
cpufreq_add_dev
cpufreq_add_dev_interface() : cpu notifier 등록으로 CPU 가 켜지는 경우 호출됨

5. cpufreq_update_policy
cpufreq_update_policy 는 최초 모듈 init 시 cpu notifier 를 등록하는 시점에 우선 먼저 호출되어 cpu frequency policy 를 세팅하도록 한다.
그리고 CPU hotplug 에 대비하여 cpu 가 켜지는 경우 다시 호출되어 policy 를 조절하게 된다.
나머지는 acpi 드라이버에서 thermal 과 관련하여 호출되어진다.

1~5. 가 모두 __cpufreq_set_policy 를 호출하고 이 함수에서 __cpufreq_set_policy 를 호출한다.

__cpufreq_set_policy
__cpufreq_governor
cpufreq_govoernor_dbs

update_ts_time_stats 을 호출하는 부분으로 다시 돌아와보자.
cpu idle 코드가 실행되는 관점에서 샆려보면 idle 함수(cpu_idle()) 가 시작하면서 tickless 를 지원할 경우 tickless 커널로 진입하고(tick_nohz_stop_sched_tick()) schedule 되기전에 다시 tickless 를 해제한다.(tick_nohz_restart_sched_tick()) 이렇게 idle 진입/해제할 경우에 각각 update_ts_time_stats을 호출해주어 ts 구조체값을 수정한다.
또한 idle 상태(tickless) 에서 interrupt 처리 루틴으로 진입하는 경우, tick을 restart 하고, 해제하는 경우에 tick을 다시 stop 시키는 코드에서 한번씩 update_ts_time_stats 을 호출하여 tick_sched 정보를 저장한다.
또한 irq_enter / irq_exit 코드에서도 idle 상태인지를 체크해서 update_ts_time_stats 을 호출하도록 한다.

이러한 작업은 실제로 idle 상태에 있던 시간을 정확하게 측정하기 위해서 필요하다.

위에서 잠깐 언급한대로 dbs_check_cpu() 함수가 주기적으로 수행되면서 cur_idle_time / cur_wall_time을 얻어온다. idle_time의 경우 ts 정보를 사용하여 얻어오고 wall_time은 가장 최근 ts를 업데이트한 시점의 정보를 가져온다.

가져온 정보를 사용하여 현재 시점과 이전에 정보를 변경했을때 idle time 을 비교하여 그 차이를 구하고(즉 특정 기간동안 idle_time 이 얼마였는지 계산), wall time 의 차이를 구한다(이것이 측정한 기간이 되겠다)

그렇게 하여 (wall_time - sleep_time) / wall_time 을 계산하게 되면 특정 특정 기간동안 얼마나 busy 했는지에 대한 정보가 나온다. 이를 사용하여, 전체 cpu 의 load, max load, avg load 값을 구한다.

구해진 avg load 값은 설정된 period 에 값에 따라, history 를 기록한다. (기본값으로 in_avg_period : 5, out_avg_period : 10으로 설정되어있고, sysfs에서 변경할 수 있다)

저장된 avg load 의 history 값은 hotplug in / out 정책에 맞게 다시 hotplug_in_avg_load / hotplug_out_avg_load 를 구하는데 사용된다.

좀더 자세하게 살펴보면 hotplug_in_avg_load 는 현재 구해진 avg load 와 이전에 저장된 avg load 값을 period 개수만큼 더한 후에 다시 평균을 계산한다. 즉 hotplug_in_period 값이 5인 경우 do_dbs_timer 함수 기본 수행주기가 100ms (100000) 이라고 한다면 500ms 에 대한 평균을 계산하여 그 load 가 up_threshold (기본값 80) 을 넘는다면, 조건을 체크하여 cpu 1 을 켜도록 하거나, __cpufreq_driver_target() 를 호출하여 cpu frequency 를 높여준다.
(hotplug 의 경우에는 무조건 최대 frequency 로 올려준다)

반대로 hotplug_out_avg_load 는 위와 동일하게 수행하여 down_threshold (기본값 20) 보다 작게되면 cpu 1 을 끄거나, __cpufreq_driver_target() 을 호출하여 cpu frequency 를 낮춰준다.


### dbs_check_cpu 호출 순서 (omap 커널 코드를 참고하였음)

late_initcall(omap_cpufreq_init);
omap_cpufreq_init
cpufreq_register_driver(&omap_driver)
ret = sysdev_driver_register(&cpu_sysdev_class, &cpufreq_sysdev_driver);

struct sysdev_class cpu_sysdev_class = {
    .name = "cpu",
    .attrs = cpu_sysdev_class_attrs,
};

static struct sysdev_driver cpufreq_sysdev_driver = {
    .add        = cpufreq_add_dev,
    .remove     = cpufreq_remove_dev,
    .suspend    = cpufreq_suspend,
    .resume     = cpufreq_resume,
};

sysdev_driver_register
cpufreq_add_dev()
cpufreq_add_dev_interface(cpu, policy, sys_dev);
__cpufreq_set_policy(policy, &new_policy);
__cpufreq_governor(data, CPUFREQ_GOV_START)
ret = policy->governor->governor(policy, event);

struct cpufreq_governor cpufreq_gov_hotplug = {
       .name                   = "hotplug",
       .governor               = cpufreq_governor_dbs,
       .owner                  = THIS_MODULE,
};

policy->governor->governor(policy, event)
cpufreq_governor_dbs()
dbs_timer_init(this_dbs_info);
INIT_DELAYED_WORK_DEFERRABLE(&dbs_info->work, do_dbs_timer);
queue_delayed_work_on(dbs_info->cpu, khotplug_wq, &dbs_info->work, delay);

do_dbs_timer()
dbs_check_cpu(dbs_info);

-----------------------------------------------------------------------

아래는 cpu_idle() 과 관련하여 정리중.


# 동작

tichless configuration on 되어있는 경우 (CONFIG_NO_HZ)

arch/arm/kernel/process.c

## cpu_idle()
- tick_nohz_stop_sched_tick() called : schedule tick off
- need_resched 가 아닌 동안 pm_idle
- need_resched 가 set 되면 loop 끝내
- tick_nohz_restart_sched_tick() called : schedule tick on
- schedule() 호출됨

## tick_nohz_stop_sched_tick() : idle tick 을 멈춤
- ts->inidle = 1; /* ts 는 tick_cpu_sched per_cpu 변수임. 현재 cpu 가 idle 상태임을 표시
- tick_nohz_start_idle() called
- 그 밖의 일들

## tick_nohz_start_idle()
- update_ts_time_stats() called
- ts->idle_entry_time = now; /* ilde 진입시간을 기록 */
- ts->idle_active = 1; /* idle 이 active 상태임을 기록 */

## update_ts_time_stats()
- 현재 idle_active 인 경우에
  1) 이전 idle 진입시간(ts->idle_entrytime) 과 현재 시간(now) 의 차이 계산
  2) 해당 값을 ts->idle_sleeptime 에 추가
  3) ts->idle_entrytime = now : idle 진입시간 재설정
- in-out parameter 에 last_update_time 을 현재 시간을 설정(now)

## tick_nohz_restart_sched_tick() : idle task 에서 idle tick 재시작
- per_cpu tick_cpu_sched (struct tick_sched) 변수 얻어옴
- idle_active 상태이면 tick_nohz_stop_idle 호출
- ts->inidle = 0
- jiffies 업데이트
- ts->tick_stopped = 0; ts->idle_exittime = now
- tick_nohz_restart 호출

## tick_nohz_stop_idle()
- update_ts_time_stats() 호출
- ts->idle_active = 0 세팅

## tick_nozh_restart()
- hrtimer for schedule tick 재설정
## tick_nohz_restart()