-
Notifications
You must be signed in to change notification settings - Fork 0
2주차 월요일 그룹 7
- 여려 명령어들의 흐름, 집합으로 하나의 실행 단위이다.
- 자세하게는 어떤 프로그램이 메모리에 올라가 인스턴스화 되면 그걸 프로세스라고 한다.
- 하나의 실행 단위이기 때문에 운영체제 입장에서는 자원 할당을 할 수 있는 단위가 된다.
- 프로그램은 사용자들에게 어떤 기능이나 서비스를 제공하기 위한 명령어들의 집합이다.
- 이 프로그램은 실행 파일 형태로 되어 있을 것이고 이를 실행하면 메모리에 필요한 명령어들과 데이터들을 올라가서 인스턴스화 되면 프로세스라고 부른다.
- 그럼 프로세스도 수많은 명령어들의 흐름을 가지고 있을 것이다. 이때 이 각각의 흐름을 수행하는 것이 스레드이다.
- 즉 실질적으로 프로세스 단위로 할당 받는 자원들을 활용하여 수많은 명령어들을 실행하는 것이 스레드이다.
- Process Control Block은 프로세스의 상태 정보를 저장하는 일종의 자료구조이다.
- 여기에는 레지스터의 정보, 프로그램 카운터 정보, 프로세스의 메모리 정보와 상태 정보, 프로세스 아이디, 우선순위, 스케줄링 정보 등이 들어잇다.
- 스레드도 스레드의 상태 정보를 저장하는 자료구조가 있고 Thread Control Block → TCB가 있다.
- TCB는 프로세스 안에서 명령어들을 처리하기 때문에 프로세스의 메모리 정보는 공유하게 된다. 즉, TCB에는 메모리와 관련된 정보가 필요 없다.
- 그렇다면 무엇을 실행할 것인지에 대한 정보만 있으면 되므로 TCB의 구조는 레지스터 정보, 프로그램 카운터 정보 등의 필수적인 정보만 포함한다.
- fork() 시스템 콜을 통해 시스템 콜을 호출한 프로세스와 동일한 프로세스를 우선 생성한다. 호출한 쪽을 부모, 생성된 쪽을 자식 프로세스라고 한다.
- 자식 프로세스에서 exec()을 호출하면 이때부터 서로 다른 프로세스처럼 보이게 한다.
- 이 과정을 통해 init 프로세스로부터 모든 프로세스가 파생된 프로세스 트리가 구현될 수 있다.
- fork()를 하여 exec()을 할 때도 단순히 복사하고 메모리를 별도로 분리하는 것이 아니다. 여러 모델들이 있다.
- 완전히 별도로 분리가 필요하기 전까지 부모의 데이터를 공유, 즉 부모의 정보를 포인터로 가리키고 있는 형태가 있다.
- 물론 계속 공유하는 모델, 바로 분리하는 모델도 있다.
- 스레드의 경우 pthread_create()를 통해 생성한다.
- 스레드는 전역 변수, 코드 영역을 공유하고 고유의 스택 영역만 생성해서 가져간다.
- 이는 스레드의 경우 프로세스의 메모리를 공유하기 때문이다
- 참고로, JAVA에서 thread 생성은 Thread class를 상속하거나 Runnable interface를 구현해
run()
을 오버라이딩 하면 된다.
- 컴파일 타임에 결정됨
- stack, heap영역은 할당되는 메모리 영역 중에서는 가장 크다고 할 수 있음. 왜냐하면 데이터, 코드 영역을 보고 그걸 기반으로 stack, heap영역을 결정하기 때문임
- 흔히 heap 영역이 동적으로 결정된다고 하는데 이건 틀렸다고 생각함
- 정확히는 heap 영역에 저장되는 객체의 크기가 동적으로 결정되는 것이고 영역 자체는 컴파일 타임에 결정되는 것이 맞음. 안그러면 알고리즘에서 객체 무한으로 생성했을 때 예외 발생하는 것도 말이 안됨.
- stack영역이 heap영역보다 상대적으로 작은데 이는 메소드 호출 특성상 금방 호출되고 사라지기 때문임. 보통 사용자가 헛짓거리 안하면 스택 영역에 계속 메소드가 쌓일 일이 없음(헛짓거리 = 탈출 방법이 없는 재귀함수)
- heap 영역이 동적 객체 생성에 사용되어 여기가 상대적으로 큰게 맞음
- stack 영역이 더 빠르다.
- 자바 기준으로 생각해보면 객체를 생성하면 heap 영역 으로 저장된다. 메소드를 호출하면 stack 영역에 메소드 메모리가 쌓인다.
- 자바 객체 생성에는 생성자를 사용한다. 이 생성자도 메소드이고 결국 스택 영역에 쌓인다. 그리고 생성된 객체는 heap 영역에 저장된다.
- 이때 heap 영역에 접근하려면 스택 영역에서 접근을 해야한다. 생성자를 거기서 호출했으니까.
- 이때 접근할 수 있는 포인터를 this로 제공한다. 스택영역에 마찬가지로 this가 저장되어 있다.(파라미터 정보 등과 함께)
- 여기까지 봤으면 heap 영역은 stack을 거쳐야 하고, stack영역은 stack 영역만 보면 되는 것을 알 수 있다.
- 다른 이유로는 stack 영역은 메모리의 시작 지점과 offset을 알면 바로 접근이 가능한 구조이다. 하지만 heap 영역의 경우 메모리의 어느 부분에 저장되었는지 모른다. 그래서 this같은걸 제공하는 것이기도 하다.
- 이런 특징을 보면 stack 영역은 캐시 활용도가 높을 것이고 heap 영역은 캐시 활용도가 낮을 것이다.
- 위의 복합적인 이유로 stack 영역이 접근 속도가 빠를 수 밖에 없다.
멀티 쓰레드의 경우 자원을 공유하기 때문에 적은 메모리와 자원을 소모하며 컨텍스트 스위칭 또한 빠르다는 장점이 있습니다. 다만 여러 개의 쓰레드가 공유하는 자원 공간의 데이터 일관성을 유지하기 위해 동기화 기법을 사용해야한다.
var s1 = new String("abc")
var s2 = "abc"
모든 스레드가 공유하는 영역은 method area와 heap 영역입니다. 그 중에서도 문자열 상수 리터럴이 저장되는 공간이 어디인지가 헷갈렸었는데 Method Area 의 Runtime Constant Pool 에 저장되는 것이 아닌 Heap 영역의 String Constant Pool
에 저장됩니다.
출처 https://kkyu0718.tistory.com/126
-
Thread t = Thread.ofPlatform()
JVM 의 플랫폼 쓰레드를 생성합니다. - 플랫폼 쓰레드가 동작하려면 CPU 에 접근해야 합니다. 그러기 위해서는 커널 쓰레드와 매핑이 1:1 로 되어야하는데 이에 대한 중간 매개체가
JNI(Java Native Interface
입니다.t.start()
를 하면 JNI 를 통해 커널 쓰레드와 매핑이 됩니다. - 플랫폼 쓰레드와 커널 쓰레드가 1:1 매핑이 되고 나서는 CPU 를 점유해야 합니다. 이를 위해서 OS 안의 스케줄러가 존재합니다. OS 의 스케줄러가 상황에 맞게 쓰레드가 CPU 코어를 점유할 수 있도록 해줍니다.
하지만 기존의 쓰레드는 다음과 같은 문제점이 있었습니다.
- 쓰레드의 생성 (Thread.ofPlatform()) 과 스케줄링(interrupt, sleep) 에 항상 os 가 관여해야 합니다. 예를 들어 플랫폼 쓰레드 1 를 중단시키는 상황이라면, 매핑되어있는 커널 쓰레드도 같이 중단시켜야 하기 때문입니다. 이 과정에서 os 를 항상 호출
시스템 콜
이 발생하여컨텍스트 스위칭 오버헤드
가 발생합니다. - 또한, 블락킹 모델이기 때문에 성능이 좋지 못합니다. 어떠한 의미인지 밑의 그림에서 보도록 하겠습니다.
출처 https://kkyu0718.tistory.com/126
Blocking 모델
JVM Tomcat 의 쓰레드 수는 단 2개 밖에 없는 상황입니다. 요청 1, 2 를 각각 플랫폼 쓰레드 1, 2 에서 처리하고 있었으나 플랫폼 쓰레드 2가 블락이 되었고 요청 3번이 왔습니다. 논블락킹 모델이었다면 플랫폼 쓰레드 2가 블락되지 않고 다음 요청을 처리할 수 있으나, 블락킹 모델이기 때문에 요청 3은 쓰레드 여유분이 나올 때까지 대기하게 됩니다.
- 자바의 스레드는 네이티브 스레드(운영체제의 스레드)와 직접적으로 매핑됨.(1대 1)
- 자바의 Thread 클래스 인스턴스를 생성하면, jvm은 운영체제의 스레드를 생성함.
- 장점: 운영체제의 스레드 라이브러리를 활용하여 스레드를 관리하므로, 운영체제의 멀티코어 및 멀티 프로세서 기능을 최대한 활용할 수 있음.
-
자바 코드에서 스레드 생성
Thread myThread = new Thread(()->{ }); thread.start();
-
jvm이 네티이브 메서드 호출
- myThread.start()가 호출되면, jvm은 네이티브 메서드를 통해 운영체제의 스레드를 생성한다.
- Thread객체를 생성할때가 아님. 필요한 시점에 정확하게 스레들를 생성하여 자원을 관리.
-
운영체제에서 스레드 생성:
- jvm의 네이티브 메서드는 운영체제의 api를 호출(시스템콜)하여 새로운 스레드를 생성합니다. ex) 리눅스 pthread_create
- 스레드 생성이 무거운 작업인 이유: 시스템 콜은 cpu가 커널 모드로 들어가기 때문에 무거운 작업임.
-
스레드 실행:
- 운영체제는 새로운 스레드를 스케줄링하고 자바 코드의 스레드 함수 run()이 실행됨.
- 서버 어플리케이션 (ex스프링)에서 요청당 스레드 할당
- 데이터베이스 시스템에서 PostgreSQL은 쿼리를 병렬로 처리하여 성능을 향상.
- JAVA NIO 라이브러리를 통한 비동기 파일 I/O → 대용량 파일을 읽고 쓰는 작업을 메인 스레드가 아닌 별도의 스레드가 처리
- 크롬에서 다운로드, 랜더링, 여러 탭 등 다양한 작업을 병렬적으로 실행
- 프로세스와 마찬가지로 스레드 간의 스케줄링도 운영체제에 의해서 스케줄링 된다. 이때 프로세스 스케줄링과 스레드 스케줄링은 독립적으로 다뤄지며 서로 다른 알고리즘이 적용될 수 있다. 스레드 스케줄링은 보통 우선순위 기반 스케줄링이나, 다중 큐 스케줄링과 같은 알고리즘을 사용한다.
- JVM의 스레드 스케줄링은 주로 운영체제의 스케줄링 정책에 의존한다. 스레드에 우선순위를 할당할 수있지만(thread.setPriority()) 실제 우선순위의 효과는 운영 체제의 스케줄링 정책에 크게 의존적이다. 우선순위가 높은 스레드가 무조건 많이 실행되는 것이 보장되지 않는다.
- PCB 교체하는 과정에서 프로세스가 idle이 됨.
- 컨텍스트 스위칭이 일어나면 페이지 교체가 일어날 확률도 많아져서 더 스루풋이 나빠질 수 있다.
- PCB가 아니라 TCB 블락만 바뀌어도 됨.
- 프로세스 컨텍스트 스위치 시에는 다른 프로세스의 메모리에 접근하지 못하도록 TLB를 clear해야함. TLB 캐시는 바꾸지 않아도 됨.
- 역할 구분
- 프로그램을 실행하면 운영체제가 프로세스를 생성하고 메모리를 할당한다. 운영체제는 프로세스의 메모리를 코드, 데이터, 스택, 힙 영역으로 구분하여 관리한다. 각 메모리 영역은 특정한 용도를 가지며 실행시 메모리 크기 변경 여부, 접근 권한이 다르다.
- 프로그램이 돌아가고 있는 상태
- 작업 중인 프로그램을 의미
- 하나의 프로세스 내에서 동시에 진행되는 작업 갈래, 흐름의 단위
- 코드 영역: 프로그래머가 작성한 프로그램 함수들의 코드가 CPU가 해석 가능한 기계어 형태로 저장되어 있다.
- 데이터 영역: 코드가 실행되면서 사용하는 전역 변수나 각종 데이터들이 모여있다.
- .data : 전역 변수 또는 static 변수 등 프로그램이 사용하는 데이터를 저장
- .BSS : 초기값 없는 전역 변수, static 변수가 저장
- .rodata : const 같은 상수 키워드 선언된 변수나 문자열 상수가 저장
- 스택 영역: 지역 변수와 같은 호출한 함수가 종료되면 되돌아올 임시적인 자료를 저장하는 독립적인 공간이다. 함수 호출과 함께 할당되며, 함수 호출이 완료되면 소멸한다.
- 힙 영역: 생성자, 인스턴스와 같은 동적으로 할당되는 데이터들을 위해 존재하는 공간이다. 사용자에 의해 메모리 공간이 동적으로 할당되고 해제 된다.
- 쓰레드는 프로세스가 할당 받은 자원을 이용하는 실행의 단위
- 쓰레드끼리 프로세스 자원을 공유하면서 프로세스 실행 흐름의 일부가 되기 때문에 동시 작업 가능
- 쓰레드는 Stack만 할당받아 복사하고 나머지는 프로세스 내의 다른 쓰레드들과 공유한다.
- 하나의 프로세스를 다수의 실행 단위인 쓰레드로 구분하여 자원을 공유하고, 자원의 생성과 관리의 중복성을 최소화하여 수행 능력을 올리기 위해서이다.
- 각 프로세스는 메모리에 별도의 주소 공간에서 실행되기 때문에 한 프로세스는 다른 프로세스의 변수나 자료구조에 접근할 수는 없다.
- 특별한 방법을 통해 프로세스가 다른 프로세스의 정보에 접근하는 것이 가능하다.
- IPC(Inter-Process Communication) 사용
- LPC(Local Inter-Process Communication) 사용
- 별도로 공유 메모리를 만들어서 정보를 주고 받도록 설정
- CPU 한 개는 여러개의 코어를 가질 수 있다.
- 코어는 말그대로 CPU 코어 유닛으로 명령어를 메모리에서 뽑아 해석하고 실행하는 반도체 유닛이다.
- 쓰레드는 논리적 코어 갯수를 말한다.
- 이는 물리적 코어가 여러 개의 쓰레드를 동시에 실행 가능하다는 의미가 된다.
- 이를 하이퍼 쓰레딩 기술이라 말한다.
- 병렬성
- 직관적으로 명령어를 메모리에서 뽑아 해석하고 실행하는 반도체 유닛인 여러개의 코어에 맞춰 여러개의 프로세스, 쓰레드를 돌려 병렬로 작업들을 동시 수행하는 것을 말한다.
- 동시성
- 둘 이상의 작업이 동시에 실행되는 것을 의미한다.
- 동시에 실행하는 것처럼 보이게 하는 것이다.
- 단, 이때 작업들을 번갈아가면서 실행할 때 작업들을 아주 잘게 나누어 아주 조금씩만 작업을 수행하고 다음 작업으로 넘어가는 식으로 동작된다.
- 여러 작업을 동시에 처리하는 것처럼 보이게 만들어, 사용자에게 더 빠른 반응성을 제공하기 위해서이다.
- 이렇게 진행 중인 작업들을 A→B→C→D 번갈아 바꾸는 것을 Context Switching이라 부른다.
- 운영체제에서 CPU를 사용할 수 있는 프로세스를 선택하고, CPU를 할당하는 작업을 말한다.
- 프로세스의 우선순위, 작업량 등을 고려하여 효율적으로 배치하여, 이를 통해 CPU를 효율적으로 사용하며 시스템 전반적인 성능을 향상시킨다.
- 프로세스의 상태는 프로세스가 실행되는 동안 변경되는 고유 상태를 의미한다.
- new: 프로세스가 생성되고 아직 준비가 되지 않은 상태
- ready: 프로세스가 실행을 위해 기다리는 상태, CPU를 할당 받을 수 있는 상태이며, 언제든지 실행될 준비가 되어 있다.
- running: 프로세스가 CPU를 할당받아 실행되는 상태
- waiting: 프로세스가 특정 이벤트가 발생하여 대기하는 상태. CPU 할당 받지 못하며, 이벤트가 발생하여 다시 READY 상태로 전환될 때까지 대기한다.
- terminated: 프로세스가 실행을 완료하고 종료된 상태. 메모리에서 제거되게 된다.
- Admitted : 프로세스 생성을 승인 받음
- Dispatch : 준비 상태에 있는 여러 프로세스들 중 하나가 스케줄러에 의해 실행됨
- Interrupt : timeout, 예기치 않은 이벤트가 발생하여 현제 실행 중인 프로세스를 준비 상태로 전환하고, 해당 작업을 먼저 처리
- I/O or event wait : 실행 중인 프로세스가 입출력이나 이벤트를 처리해야 하는 경우, 입출력이나 이벤트가 끝날 때까지 대기 상태로 전환
- I/O or event completion : 입출력이나 이벤트가 모두 끝난 프로세스를 다시 준비 상태로 만들어 스케줄러에 의해 선택될 수 있는 상태로 전환
- CPU 활용률을 높이기 위해 컨텍스트 스위칭 필요
- 동작 중인 프로세스가 대기를 하면서 해당 프로세스의 상태를 보관하고, 대기하고 있던 다음 순서의 프로세스가 동작하면서 이전에 보관했던 프로세스의 상태를 복구하는 작업을 말함
- 컨텍스트 스위칭을 하는 주체는 스케줄러이다.
-
PCB는 운영체제에서 프로세스를 관리하기 위해 해당 프로세스의 상태 정보를 담고 있는 자료구조를 말한다.
-
프로세스를 컨텍스트 스위칭 할 때 기존 프로세스의 상태를 어딘가 저장해야 다음에 똑같은 작업을 이어서 할 수 있을 것이고, 새로 해야 할 작업의 상태 또한 알아야 어디서 부터 작업을 시작할 지 결정할 수 있다.
-
PCB는 프로세스 스케줄링을 위해 프로세스에 관한 모든 정보를 저장하는 임시 저장소이다.
-
PCB에 담긴 프로세스 고유 정보를 통해 프로세스를 관리하고 프로세스 실행 상태를 파악하고 우선 순위를 조정하며 스케줄링을 수행하고 다른 프로세스와의 동기화를 제어한다.
-
포인터: 프로세스의 현재 위치를 저장하는 포인터 정보
-
프로세스 상태: 프로세스 각 상태
-
프로세스 ID: 프로세스 식별자를 지정하는 고유한 ID
-
프로그램 카운터: 프로세스를 위해 실행될 다음 명령어의 주소를 포함하는 카운터를 저장
-
레지스터: 누산기, 베이스 레지스터 및 범용 레지스터를 포함하는 CPU 레지스터에 있는 정보
-
메모리 제한: 운영 체제에서 사용하는 메모리 관리 시스템에 대한 정보
-
열린 파일 목록: 프로세스를 위해 열린 파일 목록
- CPU는 P1 실행
- 일정 시간이 지나 Interrupt 또는 system call이 발생
- 현재 실행 중인 P1의 상태를 PCB1에 저장
- 다음으로 실행할 P2 선택
- P2의 상태를 PCB2에서 불러옴
- CPU가 P2 실행
- 일정 시간이 지나 Interrupt 또는 system call이 발생
- P1 실행 차례가 되면 P1 상태를 PCB1에서 불러온다
- CPU는 P1을 중간 시점부터 실행한다.
- 위 그림에서 P1이 Execute에서 idle이 될 때 P2가 바로 Execute가 되지 않고 idle 상태에 조금 있다가 Execute가 되는 간극이 컨텍스트 스위칭 오버헤드이다.
- 오버헤드는 위 말고도 여러개가 있는데 아래와 같다
- PCB 저장 및 복원 비용
- CPU 캐시 메모리 무효화에 따른 비용
- 프로세스 스케줄링 비용
- 프로세스 자체 교체로 CPU 캐시 메모리에 저장된 데이터가 무효화 됨.
- 프로세스 선택 비용도 만만치 않음.
- 운영체제에서 다중 쓰레드를 관리하며, CPU를 사용할 수 있는 쓰레드를 선택하고 CPU를 할당하는 작업
- 쓰레드 스케쥴링은 하나의 프로세스 내에서 다수의 쓰레드가 동작하는 형태이기 때문에, 쓰레드 간의 상호작용과 동기화 문제를 고려해야 한다는 차이점이 존재
- NEW : 쓰레드가 생성되고 아직 호출되지 않은 상태
- RUNNABLE: 쓰레드가 실행되기 위해 기다리는 상태 CPU를 할당 받을 수 있는 상태이며 실행 준비가 되어 있음
- BLOCKED: 쓰레드가 특정 이벤트가 발생하여 대기하는 상태 CPU 할당 받지 못하고 이벤트가 발생하여 다시 RUNNABLE 상태로 전환될 때까지 대기
- TERMINATED: 쓰레드가 실행을 완료하고 종료된 상태. 메모리에서 제거됨
- 멀티 쓰레딩 환경에서 쓰레드 간의 실행을 전환하는 기술이다.
- PCB처럼 TCB는 각 쓰레드마다 운영 체제에서 유지하는 쓰레드에 대한 정보를 담고 있는 자료구조이다.
- PCB 안에 들어있다.
- 쓰레드 상태 정보, 쓰레드 ID, 쓰레드 우선순위, 쓰레드 스케줄링 정보를 저장한다.
- 쓰레드가 실행을 마치고 소멸될 때 함께 소멸된다.
- 쓰레드 간의 자원 공유와 동기화도 TCB를 사용하여 관리 된다.
- 뮤텍스나 세마포어와 같은 동기화 기법을 사용할 때 TCB에서 정보를 관리하고, 쓰레드가 해당 자원에 대한 접근 권한을 흭득하거나 반납할 때 TCB의 정보를 업데이트하게 된다.
- 뮤텍스 : 임계 구역에 1개 쓰레드만 들어갈 수 있는 동기화 기법
- 세마포어 : 임계 구역에 여러 쓰레드가 들어갈 수 있고 , counter를 두어서 허용 가능한 쓰레드를 제한하는 기법
-
TCB가 PCB보다 가볍다.
stack을 제외한 메모리를 공유하기 때문에 stack 및 간단한 register 포인터 정보만을 저장해서 TCB가 가볍다.
-
캐시 메모리 초기화 여부
프로세스는 CPU가 새로운 명령어와 데이터를 로드해야 하기 때문에 CPU 캐시 메모리를 초기화 하여야 한다.
쓰레드는 쓰레드 간에 스택과 레지스터 값 등 일부 컨텍스트 정보만 변경되므로 CPU 캐시 메모리는 초기화되지 않는다.
다만 쓰레드가 다른 CPU 코어에서 실행 될 때는 해당 코어의 캐시 메모리에 쓰레드 컨텍스트 정보가 로드되어야 하므로 초기화 될 수 있다.
-
자원 동기화 문제
서로 다른 쓰레드가 heap 영역의 공유 데이터에 접근할 때 이전 쓰레드가 이미 공유 자원을 사용하고 있는 경우 동기화 문제가 발생할 수 있다.
두 개의 쓰레드가 동시에 하나의 변수를 수정하려고 할 때, 쓰레드 컨텍스트 스위칭이 발생하면 변수의 값을 잘못된 값으로 업데이트 할 수 있는 것이다.
이를 쓰레드 간의 경쟁 조건이라고 한다.
프로세스도 공유 자원을 사용하는 경우 발생할 수 있다.