강의/system programming

[system programming] Big Picture of System Programming (강의 OT)

하기싫지만어떡해해야지 2025. 3. 8. 18:16

본 게시글은 

서울대학교 데이터사이언스대학원 정형수 교수님의

데이터사이언스를 위한 컴퓨팅 시스템 강의를

학습을 목적으로 재구성하였습니다


이번 학기에 수강하게된 컴퓨팅 시스템 강의

일반적으로는 시스템 프로그래밍이라고 많이 부르는 컴퓨터 과목이다

 

원래도 난 컴퓨터 시스템이나 컴퓨터 구조와 같은

컴퓨터의 low한 내용에 큰 흥미가 있는데

이번에 학점 문제로 정규 수강은 못해서

청강을 하게 되었다

 

내 학부인 고려대학교와는 다르게

서울대학교는 청강 시스템이 체계적으로 되어있어서

참 좋은 것 같다

 

고려대학교는 그냥 이메일로 교수님께 비는

흔히 말하는 빌넣(?) 느낌이었는데

서울대학교는 청강신청 시스템이 따로 있고

청강이 승인되면 과제나 시험만 의무가 없을 뿐

해당 과목을 자유롭게 들을 수 있고

모든 수업 자료에도 접근할 수 있다

(책임없는 쾌락)

 

비록 청강이라 과제나 시험의 의무는 없지만

중간, 기말고사는 참여를 못할지라도

교수님께서 수업시간에 내주시는 과제는

꼬박꼬박 수행하며 공부할 예정이다

 

아무튼 이번 OT에서

시스템 프로그래밍 수업의 Big Picture에 대해 강의해주셨는데

꽤나 공부할만한 내용이 있어 정리해보려한다

 

 

본 수업의 목적이다

System Programming을 왜 배울까?

 

system knowledge는 데이터사이언티스트에게 중요한 역할을 한다

 

컴퓨터 시스템적인 지식이 있으면

Code에서 Bug를 줄일 수 있으며

코드를 실행시켰을 때

컴퓨터 내부에서 어떻게 작동하는지를 안다면

효율적으로 코드를 작성하는 노하우가 생긴다

 

 

컴퓨터로 프로그램을 짠다는 것은

굉장히 추상적인 개념이다

 

보통 우리가 흔히 말하는 코딩은

sequential, conditional, loop 3개로 구성된다

(for, if문만 하면 코딩 끝이라는게 팩트)

 

하지만 이렇게 쉬울 것 같은 코딩도

막상 짜보면 생각보다 쉽게 돌아가지 않는다는 것을 알 수 있다

이것이 바로 abstraction과 reality의 차이이다

 

따라서 이러한 현실을 잘 이해하고 있다면

program을 좀 더 효율적으로 짤 수 있는 지혜가 생긴다

 

 

예제로 hello world를 짜는 C언어 코드를 살펴보자

보통 프로그래밍을 처음 접할 때

가장 먼저 배우는게 hello world 출력하기이다

 

사람들은 C언어가 Low level 언어라고 하지만

교수님 기준 c언어는 high level이라고한다

왜냐?

c언어는 사람이 보기에 직관적이기 때문이다

 

아무튼 이 예제를 바탕으로

위 코드를 실행시켰을 때

underlying에서 어떻게 작동하는지를

간단하게나마 살펴보자

 

 

우선 compile이라는 개념은 다들 들어봤을 것이다

compile이 무엇을 하는 과정이냐?

human readable한 코드를 컴퓨터가 이해할 수 있는

machine readable하게 바꾸는 과정이다

 

보통의 linux os에서

위 hello world 프로그램이 작성된 파일인

hello.c를 실행시키기 위해서

gcc -o hello hello.c

라는 명령어를 쳐주면 된다

 

gcc가 human readable한 코드인 c언어를

machine readable한 코드로 바꿔주는

컴파일러이다

 

그렇다면 gcc를 돌렸을 때 gcc가

단계별로 어떻게 작동하는가?

 

위 ppt에 나온대로 순서는

preprocesser를 거쳐 compiler -> assembler -> linker의

단계로 진행된다고 한다

 

여기서 최종적으로 나가는 code가

cpu가 이해할 수 있는 코드이다

위 ppt에서 main: 아래에 있는 부분이 될 것이다

 

