bdfgdfg

[OS] 컴퓨터 구조 개요 본문

CS/운영체제

[OS] 컴퓨터 구조 개요

marmelo12 2022. 3. 30. 21:13
반응형

폰노이만 구조

 - 오늘날의 컴퓨터는 대부분 폰노이만 구조를 따른다.

  -> 폰노이만 구조란 cpu,메모리,입출력장치,저장창치가 버스로 연결되어 있는 구조.

  -> 폰노이만 구조의 가장 중요한 특징은 저장된 프로그램이 메모리에 올라와 순차적으로 실행된다는 개념.

 

CPU의 구성과 동작

- CPU는 명령어를 해석하여 실행하는 장치.

- CPU는 산술논리 연산장치(ALU), 제어장치(Control Unit), 레지스터(Register)로 구성. 

 -> 산술논리 연산장치는 데이터의 덧셈,뺄셈,곱셈,나눗셈과 같은 산술 연산과 AND,OR등과 같은 논리 연산을 수행한다.

 -> 제어장치는 CPU에서 처리하는 작업을 제어한다. (명령어 해석등)

 -> 레지스터는 CPU내에 데이터를 임시로 보관하는 곳을 레지스터. (CPU내부에 있으므로 접근속도가 메모리 중 가장빠름)

 

CPU의 간단한 연산 처리 순서. 

int main()
{
    int a = 3;
    int b = 5;
    int sum = a + b;
    return 0;
}

어셈블리어로 작성된 코드를 보면, 먼저 a와 b의 메모리에 각각 3,5값을 저장한다.

이제 a,b의 값을 덧셈하여 sum에 저장하는데,

먼저 a가 있는 메모리 번지의 값을 레지스터 eax에 저장하고, b가 있는 메모리 번지의 값을 레지스터 eax에 있는 값과 덧셈(add)연산을 한다. 그 후 연산된 결과값이 저장된 eax의 값을 sum의 메모리 번지에 저장한다.

 

이 과정에서 제어장치에 의해 위 코드(실제로는 기계어)를 해석하여 메모리에서 데이터를 가져오고(레지스터에 저장) 덧셈을 실행한 후(ALU) 덧셈한 결과를 메모리에 저장하는 과정.

 

버스의 종류

- 버스는 CPU와 메모리, 주변장치간에 데이터를 주고받을 때 필요하다.

 -> 다음 작업을 지시하는 제어 신호, 메모리의 위치 정보를 알려주는 주소, 처리할 데이터가 오고 가며 각각 제어 버스,주소 버스, 데이터 버스에 실린다.

https://velog.io/@dahye-program/OS-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B3%B5%EB%B6%80-%EC%BB%B4%ED%93%A8%ED%84%B0%EC%9D%98-%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%812

 - 제어 버스

  -> 다음에 어떤 작업을 할지 지시하는 제어 신호가 오고 간다. (메모리에서 데이터를 가져올지, 처리한 데이터를 옮길지)

  ->  메모리에서 데이터를 가져올 땐 읽기 신호를 보내고 처리한 데이터를 옮길 땐 쓰기 신호를 보낸다.

  -> 제어 버스는 cpu의 제어장치와 연결되어 있음. 또한 CPU,메모리,주변장치와 양방향으로 오고 간다.

- 주소 버스

 -> 주소 버스는 메모리의 데이터를 읽거나 쓸 때 어느 위치에서 작업할 것인지를 알려주는 위치 정보(주소)가 오고 간다.

- 데이터 버스 -> 데이터가 이 버스에 실려 목적지까지 이동.

 

캐시(Cache) ★

1.캐시란 메모리다. 흔히 RAM이라 불리는 메인 메모리와 CPU사이의 거리가 멀어 CPU의 처리시간이 늦어진다.

이러한 문제를 해결하기 위해 CPU와 메인 메모리 사이에 중간다리 역할을 하는 것이 캐시 메모리.(CPU안에 존재)

 

 2.캐시는 필요한 데이터를 모아 한꺼번에 전달하는 버퍼의 일종으로 CPU가 앞으로 사용할 것으로 예상되는 데이터를 미리 가져다 놓는다.

 

 3.캐시는 메모리의 내용 중 일부를 미리 가져오고, CPU는 메모리에 접근해야 할 떄 캐시를 먼저 방문하여 해당 데이터가 존재하는지 확인을 한다.

  --> 만약 원하는 데이터가 캐시에 존재 한다면 캐시 히트(Cache Hit), 없다면 캐시 미스(Cache Miss)라고 한다.

  --> 즉 캐시 적중률이 높아야 컴퓨터의 성능이 향상된다. 캐시 적중률을 높이는 이론으로는 현재 위치에 가까운 데이터가 멀리 있는 데이터 보다 사용될 확률이 높다지역성(locality)이론이 있다.

 

