0%

운영체제 구조

응용 프로그램, 운영체제, 컴퓨터 하드웨어의 관계

  • 운영체제는 응용 프로그램이 요청하는 메모리를 허가한다.
  • 운영체제는 응용 프로그램이 요청하는 CPU 시간을 제공한다.
  • 운영체제는 응용 프로그램이 요청하는 I/O 장치 사용을 허가/ 제어한다.

운영체제는 사용자 인터페이스를 제공

쉘(shell) : 사용자가 운영체제 기능과 서비스를 조작할 수 있도록 인터페이스를 제공함.

쉘은 CLI 환경과 GUI 환경이 있음.

쉘도 응용 프로그램중에 하나라고 할 수 있다.


운영체제는 응용 프로그램을 위해서도 인터페이스 제공

AP로 제공함. (Application Programming Interface)

API는 함수 형태이거나 라이브러리 형태로 제공된다.

쉘 또한 응용 프로그램중 하나이기 때문에 사용자의 요청이 들어오면 API 를 이욯애서 시스템 리소스에 접근한다고 할 수 있다.

그래서 다음과 같은 구조가 된다.

시스템 콜

시스테 콜 인터페이스라고도 함.

운영체제는 운영체제의 각 기능을 사용할 수 있도록 시스템 콜을 제공하는데, API는 이 시스템 콜을 호출하는 방식으로 시스템 리소스에 접근함.

전체적은 구조는 다음과 같다.

운영체제를 만든다면

  1. kernel 개발
  2. 시스템 콜 개발
  3. API 개발
  4. shell 개발
  5. 응용 프로그램 개발

<br/ >

운연체제와 시스템 콜

시스템 콜의 예

  • posix API, Windows API

API: 각 언어별 운영체제 기능 호출 인터페이스 함수
시스템 콜: 운영체제 기능을 호출하는 함수


CPU Protection Rings

Cpu는 권한모드가 있다.

일반 모드는 사용자 모드 라고도 하는데 일반적인 명령에 접근할 수 있다. (Application 사용)

커널 모드는 특별한 명령어 또는 특별한 시스테 자원을 요청할 수 있는 모드이다. (Os가 사용하는)

그림에서 링으로 권한을 분리하는데 보통의 경우 ring3과 ring0을 사용해 3은 사용자 모드, 0은 커널 모드로 나눈다.

Kernel이 뭔데?

단어로는 씨앗 이라는 뜻이 있다.

즉 운영체제의 핵심 같은 거라고 할 수 있다.

shell은 껍데기 라는 뜻이 있고 이 커널을 싸고 있다고 생각하면 된다.

사용자 영역과 커널 영여의 분류

시스테 콜은 커널모드에서 실행된다.

우리가 작성한 프로그램은 대부분 사용자 모드에서 실행된다.

커널모드로 실행하려면 반드시 시스템 콜을 거처야함.

이렇게 함으로써 특정 응용프로그램이나 사용자가 전체 컴퓨터 시스템을 헤치지 못하게 막을 수도 있다.


code ex1

해당 코드 동작시 처으에는 사용자 모드로 시작했다가 특정 API를 만났을때에는 커널 모드로 전환되고 커널 모드에서 작업 완료시 다시 사용자 모드로 전환된다.

스케줄링

프로세스 스케쥴링에 대해서 알아본다.

여기서 말하는 프로세스는 응용 프로그램 정도로 생각한다.


배치처리

큐와 유사한 방식으로 동작한다.

배치처리 시스템의 문제점?

  • 시간이 적게 걸리는 프로그램이 있다고 하더라도 배치 처리 시스테으로 오래 걸리는 프로세스가 먼저 진행중이면 그만큼 기달려야 하는 문제가 있다.
  • 음악을 들으면서 문서를 작업한다 든지 응용 프로그래밍 동시 사용에 대한 처리를 못했다.
  • 여러 사용자가 동시에 하나의 컴퓨터를 쓴다고 했을테 시스템 응답 시간이 커지기 때문에 다중 사용자에 대한 지원이 되지 않았다.

시분할 시스템이 등장 : 응용 프록그램이 CPU를 점유하는 시간을 굉장히 잘게 쪼개어 실행될 수 있도록 함. (다중 사용자 지원)


멀티태스킹

단일 CPU에서 응용 프로그램이 동시에 실행되는 것처럼 보이게 하는 시스템

마찬가지로 시분할 시스템을 적용하면 작동하지 프로세스가 바뀌어서 동작하는 시간이 있다고 하더라도 그 시간은 매우 작은 시간이기 때문에 사용자가 인지 하지 못한다.

