Introduction of OSTEP

­Broside·2023년 6월 29일

운영체제의 중요한 세 가지

1. Virtualization (가상화)

1-1. Virtualizing the CPU

아래의 명령어를 실행하게 되면 하나의 cpu 라는 프로그램이 4번 백그라운드에서 실행된다.

$ ./cpu A & ./cpu B & ./cpu C & ./cpu D &

이때, CPU는 하나인데 어떻게 동시에 4개를 처리할 수 있을까? CPU가 가상화 되었기 때문에 CPU가 하나밖에 없더라도 많은 수의 CPU, 거의 무한대의 CPU가 있는 것처럼 보이게 하는 것을 CPU가 가상화 되었다고 한다.

1-2. Virtualizing Memory

프로그램이 실행되는 동안 메모리에 계속 접근이 발생한다.

(프로그램 → 메모리 (Instruction 형태) → 지속적으로 접근)

아래의 프로그램 실행 결과를 보면 두 프로세스가 각각 같은 값을 갱신하는 것으로 보이는데 보여지는 P의 주소는 0x200000으로 동일하다. 이를 보고 우리는 메모리도 가상화되었다는 것을 알 수 있다.!

이때 주소공간 (Address Space)이라는 개념으로 각 프로세스는 독립된 메모리 공간을 갖는다.


2. Concurrency (병행성)

...

counter++;

...

++ (증감연산자)를 기계어 수준으로 보면 아래와 같이 세 단계로 나눠서 볼 수 있다.

  1. 메모리에 있는 값을 레지스터로 load (LD)
  2. 연산 수행 (ADD)
  3. 연산 결과를 레지스터에 저장 (ST)

P1, P2 두 스레드가 있다고 했을 떄, P1이 LD후 ADD를 수행할 떄 P2가 LD를 하고 다음 과정을 수행한다면 P1이 하고있는 연산을 반영하지 않은 값을 새로 업데이트 하므로 충돌이 발생한다.

이러한 문제는 주어진 counter++; 라는 statement가 atomic하지 않아서 발생하는 병행성 문제이다.

(Atomic : 더이상 쪼개질 수 없는 상태)

결과적으로, 덧셈 연산은 N번 수행하는 스레드를 두번 실행했을때 기대값은 2N이 되겠지만 실제 결과는 2N에 못미치는 결과를 볼 수 있고 이런 문제를 병행성 문제라고 한다.


3. Persistance (영속성)

file, file system을 말하는 것으로 일반적인 파일부터 모니터, 키보드 등의 Device도 포함된 의미이다.

프로그램을 실행하면 CPU가 보조기억장치(HDD/SDD)로 부터 파일을 주기억장치 (RAM, 흔히 말하는 메모리) 로 Load해온다. 이때, 이 메모리는 휘발성 (Volatile)이기 때문에 전원을 끄면 데이터가 날아간다. 따라서 보조기억장치에 써서 영속성을 갖게 하는게 이슈

(주기억장치와 보조기억장치)

보조기억장치에는 File로 쓰이고 이 File을 control 할 수 있게 file system이 구성된다. 사람은 file을 path로, 프로세스는 file descriptor로 인식한다.

이러한 메모리에 접근하기 위해 사용되는 open(), write(), close() 와 같은 함수는 운영체제 커널에서 제공하는 System Call 이라고 한다.


Background

변수의 배치 및 속성

우리가 프로그래밍을 하면 다양한 변수들을 사용하게 된다.

초기화 된 전역변수, 초기화 되지 않은 전역변수, 지역변수, 동적할당, 파라미터, 상수 등등..

이러한 변수들이 메모리에 어떻게 배치되는지 또한 이 변수들의 수명은 어떻게 되는지 살펴보자.

변수는 아래와 같이 두 가지 속성을 갖는다.

  • 변수의 속성

수명 (Life time) : 생성과 소멸

범위 (Scope) : 변수가 사용 가능한 영역

  • 전역변수 지역변수의 수명과 범위

전역변수의 수명은 프로그램(main 함수)이 실행하기 전에 이미 메모리에 존재하고, 프로그램이 종료되면 소멸하게 된다. 또한, 전역변수의 범위는 모든 실행파일 (모든 소스와 lib)에 해당한다.

지역변수의 수명은 C에서는 함수 내부, 더 엄밀하게 말하면 {} 중괄호 내부가 된다.
지역변수는 함수가 호출되면 메모리에 할당되고 반환시 소멸하게 된다. (함수와 함께 생성 및 소멸)

  • static 옵션

