linux-debug/scheduling
created : 2020-12-09T04:30:45+00:00
modified : 2021-06-23T06:35:51+00:00
주요 키워드
- scheduling : 실행 대기 중인 프로세스 중에서 우선순위가 가장 높은 프로세스를 선택해 CPU에서 실행시킴
- Preemptive Scheduling : 강제로 CPU에서 실행 중인 프로세스를 비우고 새로운 프로세스 실행
- Non-Preemptive Scheduling : 프로세스가 자발적으로 스케줄링 요청
- context-switching : cpu에서 실행 중인 프로세스의 레지스터 세트를 비우고 새로운 프로세스 레지스터 세트를 채우는 동작, 아키텍쳐마다 구현 방식이 다름
- scheduling policy : 스케쥴링 시 어떤 방식과 규칙으로 다음에 실행할 프로세스를 선택할지 결정
- scheduler class : 5가지 커널 스케쥴러 세부동작을 모듈화한 자료구조 이자 인터페이스, 프로세스는 스케쥴러 클레스를 우선순위에 따라 선택할 수 있음
- run queue : 실행 대기 중인 프로세스를 관리하는 자료구조, percpu 타입 변수
- proirity : 유저 공간에서 설정한 nice와 커널 우선순위가 존재
선점 스케쥴링과 비선점 스케쥴링 비교
- Preemptive Scheduling
- 실행 중인 프로세스를 강제로 CPU에서 실행 중지
- 새로운 프로세스가 CPU에서 실행
- 선점 스케쥴링 시작점
- 인터럽트 핸들러를 처리하고 난 후 인터럽트가 발생하기 전에 코드로 되돌아가기 직전
- 시스템 콜의 핸들러 함수를 처리하고 난 후 유저 공간으로 복귀하기 직전
- 비선점 스케쥴링
- 프로세스가 자발적으로 스케줄링 요청
- 비선점 스케줄링 시작점
- 입출력(I/O) 동작을 시작할 때
- 뮤텍스를 획득하지 못하고 휴먼 상태에 진입할 때
스케줄링 정책
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
스케줄러 클래스
- stop 스케줄러
- 참고- 문C 블로그
- 어떠한 스케줄러보다 우선 순위가 더 높아 preemption 되지 않으며, 다른 cpu로 migration도 허용되지 않다. 모든 요청에 대해 대부분 거절하거나 아무런 일도 하지 않는다.
- deadline 스케줄러
- 참고- 문C 블로그
- 2013년 3월 Kernel v3.14에서 소개
- EDF + CBS 알고리즘 기반
- EDF(Earliest Deadline First)
- 작업이 N 개일때 복잡도는 O(n^2) 로 알려져 있으며, 수학적으로 최적이라는 것이 증명되어 있지만, 현실적으로 프로세스의 마감시간을 예측하는 것이 어렵다.
- 예시 : P1이 수행시간이 1, 주기가 8, P2가 수행시간이 2, 주기가 5, P3가 수행시간이 4, 주기가 10이고, 1단위 시간이 10ms 일때 CPU 사용량은 1/8 + 2/5 + 4/10 = 0.925이기에 스케줄링이 가능하다고 판단하며 스케줄링한다.
- CBS(Constatn Bandwidth Server)
- 자료가 안나오길래 아래 적어둔 논문 기반으로 정보를 적는다.
- 논문 : Intergrating multimedia applications in hard real-time systems
- 1998년도에 소개된 예약기반(reservation-based) 스케줄링 알고리즘
- \[\text{ a budget } c_s \text{ and by a ordered pair } (Q_s, T_s), \text{ where } Q_s \text{ is the maximum budget and } T_s \text{ is the period of the server }\]
- \[\text{ server bandwidth } U_s = Q_s / T_s\]
- \[\text{ At each instant, a fixed deadline } d_{s, k}\]
- \[\text{ Initially, } d_{s, 0} = 0\]
- 각 작업들은 동적인 마감시간(deadline)을 가지고 있고 이는 초기에는 현제 서버의 마감시간과 동일하게 된다.
- 작업들이 실행되면 그와 같은 양이 예산에서 빠지며, 예산이 0이 됬을 때 서버의 최대 예산인 Q_s를 다시 채우고, 새로운 서버 마감시간이 생기게된다. 이 때 새로운 마감시간은 T_s만큼 더한것이다. \(d_{s, k + 1} = d_{s, k} + T_s \)
- 흐으으음… 이 이상 자세히 적을 필요는 없을 듯, 그냥 대충 예산 개념이 존재해서 스케줄링 한다? 정도까지만 정리함. 사실상 정리하면 그냥 논문 번역이 될듯
- EDF(Earliest Deadline First)
- 처음에는 UP 시스템용으로 구현되었다가 SMP 시스템에서도 채용함. 각 cpu의 earliest deadline을 관리하고 다른 CPU로 전달하여 효과적으로 스케줄링한다.
- 상당히 최신에 추가된 내용이며, 2017년에도 새 버전의 deadline 스케줄러가 소개됬다고 한다.
- Stop 스케줄러 바로 다음의 우선순위를 가짐
- RT 스케줄러
- stop - deadline 다음 순서의 우선순위를 가지고 있다.
- rt 런큐는 CPU 수만큼 생성된다.
- CFS 스케줄러
- 참고
- rb 데이터 구조를 사용하며 O(logN) 성능을 가지는 스케줄러
- 특징
- 공평한 CPU 시간
- 가상 런타임
- 대기자 공평성
- RB-트리
- Idle 스케줄러
- 참고- 문C 블로그
- 어떠한 스케줄러보다 우선순위가 낮다.
- core 마다 1개씩 지정되어 있어 다른 cpu로의 migration이 필요 없다.
런큐
- 실행 대기상태 프로세스와 CPU에서 실행 중인 프로세스를 관리하는 자료구조
우선순위(nice)
- -20 ~ 19의 값을 가지며 커널공간에서 100 ~ 139 범위 값으로 변환되어 관리된다.(0 ~ 99 는 RT)
- RT(Real-Time) 프로세스의 우선순위 범위
- 0 ~ 99 사이의 우선순위를 가짐
- SCHED_FIFO 이며, 우선순위가 더 높은 RT 프로세스가 없으면 계속 CPU를 점유해 사용한다.
프로세스 상태 관리
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
프로세스 상태
- TASK_RUNNING : 실행대기
- 프로세스가 런큐에 삽입된 후 실행을 기다리는 상태
- 스케줄러는 TASK_RUNNING(실행 대기)상태에 있는 프로세스 중에서 CPU에서 실행할 프로세스를 선택
- TASK_RUNNING : CPU 실행
- 프로세스가 CPU에서 실행 중인 상태
- CPU 레지스터 세트에는 현재 실행 중인 프로세스의 상태 정보로 채워짐
- TASK_INTERRUPTIBLE
- 프로세스가 휴면 상태에 진입한 상태
- 프로세스를 깨우면 다시 TASK_RUNNING(실행 대기) 상태로 변경됨
- TASK_UNINTERRUPTIBLE
- 프로세스가 특정 조건으로 깨워나고 싶을 때 설정하는 상태
- 보통 스스로 깨어날 조건을 설정한 다음에 TASK_UNINTERRUPTIBLE 상태로 변경
- 뮤텍스를 얻지 못하거나 입출력(I/O) 동작 중에 TASK_UNINTERRUPTIBLE 상태로 변경
TASK_INTERRUPTIBLE과 TASK_UNINTERRUPTIBLE 상태의 차이점
- I/O 나 mutex 같이 깨어나봤자 의미가 없을때, 자신을 깨울수 있는 방법과 시기(I/O가 종료되거나, mutex를 획득할수 있어질때)를 설정하고 TASK_UNINTERRUPTIBLE 로 둔다.
- TASK_UNINTERRUPTIBLE 상태의 프로세스가 비정상으로 많은 경우
- 다수의 프로세스들이 뮤텍스를 획득하지 못해 자신을 TASK_UNINTERRUPTIBLE 상태로 바꾸고 휴면 상태에 진입
- I/O 동작 중에 외부 장치와 인터페이싱에 문제가 있음
- TASK_RUNNING 상태의 프로세스가 비정상으로 많은 경우
- 특정 프로세스가 CPU를 계속 점유하고 실행중
- 인터럽트가 비정상적으로 많이 발생해서 프로세스 선점 스케줄링이 제대로 수행되지 못함.
프로세스 상태 변화
@startuml
[*] --> TASK_RUNNING_실행대기
TASK_RUNNING_실행대기 --> TASK_RUNNING_CPU실행 : 1
TASK_RUNNING_CPU실행 --> TASK_INTERRUPTIBLE : 2
TASK_RUNNING_CPU실행 --> TASK_UNINTERRUPTIBLE : 3
TASK_INTERRUPTIBLE --> TASK_RUNNING_실행대기 : 4
TASK_RUNNING_CPU실행 --> TASK_DEAD : 5
TASK_UNINTERRUPTIBLE --> TASK_RUNNING_실행대기
TASK_DEAD --> [*]
@enduml
-
- 실행대기(TASK_RUNNING) -> CPU 실행중(TASK_RUNNING)
- context switching 이라 부르며, CPU register sets을 저장하고, 다음 실행될 THREAD의 CPU register sets을 불러온다.
- 실행대기(TASK_RUNNING) -> CPU 실행중(TASK_RUNNING)
-
- CPU 실행(TASK_RUNNING) -> 휴면(TASK_INTERRUPTIBLE)
-
- CPU 실행(TASK_RUNNING) -> 휴면(TASK_UNINTERRUPTIBLE)
- 특정 조건에서만 깨어날수 있게 됨
- 자신이 깨어날 조건 설정 -> TASK_UNINTERRUPTIBLE 상태로 변경 ->
schedule()
함수를 호출해 휴면상태에 진입 - I/O를 실행할때, 뮤텍스를 획득하지 못했을 때
- CPU 실행(TASK_RUNNING) -> 휴면(TASK_UNINTERRUPTIBLE)
-
- 휴면(TASK_INTERRUPTIBLE) -> 실행 대기(TASK_RUNNING)
wake_up_process()
함수를 호출해서 프로세스를 깨울때의 상황
- 휴면(TASK_INTERRUPTIBLE) -> 실행 대기(TASK_RUNNING)
-
- 휴면(TASK_UNINTERRUPTIBLE) -> 실행 대기(TASK_RUNNING)
- I/O 실행 완료 또는 뮤텍스를 해제한 프로세스가 뮤텍스를 기다리고 있는 프로세스를 깨울때
- 휴면(TASK_UNINTERRUPTIBLE) -> 실행 대기(TASK_RUNNING)
프로세스 상태 변화 함수 목록
- TASK_RUNNING(실행 대기)
- wake_up_interruptible()
- wake_up_new_task()
- wake_up_process()
- yeild()
- do_nanosleep()
- TASK_RUNNING(CPU 실행)
- schedule()
- TASK_INTERRUPTIBLE
- wait_event_interruptible()
- do_sigtimedwiat()
- sys_pause()
- do_nano_sleep()
- TASK_UNINTERRUPTIBLE
- io_wait_event()
- mutex_lock()
- usleep_range()
- msleep()
- wait_for_completion()
TASK_RUNNING(실행 대기)으로 바뀔 때 호출되는 함수
- 프로세스를 깨울때
- 프로세스를 처음 생성하고 실행 요청을 할 때
- 프로세스 관련 정보를 업데이트 할때
- wake_up_new_task()
void wake_up_new_task(struct task_struct *p) { struct rq_flags rf; struct rq *rq; raw_spin_lock_irqsave(&p->pi_lock, rf.flags); p->state = TASK_RUNNING; /* skip */ }
- _do_fork 에서 호출됨
- wake_up_process()
- wake_up_process() -> try_to_wake_up() -> ttwu_do_activate() -> ttwu_do_wakeup()
- ttwu : try to wake up 의 약어
static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags, struct rq_flags *rf) { check_preempt_curr(rq, p, wake_flags);/ p->state = TASK_RUNNING; trace_sched_wakeup(p); /* skip */ }
- yield()
void __sched yield(void) { set_current_state(TASK_RUNNING); do_sched_yield(); }
- do_nanosleep()
static int __sched do_nanosleep(struct hrttimer_sleepr *t, enum hrtimer_mode mode) { struct restart_block *restart; /* skip */ __set_current_state(TASK_RUNNING); /* skip */ }
TASK_RUNNING(CPU 실행)로 바뀔 때 호출하는 함수
- schedule()
static void __sched notrace __schedule(bool preempt) { struct rq *rq; int cput; cpu = smp_processor_id(); rq = cpu_rq(cpu); if (likely(prev != next)) { rq->nr_switches ++; rq->curr = next; /* Also unlocks the rq: */ rq = context_switch(rq, prev, next, &rf); } else { /* skip */ }
TASK_INTERRUPTIBLE 상태로 바뀔 때 호출하는 함수
- wait_event_interruptible()
```c
#define wait_event_interruptible(wq_head, condition)
({
int __ret = 0;
might_sleep();
if (!(condition))
__retval = __wait_event_interruptible(wq_head, condition);
__ret;
})
#define __wait_event_interruptible(wq_head, condition)
___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0,
schedule())
* ___wait_event()
```c
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
for (;;) { \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); \
/* skip */ \
} \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
- prepare_to_wait_event()
long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry. int saste) { unsigned long flags; long ret = 0; spin_lock_irqsave(&wq_head->lock, flags); if (unlikely(signal_pending_state(state, current))) { /* skip */ } else { if (list_empty(&wq_entry->entry)) { if (wq_entry->flags & WQ_FLAGS_EXCLUSIVE) __add_wait_queue_entry_tail(wq_head, wq_entry); else __add_wait_queue(wq_head, wq_entry); } set_current_state(state); } /* skip */ }
- do_sigtimedwait()
- sys_pause()
SYSCALL_DEFINE0(pause) { while (!signal_pending(current)) { __set_current_state(TASK_INTERRUPTIBLE); schedule(); } return -ERESTARTNOHAND; }
- 펜딩된 시그널(자신에게 전달된 시그널)이 없는지 점검
- 자신을 TASK_INTERRUPTIBLE(휴면 상태)로 변경
- schedule() 함수를 호출해 휴면상태로 진입
- do_nanosleep()
TASK_UNINTERRUPTIBLE 상태로 바뀔 때 호출하는 함수
- io_wait_event()
#define io_wait_event(wq_head, condition) \ do { \ might_sleep(); \ if (condition) \ break; \ __io_wait_event(wq_head, condition); \ } while (0) #define __io_wait_event(wq_head, condition) \ (void)__wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \ io_schedule())
- mutex_lock()
static int __sched __mutex_lock(struct mutex *lock, long state, unsgined int subclass, struct lockdep_map *nest_lock, unsigned long ip) { return __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false); }
static __always_inline int __sched __mutex_lock_common(struct mutex *lock, long state, unsigned int subclass, struct lockdep_map *nest_lock, unsigned long ip, struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx) { /* skip */ set_current_state(state); for (;;) { /* skip */ schedule_preempt_disabled(); /* skip */ } /* skip */ }
- usleep_range()
void __sched usleep_range(unsigned long min, unsigned long max) { ktime_t exp = ktime_add_us(ktime_get(), min); u64 delta = (u64)(max - min) * NSEC_PER_USEC; for (;;) { __set_current_state(TASK_UNINTERRUPTIBLE); /* Do not return before teh requested sleep time has elapsed */ if (!schedule_hrtimeout_range(& exp, delta, HRTIMER_MODE_ABS)) break; } }
- msleep()
void msleep(unsigned int msec) { unsigned long timeout = msecs_to_jiffies(msecs) + 1; while (timeout) timeout = schedule_timeout_uninterruptible(timeout); } signed long __sched schedule_timeout_uninterruptible(signed long timeout) { __set_current_state(TASK_UNINTERRUPTIBLE); return schedule_timeout(timeout); }
- wait_for_completion()
void __sched wait_for_completion(struct completion *x) { wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE); }
스케줄러 클래스
- 프로세스가 스케줄러 클래스르 ㄹ통해 유연하게 스케줄러를 바꾸게 지원하기 위해서
sched_class 구조체
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yeild_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
struct task_struct * (*pick_next_task) (struct rq *rq,
struct task_struct *prev,
struct rq_flags *rf);
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
int (*select_task_rq) (struct task_struct *p, int task_cpu, int sd_flag, int flags);
void (*migrate_task_rq) (struct task_struct *p);
void (*task_woken) (struct rq *this_rq, struct task_struct *task);
void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);
/* skip */
}
- enqueue_task : 프로세스가 실행 가능한 상태로 진입
- dequeue_task : 프로세스가 더 이상 실행 가능한 상태가 아닐 때
- yeild_task : 프로세스가 스스로 yield() 시스템 콜을 실행했을 때
- check_preempt_curr : 현재 실행 중인 프로세스를 선점할 수 있는지 검사
- pick_next_task : 실행할 다음 프로세스를 선택
- put_prev_task : 실행 중인 프로세스를 다시 내부 자료구조(런큐)에 삽입
- load_balance : 코어 스케줄러가 태스크 부하를 분산하고자 할 때
- 참고-문C블로그
- Passive Balancing : Fork Balancing, Exec Balancing, Wake Balancing, Idle Balancing
- Periodic Balancing
- set_current_task : 프로세스의 스케줄러 클래스나 태스크 그룹을 바꿀 때
- task_tick : 타이머 틱 함수를 호출
5가지 스케줄러 클래스
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
- 리눅스 시스템에서 전체 프로세스 가운데 RT 클래스 프로세스와 CFS 클래스 프로세스의 비율 : 99% 는 CFS, 1%정도가 나머지 그마저도 대부분 RT
스케줄러 클래스의 우선 순위
- stop -> dl -> rt -> fair -> idle
- struct 내부 변수 next를 사용해서 다음 우선 순위의 스케줄러를 가르킨다.
프로세스를 스케줄러에 등록
-
- 스케줄러 클래스 설정
- sched_fork()
int sched_fork(unsigned long clone_flags, struct task_struct *p) { unsigned long flags; /* skip */ if (dl_prio(p->prio)) return -EAGAIN; else if (rt_prio(p->prio)) p->sched_class = &rt_sched_class; else p->sched_class = &fair_sched_class; }
- sched_fork()
- 스케줄러 클래스 설정
-
- 스케줄러 클래스 변경
- __setscheduler() ```c static void __setscheduler(struct rq *rq, struct task_struct *p, const struct sched_attr *attr, bool keep_boost) { __setscheduler_params(p, attr);
p->prio = normal_prio(p); if (keep_boost) p->prio = rt_effective_prio(p, p->prio);
if (dl_prio(p->prio)) p->sched_class = &dl_sched_class; else if (rt_prio(p->prio)) p->sched_class = &rt_sched_class; else p->sched_class = &fair_sched_class; } ```
- 스케줄러 클래스 변경
세부 함수 호출
- enqueue_task()
static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags) { if (!(flags & ENQUEUE_NOCLOCK)) update_rq_clock(rq); if (!(flags & ENQUEUE_RESTOR)) sched_info_queued(rq, p); p->sched_class->enqueue_task(rq, p, flags); }
- dequeue_task()
static inline void dequeue_task(struct rq *rq, struct task_struct *p, int flags) { if (!flags & DEQUEUE_NOCLOCK) update_rq_clock(rq); if (!(flags & DEQUEUE_SAVE)) sched_info_dequeued(rq, p); p->shced_class->dequeue_task(rq, p, flags); }
런큐
- percpu 타입의 전역변수
- RT, CFS, Deadline 서브 런큐를 관리
- 실행 요청을 한 프로세스가 런큐에 삽입됨
- CPU를 점유하면서 실행 중인 current 프로세스를 관리
struct rq {
raw_spinlock_t lock;
unsigned int nr_running;
/* skip */
struct load_wait load;
unsigned long nr_load_updates;
u64 nr_switches;
struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
/* skip */
unsigned long nr_uninterruptible;
struct task_struct *curr, *dle, *stop;
/* skip */
};
- lock : 런큐 자료구조를 변경할 때 경쟁 조건을 피하기 위한 락
- nr_running : 런큐에 삽입된 모든 프로세스 개수
- nr_switches : 컨텍스트 스위칭을 수행한 개수
- cfs, rt : cfs, rt 런큐
- nr_uninterruptible : 런큐에 있는 태스크 중 TASK_UNINTERRUPTIBLE 상태의 프로세스 개수
- curr : 해당 런큐에서 CPU를 점유하면서 실행 중인 프로세스의 태스크 디스크립터
- idle : idle 프로세스의 태스크 디스크립터
- cfs_task : cfs 런큐에 삽입된 모든 일반 프로세스의 연결 리스트
runqueues 변수
DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu)))
#define this_rq() this_cpu_ptr(&runqueues)
CFS 스케줄러
- CFS(Completely Fair Scheduler)
- 2.6.23 버전 이후로 적용된 리눅스 기본 스케줄러
- 특징
- 타임 슬라이스 : 스케줄러가 프로세스에게 부여한 실행 시간
- 우선순위
- 우선순위가 높은 프로세스에 대해 가장 먼저 CPU 에서 실행 시키고, 더 많은 타임 슬라이스를 할당해준다.
- 가상 실행 시간(vruntime)
- 프로세스가 그동안 실행 시간을 정규화한 시간 정보
CFS 스케줄러 알고리즘
- load weight : 프로세스의 우선순위에 주는 가중치
static void set_load_weight(struct task_struct *p) { int prio = p->static_prio - MAX_RT_PRIO; struct load_weight *load = &p->se.load; /* skip */ if (update_load && p->sched_class == &fiar_sched_class) { reweight_task(p, prio); } else { load->weight = scale_load(sched_prio_to_weight[prio]); load->inv_weight = sched_prio_to_wmult[prio]; } }
- 타임 슬라이스
- \[(time slice) = \frac{(load weight of process)}{(sum of load weight in CFS runqueue)} \time (scheduling latency)\]
- vruntime : 프로세스가 그동안 실행한 시간을 정규화한 시간 정보
- CFS 스케줄러는 런큐에 등록된 프로세스 중 vruntime이 가장 작은 프로세스를 다음에 실행할 프로세스로 선택
- update_curr()
static void update_curr(struct cfs_rq *cfs_rq) { /* skip */ u64 delta_exec; /* skip */ curr->vruntime += calc_delta_fair(delta_exec, curr); /* skip */ } static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) { if (unlikely(se->load.weight != NICE_0_LOAD)) delta = __cal_delta(delta, NICE_0_Load, &se->load); return delta; }
- \[vruntime += delta_exc * \frac{1024}{load weight}\]
- 위의 공식대로라면, 휴면상태였던 프로세스 또는 새롭게 생성된 프로세스는 CPU를 더 많이 할당받게 된다. 따라서 주기적으로 vruntime의 최소값을 설정해준다.
- update_min_vruntime()
static void update_min_vruntime(struct cfs_rq *cfs_rq) { struct sched_entity *curr = cfs_rq->curr; struct rb_node *leftmost = rb_first_cached(&cfs_rq->tasks_timeline); u64 vruntime = cfs_rq->min_vruntime; /* skip */ /* ensure we never gain time by being placed backwards. */ cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime); /* skip */ }
CFS 관련 세부 함수
타임 슬라이스 관리
- 프로세스가 타임 슬라이스를 소진했는지 점검, 프로세스의 타임 슬라이스를 업데이트, 타임 슬라이스를 모두 소진한 프로세스를 선점될 조건임을 마킹
- scheduler_tick()
void scheduler_tick(void) { int cpu = smp_processor_id(); struct rq *rq = cpu_rq(cpu); struct task_struct *curr = rq->curr; struct rq_flags rf; sched_clock_tick(); rq_lock(rq, &rf); update_rq_clock(rq); curr->sched_class->task_tick(rq, curr, 0); /* skip */ }
- task_tick_fair()
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued) { struct cfs_rq *cfs_rq; struct shced_entity *se = &curr->se; for_each_sched_entity(se) { cfs_rq = cfs_rq_of(se); entity_tick(cfs_rq, se, queued); } /* skip */ }
- eneity_tick()
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued) { update_curr(cfs_rq); update_load_avg(curr, UPDATE_TG); update_cfs_shares(curr); /* skip */ if (cfs_rq->nr_running > 1) check_preempt_tick(cfs_rq, curr); }
- eneity_tick()
- check_preempt_tick()
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) { unsigned long ideal_runtime, delta_exec; struct sched_entity *se; s64 delta; ideal_runtime = sched_slice(cfs_rq, curr); delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; if (delta_exec > ideal_runtime) { resched_curr(rq_of(cfs_rq)); /* skip */ } if (delta_exec < sysctl_sched_min_granularity) return; se = __pick_first_entity(cfs_rq); delta = curr->vruntime - se->vruntime; if (delta < e) return; if (delta > ideal_runtime) resched_curr(rq_off(cfs_rq)); }
-
- 프로세스가 소진한 타임 슬라이스 읽기
-
- 프로세스 선점 요청 : 프로세스가 타임 슬라이스를 모두 소진했으면 선점 요청을 한다.
- 프로세스가 선점 요청을 하면, 인터럽트를 핸들링한 후 또는 시스템 콜을 핸들링한 후 유저 공간으로 복귀하기 전에 프로세스가 선점된다.
vruntime 관리와 관련된 세부 함수
-
- 프로세스를 vruntime 기준으로 CFS 런큐의 레드 블랙 트리에 등록
- CFS가 다음 프로세스를 레드 블랙 트리에서 선택(pick)하는 과정
- enqueue_entity()
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED); bool curr = cfs_rq->curr == se; /* skip */ update_curr(cfs_rq); /* skip */ account_tntity_enqueue(cfs_rq, se); /* skip */ if (!curr) __enqueue_entity(cfs_rq, se); se->on_rq = 1; /* skip */ }
- __enqueue_entity()
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node; struct rb_node *parent = NULL; struct shced_entity *entry; bool leftmost = true; while (*link) { parent = *link; entry = rb_entry(parent, struct sched_entity, run_node); if (entity_before(se, entry)) { link = &parent->rb_left; } else { link = &parent->rb_right; leftmost = false; } } rb_link_node(&se->run_node, parent, link); rb_insert_color_cached(&se->run_node, &cfs_rq->tasks_timeline, leftmost); }
선점 스케줄링
- 선점 스케줄링 : CPU에서 실행 중인 프로세스를 비우고 새로운 프로세스를 CPU에서 실행시킴
선점 스케줄링 진입점
- 인터럽트 핸들링 후 : 인터럽트 핸들러를 실행한 후 실행을 멈춘 프로세스로 복귀하기 전
- 시스템 콜 핸들링 후 : 시스템 콜 함수를 처리한 후 유저 공간으로 복귀하기 직전
선점 스케줄링의 진입점:커널 모드 중 인터럽트 발생
- arm 기준
- 인터럽트가 발생하면 실행되는 __irq_svc 레이블에서 선점 스케줄링을 시작한다.:
- 인터럽트 발생 후 인터럽트 핸들러 처리
- 프로세스 선점 스케줄링 조건 점검
- 프로세스 선점 스케줄링 실행
__irq_svc
:__irq_svc: svc_entry irq_handler #ifdef CONFIG_PREEMPTION ldr r8, [tsk, #TI_PREEMPT] @ get preempt count ldr r0, [tsk, #TI_FLAGS] @ get flags teq r8, #0 @ if preempt count != 0 movne r0, #0 @ force flags to 0 tst r0, #_TIF_NEED_RESCHED blne svc_preempt #endif svc_exit r5, irq = 1 @ return from exception UNWIND(.fnend ) ENDPROC(__irq_svc)
- 책은 라즈베리 파이를 기준으로 하는데, 책에서는 CONFIG_PREEMPT가 비활서오하 되어 있어서, 커널 코드가 실행되는 도중 인터럽트가 발생했을 때 선점 스케줄링을 시도하지 않는다.
- 하지만 대부분의 상용 리눅스 시스템에서는 CONFIG_PREEMPT가 활성화 되어 있다고 한다.
- 어셈블리를 해석해보면 r8 이 0이고 r0 & TIF_NEED_RESCHED 이 0이 아니라면
svc_preempt()
를 호출하는 구조이다. svc_preempt
:#ifdef CONFIG_PREEMPTION svc_preempt: mov r8, lr 1: bl preempt_schedule_irq @ irq en/disable is done inside ldr r0, [tsk, #TI_FLAGS] @ get new tasks TI_FLAGS tst r0, #_TIF_NEED_RESCHED reteq r8 @ go again b 1b #endif
- 그냥 preempt_schedule_irq 호출 하고 return하는 코드들이다.
- 여기까지가 architecture별로 다른 부분이고 preempt_schedule_irq() 부터는 다시 공통 c 코드로 돌아간다.
- 인터럽트가 발생하면 실행되는 __irq_svc 레이블에서 선점 스케줄링을 시작한다.:
- x86 기준:
- 잡담 : 문서를 읽기 싫어서 사고 과정이 다음과 같았습니다. preempt_schedule_irq()는 공통 코드니까 이걸 호출하는 부분을 x86에서 찾으면 되겠다.
- 근데 common 을 사용하는 거 같기는 한데 정확히는 못찾았다. ㅠㅠ 일단 한시간 동안 이걸 찾는데 실패했으니 오늘은 여기까지