2022. 9. 19. 01:09ㆍ개발 관련 책 읽기/리눅스 커널 내부구조
프로세스 또는 쓰레드마다 task_struct라는 자료 구조가 필요함을 배웠다.
리눅스 커널은 태스크들마다 이러한 정보들을 관리하고 있다.
사실 태스크마다 필요한 정보는 훨씬 많다. 예를들어) 파일 디스크립터,시그널,CPU 사용량 등 많은 자원들의 정보를 관리 해야 한다. 이러한 모든 정보를 문맥이라고 부른다
태스크의 문맥은 크게 3가지로 분류가 된다.
1. 시스템 문맥 : 태스크의 정보를 유지하기 위한 커널이 할당한 자료구조들이다
(task_struct,파일 디스크립터,파일 테이블,세그먼트 테이블,페이지 테이블 등등..)
2.메모리 문맥 : 텍스트,데이터,스택,힙,스왑 공간 등이 여기에 포함된다.
3.하드웨어 문맥으로 문맥 교환(context switching)할 때 태스크의 현재 실행 위치에 대한 정보를 유지하며,쓰레드 구조 또는 하드웨어 레지스터 문맥이라고 불린다. 이 부분은 실행 중이던 태스크가 대기 상태나 준비 상태로 전이할 때 태스크가 어디까지 실행했는지 기억해두는 공간으로,이후 다시 실행될때 저장해 두었던곳 부터 다시 시작하게 된다.
디스크 상태 전이에 대해서 알아보자
태스크가 생성되면 그 태스크는 준비상태(TASK_RUNNING)가 된다.스케줄러는 여러 태스크 중에서 실행시킬 태스크를 선택하여 수행시킨다.
따라서 TASK_RUNNING 상태는 구체적으로 준비(Ready)상태와 실제CPU를 할당받아 명령어들을 처리하고 있는 실행(running)상태 두 가지로 나뉘게 된다.
실행 상태에 있는 태스크들은 발생하는 사건에 따라 상태가 바뀌게된다
1.태스크가 자신이 해야 할 일을 다 끝내고 exit()를 호출하면 TASK_DEAD상태로 전이된다.
=>구체적으로는 task_struct 구조체 내에 존재하는 exit_state 값과 조합하여 EXIT_ZOMBIE 상태로 전이된다. ZOMBIE 상태는 말 그대로 죽어 있는 상태로, 태스크에게 할당되어 있던 자원을 대부분 커널에게 반납한 상태이다. 그러나 자신이 종료된 이유(Error 번호),자신이 사용한 자원의 통계 정보 등을 부모 태스크에게 알려주기 위해 유지 되고 있는 상태이다. 추후 부모 태스크가 wait() 함수를 호출하면 자식 태스크의 상태는 TASK_DEAD(EXIT_DEAD)상태로 바뀌게 되며, 부모는 자식의 종료 정보를 넘겨받게 된다.그런 뒤 TASK_DEAD 상태의 자식 태스크는 자신이 유지하고 있던 자원을 모두 반환하고 최종 종료된다.
만약 부모 태스크가 자식 태스크에게 wait()등의 함수를 호출하기 전에 먼저 종료되면 어떻게 될까?
부모가 없는 상태 즉,고아 태스크의 부모를 init 태스크로 바꾸어 주며 init 태스크가 wait함수를 호출할 때 고아 태스크는 최종 소멸된다.
2. 실행(TASK_RUNNING)상태에서 실제 수행되던 태스크가 자신에게 할당된 CPU시간을 다 사용했거나, 보다 높은 우선순위를 가지는 태스크로 인해 준비 (Ready)상태로 전환되는 경우이다.
3. SISSTOP,SIGTSTP,SIGTTIN,SIGTTOU 등의 시그널을 받은 태스크는 TASK_STOPPED 상태로 전이되며, 추후 SIGCONT 시그널을 받아 다시 TASK_RUNNING(Ready)상태로 전한된다.
한편 디버거의 ptrace() 호출에 의해 디버깅 되고 있는 태스크는 시그널을 받는 경우 TASK_TRACED
상태로 전이 될 수 있다.
4. 실행(Running) 상태에 있던 태스크가 특정한 사건을 기다려야 할 필요가 있으면 대기 상태
(TASK_INTERRUPTIBLE,TASK_UNINTERRUPIBLE)로 전이한다.
예를 들면 주변 장치에 요청을 보내고 기다리거나(I/O),사용 중인 시스템 자원 대기 등이다.
대기 상태로 전이한 태스크는 event에 따라 특정 큐에 대기 하게 된다.
스케줄러가 다시 호출되어 준비상태에 있는 태스크 중 하나를 선택하여 다시 실행상태로 만든다
CPU의 효율을 높이는 좋은 방법이다
실행 중인 TASK_RUNNING(running)상태의 태스크는 실행 권한에 따라 사용자 수준 실행 상태(
유저 레벨 = ring 3)와 커널 수준 실행 상태(커널 레벨 = ring 0)로 구분 할 수 있다.
사용자 수준 실행 상태는 CPU에서 사용자 수준 프로그램의 제작자가 만든 응용 프로그램이나 라이브러리 코드를 수행하고 있는 상태로, 당연히 사용자 수준의 권한으로 동작한다.
반면 커널 수준 실행 상태는 CPU에서 커널 코드의 일부분을 수행하고 있는 상태로, 사용자 수준 권한보다는 더 강력한 커널 권한으로 동작한다.
커널 수준 권한이 더 강력하다는 의미는 사용자 수준 권한에서는 접근이 금지된 커널 내부 자료구조를 접근하거나 수행이 금지된 특권 명령어를 커널 수준 권한에서 수행할 수 있다는 의미이다.
유저 레벨에서 커널 레벨로 전이 할수 있는 방법은 두가지가 있다.
1.시스템 콜 사용
태스크가 시스템 콜을 요청하면 리눅의 커널에 트랩이 걸리게 되고 그 결과 태스크의 상태가 커널 수준 실행 상태로 바뀌게 되며 커널의 시스템 콜 처리 루틴으로 제어가 넘어간다.
2.인터럽트 발생
인터럽트 발생시 커널에 인터럽트가 걸리게 된다. 이때 실행 중이던 태스크가 사용자 수준에서 동작하고 있었다면 커널 수준으로 바뀌게 되고, 커널의 인터럽트 처리 루틴으로 제어가 넘어가게 된다.
리눅스 커널은 태스크가 생성될 때 마다 태스크 별로 8KB 혹은 16KB의 스택을 할당해 준다.
A라는 태스크가 시스템 콜을 요청했다면 이를 처리하기 위해 커널은 A에게 할당해 주었던 커널 스택을 사용하여 요청된 작업을 수행해준다.
결론적으로 태스크가 생성되면 커널은 task_struct구조체와 커널 스택을 할당하게 된다
태스크 당 할당되는 커널 스택은 thread_union이라 불리며, thread_info 구조체를 포함하고 있다.
(리눅스에서 thread_info를 프로세스 디스크립터라 부르기도 한다.)
thread_info 구조체 안에는 task_struct를 가리키는 포인터와 스케줄링의 필요성 여부를 나타내는 플래그,태스크의 포맷을 나타내는 exec_domain 등의 필드가 존재한다
만약 태스크가 시스템 콜 로인해 커널 수준 실행 상태로 진입한 뒤,수행해야 할 일을 모두 마쳤다면, 다시 사용자 수준 으로 돌아가서 다시 작업을 시작해야 한다.
그러기 위해서 커널과 유저 레벨간의 변화시에 현재까지의 작업 상황을 어딘가에 저장해 놓아야 한다.
pt_regs라는 공간에 저장하게 된다.
pt_regs의 대략적인 구조이다.
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
.....
}
커널이 시스템 콜의 서비스를 완료하거나 인터럽트 처리를 완료하면 커널 수준 실행 상태에서 사용자 수준으로 바뀌게 된다. 이때 커널은 몇 가지 중요한 일들을 처리한다
- 커널은 현재 실행 중인 태스크가 시그널을 받았는지 확인하며, 받았다면 필요한 경우 시그널 처리 핸들러를 호출한다
- 다시 스케줄링 할 필요가 있다면(현재 task의 need_reshed 플래그가 1로 set되어 있는 경우)스케줄러를 호출한다.
- 커널에서 연기된(delayed)루틴이 존재하면 수행한다.
'개발 관련 책 읽기 > 리눅스 커널 내부구조' 카테고리의 다른 글
Chapter 4 - 메모리 관리(1) (0) | 2022.09.19 |
---|---|
Chapter 3 - 태스크 관리(4) (0) | 2022.09.19 |
Chapter 3 - 태스크 관리(2) (0) | 2022.09.19 |
Chapter 3 - 태스크 관리(1) (0) | 2022.09.19 |
Chapter 0 - 운영체제란 (0) | 2022.09.19 |