Software 추상화

 

System Software

  • 컴파일러(compliler), 어셈블러(assembler)
  • 링커(linker or linkage editor)
  • 로더(loader)
  • 라이브러리(library)
  • 시스템 유틸리티(system utility) : 디버거, make 등
  • 운영체제(operating system)
  • 데이터베이스(database)
  • file system, device driver(a portion of OS)

 

 

 

Operating System

 

운영체제란?

  • 자원 관리자 (Resource Manager or Resource Allocator)
  • 응용에게 자원에 대한 서비스 제공 (Computing Environment)

 

자원의 종류

  • 물리적인 차원 : 처리기, 메모리, 디스크, 터미널, 네트워크, ...
  • 추상적인 자원 : 태스크, 세그먼트/페이지, 파일, 드라이버, 통신 프로토콜, 패킷, 보안, ...

 

운영체제의 목표

  • Efficiency
  • Fairness
  • Convenience

 

운영체제 동작 사례

운영체제는 컴퓨터에 있는 처리기(CPU), 메모리, 디스크 등의 자원을 관리, 일반 사용자들이 컴퓨터를 사용할 수 있도록 지원.

 

라운드-로빈(round-robin) 한 태스크가 정해진 시간동안 CPU를 사용하고, 그 시간이 지나면 다음 태스크가 CPU를 사용하고, ...하는 단계가 반복적으로 모든 태스크들에게 적용되는 스케줄링 방식.

 

 

 

 

Chapter 3. 태스크 관리

 

프로세스와 쓰레드 그리고 태스크

 

태스크 : 저원소유권의 단위

쓰레드 : 수행의 단위

프로세스 : 동작중인 프로그램(running or runnable program)

프로그램 : 디스크에 저장되어 있는 실행 가능한 형태의 파일. 실행 가능한 형태의 파일은 컴파일 과정을 거쳐 얻어진 바이너리 기계 명령어와 수행에 필요한 자료들의 집합으로 구성

 

스케줄링 : 커널이 시스템에 존재하는 여러 개의 프로세스 중 CPU라는 자원을 어느 프로세스에게 할당해 줄 것인가를 결정하는 작업

 

 

사용자 입장에서 프로세스 구조

 

텍스트 영역: CPU에서 직접 수행되는 명령어(instructions) 

데이터 영역: 전역변수

스택: 지역변수와 인자 그리고 함수릐 리턴 주소 등이이 존재

힙: 동적 할당받은 내용이 존재. 이때 각 영역을 세그먼트(segment) 또는 가상 메모리 객체(vm_area_struct)라고도 부름

 

 

프로세스와 쓰레드의 생성과 수행

 

 

fork_test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
int g=2;
 
int main(void)
{
    pid_t pid;
    int i=3;
 
    printf("PID(%d): Parent g=%d, i=%d \n",getpid(),g,i);
 
    if((pid=fork())<0){
        perror("fork error");
        exit(1);
    }else if(pid==0){
        g++;
        i++;
    }
    else
    {
        wait();
    }
 
 
    printf("PID(%d): g=%d, i=%d \n",getpid(),g,i);
 
    return 0;
}
 
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

자식 프로세스의 연산 결과는 자식 프로세스 주소 공간의 변수에만 영향을 줄 뿐 부모 프로세스 주소 공간의 변수에는 영향이 없다.

 

 

쓰레드의 생성

clone_test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/types.h>
#define _GNU_SOURCE
#include <unistd.h>
#include <linux/sched.h>
#include <stdio.h>
#include <stdlib.h>
 
int g=2;
 
int sub_func(void *arg)
{
    g++;
    
    printf("PID(%d): Child g=%d \n",getpid(),g);
    sleep(2);
    return 0;
}
 