4.캐시는 임시 메모리이기에 메모리의 데이터를 미리 가져오고 난 후 캐시안의 데이터가 변경이 되었을 때 이를 메모리에 다시 반영을 해야한다. (즉 메모리에 있는 원래 데이터와 맞춰(동기화)줘야 한다)

  -> 이에 대한 방법으로 즉시 쓰기, 지연 쓰기방식이 존재하며 즉시 쓰기는 말 그대로 캐시의 데이터가 변경되면 즉시 메모리에 반영.

메모리와의 빈번한 데이터 전송으로 인해 성능이 느려진다는 단점이 존재하지만 캐시로 인한 가시성 문제를 고려할 필요가 없다.

(특히 멀티쓰레드 환경)

  -> 지연 쓰기는 캐시에 있는 데이터가 변경되면 즉시 메모리에 반영하지 않고 변경된 내용을 모아서 주기적으로 반영하는 방식.

메모리와의 데이터 전송 횟수가 줄어들어 시스템의 성능을 향상할 수 있지만 메모리와 캐시의 데이터간의 불일치가 발생할 수 있다.

 

위와 같은 가시성 문제는 멀티쓰레드 환경에서 조심해야 할 부분이기에 메모리 장벽과 같이 메모리의 동기화 처리를 해주어야 한다.

 -> 인텔 계열칩에서는 해당 문제를 찾아보기 힘들다고는 한다.

 -> 더 자세한 내용 https://marmelo12.tistory.com/320

 

인터럽트

초기의 컴퓨터에는 주변장치가 많지 않았기에 CPU가 직접 입출력장치에서 데이터를 가져오거나 내보내는 방식.

 -> 이러한 방식을 폴링(polling)방식이라 한다.

 -> 폴링 방식에서는 CPU가 입출력장치의 상태를 주기적으로 검사해서 일정한 조건을 만족할 때 데이터를 처리한다.

 -> 하지만 이러한 방식은 CPU 본래의 명령어 해석과 실행이라는 역할 외에 입출력까지 관여해야 하므로 작업효율은 떨어진다.

이러한 문제를 해결하기 위해 등장한 것이 인터럽트 방식.

 -> 인터럽트 방식은 CPU의 작업과 저장장치의 데이터 이동을 독립적으로 운영함으로서 시스템의 효율을 높인다.

 -> 즉 입출력이 이루어지는 동안 CPU는 다른 작업을 할 수 있다는 것.

입출력 관리자가 CPU에 보내는 완료 신호를 인터럽트라 하며 CPU는 입출력 관리자에게 작업 지시를 내리고 다른 작업을 하다가 완료 신호(인터럽트)를 받으면 하던 일을 중단하고 옮겨진 데이터를 처리한다.

 

병렬 처리

CPU의 성능을 향상시키는 가장 좋은 방법은 CPU의 클럭을 높이거나 캐시의 크기를 늘리는 것.

 -> 클럭은 CPU의 속도와 관련된 단위. 클럭이 일정한 간격의 틱을 만들면 거기에 맞추어 CPU안의 모든 구성 부품이 작업을 한다.

그러나 CPU의 클럭을 높이면 발열 문제가 존재하고, 캐시의 크기를 늘리는 것도 비용의 문제가 존재한다.

그렇기에 CPU 개발사들은 CPU의 성능을 향상하기 위해 CPU의 핵심 기능을 가진 코어(Core)를 여러개(멀티 코어)만들 거나 동시에 실행 가능한 명령의 개수를 늘리는 방법을 사용한다.

 

병렬 처리는 동시에 여러개의 명령을 처리하여 작업의 능률을 올리는 방식.

요즈음 CPU의 사양을 보면 듀얼,쿼드코어라고 쓰여있는데 이는 CPU의 주요 기능을 담당하는 코어가 2개,4개라는 의미.

 

컴퓨터에서 실제로 병렬 처리가 어떻게 이루어지는지 보면, CPU 내에서 명령어는 제어장치가 처리한다.

제어장치는 명령어를 가져와 해석한 후 실행하고 결과를 저장하는 과정을 반복한다.

 

즉 명령어가 실행되는 과정은 다음과 같이 4단계로 나뉘는데

1. 명령어 패치(Fetch) : 다음에 실행할 명령어를 명령어 레지스터에 저장.

2. 명령어 해석(Decode) : 명령어를 해석

3. 실행(Execution) : 해석한 결과를 토대로 명령어를 실행.

4. 쓰기(Write-back) : 실행된 결과를 메모리에 저장.

 

