서울대학교 컴퓨터공학과 김진수 교수님의 "고급 운영체제" 강의를 필기한 내용입니다.
다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.
Process
- 실행중인 프로그램 - instance of a program in execution
- instance 인 이유는 하나의 process 가 여러 process 를 만들 수 있고 이 각각이 다 별개이기 떄문
- Program: 실행 파일, Process: 실행중인 program, Processor: process 를 담당 hw = CPU
- Process 는 여러 정보를 담고 있음
- 뭐 cpu register 정보나 pid, state 등
- 이 정보를 담고 있는 자료구조를 Process Control Block (PCB), process descriptor, (윈도우) task descriptor 라고 한다
Thread
- 원래 UNIX 에는 thread 가 없었고, 이후 80년대쯤에 추가되었다
- 용어 자체는 오래 전부터 사용되었음 - 실행되는 instruction 의 흐름을 “thread of control” 라고 불렀는데 여기서 이름을 가져온 것
- 원래는 이런 흐름이 process 당 하나밖에 없었지만, 이것을 process 내에 여러개를 두자
- 기존에는 동시에 여러 일을 해야 할 때에는
fork
를 통해 프로세스를 여러개 만드는 방법을 사용해야 했었지만 - 근데 이 작업에는 PCB 나 memory 공간 전체를 모두 복사하는 등의 오버헤드들이 크기 때문에 더 가벼운 동시처리가 필요
- 그래서 thread 가 나온 것
- 처음에는 default thread 가 있고
pthread
라이브러리 등을 이용해 thread 를 생성해 새로운 흐름을 만들 수 있다 - 다른 것은 thread 끼리 모두 공유하지만 이것 세개는 분리해서 각자의 context 를 만든다
- Thread ID
- Register 값
- Memory 의 stack 영역
- 장점
- 동시성 처리 가능
- 코드도 깔끔해진다 -
fork
시절에는 pid 로 parent / child 구분하는 코드가 추가돼야하기때문시 - 뭐 갖가지 성능이 좋아진다
- 대표적으로는 IO 로 block 되는 동안 다른 thread 를 실행시켜 다른 처리하는 등
- 공유하는 공간(대표적으로는 메모리) 이 있기 때문에 서로간의 통신도 용이해진다.
fork
시절에는 IPC 사용- 물론 이건 알다시피 양날의 검이다? 아니 모르는데?
- Multi-core 환경에서 사용하기에도 용이띠
- Parallel programming 을 구현하는 방법은 결국에는 fork 아니면 thread 다
- OpenMP 도 결국에는 thread 로 작동한다.
- 이 비유 좋다 - Process 는 집이고 thread 는 그 안에서 돌아댕기는 사람과 유사하다.
- 집이라는 공유 공간에서 사람들이 각자의 문맥을 가지며 작업을 함
- Code, data, heap 은 처음에 exec 할 때 메모리로 올라가고
- Thread 간에는 이것들을 공유
- Thread 를 생성하면 그때마다 stack 을 메모리에 새로 생성
- 즉, 위처럼 된다
- Program Counter (PC) 는 모든 thread 가 공유된 code 를 향하고 있고
- Stack Pointer (SP) 는 thread 각각 다른 stack 을 참조하는
- 가로는 몇개의 process 가 동시에 존재할 수 있나
- 왼쪽위의 경우에는 fork 라는 개념 없이 그냥 프로세스 하나만 존재
- 오른쪽위의 경우에는 fork 로 프로세스 생성 가능 - 물론 core 가 하나여서 running 중인 것은 하나일 수는 있지만 어쨋든
- 세로는 몇개의 thread 가 동시에 존재할 수 있나
- 왼쪽아래의 경우에는 embedded os 에서 최대한 가볍게 하기 위해 1개의 프로세스만 사용하고 여기에 thread 를 여러개 생성하는 방법을 사용한다고 한다
Linux Task
- Process 와 thread 를 합쳐서 부르는 개념
- 이것은 linux 가 제대로 된 thread 를 제공하지 않기 때문
- 정석적으로는, Kernel level thread 의 경우에는 syscall 로 thread 를 생성해 kernel 이 알고 있음
- 이 경우에는 process 를 위한 정보와 thread 를 위한 정보를 저장할 공간이 모두 필요
- 즉, PCB 에 추가적으로 Thread Control Block (TCB) 같은게 필요
- 그리고 각 TCB 는 PCB 에 linked ilst 같은것으로 묶여서 process 안에 thread 가 속해있다는 것을 계층적으로 나타내면 베스트이다.
- 하지만 linux 는 이렇게 구현되지 않았다.
- 시발즈씨는 linux 에 PCB 와 유사한 목적으로
task_struct
를 구현해 놓았다. - 그리고 이놈으로 thread 까지 구현해 버리는데..
- 시발즈씨는 linux 에 PCB 와 유사한 목적으로
task_struct
- 위 그림이
task_struct
의 대략적인 구조인데- 보면
task_struct
는 대부분의 field 들이 pointer 로 되어 있어 여기의 field 가 다른 자료구조를 가리키게 되어 있다. - Process 를 fork 를 할 때에는
task_struct
를 새로 만들고, 여기에 딸려 있는 다른 자료 구조도 전부 새로 만든 다음 내용을 copy 하는 식으로 진행된다. - Thread 를 만들 때에는 마찬가지로
task_struct
를 새로 하나 만들되 여기에 딸려 있는 것들을 새로 만들지 않고 pointer 로 기존의 것을 가리키게만 구현한다. - 따라서 fork 와 threading 모두
clone
syscall 하나로 처리한다고 하네- #draft 더블체크 필요
- 보면
task_struct
의 몇가지 필드를 봐보자.thread_info
는 process 인지 thread 인지 구분하는 flag 및 그 외 여러 상태값들이 저장된다.tasks
는 다른task_struct
를 가리키는 포인터로, 모든task_struct
는 circular doubly-linked list 로 연결되어 있다고 한다.
task_struct
는 자주 사용되기도 하고 크기가 고정되어 있어 미리 mem 공간에 확보해 놨다가 요청하면 바로바로 주게 하는 방법을 사용한다 - 이것은 Slab Allocator 라고 한다./proc/slabinfo
에 보면 몇개가 만들어져 있고 몇개가 남았는지 등의 정보가 뜬다- 뭐 참고로
task_struct
이외에도 자주사용되지 않는 struct 도 pool 을 만들어 관리한다고 한다.
현재 task 위치 찾기
- 현재의 task 를 찾는 방법은 CPU arch 에 따라 다르며, 현재 x86 의 경우에는 그냥 단순하게
current_task
라는 per-CPU variable 을 사용한다고 한다.- 이
current_task
변수는get_current()
매크로로 가져올 수 있다. - syscall 나 exception 이 걸리면 지금 실행중인 task 가 걸었을 것이므로 이 값은 유효한 값으로서 사용할 수 있지만,
- 하지만 interrupt 가 걸렸을 때의 interrupt handler 는 task 가 아니기 때문에 current 가 invalid 하다 - 이 값은 (scheduler 를 제외하고는) 사용되어서는 안된다고 한다.
- 이
- 그 이전에는 아래와 같았다고 함.
- Kernel stack 은 32bit 시절에는 8KB, 64bit 에서는 16KB (4 페이지 크기) 사이즈 크기를 가진다고 한다.
- 두배가 된 이유는 pointer addr 크기가 두배가 되었으니까
- 이 kernel stack 의 아래에
thread_info
를 저장하게 된다.- Kernel stack 은 상위 주소에서 하위 주소로 자라기 때문에
thread_info
는 최하단 주소의, kernel stack 공간 최상단에 있게 된다.- kernel stack pointer (SP) 의 하위 12bit 를 지우면
thread_info
가 나오게 된다. - 그리고 여기에 들어가면 thread 에 대한 task struct 로 이동할 수 있게 포인터로 연결된다.
Creating task_struct
Process task_struct
sys_fork()
->_do_fork()
가 호출- 여기서는 우선
copy_process()
가 호출되는데 그 안에서dup_task_struct()
로 kernel stack 을 만들고task_struct
를 하나 할당받는다.sched_fork()
: Scheduler 관련 자료구조 초기화copy_files()
,copy_fs()
,copy_sighand()
,copy_signam()
,copy_mm()
,copy_thread_tls()
: 여러 자료구조들 값 복사alloc_pid()
: PID 할당attach_pid()
: parent 의 PID hash table 에 child PID 를 집어넣는다
wake_up_new_task()
->activate_task()
를 해서 runqueue 에 넣는다.
Thread task_struct
- Thread 를 새로 생성할 때에는
- 마찬가지로 task struct 를 새로 만드는데
- 값들을 복사하는 것이 아닌 shallow copy 로 주소만 복사해온다
- Thread 를 새로 생성하는 syscall 은
clone()
으로, 여기에flags
arg 로 어떤 값들을 share 할 지 명시하게 됨
Linux task 의 POSIX 호환성…
- POSIX (Portable Operating System Interface) 는 OS 표준 인터페이스 인데
- POSIX 에서는 thread 에 대해 share 하지 말아야 할 것들만 정의하고 나머지는 모두 share 한다로 정의
- 근데 Linux thread 에서는 반대로 share 할 것들만 정의하고 나머지는 전부 share 하지 않는다는 식으로 정의해 놓았다.
- 이것이 딱 맞아 떨어지면 좋지만 아쉽게도 그렇지 않았다..
- 가령 이전의 linux thread 에서는 pid 가 thread 마다 각기 달랐다고 한다.
- 이 호환성을 위해 IBM 에서는 갈아없는 선택을, redhat 은 최소한의 변경만 하기로 하고 개발에 나섰으나,, 결론적으로는 readhat 이 이김
Thread Group
- 하나의 process 에 속하는 thread 들을 linux 에서는 Thread Group 이라고 지칭하며 process 전체에 대한 syscall 이 왔을 때 동일하게 처리한다.
- 대충 이런식이다.
- Thread 의
task_struct
들을 thread group 으로 묶고 이 안의 leader 를 정한다. - 그리고
get_pid()
syscall 이 왔다고 해보자. - 그럼 이 leader 의 PID 를 반환하는 식으로 thread group 내의 모든 thread 들이 동일한 응답을 하게 된다.
- Thread 의
- 우선적으로는 제일 먼저 생성된
task_struct
가 leader 가 되며,- Leader 가 죽으면 딴 leader 를 새로 뽑고
exec()
syscall 에 대해서는 이 leader 만 제외하고 전부 삭제한 뒤에 leader 에서exec()
의 인자로 들어온 프로그램이 실행되도록 한다.
One-to-one, Many-to-one
여기 내용에 대해서는 17. Scheduler Activation 강의록을 좀 더 참고하자.
- User level thread 와 kernel level thread 가 1:1 로 매핑된다는 것인데
- Process 의 경우에 kernel 에 exec context 를 만들고 이것을 user level 로 올려서 실행하다가 syscall 하면 다시 kernel level 로 들어오는 흐름을 가진다.
- 즉, kernel context 와 이에 대응되는 user context 가 있게 되는 것.
- Thread 에서도 이와 동일하게 하자는 아이디어인 셈.
- User level thread 를 생성할 때,
clone()
syscall 을 호출해 user level thread 에 대응되는 kernel level thread 를 생성하도록 kernel 에 알린다.
- User level thread 를 생성할 때,
- 근데 이것이 너무 낭비이기 때문에 1:1 아닌 더 적은 갯수의 kernel level thread 를 맹글어 user level thread 에 대응되도록 하자는 Many-to-one 아이디어도 있었다.
- 만들어 놓은 thread 는 많지만 실제로 동시에 작동되는 thread 는 그보다는 적다는 것에 착안한 것.
- 근데 구현은 힘들다고 한다 - IBM 이 이것을 구현하지 못해 실패한 것
Kernel Thread
- 나도 안다. 이 문맥에서 kernel thread 라고 해버리면 kernel level thread 를 의미하는 것처럼 보인다는 것.
- Kernel thread 는 kernel process 가 필요로 하는 thread 를 의미하고,
kthread_create()
로 생성한다.