실제로는 동시에 작동하지 않지만 사용자가 느끼기에 동시에 실행된다고 느끼게 되는 것이다.


멀티 프로세싱

멀티태스킹이 단일 CPU를 기준으로 동시에 보이는것을 목저으로 한다면 멀티 프로세싱은 여러 CPU 가 존재한다.

멀티 프로세싱은 여러 CPU가 하나의 응용 프로그램을 병렬로 실행시키면서 실행 속도를 극대화 하는 기술이다.


멀티 프로그래밍

최대한 CPU를 많이 활용하도록 하는 시스템

응용 프로그램은 온전히 CPU를 사용하기 보다 다른 작업을 수행하는 일이 많다.

어쩔 수 없이 CPU를 사용하지 못하는 상황이 오게 되는데 이때 그냥 대기하는 것이 아니라 다른 프로그래을 그 사이에 실행 시킬 수 있다면 CPU 활용률은 올라갈 것이다.

이런 개념을 적용한것이 멀티 프로그래밍이다.

메모리 계층

CPU 밖에서 일어나는 프로세스는 속도가 느릴 수 밖에 없다.

특히나 저장매체에서 진행되는 프로세스는 굉장히 느리다.

상태 기반 스케쥴링

프로세스 상태

  • running state: 현재 CPU에서 실행 중인 상태
  • ready state : CPU 에서 실행 가능 상태 (실행 대기 상태)
  • block state: 특정 이벤트 발생 대기 상태 (파일을 다 읽었다!)

프로세스 생성과 종료는 짧은 시간 동안 발생.

종료의 경우 실행하면서 갖고 있던 리소스를 해제하는 과정이라고 생각할 수 있다.


프로세스 상태간 관계

  1. process 가 특정 특정 cpu 사용아 아닌 다른 작업을 수행해야 하는 상태가 됨. 완료 이벤트를 기다리는 상태
  2. scheduler 가 실행가능한 프로세스 중에서 하나를 골라서 실행 시킴.
  3. 시분할 시스템과 같은 시스템에서 scheduler가 실행 중인 프로세스를 다른 프로세스에 실행을 위해서 ready 상태로 바꿈.
  4. block 상태에 있는 프로세스가 작업을 완료 후 (완료 이벤트를 받고) ready 상태로 변화.

but, 단순한 상태만 가지고서 어떻게 스케쥴링을 하지? ready 상태가 여러개일 때에는 어떤걸 선택해야 할까?


상태별 큐를 만들자.

큐 : 앞 뒤 로 본다.
초기 상태

  • ready state queue: 1, 2, 3
  • runnig state queue
  • block state queue

cpu가 아무것도 실행되고 있지 않은 상태를 idle 상태라고 한다.

상태 별 큐를 생각하면서 어떻게 돌아갈지 생각해 보기.

결과 [1, 2, 3, 1, 3, 2, idle, idle, 3]


선점형과 비선점형 스케쥴러

선점형 스케쥴러는 프로세스가 끝나거나 block 상태가 되지 않더라도 스케쥴러가 ready 상태로 바꾸고 다른 프로세스를 실행시키는 것이 가능한 스케쥴러이다.

비선점형 스케쥴러는 프로세스가 block 상태가 되거나 끝나지 않으면 다른 프로세스를 실행 시키는것이 불가능한 스케쥴러이다.

시분할 시스템을 구현하려면 선점형 스케쥴러를 구현할 수 있어야 했는데, 초기에 운영체제는 선점형 스케쥴러 구현의 어려움이 있었다.

선점형과 비선점형의 차이

위에 예시에서 선점형과 비선점형을 나눠서 생각해봄.

이번엔 시분할을 2칸으로 봄. 왜 이렇게 되는지 생각해 보자.

비선점형은 시스템 응답시간이 길어짐.


스케쥴러의 구분

  • FIFO(FCFS), SJF, Priority-based는 어떤 프로세스를 먼저 실행시킬지에 대한 알고리즘 (비선점형에 가까움)
  • RoundRobin 은 시분할 시스템을 위한 기본 알고리즘 (선점형 스케쥴러)

보통 여러가지 알고리즘을 섞어서 사용하는것이 일반적임

스케쥴링 알고리즘 기본

프로세스란

실행중인 프로그램을 프로세스라고 한다.

메모리에 올려져서 실행중인 프로그램이라고 보면 된다.

응용 프로그램은 =! 프로세스