int main(void)
{
    int pid;
    int child_stack[4096];
    int i=3;
 
    printf("PID(%d): Parent g=%d, i=%d \n",getpid(),g,i);
 
    clone (sub_func,(void *)(child_stack+4095),CLONE_VM | CLONE_THREAD |
            CLONE_SIGHAND,NULL);
    sleep(1);
    printf("PID(%d): Parent g=%d, i=%d \n",getpid(),g,i);
 
    return 0;
}
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

 리눅스에서는 쓰레드 생성을 위해 clone()이라는 시스템 호출을 제공. 또는 pthread(POSIX thread)라는 라이브러리를 사용하여 쓰레드를 생성할 수도 있다.

새로운 쓰레드를 생성한 것이며, 새로운 프로세스를 생성한 것이 아니다. 따라서 같은 PID g값도 3으로 바뀐다.

 

  1. 새로운 프로세스를 생성하면, 생성된 프로세스(자식 프로세스)와 생성한 프로세스(부모 프로세스)는 서로 다른 주소 공간을 갖는다. 반면 새로운 쓰레드를 생성하면 생성된 쓰레드(자식 쓰레드)와 생성한 쓰레드(부모 쓰레드)는 서로 같은 주소 공간을 공유한다.
  2. 같은 프로세스에서 새로운 쓰레드를 생성할 경우 기존 쓰레드와 생성된 다른 쓰레드가 함께 동자하고 있는 것으로 볼 수 있다. 즉 한 프로세스에 2개의 쓰레드가 동작. 한 프로세스에서 여러 쓰레드가 동작하는 모델을 다중 쓰레드 시스템이라고 한다. 쓰레드 생성은 새로이 모든 자원을 생성해 주어야 했던 프로세스에 비해 생성에 드는 비용이 비교적 적다.
  3. 자식 쓰레드에서 결함이 발생하면 그것은 부모 쓰레드로 전파 된다. 반면 자식 프로세스에서 발생한 결함은 부모 프로세스에게 전파 되지 않는다. 결국 쓰레드 모델은 자원공유에 적합, 프로세스 모델은 결함 고립에 적합

 

새로운 프로세스의 수행. 리눅스는 태스크의 수행을 위해 execl()이라는 시스템 호출을 제공