만약에 코드 내부에 library에 정의된 function이 있다면

나중에 프로그램 execute시 라이브러리 코드에 들어가서

해당 function이 어디에있는지 찾아낸다음 resolve한다

 

 

컴퓨터의 기본적인 아키텍처 구조이다

 

내부에 cpu가 있고 cpu와 주변장치들이 연결되어있다

main memory, disk, display 담당 graphics adapter,

usb controller, IO bus들이 와이어링 되어있다

 

cpu 내부에는 연산장치인 ALU와

메모리 중에 가장 빠른 메모리인

register file, 즉 cpu register가 있다

 

우리가 hello world 코드를 실행시킬 때

각 영역에서 어떻게 작동하는지 보자

 

 

우리가 keyboard라는 interface를 통해서

command를 날린다

우리가 keyboard로 친 명령어가

입력되어서 IO bus를 타고 들어간다

 

 

그런 다음 disk에 있는 executable한 hello.c 파일을 찾아

main memory에 올려놓는다

그러고 실행을 시키는데

code의 첫 번째 주소를 CPU의 PC에 넣는다

PC는 하드웨어적으로 자동으로 숫자를

메모리의 주솟값으로 인식하게 되어있다

따라서 해당 주솟값을 따라서 들어가

해당 주소에 있는 code를 fetch해서 가져온다

 

그런 다음 그 code를 가져와서

decoding을 한 후 코드를 파악한다

hello world 코드의 경우 해당 코드가

string을 출력하는 코드이므로

string output을 display에 쏟아내기위한

작업을 시작한다

string이 메모리에 저장되어있는데

그걸 가져와서 display unit에 뿌려주는 것이다

 

이것이 print("hello world")를 실행했을 때

컴퓨터 내부에서 돌아가는 과정을

간단하게 설명한 것이다

 

 

컴퓨터의 메모리에서 physical distance는

극복이 쉽지않다

메모리간의 물리적인 거리는

프로그램의 성능에 지대한 영향을 미친다

 

이러한 이유는 program이 동작을 할 때

어딘가에 저장되어있는 것을 가져와서 읽어야하고

이 과정에서 memory access가 필요한데

당연히 memory access를 할 때

가까이에 있다면 빠르게 된다

 

따라서 메모리는 빠른 것과 느린 것이 있는데

이것에 계층이 있다

메모리의 속도, 가격, 크기를 고려했을 때

피라미드 계층이 만들어진다

 

모든 메모리가 가격이 동일하면 좋겠지만

안타깝게도 각박한 세상이라

빠른 메모리는 작고 비싸며

느린 메모리는 크고 싸다

 

피라미드는 가장 빠른 메모리인 register부터

가장 느린 메모리인 disk까지 있고

L0, L1, L2, L3까지는 CPU 내부의 메모리이다

 

피라미드 위로 올라갈수록

크기의 제약이 정말 심해진다

 

 

우리가 보통 컴퓨터에서 program을 실행시킬 때

program 한 개만 컴퓨터에서 동작하는 것처럼 보인다

과연 실제로 컴퓨터 내부에서도 그럴까?

 

절대 그렇지 않다

사용자에게 컴퓨터에서 오직 한개만 프로그램이

동작하는 것처럼 보이는 것을

os에서 illusion이라고 한다

 

이걸 가능하게 하는 가장 큰 layer를

OS의 virtualization이라고 한다

 

OS의 key frame은 virtualization이며

이는 크게 3가지로 구분되는데

cpu virtualization

memory virtualization

I/O devices(주변장치) virtualization

이다

(이를 3 easypeasy라고 부른다고한다)

 

cpu의 특성상 시간의 제약을 많이 받기 때문에

cpu를 혼자서 쓰는 것처럼 만들어주기위해

os는 scheduling이라는 것을 한다

이를 흔히 cpu scheduling이라고 한다

 

 

physical memory의 용량과

우리가 쓰는 프로그램 사이의 gap이 있다

이 gap을 메꾸기 위해서 os는

memory virtualization을 수행한다

 

예를들어서 물리적 메모리는 16G밖에 안되지만

마치 사용자에게는 무한대인 것처럼 보이게 하는 것이다

 

이러한 memory virtualization을 어떻게 수행하는지

정말 간단하게 설명을 하자면

만약 main memory가 16G라고 한다면

