저번 포스팅에서는 MIPS 명령어에 따라 달라지는 주소 지정 방식을 설명하였습니다.
이번에는 동기화와 C언어의 번역계층에 대해 상세하게 다루어보고, 프로그램 번역과 실행 과정에서 중요한 역할을 하는 컴파일러, 어셈블러, 링커, 로더의 역할에 대해 설명드리려고 합니다.
동기화란 무엇인가?
태스크(작업)가 서로 독립적인 경우에는 병렬 처리가 쉽지만, 서로 협력해야 하는 경우가 있습니다. 협력은 일반적으로 다른 태스크들이 읽어야 하는 값을 어떠한 다른 태스크가 읽고 있음을 의미합니다. 태스크가 언제까지 쓰기를 마쳐야 다른 태스크들이 안전하게 데이터를 읽을 수 있는지 알려면 태스크 동기화가 필요합니다. "동기화"가 존재하지 않으면, 데이터 경쟁관계(data race)의 위험이 있습니다. 여기에서 데이터 경쟁관계란 이벤트가 발생하는 순서에 따라 프로그램의 결과가 달라질 수 있음을 의미합니다. 이를 방지하기 위해 동기화는 컴퓨터 프로그래머가 반드시 알아야 할 기술입니다.
동기화는 다른 프로세스나 스레드 등이 공유하는 자원(메모리, 파일, 네트워크..)들을 동시에 접근하려고 할 때, 그 접근을 조정하고 제어하기 위한 기술입니다.
다시 정리해 보겠습니다.
동기화가 없으면 발생할 수 있는 문제 상황
- 두 개의 프로세서가 같은 메모리 영역을 공유 (P1이 데이터를 쓰고, P2가 P1이 쓴 데이터를 읽는 상황에서, 동기화 기술이 적용되지 않는다면, 데이터 경쟁관계(data race)에 직면할 수 있습니다.
해결 방안
- 동기화는 사용자 수준 소프트웨어 루틴에서 제공
- 프로그래머가 하드웨어가 제공하는 동기화 명령어를 사용하고, lock(), unlock()과 같이 하나의 프로세서만 작업할 수 있는 영역(상호 배제(mutual exclusion)을 생성하는 연산의 명령어를 적용합니다.
멀티프로세서에서 동기화를 구현하기 위해, 메모리 주소에서 읽고 수정하는 것을 원자적으로(atomically) 처리할 능력을 가진 "하드웨어 프리미티브"가 존재해야 합니다. 즉 메모리에서 읽고 쓰는 중간에 다른 프로세서가 개입할 수 없어야 합니다. 이러한 기능이 존재하짖 않으면 기본 동기화 프리미티브 구현 비용이 굉장히 많이 들 것이며, 프로세서 수가 증가함에 따라 더욱 값이 늘어날 것입니다.
기본 하드웨어 프리미티브를 대신하는 방법
- 원자적 교환
- 레지스터의 값을 메모리 값과 맞바꾸는 연산 : 레지스터의 값을 먼저 읽어 들인 후, 메모리에 저장된 값을 레지스터 값으로 업데이트하고 이전의 메모리 값을 반환합니다.
- 이를 하나의 명령어로 동작시키기에 프로세서 설계에서 어려움이 있으므로(메모리 읽기와 쓰기를 하나의 명령어로 처리하는 것이 난해함), MIPS는 이를 위한 "두 개의 명령어"를 사용
예시
프로세스 A, B가 공유 자원에 접근하려고 할 때, A 프로세스는 원자적 교환 연산을 통해 공유 자원을 잠그고, 이때 B 프로세스는 공유 자원에 접근할 수 없습니다. A 프로세스가 공유 자원에 대한 작업을 완료하면, 다시 원자적 교환 연산을 통해 잠금을 해제합니다. 이러한 과정이 있기에 데이터 경쟁 없이 안전하고 동기화된 액세스를 보장할 수 있게 됩니다.
MIPS에서의 동기화
ll(load linked) : 특수 적재 명령어
sc(store conditional) : 특수 저장 명령어
$s1에 있는 주소의 메모리 값과 $s4 레지스터 값을 원자적으로 교환하는 MIPS 코드
- load linked 명령어에 의해 명시된 메모리 주소의 내용이 같은 주소에 대한 store conditional 명령어가 실행되기 전에 바뀐다면 store conditional 명령은 실패하게 됩니다.
- 레지스터 $t0값을 메모리에 저장하고, ll후에 값이 바뀌지 않으면 성공($t1의 값 : 1), 그렇지 않으면 실패($t1의 값 : 0)
- 만약 $t0의 값이 0이 반환된다면 try구문으로 돌아가 코드 시퀀스를 다시 실행하게 됩니다.
원자적 교환은 멀티프로세서 동기화를 위해 제시되었지만, 운영체제가 단일프로세서 내의 다중 프로세스를 다루는 것에서도 적용됩니다.
C언어의 번역 계층
- 컴파일러 : C프로그램을 어셈블리 언어 프로그램으로 바꿈
- 어셈블러 : 어셈블리 프로그램을 기계어로 번역(목적 파일(Object file)로 바꿈)
- 목적 파일의 구성요소
- 헤더 : 목적 파일의 각 부분의 크기와 위치
- 텍스트 세그먼트 : 번역된 기계어 코드
- 정적 데이터 세그먼트 : 프로그램 수명 동안 할당되는 데이터
- 재배치 정보 : 프로그램이 메모리에 적재될 때 절대 주소를 사용해야 하는 명령과 데이터 워드를 표시
- 심벌 테이블 : 외부참조와 같이 아직 정의되지 않고, 남아있는 레이블 저장
- 디버깅 정보 : 어떻게 번역되었는지 설명해 주는 정보
- 목적 파일의 구성요소
- 링커 : 어셈블 된 목적 파일들을 링크
- 실행 파일을 생성
- 목적 파일들의 세그먼트들을 합치고, 레이블의 주소를 결정
- 동적 링킹 vs 정적 링킹
- 라이브러리를 실행파일에 포함시킴.
- 정적 링킹은 실행파일이 너무 커져서 메모리 낭비가 될 수 있음
- 동적 링킹은 메모리에 라이브러리를 올리고 각 실행파일은 run-time 때만 해당 라이브러리가 실행됨
- 실행 파일을 생성
- 로더 : 목적 프로그램을 메인 메모리에 적재해서 실행하게 해주는 시스템 프로그램
- 프로그램 로딩 순서(보조기억장치의 실행파일을 읽어서, 메모리에 넣고 실행하는 순서)
- 실행파일 헤더를 읽어서 텍스트와 데이터 세그먼트의 크기를 파악
- 텍스트와 데이터가 들어갈 만한 주소공간 확보
- 실행파일 명령어와 데이터를 메모리에 복사
- 주 프로그램에 전달해야 할 인수가 있으면 이를 스택에 복사
- 레지스터를 초기화, 스택 포인터를 사용가능한 첫 주소를 가리키게 함
- 기동 루틴으로 점프 : 인수를 인수 레지스터에 넣고, 프로그램의 주 루틴을 호출, 기동 루틴이 끝나면 exit 시스템을 호출하여 프로그램을 종료
- 프로그램 로딩 순서(보조기억장치의 실행파일을 읽어서, 메모리에 넣고 실행하는 순서)
'CS' 카테고리의 다른 글
[컴퓨터 구조] chapter3 정리 (컴퓨터 연산 : 덧셈, 뺄셈, 곱셈, 나눗셈) (0) | 2023.05.22 |
---|---|
[CS] 복잡도, 선형 자료 구조, 비선형 자료 구조 (0) | 2023.05.20 |
[컴퓨터 구조] chapter2-2 정리(컴퓨터 언어 : 명령어) (2) | 2023.04.20 |
[CS] 객체지향 생활체조 9가지 원칙 (0) | 2023.04.15 |
[컴퓨터 구조] chapter2-1 정리(컴퓨터 언어 : 명령어) (0) | 2023.04.11 |