커널이 프로세스에게 상태 정보를 설정하는 이유
- 다수의 프로세스를 잘 관리하기 위해
- 간단한 베어메탈 시스템에서는 상세한 프로세스의 상태 정보
가 필요하지 않음
커널이 프로세스에게 설정하는 상태 정보
https://elixir.bootlin.com/linux/v5.15.30/source/include/linux/sched.h
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000 //
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
프로세스가 슬립 상태일 때
- TASK_INTERRUPTIBLE 0x0001 // 프로세스가 슬립에 진행할 때
- TASK_UNINTERRUPTIBLE 0x0002 // 슬립이 깨어날 조건을 명시
프로세스가 실행 대기 상태일 때
- TASK_RUNNING 0x0000 // 런큐에 큐잉할때 이런 상태에 돌입할 때가 있음
프로세스가 'current 프로세스 상태'(CPU 코어에서 실행)
- 런큐: rq->curr //런큐에 큐잉이 되지않으면 cpu코어에서 실행되지 않는다
프로세스 상태가 저장되는 필드
(struct task_struct *) (struct task_struct*)0xFFFFFF805D66DAC0 -> (
(struct thread_info) thread_info = (
(long unsigned int) flags = 0x0,
(u64) preempt_count = 0x0000000100000001,
(struct) preempt = ((u32) count = 0x1, (u32) need_resched = 0x1)),
(unsigned int) __state = 0x0, // 0의 의미 : 태스크 러닝
(void *) stack = 0xFFFFFFC009328000,
(refcount_t) usage = ((atomic_t) refs = ((int) counter = 0x1)),
(unsigned int) flags = 0x00400100,
(unsigned int) ptrace = 0x0,
(int) on_cpu = 0x1,
(struct __call_single_node) wake_entry = ((struct llist_node) llist = ((struct llist_node *) nex
(unsigned int) cpu = 0x2,
프로세스
- 생성이 되자마자 태스크 러닝상태로, 런큐에 큐잉
- 런큐에 프로세스가 생성되면 우선순위 점검하고 cpu실행(current 프로세스)
- 슬립에 진입하면 태스크 인터럽트블 , 슬립에 있다가 다시 실행을 하게 되면 태스크 러닝, 런큐에 큐잉
태스크 스케줄링 관점으로 프로세스의 상태란
- 런큐에 실행 대기(TASK_RUNNING) 상태에 있는 프로세스 중 가
장 우선 순위가 높은(vruntime 가장 작은) 하나를 선택해서 CPU
실행(TASK_RUNNING) 상태로 바꿔주는 동작
- 태스크 스케줄링 관련 커널 API는 프로세스의 상태를 보고 실행
대기 중인 프로세스를 어떤 방식으로 실행할지를 결정
- 프로세스의 상태 정보는 중요
wake_up_new_task() 함수
- 프로세스가 생성된 직후 커널은 생성한 프로세스의 상태를 실행 대기(TASK_RUNNING)
상태로 변경
- 프로세스 상태 변경 관련 커널 API
https://elixir.bootlin.com/linux/v5.15.30/source/kernel/sched/core.c
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);
WRITE_ONCE(p->__state, TASK_RUNNING); // 태스크 디스크립터의 스태이트에 접근
yield() 함수
- yield() 함수에서도 프로세스의 상태를 실행 대기(TASK_RUNNING)로 변경
https://elixir.bootlin.com/linux/v5.15.30/source/kernel/sched/core.c
void __sched yield(void)
{
set_current_state(TASK_RUNNING); // 태스크 러닝 상태로 변경
do_sched_yield();
}
EXPORT_SYMBO
__schedule() 함수
https://elixir.bootlin.com/linux/v5.15.30/source/kernel/sched/core.c
static void __sched notrace __schedule(unsigned int sched_mode)
{
...
cpu = smp_processor_id();
rq = cpu_rq(cpu);
...
if (likely(prev != next)) {
rq->nr_switches++;
RCU_INIT_POINTER(rq->curr, next); // rq = 런큐, curr : next의 주소를 저장, 스케줄링으로 다음의 cpu에서 실행할 태스크 디스크립터, 이 코드가 실행하게되면 이 프로세스는 cpu코어를 점유하면서 실행할 프로세스이므로 current 프로세스로 바뀐다
++*switch_count;
}
}
__schedule() 함수 분석
- 런큐 구조체의 curr 필드에 next(프로세스의 태스크 디스크립터 주소)를 저장
- 런큐 구조체의 curr 필드에 있는 프로세스는 CPU를 점유하면서 실행
- 이를 current 프로세스로 명시
- 커널 개발자가 분석하는 커널 코드를 실행하는 프로세스의 태스크 디스크립터는 런큐를
나타내는 rq 구조체의 curr 필드에서 확인
current 프로세스의 정체
crash64> runq -m // 태스크디스크립터 주소 확인 가능,
CPU 0: [0 01:10:11.687] PID: 0 TASK: ffffffd17553fac0 COMMAND: "swapper/0"
CPU 1: [0 01:10:11.687] PID: 0 TASK: ffffff80402b1e40 COMMAND: "swapper/1"
CPU 2: [0 00:00:00.000] PID: 1591 TASK: ffffff805d66dac0 COMMAND: "bash"
CPU 3: [0 01:10:11.686] PID: 0 TASK: ffffff80402b5ac0 COMMAND: "swapper/3"
crash64> p runqueues // 런큐라는 변수를 확인하기위해
PER-CPU DATA TYPE: // percpu타입
struct rq runqueues;
PER-CPU ADDRESSES:
[0]: ffffff80fb788bc0
[1]: ffffff80fb7a4bc0
[2]: ffffff80fb7c0bc0 // cpu2의 자료구조에 해당하는 cpu의 시작주소
[3]: ffffff80fb7dcbc0
crash64> struct rq.curr ffffff80fb7c0bc0 // 런큐의 curr에 저장된 cpu를 점유하면서 실행하고있는 프로세스의 태스크 디스크립터를 확인 가능하다.
curr = 0xffffff805d66dac0,
프로세스 상태가 TASK_INTERRUPTIBLE(슬립)로 바뀔 때 호출되는 함수 목록
- wait_event_interruptible() 함수
- do_sigtimedwait() 함수
- __arm64_sys_pause() 함수
wait_event_interruptible() 함수
- wait_event_interruptible() 함수를 호출하면 휴면 상태(TASK_INTERRUTIBLE)로 프
로세스의 상태를 바꿈
- wait_event_interruptible() 함수를 호출하면 웨이트 큐에서 웨이트 큐 이벤트가 실행
될 때까지 기다림
https://elixir.bootlin.com/linux/v5.15.30/source/include/linux/wait.h
#define __wait_event_interruptible(wq_head, condition) \
___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
schedule())
#define wait_event_interruptible(wq_head, condition) \
({ \
int __ret = 0; \
might_sleep(); \
if (!(condition)) \
__ret = __wait_event_interruptible(wq_head, condition); \
__ret; \
}
§ __arm64_sys_pause() 함수
https://elixir.bootlin.com/linux/v5.15.30/source/kernel/signal.c
SYSCALL_DEFINE0(pause)
{
while (!signal_pending(current)) {
__set_current_state(TASK_INTERRUPTIBLE);
schedule();
}
return -ERESTARTNOHAND;
}
- 프로세스의 상태를 TASK_INTERRUPTIBLE(슬립)로 바꿈
프로세스의 상태를 TASK_UNINTERRUPTIBLE로 변경
- io_wait_event()
- mutex_lock()
- usleep_range()
- msleep()
- ait_for_completion()
프로세스 상태태를 바꿀 때 주의 사항
- set_current_state() 함수를 사용 권장
- 코드의 실행 순서가 매우 중요
set_current_state() 함수
crash64> hex
output radix: 16 (hex)
crash64> dis yield
0xffffffd174d50784 <yield>: mov x9, x30
…
0xffffffd174d50794 <yield+0x10>: mrs x0, sp_el0
0xffffffd174d50798 <yield+0x14>: mov x29, sp
0xffffffd174d5079c <yield+0x18>: str wzr, [x0, #16]
0xffffffd174d507a0 <yield+0x1c>: dmb ish // 배리어 명령어, 아래에 있는 코드가 배리어 위쪽으로 실행되지 않는다.
0xffffffd174d507a4 <yield+0x20>: bl 0xffffffd1742c4bd0 <do_sched_yield>
…
https://elixir.bootlin.com/linux/v5.15.30/source/kernel/sched/core.c
void __sched yield(void)
{
set_current_state(TASK_RUNNING);
do_sched_yield(); // 최적화를 위해서 명령어 실행 순서를 바꿀 수 있다.
}
EXPORT_SYMBOL(yield);
set_current_state() 함수
https://elixir.bootlin.com/linux/v5.15.30/source/kernel/exit.c
void __noreturn do_exit(long code)
{
...
if (unlikely(tsk->flags & PF_EXITING)) {
pr_alert("Fixing recursive fault but reboot is needed!\n");
futex_exit_recursive(tsk);
set_current_state(TASK_UNINTERRUPTIBLE);
schedule();
}
set_current_state() 함수
#define TASK_UNINTERRUPTIBLE 0x0002
0xffffffd17428c8e0 <do_exit>: mov x9, x30
0xffffffd17428c8e4 <do_exit+0x4>: nop
0xffffffd17428c8e8 <do_exit+0x8>: paciasp
...
0xffffffd17428d25c <do_exit+0x97c>: mov w1, #0x2 // x1레지스터를 4바이트 단위로
0xffffffd17428d260 <do_exit+0x980>: mrs x0, sp_el0
0xffffffd17428d264 <do_exit+0x984>: str w1, [x0, #16] // 16 : 태스크 디스크립터의 시작주소 기준으로 state라는 프로세스의 상태를 나타내는 필드가 위치한 주소 오프셋정보
0xffffffd17428d268 <do_exit+0x988>: dmb ish