지역변수에 static을 붙이면 수명이 바뀌게 된다.
life time이 함수를 초월해서 스택(stack)이 아니라 전역변수들이 있는 힙(Heap)영역에 데이터(Data) 영역에 저장된다.

전역변수에 static을 붙이게 되면 범위가 바뀌게 된다.

원래는 lib, 각종 obj코드 다 통합해서 a.out을 만드는데에 참여한 모든 범위에서 축소되어서 해당 파일로만 한정된다. (자주 쓰이는 변수를 이렇게 처리하면 충돌이 생기지 않겠지)

따라서 다른 소스파일에서 extern 키워드를 이용해 전역변수를 사용하는데, static키워드를 붙인 변수는 해당 소스파일로만 범위가 축소되어 외부 소스파일에서 사용할 수 없게 된다.

실제 메모리 영역은 크게 4가지 파트로 분류할 수 있다.

  • Heap Dynamic allocation을 통해 선언된 변수가 배치된다.
  • Stack 지역변수 일반적으로 올라가고, 함수 호출마다 activation record 라는 블록이 생겨서 Return address, Return value, Parameter 등이 배치된다.
  • Data 전역변수와 같이 인스턴스가 하나인 변수가 배치되는 곳으로 전역변수의 초기화 유무에 따라 초기화 된 전역 변수가 배치되는 부분이 있고, 초기화 되지 않은 전역 변수가 배치되어 NULL로 초기화되는 영역이 있다. (이 영역을 BSS 영역 이라고 한다.) 또한, printf("Hello World");에서 "Hello World"도 이 영역에 배치된다.
  • Code 컴파일해서 얻은 실행파일(기계어 코드)이 메모리에서 Loading된다.

실행파일의 구성

"Program 시작 전에는 무슨 일을 해야할까 ?"

일반적으로 보조기억장치(HDD/SSD)에는 file들이 존재한다. 이 파일들은 txt, mp4, doc, exe.. 등 다양한 타입으로 존재한다.

이중에서 실행파일에 대해 좀 더 살펴보자. 먼저 ****실행파일에는 어떠한 값들이 잘 저장되어야 할까?

실행할 준비를 한다는 것은 보조기억장치에 있는 실행파일을 메모리로 올려놓는(Loading) 것이다.

code와 초기화된 전역변수, 초기화 되지 않은 전역변수의 크기가 실행파일에 저장된다. 프로그램이 실행되면 초기화되지 않은 전역변수의 크기만큼 Data의 BSS 영역을 할당받고 이 영역은 0으로 초기화된다.

스택(Stack)은 Execution stack이라고도 한다. 말 그대로 실행되면서 동적으로 데이터가 오고가기 때문이다. , 반환값, , 등은 스택(Stack) 영역에 함수를 만났을 때 생기는 Activation Record 라는 Block에 저장된다.

  • 함수 호출 전 → 함수의 파라미터 PUSH
  • 함수 호출 (Call) → 함수 실행이 끝나면 돌아올 주소인 Return Address가 PUSH
  • 함수 실행 → 필요한 지역변수를 PUSH
  • 함수 반환 → 함수 종료시 return value가 있으면 일반적으로 레지스터에 할당이 되는데,
    재귀함수와 같이 return value를 갖고있어야 하는 상황에서는 스택 영역에 저장한다고 한다.

프로그램이 실행될 때 CPU와 메모리 사이의 동작

  • load : 메모리에 있는 값을 레지스터로 읽어온다
  • store : 레지스터에 있는 값을 메모리에 쓴다
  • Booting : HDD에 있던 Kernel 이미지(일종의 실행파일)를 메모리에 읽어온다. → File을 메모리에 가져오는걸 Loading이라고 한다.

For Thtread

!https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1dc74ff-11aa-4cc8-9524-5bd646a7568b/Untitled.png

!https://s3-us-west-2.amazonaws.com/secure.notion-static.com/07a61aeb-2805-4a91-a0c9-551ec776a881/Untitled.png

위의 코드에서 thread를 create하면서 worker 함수를 parameter로 전달했으면, 생성된 스레드 P1, P2는 최초의 일의 시작을 worker를 호출하는 것으로 된다.

(스레드의 main함수 같은) 이렇게 되면 각 스레드가 갖는 stack을 새로 만든 것이 되고 그 stack에 worker함수의 Activation Record가 생기게 된다.

join 함수의 역할은 실행흐름이 끝났을때 어떻게 처리할지에 대한 이슈이다.

main이 생성한 두 스레드와 상관없이 return하고 끝내는 것이 아니라 두 스레드의 종료를 기다리겠다. (join하겠다는 것)

0개의 댓글