응용 프로그램은 여러개의 프로세스의 조합이기 때문이다.


스케쥴러와 프로세스

프로세스 실행을 관리 하는 것을 스케쥴러라고 한다.

스케쥴링 알고리즘

어느 순서대로 프로세스를 실행시킬까?

특정한 목표를 잡고 계획을 세우는 것


FIFO 스케쥴러

프로세스가 온전히 CPU만 사용하는 작업이라고 가정한다.

먼저 요청이 들어오면 먼저 실행 시킨다.

요청은 자료 구조중 큐와 같다.

  1. 1번 프로세스 실행(1초)
  2. 2번 실행 (3초)
  3. 3번 프로세스 실행(1초)

가장 간단한 스케쥴러 (배치 처리 시스템)

FCFS 스케쥴러라고도 불림.


최단 작업 우선(SJF) 스케쥴러

Shortest Job First : 가장 프로세스 실행시간이 짧은 것부터 실행함.

사실 일반적인 상황에서 프로세스 실행시간을 예측하는것은 불가능에 가까움.

이상적인 스케쥴러라고 할 수 있고 특별한 상황에서 사용함.

처리 순서 비교



우선순위 기반 스케쥴러

Priority-Based : 우선순위에 기반한 스케쥴러

우선순위는 정적으로 미리 정하는 경우도 있고, 스케쥴러가 상황에 따라 동적으로 정하는 경우도 있다.

처리 순서 비교



Round Robin 스케쥴러

시분할 시스템을 적용하여 생각한다.

Round Robin Read Queue를 가져서 실행할 프로세스를 큐에 넣는다.

큐에서 요청을 하나 빼고 실행한 프로세스가 아직 끝나지 않았다면 다시 큐에다가 해당 프로세스를 넣는다.

모든 프로세스가 끝날때까지 반복한다.

처리 순서

RoundRobin Ready Queue

  • 3 2 1 (1번 꺼내서 실행, 1번 끝)
  • 3 2 (2번 꺼내서 실행 , 2번은 아직 안끝남)
  • 2 3 (3번 꺼내서 실행 , 3번은 아직 안끝남)
  • 3 2 (2번 꺼내서 실행, 2번은 아직 안끝남)
  • 2 3 (3번 꺼내서 실행, 3번 끝)
  • 2 ( 2번 꺼내서 실행, 2번 끝)

인터럽트

인터럽트란?

cpu 는 pc 에서 가르키는 code 를 실행하게 되는데 외부 I/O 나 cpu 외부에서 처리할 작업에 이벤트가 발생하게 되면 cpu 에서 이벤트 발생 여부를 알아야 할 필요가 있다.

이때 이벤트가 발생했다고 알려주는 것을 인터럽트 라고 하며 어떤 일을 하닥 인터럽트가 발생하면 cpu는 이벤트가 발생했음을 알고 해당 작업을 처리한 후 다시 원래 작업으로 돌아오는 흐름을 가질 수 있다.

인터럽트가 필요한 이유

선점형 스케쥴러의 구현에서 프로세스가 실행 중에서 레디 상태로 상태가 변할 수 있어야 한다.

이때 프로세스가 레디상태로 변하거나 wait가 된다는 신호를 인터럽트를 통해서 발생 시킬 수 있다.

I/O 에서 작업을 완료 한 후 wait 상태에서 다시 ready 상태가 되기 위해서는 I/O 작업이 끝났다는 신호를 알아야 한다.

이때에도 인터럽트를 통해서 I/O 처리가 끝났음을 알려준다.

입출력 하드웨어 등의 예외 상황이 발생했을 때도 CPU 에게 알려주어야 하는데 이때도 인터럽트를 사용해서 알려준다.

또한 코드 상의서 에러가 발생해서 따로 처리해 주어야 할때에도 에러가 발생했음을 알리느 인터럽트가 발생한다.


인터럽트 종류

주요 인터럽트

  1. 계산하는 코드에서 0으로 나누느 코드 실행시 (Divide-by-zero interrupt)
1
2
3
4
5
6
7
8
#incllude <studio.h>

int main() {
int data;
int divider = 0;
data = 1/divider; // 이 부분에서 인터럽트 발생
return 0;
}
  1. 타이머 인터럽트

하드웨어로 부터 일정한 주기마다 인터럽트를 운영체제에게 알려줌.

선점형 스케쥴러를 위해서 필요함.

  1. 입출력(I/O) 인터럽트

내부/외부 인터럽트

  • 내부 인터럽트 (소프트 인터럽트)