fork_exec.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
int main(void)
{
    pid_t pid;
    int exit_status;
 
    if((pid=fork())<0){
        perror("fork error");
        exit(1);
    } else if(pid==0){
        printf("Before exec\n");
        execl("./fork_test","fork",(char *)0);
        printf("After exec\n");
    } else {
        pid = wait(&exit_status);
    }
    printf("Parent\n");
    return 0;
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

 

 

 

fork()와 vfork()의 차이

fork()는 부모 프로세스의 주소 공간을 복사하여 자식 프로세스의 주소 공간을 따로 만들지만, vfork()의 경우 일단 같은 주소 공간을 가리킨다. 이때 자식에서 execl()이 호출되면 기존에 사용하던 프로세스의 주소 공간으 ㄹ모두 없애고, 요청된 바이너리를 기반으로 새로운 주소 공간 생성.

fork() 이후 바로 execl()이 되면 결국 fork() 때 수행했던 부모 프로세스의 주소 공간을 복사하여 자식 프로세스의 주소 공간을 따로 만들어 주었던 작업이 불필요한 작업이 되고 만다.

 

 

 

리눅스의 태스크 모델

 

프로세스는 자신이 사용하는 자원과 그 자원에서 수행되는 수행 흐름으로 구성

리눅스에서는 이를 관리하기 위해 각 프로세스마다 task_struct 라는 자료 구조를 생성

프로세스가 생성되든 쓰레드가 생성되든 task_struct라는 동일한 자료 구조를 생성하여 관리. 프로세스이던 쓰레드이던 커널 내부에서는 태스크라는 객체로 관리.

fork(), clone() 모두 커널 내부에서 마지막으로는 do_fork()로 동일. why? 모두 '태스크'를 생성하기 때문.

 

do_fork()는 새로 생성되는 태스크를 위해 일종의 이름표를 하나 준비. 새로이 생성된 태스크의 정보를 자세히 기록. 이름표는 즉 task_struct 구조체. 이 태스크가 수행되기 위해 필요한 자원 등을 할당한 뒤 수행 가능한 상태로 만들어준다.

 

시스템에 존재하는 모든 태스크는 유일하게 구분이 가능해야 한다. 태스크 별로 유일한 이 값은 task_struct 구조체 내의 pid 필드에 담겨있다. 그런데 POSIX 표준에 의하면 ' 한 프로세스 내의 쓰레드는 동일한 PID를 공유 해야 한다. ' 라고 명시되어 있다. 리눅스에선 이를 위해 tgid(Thread Group ID) 라는 개념을 도입

 

태스크가 생성되면 이 태스크를 위한 유일한 번호를 pid로 할당. 만약 사용자가 프로세스를 원하는 경우라면 생성된 태스크의 tgid 값을 새로 할당된 pid 값과 동일하게 넣어준다. 따라서 tgid 값도 유일한 번호를 갖게 됨.

사용자가 쓰레드를 원하는 경우라면 부모의 쓰레드의 tgid 값과 동일한 값으로 생성된 태스크의 tgid를 설정. 부모 태스크와 자식 태스크는 동일한 tgid 동일 프로세스로 해석

 

fork_test2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/unistd.h>
 
int main(void)
{
    pid_t pid;
 
    printf("before fork\n");
 
    if((pid=fork())<0){
        printf("fork() error\n");
        exit(-2);
    }else if(pid==0){
        printf("TGID(%d), PID(%d): Child \n",getpid(),syscall(__NR_gettid));
        
    }else {
        printf("TGID(%d), PID(%d): Parent \n",getpid(),syscall(__NR_gettid));
        sleep(2);
    }
    printf("after vfork\n\n");
    return 0;
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

 

vfork_test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/unistd.h>
 
int main(void)
{
    pid_t pid;
 
    printf("before vfork\n");
 
    if((pid=vfork())<0){
        printf("vfork() error\n");
        exit(-2);
    }else if(pid==0){
        printf("TGID(%d), PID(%d): Child \n",getpid(),syscall(__NR_gettid));
        _exit(0);
    }else {
        printf("TGID(%d), PID(%d): Parent \n",getpid(),syscall(__NR_gettid));
    }
    printf("after vfork\n\n");
    exit(0);
}
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

 

 

pthread_pt.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/unistd.h>
 
void *t_function(void *data)
{
    int id;
    int i=0;
    pthread_t t_id;
    id=*((int *)data);
    printf("TGID(%d), PID(%d), pthread_self(%d) : Child \n",
            getpid(),syscall(__NR_gettid),pthread_self());
    sleep(2);
}
 
int main(void)
{
    int pid,status;
    int a=1;
    int b=2;
    pthread_t p_thread[2];
    
    printf("before pthread_create\n\n");
    if((pid=pthread_create(&p_thread[0],NULL,t_function,(void*)&a))<0){
        perror("thread create error : ");
        exit(1);
    }
 
    if((pid=pthread_create(&p_thread[1],NULL,t_function,(void*)&b))<0){
        perror("thread create error : ");
        exit(2);
    }
 
    pthread_join(p_thread[0],(void **)&status);
    printf("pthread_join(%d)\n",status);
 
    pthread_join(p_thread[1],(void **)&status);
    printf("pthread_join(%d)\n",status);
 
 
    printf("TGID(%d), PID(%d) : Parent \n",getpid(),syscall(__NR_gettid));
    return 0;
}
 
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

 

 

clone_pt.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/unistd.h>
#include <linux/sched.h>
 
int sub_func_b(void *arg)
{
    printf("TGID(%d), PID(%d) : Child \n",getpid(),syscall(__NR_gettid));
    sleep(2);
    return 0;
}
 
int main(void)
{
    int pid;
    int child_a_stack[4096],child_b_stack[4096];
 
    printf("before clone \n\n");
    printf("TGID(%d), PID(%d) : Parent \n",getpid(),syscall(__NR_gettid));
 
    clone (sub_func_b,(void *)(child_a_stack+4095),CLONE_CHILD_CLEARTID |
            CLONE_CHILD_SETTID,NULL);
 
    clone(sub_func_b,(void *)(child_b_stack+4095),CLONE_VM |
            CLONE_THREAD | CLONE_SIGHAND,NULL);
 
    sleep(1);
 
    printf("after clone\n\n");
    return 0;
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4f; text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none; color:white">cs

 

 

 

 

 

태스크 문맥

 

태스크와 관련된 '모든' 정보를 문맥(context)라고 부른다.

커널이 관리하는 태스크의 자원과 수행 환경 집합

 

태스크의 문맥은 크게 세 부분으로 구분

 

1. 시스템 문맥 (System Context)

태스크의 정보를 유지하기 위해 커널이 할당한 자료구조들이다. 대표적인 자료 구조로는 task_struct, 파일 디스크립터, 파일 테이블, 세그먼트 테이블, 페이지 테이블 등이 있다.

 

2. 메모리 문맥(memory context)

텍스트, 데이터, 스택, heal 영역, 스왑 공간 등

 

3. 하드웨어 문맥(hardware context)

문맥 교환(context switch)할 때 태스크의 현재 실행 위치에 대한 정보를 유지하며, 쓰레드(thread) 구조 또는 하드웨어 레지스터 문맥이라고 불림. 실행 중이던 태스크가 대기 상태나 준비 상태로 전이할 때 이 태스크가 어디까지 실행했는지 기억해 두는 공간으로, 이후 이 태스크가 다시 실행될 때 기억해 두었던 곳부터 다시 시작하게됨.

 

1. task identification

태스크를 인식하기 위한 변수들.

태스크 ID = pid, 태스크가 속해있는 쓰레드 그룹 ID = tgid, pid를 통해 해당 태스크의 task_struct를 빠르게 찾기 위해 커널이 유지하고 있는 해쉬 관련 필드 등의 변수가 있다.

 

2. state

태스크는 생성에서 소멸까지 많은 상태를 거치며, 이를 관리하기 위한 state변수가 존재.

TASK_RUNNING(0), TASK_INTERRUPTIBLE(1), TASK_UNINTERRUPTIBLE(2), TASK_STOPPED(4), TASK_TRACED(8), EXIT_DEAD(16), EXIT_ZOMBIE(32) 등의 값이 들어감

 

3. task relationship

태스크는 생성되면서 가족 관계를 갖는다. 대표적으로 현재 태스크를 생성한 부모 태스크의 task_struct 구조체를 가리키는 real_parent와 현재 부모 태스크의 task_struct 구조체를 가리키는 parent필드가 존재

 

4. scheduling information

task_struct에서 스케줄링과 관련된 변수는 prio, policy, cpus_allowed, time_slice, rt_priority 등이 있다.

 

5. signal information

시그널은 태스크에게 비동기적이 사건의 발생을 알리는 매커니즘

task_struct 에서 시그널과 관련된 변수는 signal, sighand, blocked, pending 등이 있다.

 

6. memory information

태스크는 자신의 명렁어와 데이터를 텍스트, 데이터, 스택, 그리고 힙 공간 등에 저장.

task_struct에는 이 공간에 대한 위치와 크기, 접근 제어 정보 등을 관리하는 변수들이 존재

 

7. file information

태스크가 오픈한 파일들은 task_struct 에서 file_struct구조체 형태인 files라는 이름의 변수로 접근 가능

루트 디렉토리의 inode와 현재 디렉토리의 inode는 fs_struct 구조체 형태인 fs라는 변수로 접근 가능

 

8. thread structure

쓰레드 구조(thread_struct:TSS)는 문맥 교환을 수행할 때 태스크가 현재 어디까지 실행되었는지 기억해놓은 공간

 

9. time information

태스크의 시간 정보를 위한 변수로는 태스크가 시작된 시간을 가리키는 start_time, real_start_time 등이 있다.

 

10. format

리눅스는 Linux exec 도메인뿐만 아니라 BSD나 SVR4 exec 도메인도 지원. 즉 BSD나 SVR4 커널에서 컴파일 된 프로그램도 리눅스에서 재 컴파일 없이 수행 가능. 

이를 위해 personality와 같은 변수가 사용, 다양한 이진 포맷(binary format)을 지원하기 위한 필드가 thread_info내에 존재

 

11. resource limits

태스크가 사용할 수 있는 자원의 한계를 의미.

rlim_max는 최대 허용 자원의 수, rlim_cur은 현재 설정된 허용 자원의 수

자원의 한계는 배열로 구현되어 있다.

 

 

6. 상태 전이(State Transition)와 실행 수준 변화

 태스크는 생성된 뒤, 자신에게 주어진 일을 수행하며, 이를 위해 디스크 I/O나 락(Lock)등 CPU 이외의 자원을 요청하기도 한다. 만약 당장 제공할 수 없다면 커널은 이 태스크를 잠시 '대기' 시키고 다른 태스크를 먼저 수행시키고, 제공이 가능해지면 다시 '수행'시켜 줌으로써 보다 높은 시스템 활용률을 제공

따라서 태스크는 상태 전이(state transition)라는 특성을 가지게 된다.

 

태스크가 생성되면 그 태스크는 준비 상태(TASK_RUNNING)가 된다. 스케줄러는 여러 태스크 중에서 실행시킬 태스크를 선택하여 수행시킨다. TASK_RUNNING 상태는 구체적으로 준비상태와 실제 cpu를 배정받아 명령어 들을 처리하고 있는 실행 상태 두 가지로 나뉨

 

실행 상태에 있는 태스크들은 발생하는 사건에 따라 다음과 같은 상태로 전이 할 수 있다. 

1. 태스크가 자신이 해야 할 일을 다 끝내고 exit()를 호출하면 TASK_DEAD 상태로 전이된다.

ZOMBIE 상태는 말 그대로 죽어있는 상태로써, 태스크에게 할당되어 있던 자원을 대부분 커널에게 반납한 상태. 자신이 종료된 이유, 자신이 사용한 자원의 통게 정보 등을 부모 태스크에게 알려주기 위해 유지되고 있는 상태.

추후, 부모 태스크가 wait()등의 함수를 호출하면 자식의 태스크의 상태는 TASK_DEAD(EXIT_DEAD) 상태로 바뀌게 되며, 부모는 자식의 종료 정보를 넘겨 받는다. 그런 뒤 자식 태스크는 자신이 유지하고 있던 자원을 모두 반환하고 최종 종료

 

만약 부모 태스크가 자식 태스크에게 wait()등의 함수를 호출하기 전에 먼저 종료되어 없어지면 부모가 없는 ZOMBIE 상태의 자식 태스크 즉, 고아 태스크가 시스템에 영원히 존재하게 되어 시스템 오버헤드로 작용될 수 있는데 이 문제를 해결하기 위해 커널은 고아 태스크의 부모를 init 태스크로 바꾸어 주며 init 태스크가 wait()등의 함수를 호출할 때 고아 태스크는 최종 소멸된다.

 

실행(TASK_RUNNING(running)) 상태에서 실제 수행 되던 태스크가 자신에게 할당된 CPU시간을 모두 사용 하였거나, 보다 높은 우선순위를 가지는 태스크로 인해 준비(TASK_RUNNING(read))상태로 전환 되는 경우.

리눅스는 여러 태스크들이 cpu를 공평하게 사용 할 수 있도록 일반 태스크인 경우 CFS(Completely Fair Scheduling) 기법을 사용

 

SIGSTOP, SIGTSTP, SIGTIN, SIGTTOU 등의 시그널은 받은 태스크는 TASK_STOPPED 상태로 전이, 추후 다시 SIGCONT 시그널을 받아 다시 TASK_RUNNING(read) 상태로 전환

 

실행(TASK_RUNNING(running)) 상태에 있던 태스크가 특정한 사건을 기다려야 할 필요가 있으면 대기 상태(TASK_INTERRUPTIBLE, TASK_UNINTER-RUPTIBLE, TASK_KILLABLE)로 전이 한다.

 

 

 

 

 

 

+ Recent posts