reference:
- "리눅스 커널 내부구조" / 백승재, 최종무
- "Operating Systems: Three Easy Pieces" / Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
이전 포스팅은 사용자 입장에서 프로세스와 쓰레드를 어떻게 생성하는지 살펴본 글이다. 커널에서는 이러한 객체들을 어떻게 구현하는 살펴본다.
프로세스는 자신이 사용하는 '자원'과 그 자원에서 수행되는 '수행 흐름'으로 구성된다. 그리고 리눅스에서는 이를 관리하기 위해 각 프로세스마다 task_struct라는 자료 구조를 생성한다. 만약 프로세스에서 새로운 쓰레드를 하나 생성하면 새로운 쓰레드를 위해 task_struct 자료 구조를 하나 더 생성한다. 결국 리눅스에서는 프로세스가 생성되든 쓰레드가 생성되든 task_struct라는 동일한 자료 구조를 생성하여 관리한다. => "1대1 모델"
(단지 task_struct 자료 구조 중에서 수행 이미지를 공유하는가, 같은 쓰레드 그룹에 속해 있는가 등의 여부에 따라 프로세스 또는 쓰레드로 사용자에게 해석되는 차이가 있을 뿐이다.)
이러한 1대 1 모델은 기존의 운영체제와 다른 리눅스 특유의 태스크 개념을 유도한다.
프로세스가 수행되려면 자원과 수행 흐름(flow of control)이 필요한데, 기존 운영체제 연구자들은 자원을 태스크로 제어 흐름을 쓰레드로 정의하였다.
반면, 리눅스에서는 프로세스이든 쓰레드이든 커널 내부에서는 '태스크'라는 객체로 관리된다. 태스크가 관리하는 "자원을 어떻게 공유하고 접근 제어하느냐"에 따라 프로세스 또는 쓰레드로 해석된다.
추가로 이러한 특성은 함수들이 구현된 방식에서도 나타난다.
fork()는 프로세스를 생성하는 함수이고, clone은 쓰레드를 생성하는 함수인데 커널 내부에서 마지막으로 호출되는 함수는 do_fork()로 동일하다. 이는 리눅스 입장에서 본다면 모두 태스크를 생성하는 것이기 떄문이다. do_fork()를 호출할 때 함수의 인자로 부모 태스크와 얼마나 공유할지(공유 정도)를 정함으로써 fork(), clone()을 지원한다.
do_fork(): 새로 생성되는 태스크를 위해 일종의 이름표를 하나 준비. '새로 생성된 태스크의 이름', '생성된 시간', '부모 이름' 등 매우 상세한 정보 기록. 이 이름표를 리눅스의 용어로 바꾼다면 task_struct 구조체이다. 이 후 태스크가 수행되기 위해 필요한 자원 등을 할당한 뒤 수행 가능한 상태로 만들어 줌.
시스템에 존재하는 모든 태스크는 유일하게 구분 가능해야 한다. 태스크 별로 유일한 이 값은 task_struct 구조체 내의 pid 필드에 담긴다. 그런데 POSIX 표준에는 '한 프로세스 내의 쓰레드는 동일한 PID를 공유해야 한다.'라고 명시되어 있다. 리눅스에서는 이를 위해 tgid(Thread Group ID)라는 개념을 도입했다.
pid, tgid 부여 흐름
태스크가 생성되면 이 태스크를 위한 유일한 번호를 pid로 할당.
- 프로세스 생성: 생성된 태스크의 tgid 값을 새로 할당된 pid 값과 동일하게 넣어줌. 따라서 tgid 값도 유일한 번호를 갖음.
- 쓰레드 생성: 부모 쓰레드의 tgid 값과 동일한 값으로 생성된 태스크의 tgid를 결정. 결국 부모 태스크와 자식 태스크는 동일한 tgid를 갖게 되며 동일한 프로세스에 속해 있는 것으로 해석.
getpid() 함수의 진실
task_struct 내 tgid 값을 출력하는 함수는 여태껏 우리가 pid를 출력해 준다고 알고 있던 getpid() 함수. 실제로 task_struct 내 pid를 출력해 주는 함수는 gettid().
태스크는 pid, tgid 외에도 훨씬 많은 정보들이 필요하다. 예를 들어 아래 몇가지들이 있다.
운영체제 연구자들은 태스크와 관련된 이러한 '모든' 정보를 문맥(Context)라고 부른다.
시스템 문맥(system context): 태스크의 정보를 유지하기 위해 커널이 할당한 자료구조들.
ex) task_struct, 파일 디스크립터, 파일 테이블, 세그먼트 테이블, 페이지 테이블 등.
메모리 문맥(memory context): 텍스트, 데이터, 스택, 힙 영역, 스왑 공간(swap space) 등.
하드웨어 문맥(hardware context): 문맥 교환(context switch)시 태스크의 현재 실행 위치에 대한 정보를 유지하며, 쓰레드 구조 또는 하드웨어 레지스터 문맥이라고 부름. 실행 중인 태스크가 대기 상태나 준비 상태로 전이할 때 이 태스크가 어디까지 실행했는지 기억해 두는 공간.
task identification
: 태스크를 인식하기 위한 변수들. pid(태스크 id를 나타냄), tgid(태스크가 속해있는 쓰레드 그룹 id를 나타냄), pid를 통해 해당 태스크의 task_struct를 빠르게 찾기 위한 해쉬 관련 필드 등의 변수. 이 밖에 사용자 접근 권한을 제어할 때 사용하는 변수들 존재.
state(프로세스 상태)
: 태스크가 생성에서 소멸까지 거치는 많은 상태를 관리 하기 위한 state 변수. TASK_RUNNING(0), TASK_INTERRUPTIBLE(1), TASK_STOPPED(4) 등과 같은 값이 들어감.
task relationship
: 생성된 태스크의 가족 관계에 대한 정보. 현재 태스크를 생성한 부모 태스크의 task_struct 구조체를 가리키는 real_parent 필드, 현재 부모 태스크의 task_struct 구조체를 가리키는 parent 필드 존재. 또한 자식과 형제를 리스트로 연결한 뒤 그 리스트의 헤드를 각각 children, sibling 필드에 저장.
sheduling information
: taks_struct에서 스케줄링과 관련된 변수는 prio, policy, cpus_allowed, time_slice, rt_priority 등이다.
signal information
: task_struct에서 시그널 관련된 변수는 signal, sighand, blocked, pending 등이다.
시그널(signal): 태스크에게 비동기적인 사건의 발생을 알리는 메커니즘.
memory information
: 태스크의 명령, 함수 그리고 데이터를 저장하는 텍스트, 데이터, 스택, 힙 공간 등에 대한 위치와 크기, 접근 제어 정보 등을 관리하는 변수들 존재. 또한 가상 주소를 물리 주소로 변환하기 위한 페이지 디렉터리와 페이지 테이블 등의 주소 변환 정보들도 task_struct에 존재. 이러한 정보들은 mm_struct라는 이름의 변수로 접근할 수 있다.
file information
: 태스크가 오픈한 파일들을 file_struct 구조체 형태인 files라는 이름의 변수로 접근할 수 있다.
참고로 루트 디렉터리의 inode와 현재 디렉터리의 inode 또한 fs_struct 구조체 형태인 fs라는 변수로 접근할 수 있다.
thread structure
: 쓰레드 구조(또는 하드웨어 레지스터 문맥)는 문맥 교환시 태스크가 현재 어디까지 실행되었는지 기억해 놓는 공간.
time information
: 태스크의 시간 정보를 위한 변수로는 태스크가 시작된 시간을 나타내는 start_time, real_time 등이 있으며, 사용한 CPU 시간의 통계를 담는 필드도 있음.
format
: personality(BSD, SVR4 커널에서 컴파일된 프로그램을 재컴파일 없이 수행하기 위해 필요한 변수)
resource limits
: rlim_max(최대 허용 자원의 수), rlim_cur(현재 설정된 허용 자원의 수)