본문 바로가기

컴퓨터 구조론( MIPS )

명령어 : 컴퓨터 언어 7 ( feat. 동기화, 번역, 실행)

728x90
반응형
반응형

안녕하세요. 개발자 WH 입니다.

이번 글은 병렬성과 동기화에 대한 글입니다.

시작해보도록 할게요.

 

병렬성과 명령어 : 동기화

테스크가 서로 독립적일 때는 병렬처리가 쉽지만 테스크들이 얽혀있어, 새로운 값을 어떤 테스크가 쓰게 된다면, 복잡해지게 되죠. 이런 상황에서는 특정 테스크가 언제까지 쓰기를 마쳐야 하는지를 알아야 다른 테스크들이 안전하게 읽을 수 있게 되는데, 이를 알기 위해서는 동기화가되어야 합니다. 즉 이런 동기화가 되지 않으면, 이벤트가 일어나는 순서에 따라 프로그램의 결과가 바뀌는 상황이 오는 데 이런 상황을 데이터 경쟁 관계라 합니다. 

 

  컴퓨팅에 있어서 동기화 메커니즘은 일반적으로 사용자 수준 소프트웨어 루틴에서 제공 됩니다. 물론 이 소프트웨어 루틴들은 하드웨어가 제공하는 동기화 명령을 사용하죠. lock과 unlock은 동기화 연산에 활용되며 그대로 사용하면 단 하나의 프로세서만이 작업할 수 있는 영역을 생성( 이를 mutual exclusion 상호 배제라 부르죠. )할 수 있고 더 복잡한 동기화 메커니즘의 구현이 가능합니다. 

 

  멀티프로세서에서 동기화를 구현하기 위해서는 메모리 주소에서 읽고 수정하는 것을 atomically 하게 처리할 수 있는 하드웨어 프리미티브가 있어야합니다. 즉, 메모리에서 읽고 쓰는 중간에 아무것도 끼어들 수 없어야 한다는 말이죠.

 

  동기화 연산 구축을 위한 연산은 atomic exchange 또는 atomic swap 연산인데, 이 연산은 레지스터 값을 교환하는 연산이며, MIPS 에서는 SWP 명령을 이용합니다. 기본적인 설명은 끝났으니, 기본 동기화 프리미티브 구축에 어떻게 사용되는지를 보도록 할게요. 

 

방법 1.

  먼저 간단한 lock 입니다. lock은 0이면 사용가능, 1이면 사용 불가를 표시한다고 가정합시다. 프로세서는 레지스터에 있는 값 1과 메모리에 있는 lock을 바꿈으로써 lock을 1로 만들겁니다. 만약 다른 프로세서가 이미 접근하였다면, 교환 명령어가 가져온 값은 1일 것이고, 그렇지 않은 경우에는 0을 가져올 겁니다. 만약 0을 가져왔다면, 그 값이 1로 바뀌어 다른 프로세서에서 가져가지 못하도록 해봅시다. 핵심은 연산은 나누어 이루어질 수 없기 때문에, 두 프로세서는 동시에 변수를 설정할 수 없다는 겁니다. 누가 먼저 실행될 것인가는 하드웨어에 의해 정해지겠죠.

 

방법 2.

  한 쌍의 명령어를 갖도록 하는데 두 번째 명령어는 한 쌍의 명령어가 마치 원자적(나누어 질 수 없는 것처럼)인 것처럼 실행되었는지를 나타내는 값을 반환하게 하는 겁니다. 어느 프로세서에서도 실행되는 모든 연산들이 한 쌍의 명령어 전이나 후에 실행 되는 것처럼 보인다면, 이 한쌍의 명령어는 실질적으로 원자적이라 말할 수 있습니다. 즉, 이 명령어 쌍이 실제적으로 원자적이라면 다른 프로세서도 명령어 쌍 사이에서 값을 바꿀 수 없다는 말이죠. MIPS에서는 이러한 명령어 쌍으로 load linked라 불리는 특수 적재 명령어와 store conditional이라 불리는 특수 저장 명령어가 있습니다. 이 명령어 쌍은 순차적으로 사용되는데 만약 load linked 명령어에 의해 명시된 메모리 주소의 내용이 같은 주소에 대한 store conditional 명령어가 실행되기 전에 바뀐다면 store conditional 명령은 실패 하게 된답니다. store conditional 명령어는 레지스터 값을 메모리에 저장하고 동시에 그 레지스터 값을 1로 바꾸게 되면 성공이고 만약 실패한다면 레지스터 값은 0이 됩니다. load linked 명령어는 초깃값을 반환하고 store conditional 은 성공할 때만 1을 반환하기 때문에 다음 명령어 시퀀스는 $s1의 내용이 가리키는 메모리 주소에 대해 원자적 교환을 구현하게 된답니다.

 