virtual address와 machine address를 이어줄

address table을 갖고있고, 이를 바탕으로

virtual address와 machine address 사이의

traslation을 끊임없이 수행한다고 한다

 

여기서 나온 개념이 바로

TLB(Translation Lookaside Buffer)인데

cpu안에 내장되어있으며

address를 바로 translate 할 수 있도록 돕는다고한다

 

TLB는 매우 중요한 역할을 하며

TLB가 크면 클수록 소프트웨어의 수행속도가 올라가지만

역시 각박한 세상답게 TLB는 매우 비싼 메모리라

많이 담을 수 없다고한다

 

 

storage를 가상화하기 위해 나온 것이 바로 file이다

그리고 network를 가상화하기 위해 나온 것이 바로 socket이다

 

file I/O와 network I/O는

나중에 수업시간에 자세하게 다룰 예정이다

 

 

Great Reality

현실..?을 알려준다고 하신다

 

우리가 컴퓨터 시스템을 알아야 코딩을 할 수 있는

현실적인 이유를 말씀하시는 것 같다

 

 

우리가 흔히 알고있듯이

정수는 INT, 실수는 FLOAT이다

그런데 과연 우리가 인식하는 정수, 실수와

컴퓨터가 인식하는 정수, 실수가 같을까?

 

정답은 당연히 같지 않다

 

모두가 알고있겠지만 컴퓨터에서는

문자열이든 실수이든 정수이든

어떠한 데이터를 이해하기 위해서

현실 세계의 데이터를 0과 1인 이진수로 저장하고

각 데이터타입에는 할당된 고정 크기가 있다

int는 보통 4btye이다

 

그래서 c나 c++에서 코딩을 해봤다면 알겠지만

int 변수를 선언해놓고

int가 표현할 수 있는 크기를 초과하는 수를 선언하면

overflow가 발생하여 이상한 값,

흔히 말하는 쓰레기 값이 나오게 된다

 

float도 마찬가지로 공간 제약이 존재한다

따라서 컴퓨터는 float이 너무 커지면

자동으로 round를 시켜서 잘라버린다

 

그래서 예전에 회사에서 개발할 때

금액과 같은 절대 변하면 안되는 중요한 숫자는

Big Decimal이라는 다른 데이터타입을 사용했고

위경도 같은 float타입은 아예 그냥 string으로 저장해버리기도했다

 

아무튼 따라서

우리 현실 세계에서는 int나 float을 더할 때

순서가 어떻게 되든 결과는 똑같지만

컴퓨터에서 수행할 때는 달라질 수가 있다고 한다

 

 

수를 표현하는데 있어서 컴퓨터는 finiteness(유한)하다

 

그렇다면 상식적으로 숫자 데이터 타입에 할당된 메모리 사이즈를

그냥 크게 주면 되지 않나?하는 생각이 들 것이다

현재 대부분 int는 4byte지만 

만약 int를 표현하기위해서 32byte 정도를 할당해서

큰 int도 표현할 수 있게 해주면 안되냐는 것이다

 

당연히 그렇게 해도 되긴 하지만

os 수업을 듣다보면 나오게되는

memory fragment 문제 때문에

현실적이지 못해서 좋지 못하다

 

 

우리는 assembly code가

어떻게 구성되어있는지 알아야한다고한다

assembly를 알아야 애초에 코드를 짤 때

compile에 optimization한 형태로 짤 수 있다고한다

 

메모리는 제한되어있다

이를 memory matters라고 한다

 

실제 메모리 capacity가 제한적이기 때문에

주어진 용량을 가지고 큰 양의 데이터를 받아오려면

세심한 고려가 필요하다

 

이 중에 가장 많은 개발자들이 실수하는 것 중 하나가

function 내부에 굉장히 많은

local variables를 선언하는 것이라고한다

(이건 나도 안좋은지 처음알았다)

 

function 내부에 지역변수를 많이 선언하면

stack frame자체가 매우 커진다고한다

 

그래서 컴파일 할 때 gcc에게 -o3 이런 옵션을 주면

컴파일러가 알아서 사용하지 않는 지역변수들을

기가막히게 찾아서 할당해제 해준다고한다

 

코딩을 하려면 메모리에 대해 매우 잘알아야한다

python으로만 코딩을 할 땐 잘 몰라도 되는 것 같겠지만

