서울대학교 컴퓨터공학과 김진수 교수님의 "고급 운영체제" 강의를 필기한 내용입니다.

다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.

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 까지 구현해 버리는데..

task_struct

  • 위 그림이 task_struct 의 대략적인 구조인데
    • 보면 task_struct 는 대부분의 field 들이 pointer 로 되어 있어 여기의 field 가 다른 자료구조를 가리키게 되어 있다.
    • Process 를 fork 를 할 때에는 task_struct 를 새로 만들고, 여기에 딸려 있는 다른 자료 구조도 전부 새로 만든 다음 내용을 copy 하는 식으로 진행된다.
    • Thread 를 만들 때에는 마찬가지로 task_struct 를 새로 하나 만들되 여기에 딸려 있는 것들을 새로 만들지 않고 pointer 로 기존의 것을 가리키게만 구현한다.
    • 따라서 fork 와 threading 모두 clone syscall 하나로 처리한다고 하네
  • 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

  1. sys_fork() -> _do_fork() 가 호출
  2. 여기서는 우선 copy_process() 가 호출되는데 그 안에서
    1. dup_task_struct() 로 kernel stack 을 만들고 task_struct 를 하나 할당받는다.
    2. sched_fork(): Scheduler 관련 자료구조 초기화
    3. copy_files(), copy_fs(), copy_sighand(), copy_signam(), copy_mm(), copy_thread_tls(): 여러 자료구조들 값 복사
    4. alloc_pid(): PID 할당
    5. attach_pid(): parent 의 PID hash table 에 child PID 를 집어넣는다
  3. 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 들이 동일한 응답을 하게 된다.
  • 우선적으로는 제일 먼저 생성된 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 에 알린다.
  • 근데 이것이 너무 낭비이기 때문에 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() 로 생성한다.