again : addi $t0, $zero, 1 # copy locked value

        ll $t1, 0($s1) # load linked

        sc $t0, 0($s1) # store conditional

        beq $t0, $zero, again # branch if store fails

        add $s4, $zero, $t1 # put load value in $s4

 

  ll 명령어와 sc 명령어 사이에 프로세서가 끼어들거나, 메모리 값을 수정하는 경우 sc는 $t0에 0을 반환하기 때문에 코드 시퀀스가 다시 실행하게 된답니다. 결국 이 시퀀스 끝에는 $s4 의 내용과 $s1에 의해 명시된 주소의 메모리 사이에 원자적으로 교환이 이루어 지게 되는 것이지요.

 

프로그램 번역과 실행

  전에 한번 간단하게 다루었던 내용이지만, 컴퓨터 구조론에서 빠질 수 없는 번역 과정에 대해 다시 다뤄보고자 합니다.다.  아래 내용과 더불어 함께 보면 좋은 아래 글을 읽고 읽어 주시길 바랍니다.

2022.01.25 - [임베디드 리눅스] - c언어 컴파일 과정

 

c언어 컴파일 과정

안녕하세요. 매일이 새롭고 싶은 WH 입니다. 컴파일 ' 기계어로 번역해주는 과정 ' 참 간단합니다. 컴파일 과정은 반드시 알고 있어야하는 과정입니다. 왜냐고요? 파일간의 관계를 파악해야하거

developer-wh.tistory.com

 

컴파일러

  컴파일러는 C 프로그램을 어셈블리 언어 프로그램으로 바꿉니다. 어셈블리 언어 프로그램은 컴퓨터가 이해할 수 있는 심벌 형태라고 생각하시면 됩니다. 

 

어셈블러

  어셈블리 언어는 상위 수준 소프트웨어와의 인터페이스인데요. 때문에 원래 없는 명령어라도 어셈블러가 독자적으로 제공할 수 있답니다. 물론 이 명령어들은 하드웨어로 구현이 되어 있지 않더라도, 어셈블러가 처리해 주기 때문에 걱정하지 않으셔도 됩니다. 이런 명령어들을 의사명령어( psedoinstruction ) 라고 합니다.

  이게 무슨 말인가 하면 예를 들면 MIPS 하드웨어에는 move 라는 명령어가 없지만 MIPS 어셈블러는 이를 처리해주죠.

즉,

move $t0, $t1 을

add $t0, $zero, $t1 에

해당하는 기계어로 바꿉니다. 뭐 이외에도 명령어 수치 필드 크기는 16비트로 제한되어 있지만 레지스터에 32비트 상수를 넣어 준다거나, blt( branch on less than ) 명령어를 slt 와 bne 로 바꿔주는 등의 일을 해줍니다. 어셈블러는 여러 진수의 숫자를 받아들이는 데, 이진수 뿐만 아니라 십진수 16진수등도 받아드립니다.

   어셈블러는 어셈블리 언어 프로그램을 목적 파일로 바꾼답니다. 목적 파일에는 기계어, 명령어, 데이터, 명령어를 메모리에 적절히 배치하기 위해 필요한 각종 정보들이 들어있습니다.

  레이블 이름을 명령어가 기억된 메모리 워드 주소와 짝지어 주는 테이블을 심벌 테이블이라고 하는데요. 어셈 블러는 분기나 데이터 전송 명령에서 사용된 모든 레이블을 심벌 테이블에 저장하며 이 테이블은 심벌과 그 주소를 저장한답니다. 목적 파일은 6 부분으로 구성되고 그 구성을 보여드리며 다음으로 넘어가겠습니다.

 