c나 c++을 사용할 때는 더더욱 잘 알아야한다

코딩을 해본 사람은 알겠지만

잘못된 메모리에 접근하면

segmentation fault 에러가 나기 때문이다

 

 

c언어에서 reference를 잘못해준 예제를 보자

 

위와같이 size가 2인 int array와

double인 d를 struct으로 선언해준다

 

그런 다음 int array인 a의 index에 접근해서

숫자를 할당하는 함수를 작성해준다

 

상식적으로 생각해도 a에 접근하는

index에 bound check를 안해줬다는

생각이 들긴 할 것이다

 

 

위처럼 코드를 작성했을 때 함수의 stack을 잘 보자

struct을 선언하고 함수 내부에서 struct_t인 s를 선언하면서

메모리는 struct_t를 위한 공간을 할당받는데

 

처음과 두 번째 각 4바이트에는 a[0], a[1]이 할당되고

세번째 네번째 stack에는 double을위한 8바이트가 할당된다

 

처음 i가 0, 1일때는 a[0], a[1]의 공간에

의도한대로 잘 쌓이겠지만

i가 2가되는 순간에는 d의 자리를 침범하여

d를 표현하는 double의 앞의 4바이트에

1073741824가 들어가게된다

따라서 d를 출력하면 이상한 숫자가 나오게 된다

(근데 왜 3.13이나 2.0이 나오는지는 잘 모르겠다

수에 큰 차이가 왜 없는거지?)

 

i에 3이들어가도 d의 영역을 침범하게 되고

fun(i)가 접근할 수 있는 메모리 공간은

총 5번째까지이기때문에

i가 6이되는 순간 접근할 수 없는 메모리에 접근하게되어

segmentation fault가 뜨게된다

 

교수님왈 이런 문제가 까다로운 이유는

에러가 떠야할 부분에 에러가 안뜨기 때문이라고한다

i가 2가되는 순간부터 a의 메모리를 초과했다고

에러를 띄워주면 뭐가 잘못됐는지 알고 프로그래머가 고칠 수 있지만

실제로 이런 상황에서는 에러가 뜨지않고

d의 영역을 침범한채로 계속 실행된다고 한다

이러한 이유로 프로그래머가 의도한 결과가 안나오게되고

이렇게 되면 디버깅도 굉장히 힘들어진다고한다

 

cache나 memory hierarchy에 의해서

behavior가 완전히 달라질 수 있다고한다

 

코딩의 결과는 같을 수 있어도

실제 running behavior가 달라지는 경우가 발생한다

이것도 예시로 한 번 알아보자

 

 

위 예제에서 array i와 j를 모두 스캔하며

dst라는 2-dimentional array를 채워주는 코드이다

 

왼쪽은 i를 먼저 돌리고 j를 돌리는 것이고

오른쪽은 j를 먼저 돌리고 i를 돌리는 것이다

 

결과값은 똑같이 되겠지만

수행속도를 비교해보면

왼쪽은 4.3ms, 오른쪽은 81.8ms로

엄청나게 차이가 난다

 

왜 이런 결과가 나타나는 것일까?

 

이는 컴퓨터가 메모리를 할당하는 방법을 알면

이유를 알 수 있다

 

array를 2차원으로 만드는 순간

컴퓨터는 i를 쭈욱 먼저 할당한다음 j를 할당한다

 

따라서 i부터 for문을 돌리게되면

메모리는 순차적으로 접근하게 된다

이렇게 되면 보통 메모리가 근방에 있는 것들을

cache 메모리에 fetch해버리는데

이 cache에 저장된 데이터를

바로바로 가져다 쓸 수 있기 때문에 수행속도가 빨라진다

 

반면에 j부터 for loops를 돌리게되면

계속해서 2048만큼 건너뛴 메모리에 접근하게된다

이렇게 되면 cache memory에 있는 데이터를

사용할 수가 없게 된다

따라서 속도가 매우 느려지게 되는 것이다

 

 

 

그래프로 표현하면 이렇다고 한다

 

 

비록 OT였지만 정말

시스템 프로그래밍의 큰 그림을

이해할 수 있게 된 것 같다

 

다음 수업은 이제 본격적으로

비트와 바이트부터 시작해서 강의해주신다고한다

 

이번학기도 화이팅-!