주로 프로그램 내부에서 명령 또는 잘못된 데이터 사용시 발생

0으로 나눴을때, 사용자 모드에서 허용되지 않은 명령 또는 공간 접근시, 계산된 결과가 Overflow/ Underflow 날때

  • 외부 인터럽트 (하드웨어 인터럽트)

주로 하드웨어에서 발생되는 이벤트 (프로그램 외부)

전원이상, 기계문제, I/O, Timer 이벤트


인터럽트 내부 동작

인터럽트 내부에서 어떻게 동작하는지 알아본다.

시스템 콜 인터럽트

시스템 콜을 실행하기 위해서는 강제로 코드에 인터럽트를 넣어서, cpu에게 실행 시켜야 한다.

1
2
3
mov eax, 1
mov ebx, 0
int 0x80
  • eax 레지스터에 시스템 콜 번호를 넣음.
  • ebx 레지스테에 시스템 콜에 해당하는 인자값을 넣음.
  • 소프트웨어 인터럽트를 호출하면서 0x80 을 넘겨줌. (int 는 opcode)

예에서 0x80이라는 인터럽트는 시스템 콜을 호출하는 인터럽트 번호라는 의미임.

  1. int opcode 를 통해서 인터럽트 발생한것을 알게 됨.
  2. CPU는 사용자 모드에서 커널 모드로 바꿈
  3. IDT(interrupt Descriptor Table) 에서 0x80 에 해당하는 함수를 실행함. 0x80은 system_call()
  4. system_call() 함수에서 eax 로부터 시스템 콜 번호를 찾아서, 해당 번호에 해당하는 함수로 시스템 콜 이동.
  5. 해당 함수 실행 후 커널모드에서 다시 사용자 모드로 변경. 다시 해당 프로세스에 다음 실행.

인터럽트와 IDT

인터럽트는 IDT(Interrupt Descriptor Table)이라 불리는 곳에 각각 번호와 실행 코드를 가리키는 주소가 기록되어 있음.

운영체제가 컴퓨터 부팅시 기록한다.

프로세스와 컨텍스트 스위칭

컨텍스트 스위칭이란 스케쥴러가 어떤 프로세스 A를 실행할때 B로 바꿔주는 메커니즘이다. 컨텍스트 스위칭을 이해하기 위해서는 프로세스 구조에 대해서 이해하는 것이 필요하다.


프로세스 구조

  • code 라는 영역에 컴파일된 소스코드가 위치한다.
  • data 라는 영역에 변수가 설정된다. (전역 변수)
  • stack 이라는 영역에 함수에 대한 코드가 설정된다.
    • return address 함수가 실행 된 후에 가야 할 주소 이다.
    • 스택 이라는 자료구조처럼 할당한다음 마지막 할당한 것부터 처리해서 처음 할당한 return address 까지 처리하게 된다.
  • heap에 동적으로 할당한 변수들이 위치하게 된다.

컴퓨터 구조

PC(Program Counter) + SP(Stack Pointer) 가 어떻게 작동하는지

  • Program Counter는 초기값이 0000h 이고 1씩 늘어나면서 증가할 것이다.
  • EBP 에는 Stack Pointer 가 가리키는 최상단 주소가 적혀 있고, Stack return adress 보다 이 주소가 먼저 적힌다.
    • EBP 가 있음으로 인해서 함수에 문제가 생겼을때 어디서 문제가 생겼는지 확인할 수 있다. EBP 에는 함수가 호출된 최상단을 항상 가르키고 있기 때문에 문제가 생긴 호출이 어디서 발생했는지 알 수 있는 것이다.

0003h 에서 함수를 실행시켰을때 stack Pointr는 1000h를 가르키고 있었고 EBP 또한 1000h 일 것이다. 따라서 stack 영역에는 다음과 같이 할당 되게 된다. stack이 점점 쌓임에 따라서 stack Pointer 도 1씩 감소하게 될 것이다. 그리고 그림과 같은 상황에서 stack pointer 는 0FFCh 가 될 것이고, EBP 또한 0FFCh가 된다.

만약 함수 안에서 또다른 함수가 호출되었다면? 처음 알고리즘과 똑같이 EBP에 값인 0FFCh 가 적힌다. 이 주소를 통해서 만약 문제가 생겼다면 어디서 문제가 생긴건지 알 수 있게 되는 것이다.

  • 반환 값은 EAX에 들어간다.
  • c라는 변수의 값은 EAX를 참조해서 가져간다.