1. 목적 파일 헤더 : 목적 파일을 구성하는 각 부분의 크기와 위치를 서술

 

2. 텍스트 세그먼트 : 기계어 코드

 

3. 정적 데이터 세그먼트 : 프로그램 실행 동안 할당되는 데이터가 들어 있음 ( 실행이 끝날때까지 할당되는 정적 데이터

와 프로그램의 요고에 따라 변하는 동적 데이터를 프로그램이 사용할 수 있게 해줌 )

 

4. 재배치 : 프로그램이 메모리에 적재될 때 절대 주소에 의존하는 명령어와 데이터 워드 표시

 

5. 심벌테이블 : 외부 참조같이 아직 정의되지 않고 남아 있는 레이블들을 저장

 

6. 디버깅 정보 : 각 모듈이 어떻게 번역되었는지 대한 간단한 설명. ( 디버거는 이정보를 이용해서 기계어와 C 소스 파일을 연관 짓고 자료 구조를 판독 )

 

링커

 링커가 필요한 이유는 무엇일까요? 프로시저가 고쳐진다면 프로그램 전체를 컴파일 하는 비효율을 막기 위함입니다. 링커는 따로 따로 어셈블된 기계어 프로그램을 하나로 연결해 주는 일을 한답니다.

 

링커의 3 단계 동작 과정

 

1. 코드와 데이터 모듈을 메모리에 심벌 형태로 올려 놓음

2. 데이터와 명령어 레이블의 주소를 결정함

3. 외부 및 내부 참조를 해결함

 

  링커는 각 목적 모듈의 재배치 정보와 심벌 테이블을 이용해서 미정 레이블의 주소를 결정합니다. 분기 명령어, 점프 명령어, 데이터 주소 등에 나타나는 구주소를 신주소로 바꾸는 일이죠. 이런 일은 에디터와 유사하여 링크 에디터라고 부르며, 줄여 링커라고 합니다. 프로그램 전체를 다시 컴파일하고 어셈블하는 대신 링커를 써서 번역된 모듈을 연결하면 시간이 절약된답니다.

  링커가 외부 참조를 모두 해결하고 나면 각 모듈의 메모리 주소를 결정하는데요, 각 파일을 독립적으로 어셈블하기 때문에 어셈블러는 어떤 모듈의 명령어와 데이터가 다른 모듈과 비교해서 어떤 위치에 있게 되는지 알 수가 없습니다. 링커가 모듈을 메모리에 적재할 때 절대 참조 ( 실제 메모리 주소 )는 모두 실제 위치에 해당하는 값으로 재설정 되어야 합니다.

  링커는 컴퓨터에서 실행될 수 있는 실행 파일을 생성하는데요. 이 파일은 대게 목적 파일과 같은 형식을 갖는데, 모든 참조가 해결되었다는 차이를 가집니다. 라이브러리 루틴같이 일부만 링크된 파일은 미해결 주소를 가지고 있으므로 목적 파일에 속합니다.

 

로더

  운영체제는 디스크에 있는 실행 파일을 메모리에 넣고 이를 시작하는데요. 로더의 실행 순서입니다.

 

1. 실행 파일 헤더를 읽어서 텍스트와 데이터 세그먼트의 크기를 알아냄

2. 텍스트와 데이터가 들어갈 만한 주소 공간을 확보함

3. 실행 파일의 명령어와 데이터를 메모리에 복사함

4. 주 프로그램에 전달해야 할 인수가 있으면 이를 스텍에 복사함

5. 레지스터를 초기화하고 스택 포인터는 사용 가능한 첫 주소를 가리키게함

6. start-up 루틴으로 점프함. start-up 루틴은 인수를 인수 레지스터에 넣고 프로그램의 주 리틴을 호출함. 주 프로그램에서 기동 루틴으로 복귀하면 exit 시스템 호출을 사용하여 프로그램을 종료시킴

 

이번 글은 여기까지 정리하도록 하겠습니다. 이상 WH입니다. 읽어주셔서 감사합니다.

728x90
반응형