이러한 명령어의 처리과정에서 병렬 처리 기법은 하나의 코어에서 작업을 나누어 병렬로 처리하는 파이프라인 기법을 사용한다.

파이프라인 기법에서는 명령어를 여러 개의 단계로 분할한 후, 각 단계를 동시에 처리하는 하드웨어를 독립적으로 구성한다.

기존 방식에서는 한 명령어를 처리하기 위해 명령어의 처리 4단계를 모두 마치고 다음 명령어를 실행하지만, 파이프라인 기법에서는 명령어 처리의 단계마다 독립적으로 구성하여 각 단계가 쉬지 ㅇ낳고 명령어를 처리할 수 있게 한다.

 

이 기법에 대한 아주 좋은 예시를 보았는데

출처 - https://modoocode.com/271 갓..

빨래가 담긴 여러 바구니가 존재한다고 생각하고, 빨래를 하기 위한 과정이

세탁기 돌리기 -> 건조기 돌리기 -> 빨래 개기 등이 존재한다고 보자.

기존의 방식에서는 하나의 명령어(하나의 빨래 바구니)를 모두 처리하고 나서 다음 명령어(다음 빨래 바구니)를 처리하는 방식이었다고 했지만 위와 같은 빨래를 돌리는 방식을 생각해보면 이는 비효율적.

 

즉 하나의 빨래 바구니가 세탁기에 넣고 다 돌렸다면, 건조기에 넣어 빨래를 건조시키고, 그 다음 빨래 바구니를 가져와 세탁기를 돌리는 방식으로 각 빨래를 하는 단계(각 명령어의 처리 단계)를 쉬지 않고 처리할 수 있는 것.

 

위 그림들의 예시에서는 명령어들이 다 같은것으로 예시를 들었지만, 실제로는 명령어의 실행속도가 천차만별이다.

 -> 실행 시간이 짧은 명령어도 있을것이고 매우 긴 명령어도 있을 것이기에.

 

그렇기에 컴파일러는 이 파이프라인을 효율적으로 활용할 수 있도록 이 명령어들을 재배치하게 된다.

 -> 물론 최종 결과물은 달라지면 안된다.

 -> 단일 쓰레드 환경에서는 문제가 되지 않지만, 멀티 쓰레드 환경에서는 이러한 명령어 재배치도 문제가 된다.

  --> 이러한 문제는 atomic연산의 메모리 모델의 정책을 설정하여 해결할 수 있다.(seq_cst등)

   ---> atomic연산 이전의 명령어들이 atomic연산 이후로 재배치 되는것을 막는 등.

 

가장 직관적인 이해를 위한 Consumer-Producer

void producer(std::atomic<bool>* is_ready, int* data) {
  *data = 10;
  is_ready->store(true, std::memory_order_release);
}

void consumer(std::atomic<bool>* is_ready, int* data) {
  // data 가 준비될 때 까지 기다린다.
  while (!is_ready->load(std::memory_order_acquire)) {
  }

  std::cout << "Data : " << *data << std::endl;
}

producer,consumer 함수는 각각 하나의 쓰레드의 메인 함수(시작 함수).

즉 producer에서 두번째 인자로 건너온 값인 data에 10을 저장하고 있다. 위에서 바라는 상황은 consumer에서 is_ready가 true일 경우 무한반복문을 빠져나와 data가 10인 값을 제대로 관찰하고 싶은 경우.

 

여기서 메모리 모델 정책을 각각 release,acquire을 사용하였는데, 

memory_order_release 는 해당 명령 이전의 모든 메모리 명령들이 해당 명령 이후로 재배치 되는 것을 금지 한다. 또한, 만약에 같은 변수를 memory_order_acquire 으로 읽는 쓰레드가 있다면, memory_order_release 이전에 오는 모든 메모리 명령들이 해당 쓰레드에 의해서 관찰 될 수 있어야 한다.

 

가장 느슨한 정책인 relaxed는 재배치가 일어날 수 있으므로 거의 사용되지 않는다.

만약 컴파일러에 의해 명령어가 재배치 되었다면 is_ready와 data의 순서가 바뀌어 consumer에서 is_ready가 true인 값을 관찰하였지만 data는 0인 경우를 관찰해버리는 것.

반응형

'CS > 운영체제' 카테고리의 다른 글

[OS] 가상 메모리  (0) 2022.04.09
[OS] 스레드 동기화와 교착 상태(Deadlock)  (0) 2022.04.06
[OS] 스케줄링 알고리즘과 우선순위  (0) 2022.04.04
[OS] 프로세스와 쓰레드  (0) 2022.04.03
[OS] 운영체제 개요  (0) 2022.03.30
Comments