malloc 을 통해서 동적으로 생성한 변수 data는 heap 에 위치하게 된다. 지역변수인 *data (data의 주소 int *data) 는 stack 에 위치하게 된다. 힙에다가 공간을 마련한다고 생각하면 된다.


DATA 분리

전역 변수의 공간인 DATA는 초기화 되지 않는 전역 변수를 위한 공간인 BSS와, 초기값이 있는 전역 변수를 위한 공간인 DATA로 나뉜다.


stack 오버플로우?

data 배열의 사이즈로 6으로 정의했기 때문에 스택에 data를 할당함. 그런데 bar의 크기가 6을 넘어가는 크기라면 원래 스택에 data로 정의되어야 할 크기를 넘어서 return adress 가 적혀햘 부분에 덮어씌우기가 될 수 있음. 이것을 악의적으로 이용해서 프로세스를 원하는 주소로 이동시키는 방법을 해커들이 많이 사용했음.


컨텍스트 스위칭

PC와 SP에 집중해 보자. 컨텍스트 스위칭은 스케쥴러가 실행중인 프로세스를 변경시키는 것이다. 생각해보면 변경하는 프로세스가 어디만큼 진행했는지에 대한 정보를 저장해야지 다음 프로세서로 변경시킬수 있지 않을까?

PCB (Process Control Block)

프로세슬 변경하기전에 PCB라고 하는 곳에 PC와 SP 정보를 저장한다.
그다음 다른 프로세스로 가서 PCB에 정보를 확인하고 PC와 SP 정보를 CPU에 덮어 씌운다.
그리고 프로세스를 실행시킨다.

PCB에는 몇가지 정보가 적혀있음.

  1. Process ID
  2. Register값 (PC, SP)
  3. Schduling Info (Process State)
  4. Memory Info(메모리 사이즈 limit)

리눅스 PCB

개념 정리

디스패치 : ready 상태의 프로세스를 running 상태로 바꾸는 것

컨텍스트 스위칭은 굉장히 짧은 시간에 진행되어야 하기 때문에 어셈블리어로 구현된다.

IPC

프로세스가 커널 공간을 공유한다.


파이프

부모 에서 자식으로 단방향 통신이 가능하다.
fork()를 통해서 자식 프로세스를 만들었을때 PID 는 자식 프로세스가 0 부모 프로세스가 0이 아닌 값이다.
이를 이용해서 서로 다른 로직을 작성한다.

fd라는 배열을 이욯새통신한다. 부모 프로세스에서 fd[1] 를에 write를 진행하면 자식 프로세스에서 fd[0]을 통해서 읽을 수 있다. 이때 파이프를 통해서 통신이 이루어지는데 이 파이프가 커널 영역에 존재해서 통신이 가능하게 된다.



메세지 큐

키 값을 통해 메세지 큐에 접근한다. 다른 프로세스에서 메세지 큐로 전송하면 또 다른 프로세스에서 메세지 큐에 내용을 읽는 방법으로 통신이 일어난다.

큐에 이기 때문에 FIFO 정책을 따른다.
메시지 큐 또한 커널 영역에 있다.

메시지 큐와 파이프에 차이점

메시지 큐는 fork 할필요가 없다.
또한, 메세지 큐를 두개 사용하면 양방향 통신도 가능하다.


공유 메모리

메모리에 커널영역에 공유 할수있는 메모리 여역을 할당하고 참조해서 통신하는 방식.
마치 변수처럼 공유된 메모리를 사용할 수 있다.


시그널

프로세스가 프로세스에게 어떤 이벤트가 발생했음을 알려줌.
여러가지 시그널이 존재한다.

기본동작이 없는 시그널에 동작을 정의하고 시그널을 발생 시키는 방식으로 프로세스간 통신을 한다.
커널모드에서 사용자모드로 전환시 시그널을 처리한다.


소켓

네트워크 통신을 위한 기술이다.
소켓을 이용해서 다른 컴퓨터와 통신하는 것이 아니라 컴퓨터 내에서 다른 프로세스간에 통시에 이용하면 프로세스간 통신을 할 수 있다.

소캣을 사용하면 네트워크 디바이스 드라이버 까지 가서 통신을 하게 되는데, 이부분이 커널 영역에 있기 때문에 가능하다.

프로세스간 커뮤니케이션

InterProcess Communication 으로서 (IPC)라는 용어를 많이 사용한다.


프로세스간 커뮤니케이션이 필요할까?

원칙적으로는 프로세스간 커뮤니케이션은 위험하다.
프로세스가 다른 프로세스를 제어 하게 되면 해킹이랑 같다.

