1. process 가 제공하는 가상 환경
- virtualized processor : process 혼자 system을 사용하는 듯한 가상환경 제공
- virtualized memory : process가 system 전체 메모리를 혼자 사용하는 것 처럼 메모리를 할당하고 관리
2. process descriptor
- process를 실행하는데 필요한 모든 정보를 포함
- task_struct 구조체 형식으로 task_list(circular doubly linked list)에 저장
- slab allocator가 메모리 할당 -> 이를 통해 task_struct를 가리키는 thread_info 구조체로 wrapping되어 thread_info 구조체는 kernel mode stack 가장 밑이나 꼭대기에 저장
- current 매크로를 사용해 현재 실행중인 task의 task_struct 찾음
-> (x86 아키텍쳐) %esp의 하위 13-bit 0으로 만들어주어 계산해 thread_info 위치 계산
3. slab allocator
(; 사용 빈번한 자료구조의 type별로 cache(kernel memory cache)를 이용해 type에 맞추어 미리 할당하여 할당/해제를 관리하는 memory 할당자)
- cache : slab 들의 pool
- slab : object 들을 저장하는 단위 (1개 이상의 page로 구성, 여러개의 object가 들어감)
- object : memory 할당 단위
- object type별로 같은 cache에 저장
eg) task_struct cache, i_node cache etc.
->kernel이 빈번하게 사용하는 기존 task_struct같은 자료구조를 미리 할당해놓아 새로 메모리 공간을 할당받는 초기화에 드는 비용을 없애 효과적으로 관리한다.
->같은 object type이기 때문에 사용을 다 한 후에 해제할 필요가 없다. (구조체 형식이 같아 다음에 사용할 때 속의 변수만 바꿔주면 된다.) 따라서 사용을 다 한후에 해당 object가 free하다는 표시만 해두고 메모리상에서 해제하지 않는다. object를 할당해야 할 때 메모리공간을 새로 할당받는 것이 아니라, cache안에 할당해놓은 object중 free한 object에 변수값만 수정한다.
=> 메모리를 새로 할당받는 overhead 감소
=> 같은 type이기 때문에 fragmentation문제 해결
why?) 이것을 사용안하면 여러가지 크기가 다른 type의 object들이 memory상에서 중구난방으로 저장될 수 있다. 따라서 메모리상에 free한 공간이 여러 작은 공간으로 나누어 분포하게 되는 fragmentation문제를 유발할 수 있다.
- cache 크기를 현재 system 상황에 맞추어 유동적으로 변동할 수 있다.
현재 kernel memory의 space가 tight하다면 사용중인 cache의 free한 space를 해제하여 kernel memory가 사용할 수 있게 한다. 반대로 현재 cache가 가득 찼다면 kernel memory에게 memory allocation을 요청하여 slab을 더 할당 받는다.
=> memory 활용도가 좋아진다.
- cache 및 slab descriptor를 유동적으로 cache/slab 내에 둘 수 있고 밖에 둘 수 있다.
- cache 내부에는 3개의 list가 존재한다.
full : 현재 slab이 가득 참
partial : slab에 남는 부분이 있음
free : slab이 완전 free함
full - partial - free 순으로 object를 할당하여 효과적으로 memory를 사용한다.
- cache coloring 을 통해 H/W cache의 성능을 최적화 한다.
cache의 set-associative에 의해 virtual memory상에서는 연속적일 수 있으나 physical memory에서는 연속된 할당을 보장하지 않는다. 따라서 한 cache line에 virtual memory상에서는 연속적으로 할당된 object가 번갈아가며 한 cache line을 두고 경쟁을 벌일 수 있다. 이러한 문제를 cache coloring이라는 기법을 사용하여 cache의 성능을 최적화한다.
TODO.... cache (tag/index/offset) / cache coloring slab's offset 연관성
* coloring 기법
slab의 빈공간을 활용해 빈공간 할당 크기를 다르게하여 서로 다른 slab임을 구분 (같은 type이라도 address offset이 모두 다르게 함),
이를 slab마다 다른 색깔을 부여한다고 함.
eg. slab에 고정적으로 빈공간이 64byte일 경우
slab중 첫 번째, descriptor 및 object 시작 address 0 (이 경우 가장 뒤에 64byte의 빈공간이 있을 것임)
slab중 두 번째, descriptor 및 object 시작 address 8-byte free공간 띄우고 할당
slab중 세 번째, descriptor 및 object 시작 address 16-byte free공간 띄우고 할당
slab중 네 번째, descriptor 및 object 시작 address 24-byte free공간 띄우고 할당
.... slab중 여덟번째, descriptor 및 object 시작 address 64-byte free공간 띄우고 할당
4. process 생성
fork() : 현재 task 복사해 child process만듬
exec() : 새로운 program의 code, data, heap, stack등을 process로 불러와 실행
*Copy On Write (COW)
-data의 복사를 지연, 방지하는 기술
-parent의 address space를 copy하는 대신 child는 parent의 space를 공유
-parent나 child가 write request가 있기 전 까지 read-only로 공유함
-두 process중 하나가 write request를 하면 page-fault가 발생하여 새 address space를 할당하고 write를 하게 됨
이 때부터 공유가 깨지고 서로 다른 space를 가지게 됨
-사실 fork후 변경되는 data space는 많지 않아 효과적으로 필요한 부분만 copy하여 time적으로나 space적으로나 효율적으로 동작할 수 있게함.
fork() -> clone() -> do_fork() -> copy_process() 호출
<do_fork()>
1. parent가 trace되고 있다면 debugger가 child를 추적하기 원하는 지 확인
- 추적중이라면 CLONE_PTRACE flag set
2. copy_process() 호출
<copy_process()>
1. child process의 task_struct 새로 만들고 parent process의 것을 복사
- 따라서 이 때 parent와 child의 descriptor는 같다. child [TASK_RUNNING]
2. process 개수 제한 확인
3. child의 process descriptor를 다양한 항목을 초기화 해 parent와 구별한다.
4. child [TASK_UNINTERRUPTIBLE] 로 만들어 실행되지 않게 한다.
(아직 완전한 초기화가 안되서 uninterruptble로 하는 이유는 signal에 의해서 깨어나면 아직 초기화되지 않은 process가 문제를 야기할 수 잇다.)
5. PID 할당
6. clone에 전달된 flag에 따라 parent의 resource를 copy하거나 share한다.
7. do_fork()로 새로운 child process의 포인터 return
<do_fork()>
3. if(CLONE_PTRACE flag == set) child [TASK_STOPPED]
else child [TASK_RUNNING]
4. wake_up_new_task()
- 새로운 child wake하여 runqueue에 삽입 scheduling
scheduler에 의해서 바로 실행되지 않을 수 도 있다.
- parent보다 child를 먼저 깨움
parent를 먼저 깨울 경우, 어차피 child가 wake up하면 COW정책에 의해 의도하지 않은copy가 두번(parent에서 한 번 child에서 한 번)이나 일어나게됨. 뻘짓. (비효율적으로 메모리공간 할당)
5. %eax에 child의 PID 저장하여 user mode로 반환
5. Thread
리눅스는 thread를 따로 두지 않음. 리눅스는 모두 process로 보며 process의 공유 유무를 설정해 thread와 같은 process를 구현.
-장점
사용중인 address space, resource, file system등을 공유
-> thread가 process보다 context-switch시 overhead가 적은 가장 큰 이유는address space공유 때문이며, context-switch시 cr3 register에 page directory table pointer를 바꾸며 task의 사용 virtual address space를 switch하게 된다. 이 때 대부분의 overhead는 TLB에서 발생하며 cache flush 동작과 warming-up을 하는 과정이 overhead의 주요 원인이다.
- cache flush : cache를 비우며 cache to memory로 write를 함.
- cache warming-up : 처음 비어있는 cache가 cache miss를 계속 일으키며 locality가 형성되기 전까지의 시간
-생성 비교
fork() : clone(SIGCHLD, 0)
thread : clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
*thread group in linux
리눅스는 thread도 동일한 process로 보기 때문에 Posix 표준("한 process내의 thread는 동일한 PID를 가져야한다.")에 맞추기위해 thread group id(TGID)라는 것을 만듬. 실제는 다른 프로세스(PID가 다름)이나 parent에서 thread 함수로 생성시 child's tgid = parent's pid가 됨. 즉, 사실은 다른 process이나 thread표준의 호환에 맞추기위해 만듬. application에서 보면 같은 process내의 thread가 여러개인것으로 보이나 실제는 다른 process 2개인 것. 같은 thread group이라는 것은 tgid가 같다는 것. leader thread는 pid = tgid인 process
* kernel thread
-kernel의 일부 동작을 back ground에서 실행할 경우 사용 (데몬 thread)
-주소 공간이 없고 (mm 포인터가 NULL), kernel space에서만 동작, user mode로 context-switch가 일어나지 않음
-kthread_run()으로 생성 및 실행
(desc) task들은 kernel space를 공유하고 user space는 process마다 각각 가지고 있는다 user->kernel mode로 전환시 user의 task를 kernel에서 처리하기 위해서 process마다 kernel stack을 둔다. 각각의 task는 user space address를 모두 자신이 사용하는 것 처럼 느낀다.(virtual memory)
*user space, kernel space size
- default
<32bit> user space 0~3G / kernel space 3~4G
<64bit> user space
TODO....32-bit 64-bit상에서 user space와 kernel space크기 변환
128TiB 이해
- kernel build시 VMSPLIT option에 따라 user space size를 조절할 수 있다.
if. user space 크게한다면
장) user application에서 사용할 수 있는 virtual memory space가 더 커진다.
단) NORMAL ZONE의 공간이 작아져 HIMEM ZONE사용이 많아지게 되어 memory access 속도가 느려진다.
TODO....NORMAL ZONE, HIMEM ZONE
6. process 종료
-do_exit() : process가 가지고 있는 resource 해제
-사용 주소공간, 커널 타이머, semaphore 대기 해제, file 참조횟수 줄이는 등 사용 resource 반환 및 해제
-종료 code를 task_struct의 exit_code에 저장. (종료되도 task_struct는 남게 된다.)
- [EXIT_ZOMBIE] 상태로 설정, exit_notify()를 통해 parent process에게 signal을 보냄
-> signal을 받은 parent는 wait()를 통해 task_struct의 exit_code를 회수한다.
-schedule() 호출해 새로운 process로 전환
-release_task() : task_struct 해제 (이 때 비로소 모든 process resource가 해제된다.)
;parent가 처리하거나 parent가 child에게 관심이 없는 경우, task_struct를 해제한다.
- pidhash (pid를 통해 descriptor를 찾는 hash table, pid를 통해 descriptor를 찾을 경우 빠르게 처리 할 수 있게 따로 둔다.) 와 task list에서 descriptor 제거
- 마지막 thread 라면 해당 thread group leader의 parent에게 signal을 보내 해당 작업이 종료되었음을 알림.
- kernel stack, thread_info 반환, task_struct 가지고 있던 slab cache 반환.
- parent process가 먼저 종료 된경우 reparent
해당 parent가 종료하며 자식이 reparent를 할 수 있게 끔 reparent 관련함수를 호출해 다음을 수행한다.
-해당 process가 속한 thread group의 다른 process를 parent process로 지정
-실패시, init process(PID=1)를 parent로 지정 (init process는 wait()를 주기적으로 호출해 할당된 좀비들을 정리)
- trace의 경우 reparent 전략
trace의 경우 해당 process의 parent는 debugger process가 된다. 이 때 original parent가 종료가 되면 trace를 끝 마쳤을 때 reparent를 해주어야한다. 이러한 reparent를 위해서 trace에 진입한 process들을 list로 관리해 debug가 종료될 때 list에서 parent상태를 보고 reparent를 하게 된다. (이전에는 이러한 process를 찾기 위해 모든 process를 뒤졌었다.)
'OS > Linux 2.6 kernel basic (LKD)' 카테고리의 다른 글
08. Interrupt (2) Bottom-Halves [예정] (0) | 2019.02.11 |
---|---|
07. Interrupt (1) Top-Halves (0) | 2019.02.11 |
05. System Calls (0) | 2019.02.01 |
04. Process Scheduling (0) | 2019.01.25 |