따라서 기본적으로 모든 프로세스는 독립되어 있고 서로 가상 메모리를 사용하기 때문에 프로세스 내에서 다른 프로세스에 메모리 주소와 같은 주소로 접근한다고 해도 프로세스를 넘어서는 참조가 불가능하다.
왜냐하면 그 주소는 가상 메모리 이고 실제 메모리에는 무엇이 있을지 모르기 때문이다.
프로세스 내에서 접근하는 메모리 주소는 가상 메모리 주소임을 기억하자.

그럼에도 불구하고 필요하다.

요즘에 CPU는 다중 코어를 지원하기 때문에 성능을 잘 활용하기 위해서는 프로세스간의 통신이 필요하다.


공유하는 파일을 통한 방법

어떤 프로세스든 저장매체는 사용할 것이다.
프로세스 끼리에 접근은 불가능하지만 저장매체를 통한 통신은 가능하기 때문에 파일을 통해서 결과값을 공유한다던지에 기능을 사용할 수 있다.

문제점?

공유하는 파일에 동기화가 어렵다. 프로세스1에서 공유하는 파일을 최신화 했더라도 프로세스 2에서는 확인할 방법이 없다. 확인하려면 확인하는 작업을 위한 코드를 또 작성해야 하고 언제 최신화 할지 모르는 작업을 위해 이런것을 반복해야 한다.

또한, 기본적으로 저장매체를 갔다 오는데 메모리에 비해서 시간이 오래 걸린다.


프로세스 구조 실제 예 리눅스

모든 프로세스는 4기가에 공간을 갖는다.
실제로 4기가는 아니고 가상 메모리로 4기가를 갖는다.
사용자 영역인 0~3 기가에 코드에서 커널 영역인 3~4기가 영역을 접근하는 것은 불가능하다.


커널 영역은 공유 가능하다.

프로세스가 10 존재한다면 운영체제 부분이 있는 커널 영역도 10개가 존재한다는 말인데 그러면 비효율 적이지 않나?

간단히 말하면 프로세스에 커널 영역은 실제 메모리 영역 부분을 참조한다. 그래서 같은 영역을 바라보기 때문에 공유 하고 있다. 이렇게 보면 10개 다 만들어지는 것이 아니라 운영체제 부분인 커널 영역은 물리 메모리에 존재하고 10개의 프로세스가 그 물리 메모리에 주소를 참조한다고 볼 수 있다.


핵심은 커널이다.

다양한 IPC 기법이 존재한다.
앞에서 보았듯이 모든 프로세스는 커널 영역을 공유하고 있다.
따라서 다양한 IPC 기법에 들어가는 핵심은 커널 영역을 사용하는 것이다.

Thread

Light Weight Process 라고도 하며, 작은 프로세스라고 생각하면 된다.
스레드는 하나의 프로세스 안에서 여러개 생성이 가능하기 때문에 IPC 이를 이요하지 않더라도 각 스래드 끼리 데이터를 통신할 수 있다.

스레드는 프로세스에서 스택과 힙 사이 영역에 각각의 스레드를 위한 공간을 할당하는 방식을 동작한다.
각각의 영역 안에서 PC와 SP 를 가지기 때문에 독립적으로 동작할 수 있으면서도 프로세스에 나머지 영역을 공유 하기 때문에 데이터를 다른 스레드의 결과로 생긴 데이터라도 참조 할 수 있다.


멀티 프로세싱과 Thread

  • 멀티 태스킹 : 하나의 CPU에 여러 프로세슬 조금씩 실행시켜서 동시에 실행 되는 것처럼 보이게 하는 기술
  • 멀티 프로세싱 : 여러 CPU에 여러 프로세스를 실행시켜서 실행속도를 높이는 기술

하나의 프로세스를 나누서 여러개의 CPU를 넣는 작업을 어떻게 진행할까?

하나의 프로세스에 스레드를 나눠서 각가의 CPU에 넣는 방법을 사용할 수 있다. Thread는 멀티 코어 환경에서 실행 속도를 높이는데 기여할 수 있다.


Thread 의 장점

  1. 사용자에 대한 응답성 향상 : 특정 작업을 위한 스레드를 만들고, 사용자와 커뮤니케이션을 위한 스레드를 만들어서 사용자와 상호작용 하면 응답성이 높아진다.
  2. 자원 공유가 효율적이다. : 프로세스 안에서 나누어 지기 때문에 자원 공유를 위한 별도의 작업을 안해도 된다.
  3. 작업이 분리되어 코드가 간결하다.

Thread 의 단점

프로세스안에서 여러 프로세스를 나누기 대문에 하나의 스레드의 문제가 생겨도 감싸고 있는 프로세스의 문제가 생긴다.

리눅스 os에 경우 thread를 프로세스와 같이 처리하기 때문에 스레드가 많아지면 컨텍스트 스위칭이 많이 일어나고 이에 따라 성능이 저하될 수 있다.


동기화 이슈

프로세스 내에서 같은 공간을 공유 하기 때문에 스레드 간 동기화 이슈가 있다.
하나의 스레드가 오래 걸리는 작업을 수행하는데 컨텍스트 스위칭이 실행됬다면 공유하는 메모리에 저장하기도 전에 다른 스레드에서 작업이 실행될 것이다.
원하는 동작을 지원하지 않게되고 이런 이유로 동기화 이슈가 생긴다.


해결 방법

mutual excrusion

임계 영역을 설정하고 한번에 한스레드만 접근할 수 있도록 설정한다.

1
2
3
4
5
6
def thread_main():
global g_count
lock.acquire() # 임계영역 설정
for i in range(1000000):
g_count=g_count + 1
lock.release() # 임계영역 해제

컨텍스트 스위칭이 일어나도 임계영역에서는 하나의 스레드만 실행 되기 때문에 지금 실행할 스레드에서 전에 실행중인 스레드에 임계영역에 접근할 수 없다.
전에 스레드에서 임계영역을 빠져나오면 키를 다음 스레드에 넘겨주는 형식이라고 생각하면 된다.


Mutex와 세마포어

mutal excrusion 에서 임계 구역에 하나의 스레드만 들어갈 수 있으면 Mutex(binary semaphore) 라고 한다.

반대로 임계 구역의 여러개의 스레드가 들어갈 수 있으면 Semaphore 라고 한다.
counter를 두어서 동시에 들어갈 수 있는 스레드 수를 제어한다.


세마포어

  • P: 검사 (임계 영역에 들어갈 때)
    • s 가 0이 아니면 들어가고 s를 1 차감
  • V: 증가 (임계 여역에서 나올 때)
    • s에 1를 더하고 나옴.
  • S: 세마포어 값 (초기 값만큼 여러 프로세스가 동시 임계 영역에 접근 가능)

세마포어-바쁜대기

s가 0이라면 임계영역에 들어가기 위해 반복문 계속 수행
해결하려면??

세마포어-대기큐

반복문을 돌리지 않고 s값이 0이면 대기큐에 넣고 슬립시킴.
s가 1 이상이 되면 대기큐에서 하나 없애고 깨우며는 방식을 사용한다.


교착 상태(deadlock)란?

무한 대기 상태 : 두개 이상의 작업이 상대방 작업이 끝나기만을 기다리고 있기 때문에, 다음 단계로 진행하지 못하는 상태

기아상태 (starvation)

스레드 각각 우선순위를 지정할때 특정 스레드는 우선순위가 낮다고 가정한다. 그렇게 되면 프로세스를 실행할때 특정 스레드는 계속해서 실행되지 않게 될 수 있는데 이런 상태를 기아 상태라고 한다.

우선순위를 변경하는 방법으로 해결할수도 있고, 우선순위 없이 처리하도록 함으로서 해결할 수 도 있다.

가상메모리

하나의 프로세스는 4기가의 크기를 가짐.
모든 컴퓨터는 폰노이만 구조를 따르기 때문에 코드는 메모리에 반드시 올라가야 한다.
그러나 보통의 컴퓨터는 8~16 기가에 크기를 가지는데 어떻게 여러 프로세스를 구동 시키는 걸까?

프로세스마다 특정 시점에서 cpu가 실행시킬수 있는 부분은 극히 작은 영역 이다.
따라서 작은 영역만 물리 메모리에 올려 놓고 주소를 참조할 수 있게 하는 방법을 사용한다.

기본 아이디어

프로세스는 가상 주소를 참조하여 실행시키고 실제 데이터를 읽고 쓸때만 물리 주소로 변경하여 준다.
가상 주소를 물리 주소로 바꿔주는 과정이 필요하다.

MMU (Memory Management Unit)

CPU에서 가상 메모리 주소 접근이 필요할때, 가상 메모리 주소를 물리 주소값으로 변환 시켜주는 하드웨어 장치


페이징 시스템 (Paging system)

크기가 동일한 페이지로 가상 주소공관과 이에 매칭하는 물리 주소 공관을 관리 하는 것이다.
하드웨어 지원이 필요하고 각가의 운영체제마다 관리하는 페이지의 크기는 다 다르다.

리눅수에서는 4kb로 페이지를 관리하는데 프로세스 크기가 4gb이기 때문에 4gb를 4kb로 나눠서 각각의 페이지에 번호를 붙혀서 관리한다.

프로세스의 PCB 안에 Page Table 이라는 구조체가 있다.
여기에 각각의 페이지에 대한 실제 물리 주소 매핑 정보가 들어 있는데, CPU 에서 이를 참고하여 페이지를 관리한다.

페이지 시스템의 구조

9kb 에 프로세스가 있다고 하고 페이지에 크기가 4kb 라고 하면 나머지인 1kb만 실제 물리 메모리 매핑으로 넣는 것이 아니라 1kb코드 영역 나머지 3kb 는 빈 영역으로 해서 매핑시킨다.

가상 주소 V = (p, d)

  • p: 가상 메모리 페이지
  • d: p안에서 참조하는 위치 (변위)

페이지는 무조건 공통된 크기만큼을 넣어주어야 하기 때문에 페이지 내에서 실제 코드가 있는 부분과 페이지 주소가 같지 않을 수 있다.
따라서, 특정 역역만큼 변위를 저장할 수 있도록 해서 정확하게 코드에 매칭될 수 있도록 한다.

실제로는 Page Tbale 안에 물리 주소로 들어가 있는지 여부를 확인하는 valid-invalid bit 가 존재한다.


다중 단계 페이징 시스템

프로세스를 구성하는 코드 부분이 실제로 4기가를 다 채우는 일은 적다.
따라서 모든 페이지를 페이지 테이블로 만들어서 메모리에 올리는 일은 굉장히 비효율적인 일이 될것이다.

페이지 정보를 단계를 나누어 생성하면 이런 문제를 해결 할 수 있다.
이를 다중 단계 페이징 시스템이라고 한다.

CR3 레지스터에서 페이지 디렉터리를 읽어옴.
페이지 데릭터리를 통해서 페이지 테이블을 읽어옴. (페이지 디렉터리를 활용해서 필요한 부분의 페이지 테이블만 만듦.)


MMU와 TLB(컴퓨터 구조)

이런 식으로 물리 메모리를 구하게 되는데 문제는 메모리로 이동하는 시간이 오래 걸린다는 것이다.

TLB(Translation Lookaside Buffer): 페이지 정보 캐쉬

TLB를 통해서 최근에 흭득한 매핑 정보를 캐쉬 해서 메모리에 접근을 안하도록 한다.



페이징 시스템과 공유 메모리

어떤 프로세스를 포크 했다면 포크한 포르세스에서 페이지 테이블을 이용해 특정 공간을 복사한 프로세스에 특정 공간이 실제 물리 메모리에서 바라보는 공간으로 매칭 시킨다면 공유 메모리가 되어 공간을 절약하고, 메모리 할당 시간을 절약할 수 있다.

다만, 포크한 프로세스에서 공유하고 있는 공간에서 쓰기 작업을 해야할때에는 복사한 프로세스에 영향을 주면 안되기 때문에 이때에는 실제 메모리에서 복사가 일어나고 포크한 페이지 테이블에서 복사한 공간을 바라보도록 수정한다.


요구 페이징(Demand Paging or Demanded Paging)

선행 페이징의 반대 개념. (미리 메모리에 올려 놓는 개념)

미리 메모리에 올려 놓는 것이 아니라 특정 시점에서 필요한 부분만 메모리에 올려놓는 것을 요구 페이징이라고 한다. 페이지 테이블에서 valid-invalid bit를 확인하여 메모리에 올라가 있지 않다면 올리거나 따로 필요하지 않게 되면 내리는 시스템이다.
메모리에 올려 놓는 페이지를 교체해야 하기 때문에 교체 알고리즘이 필요하다.

페이지 폴트

어떤 페이지가 메모리에 없을때 발생되는 인터럽트, 인터럽트가 발생되면 해당 페이지를 메모리에 올려 놓게 된다.

전체적인 구조 & 순서

페이지 폴트가 자주 일어나면 이런 작업을 많이 해야 하기 때문에 시간이 올래 걸린다.
페이지 폴트가 안 일어나게 하는게 제일 좋은데 그렇게 하려면 앞으로 메모리에 올릴 페이지가 무엇인지 예측하는 방법을 사용해야 한다.
이 작업에 여러 알고리즘이 있지만 여전히 어려운 부분이다.