이전 포스팅을 보면 알겠지만, fork()를 통해 자식 프로세스를 생성하고(현재 자식 프로세스는 부모 프로세스의 속성들을 상속받은 상태, 즉 두개의 자식, 프로세스는 같은 일을 한다), exec()을 통해 자식 프로세스에게 새로운 자신만의 프로세스를 실행토록 했다. 

posix_spawn은 fork()와 exec()을 완전히 대체하는 방법으로, 여러 프로세스를 if-else문으로 표현하는 대신 코드가 '선형'으로 구현된다. 

 

※ I/O Piping

ls -l | grep *.txt | wc

 

주어진 명령어인 `ls -l | grep *.txt | wc`은 세 개의 명령어가 파이프(`|`)로 연결된 명령어다. 파이프는 한 명령어의 출력을 다른 명령어의 입력으로 전달하는데 사용된다(앞에 오는 명령어의 STDOUT이 파이프(' | ') 다음 명령어의 STDIN으로 연결된다). 이 명령어를 파이프의 관점에서 바라보자.

1. **ls -l:**
   - `ls -l` 명령은 현재 디렉토리의 파일 및 디렉토리 목록을 나열한다.
   - 이 명령의 출력은 터미널에 표시되고 파이프를 통해 다음 명령어로 전달된다.

2. **grep *.txt:**
   - `grep *.txt` 명령은 `ls -l`의 출력에서 ".txt"로 끝나는 파일들을 찾아내어 출력한다.
   - `grep`은 특정 패턴(여기서는 "*.txt")과 일치하는 라인을 찾아서 출력한다.
   - 이 명령의 출력은 다시 파이프를 통해 다음 명령어로 전달된다.

3. **wc:**
   - `wc` 명령은 입력으로 주어진 텍스트의 행 수, 단어 수, 문자 수를 세어 출력한다.
   - 여기서는 `wc` 명령이 `grep` 명령의 출력을 받아서 파일(.txt로 끝나는 것들)의 행 수, 단어 수, 문자 수를 세어 출력한다.

파이프를 사용하는 경우, 각 명령어는 별도의 프로세스로 실행되며, 파이프로 연결된 명령어 간에는 프로세스 간 통신이 이루어진다. 아래는 파이프를 통해 명령어 간에 데이터가 어떻게 전달되고 fork가 발생하는지 간략한 설명이다.

1. **ls -l의 실행:**
   - `ls -l` 명령이 실행되면 새로운 프로세스가 fork되어 해당 명령이 실행된다.
   - 이 프로세스는 현재 디렉토리의 파일 및 디렉토리 목록을 출력한다.

2. **grep *.txt의 실행:**
   - `ls -l` 명령의 출력이 파이프를 통해 `grep *.txt` 명령에 전달된다.
   - `grep *.txt` 명령이 실행되면 또 다른 프로세스가 fork되어 해당 명령이 실행된다.
   - 이 프로세스는 `ls -l` 명령의 출력에서 ".txt"로 끝나는 파일들을 찾아내어 출력한다.

3. **wc의 실행:**
   - `grep *.txt` 명령의 출력이 파이프를 통해 `wc` 명령에 전달된다.
   - `wc` 명령이 실행되면 또 다른 프로세스가 fork되어 해당 명령이 실행된다.
   - 이 프로세스는 `grep *.txt` 명령의 출력을 받아서 행 수, 단어 수, 문자 수를 세어 출력한다.

이러한 과정을 통해 각 명령어는 '독립적인 프로세스'로 실행되며, 파이프를 통해 데이터가 전달되고, 각각의 명령어가 그 결과를 만든다.

 

※ I/O 리디렉션

  • 리디렉션(Redirection)은 리눅스/유닉스 기반 운영 체제에서 프로세스의 입출력 방향을 변경하는 기능이다. 이를 통해 표준 입력(STDIN), 표준 출력(STDOUT), 표준 오류(STDERR)를 파일이나 다른 디바이스로 변경하거나 조작할 수 있습니다.

    다음은 주요 리디렉션 기호들이다.

    - `>`: 출력을 파일로 리디렉션한다. 기존 파일이 있으면 덮어쓴다.
    - `>>`: 출력을 파일로 리디렉션한다. 기존 파일이 있으면 덧붙인다.
    - `<`: 파일로부터 입력을 받아오는 리디렉션이다.
    - `2>`: 표준 오류(STDERR)를 파일로 리디렉션한다.
    - `2>>`: 표준 오류(STDERR)를 파일로 리디렉션하며, 기존 파일이 있으면 덧붙인다.


  • I / O 리디렉션(출력) 

 

위는 'Welcome to Systems'라는 문자열을 output.txt로 리디렉션하여 output.txt에 문자열이 출력되도록 리디렉션을 한것이다.

 

  • I /O 리디렉션(입력)

wc는 단어의 수를 세서 출력하는 커맨드인데, hello.txt로 리디렉션 하여 hello.txt를 입력값으로 받아 hello.txt의 word count를 출력한다.

 

Signal Handling

 "신호(Signal)"는 프로세스에게 특정 이벤트나 조작을 알리는 비동기적인 이벤트다. 이러한 신호는 특정 상황이나 이벤트가 발생했을 때 프로세스에게 알려주어, 해당 프로세스가 특별한 동작을 수행하도록 한다.

Signal Handling은 주로 프로세스 간의 통신, 예외 처리, 프로세스 간 협력 등 다양한 상황에서 사용된다. 아래는 자주 쓰이는 신호이다.

 

예)
- SIGINT (Interrupt): Ctrl+C로 발생하며, 프로세스에게 중단을 요청한다.

 

 



Signal Handling은 프로세스가 특정 신호를 받았을 때 어떻게 동작해야 하는지를 정의한다. 각각의 신호는 특정한 숫자로 식별되며, 이를 통해 운영 체제는 프로세스에게 어떤 신호가 발생했는지를 알린다.
프로세스는 특정 신호를 받았을 때 이에 대응하는 "시그널 핸들러(Signal Handler)"를 등록할 수 있다. 시그널 핸들러는 특정 신호를 받았을 때 실행되는 사용자 정의 함수로, 프로세스가 해당 신호를 처리하는 방법을 결정한다.

운영체제를 공부하다 보면, 제일 처음 마주하는게 '셸' 이다. '셸'은 유저로부터 입력을 받아 커맨드를 실행하는 프로세스 라고 배웠는데 도무지 무슨 말인지 이해가 되지 않았다. 그러면 그냥 '터미널' 이라고 하면 되지 왜 '셸' 이라고 부르지? 그런데 터미널과 셸은 엄연히 다른 것이였다.
 

셸 / 터미널의 차이

 

※셸은 어떻게 동작할까?

 
위의 그림을 보면 우리가 항상 사용하던 '터미널'은 셸을 위한 GUI였다. 그리고 오른쪽의 '셸'은 터미널 같은 GUI 없이 실행 가능한 프로세스이다. 그렇다면, 셸은 어떻게 동작할까? 아래는 셸의 동작을 간편하게 도식화 한 것이다.



1. **Shell waits for user input (셸은 사용자 입력을 기다린다):**
   - 사용자가 키보드 또는 다른 입력 장치를 통해 명령을 입력하면, 셸은 사용자의 입력을 기다리는 상태에 있다.
2. **Shell interprets command (셸은 명령을 해석한다):**
   - 사용자가 입력한 명령을 셸이 이해하고 해석한다. 이 과정에서 내장 명령인지 또는 외부 명령인지 판별된다.
   - 만약 내장 명령이라면, 해당 명령은 셸 내에서 직접 실행된다. 내장 명령은 외부 프로그램이 아니라 셸 자체에 구현된 명령어를 말한다.(cd, ls,,,등등을 일컫는다)
   - 외부 명령인 경우, 해당 명령에 대한 실행 파일이나 프로세스를 시작하기 위해 다음 단계로 진행된다.
3. **Forks a process (자식 프로세스를 생성한다):**
   - 외부 명령의 경우, 셸은 새로운 프로세스를 생성한다. 프로세스 생성은 일반적으로 fork 시스템 호출을 사용하여 이루어진다.
   - 새로운 프로세스는 명령을 실행하고, 결과를 출력하거나 다른 작업을 수행한다.
4. **Process handling (프로세스 처리):**
   - 만약 명령이 foreground process(전경 프로세스)인 경우, 부모 프로세스(셸)는 자식 프로세스가 종료될 때까지 기다린다.
   - Background process(백그라운드 프로세스)인 경우, 부모 프로세스는 자식 프로세스의 종료를 기다리지 않고 다른 명령을 계속해서 받아들인다.
   - 부모 프로세스는 자식 프로세스로부터의 종료 신호를 받거나, 자식 프로세스의 실행이 완료되면 적절한 조치를 취한다.


※Foreground, background 프로세스

이전 포스팅에서 이것에 대해 다루었지만 이번에는 조금 더 자세하게 예시와 함께 보도록 하겠다.

1. **Foreground Process (fg):**
Foreground process는 화면에 나타나 유저와 상호작용할 수 있는 프로세스를 이다. 이 프로세스가 실행 중일 때, 해당 터미널 또는 쉘은 해당 프로세스의 출력을 받아들이고 사용자가 해당 프로세스와 상호작용할 수 있다. 일반적으로 프로그램을 실행하면 해당 프로그램이 foreground로 실행되어, 프로그램이 완료될 때까지 터미널에서 입력을 받지 않고 기다린다.

2. **Background Process (bg):**
Background process는 화면에 나타나지 않고, 백그라운드에서 실행 중인 프로세스이다. 사용자가 해당 프로세스와 상호작용할 필요가 없으며, 다른 작업을 계속할 수 있다. 프로세스를 백그라운드로 전환하면 해당 프로세스가 실행 중일 때에도 터미널이 다른 명령어를 받아들일 수 있게 된다

   -  백그라운드에서 실행된 프로세스는 종료되면 터미널에 알림이 표시된다. 일반적으로 "Done"이라는 메시지와 함께 해당 프로세스의 종료 상태가 표시됩니다.

이해가 가지 않으니 예시를 보자. 터미널을 열고 sleep 10이라고 쳐보자. 그러면 터미널은 10초동안 아무것도 하지 않을것이다(wait). 이 동안 프로그램이 완료 될때까지 터미널에서 입력을 받지 않고 기다린다.

위의 sleep은 foreground에서 실행된 명령어이다. 이제 10초간 대기후, ls명령어를 실행한다. 이처럼 fg는 유저와 상호작용 할 수 있다.
 
아래는 기호를 통해 bg로 보내는 과정이다.

뒤에 &를 붙여줌으로써 bg로 실행할 수 있다. 위의 fg는 우리가 sleep 10, ls를 치면 터미널에서 ls가 실행되며 대기가 끝나는것을 눈으로 확인 할 수 있지만, 위의 [1] 46181은 작업번호, PID이다. 즉, 46181이라는 PID를 가진 프로세스가 백그라운드에서 현재 sleep 100을 실행중이라는 말이다. 

※프로세스(process)란?

  • 컴퓨터의 하드디스크에 존재하는 프로그램이 메모리 영역에 상주하며 실행되는것
  • 각 프로세스는 고유한 자신만의 아이디(PID)를 가지며, 

즉, 우리가 실행하는 모든 프로그램이 전부 다 하나의프로세스이다. 작업 관리자를 켜보면 '프로세스' 라는 탭이 있는데 거길 보면 수많은 프로세스들이 실행중인것을 볼 수 있다.

 

※프로세스 생성 절차

더보기
리눅스의 프로세스 생성 절차를 간단히 나타낸 그림
  • 뗏목 : 부모 프로세스
    • 부모 프로세스는 여러개의 자식 프로세스들을 복제할 수 있으며 이러한 자식 프로세스들을 통해 다수의 작은 작업들을 동시에 처리하도록 한다.
    • 모든 프로세스는 혼자서 실행 될 수 없으며 부모 프로세스에 종속되어 사용된다.
    • 부모 프로세스가 작업을 종료하면, 그 하위에 있는 자식 프로세스들도 강제 종료된다.
  • 고무보트 : 자식 프로세스
    • fork()를 통해 자식 프로세스가 생성(spawn)되고, 자식 프로세스는 exec호출을 통해 다른 프로세스를 실행하고, 새로운 프로세스로 자신을 대체할 수 있다.
  • 유리병 : exit()의 결괏값

위 그림을 보면서 이해하면 이해하기 쉬울것이다. 먼저, 부모 프로세스는 fork()명령어를 통해 자기 자신을 복제한다. 그렇게 되면 자식 프로세스가 생성되는데, 이 자식 프로세스는 부모 프로세스의 속성들을 상속받았다. 그런데 여기서 의문이 들것이다. 이미 부모 프로세스가 일을 하고있는데 왜 굳이 자신의 복제본인 자식 프로세스를 생성하지? 

 

자식 프로세스에게 부모 프로세스의 일이 아닌 자식 프로세스만의 일을 시키고 싶다면 exec() 커맨드를 통해 다른 프로세스를 실행 시킬 수 있다. 그동안, 부모 프로세스는 자식 프로세스의 일이 끝날때까지 기다린다.  그 다음, exit()을 통해 자식 프로세스가 종료되고, 정상적으로 종료되었다면 0을 반환한다. 이 상황에서 부모 프로세스는 wait()을 통해 자식 프로세스의 일이 잘 처리되었는지 확인하고, 그 다음에 부모 프로세스의 프로세스가 진행된다.

 

한마디로 줄여보면, 부모 프로세스는 fork후 자식 프로세스의 동작 완료까지 대기하고, 자식 프로세스는 동작 완료 후 부모 프로세스에게 동작이 끝났다는걸 알려야 한다.

 

 

※포그라운드, 백그라운드 프로세스란?

  • 포그라운드 프로세스(fg)
    • 실행화면에 나타나 사용자와 상호작용 하는 프로세스
    • 취소하고 싶을땐 Ctrl + C를 통해 프로세스를 종료한다.
    • shell 상태에서 명령을 내리면 사용자는 해당 프로세스가 종료 될때까지 기다려야함
    • 신호 송/수신 가능
  • 백그라운드 프로세스(bg)
    • 실행중이지만, 화면에 나타나지 않고, 저 뒤 어딘가에서 실행중인 프로세스
    • 커맨드 뒤에 &를 붙여 동작시키며, 명령을 내린 사용자는 자신이 하고자 하는 명령어를 실행 시킬 수 있음
  • 프로세스 변경 방식
    • fg를 bg로 보내고 싶을때 : ctrl + z로 잠시 작업을 멈춘다음 '#bg'로 다시 백그라운드로 보낼 수 있음.
    • bg를 fg로 보내고 싶을때 :  (fg % [작업번호])

 

※SIGNAL(신호) 란?

  • 시그널이란 프로세스에서 일어나는 비동기적인 사건.
  • 어느 시점에서 이벤트가 발생할지 예측불가
  • 시그널이 전달되는 방식은 다음과 같다 : '시그널 발생' -> '프로세스' -> '동작을 취함'

조금 더 자세하게 표현하자면,

1. main 프로그램 실행중 시그널이 발생했다. 

2. 이 시그널을 signal handler에게 보낸다. 

3. signal handler가 이 시그널을 처리한다.

4. 시그널이 맨 처음 발생한곳으로 돌아간다.

5. 시그널 처리 후 프로그램 명령어를 다시 수행한다.

 

여기서 가장 많이 쓰이는 커맨드가 kill이다. kill은 프로세스에게 지정된 시그널을 보내준다. 그렇지만 kill 명령어를 사용하기 위해서는 PID를 알아야 한다. 그래야 어떤 프로세스에게 어떤 시그널을 보낼지 알 수 있으니까!

 

아래는 기본적인 명령어들이다

  • ps : 현재 실행중인 프로세스 목록을 확인한다. 여기서 PID를 찾은 후 kill 명령어를 써보자

기본적인 kill 명령어는 아래와 같다.

kill[option] PID

 

kill 명령어는 다양한 옵션을 사용할 수 있다.

 

  • -s <signal>: 특정 시그널(signal)을 사용하여 프로세스를 종료한다. 기본적으로 SIGTERM 시그널이 사용된다.
  • -l, --list: 지원되는 시그널(signal) 목록을 출력한다.
  • -a, --all: 현재 사용자에 속한 모든 프로세스를 종료한다.
  • -q, --queue: 프로세스에 시그널을 보내는 대신 시그널을 대기열에 추가한다.

 다양한 시그널들 중에 아래는 자주 쓰이는 대표적인 시그널들이다.

  1. SIGINT : 프로그램 실행 도중 Ctrl+C를 누르면 발생한다. 키보드로부터 명시적으로 발생시키는 시그널이다. SIGINT가 발생한 경우 기본적으로 프로세스가 종료된다.
  2. SIGKILL : 프로세스를 강제로 종료시키는 시그널이다.
  3. SIGSTOP : 프로그램 실행 도중 Ctrl+Z를 누르면 발생한다. 터미널에서 입력된 정지 시그널이다. 
  4. SIGCHILD : 자식 프로세스가 중지되거나 종료되었을 때 부모 프로세스에 전달되는 시그널.
  5. SIGTERM : 가능한 정상 종료를 시키는 시그널이다. 
  6. SIGSEGV : 메모리 참조가 잘못되었을 경우 발생한다.
  7. SIGQUIT : 프로그램 실행 도중 Ctrl+\을 누르면 발생한다. 프로세스를 종료시킨 뒤 core dump를 수행한다.

 위의 시그널들 중 핸들링이 안되는 시그널은 SIGKILL SIGSTOP이다. 그리고 SIGSTOP로 중지시켰던 프로세스를 SIGCONT로 다시 원상복귀 시키면 터미널에서 제어권이 없어져서 Ctrl+C로 안죽는다. 이때는 SIGKILL(9)로 죽여야한다!

 

 

 

출처: https://hasumang.tistory.com/11 [하서망 블로그:티스토리]

운영체제 강의를 들었을때 수업 처음부터 많이 등장하는 용어가 있다. "프로세스". 

 

그렇다면 과연 프로세스란 무엇일까? 구글링을 하면 가장 많이 나오는 답변은 '현재 실행중인 프로그램'이다. 정말 간단하다. 예를들어 컴퓨터를 켜고 '메모장'을 켠다면, 나의 컴퓨터는 '메모장' 이라는 프로세스를 실행중이다. 그렇다면 조금 더 깊이 들어가서, "프로세스가 실행되려면 무엇이 필요할까?"

 

모든 프로세스는 실행을 위해  CPU가 필요하다. 그 러 나 CPU의 자원은 한정되어 있다.


이 말인 즉슨, 프로세스는 돌아가며 한정된 시간 만큼만 CPU를 이용한다. 아래는 여러 프로세스들이 CPU를 공유하는 모습을 간단히 나타낸 그림이다.

CPU

위의 그림과 같이 프로세스 1이 CPU에 할당을 받는다면, 주어진 시간만큼 CPU를 이용하고, 그 시간이 끝나면 queue에 있는 다른 프로세스에게 CPU를 이용할 권리를 부여한다. 위 그림에 보이는 '타이머 인터럽트'는 현재 프로세스에게 시간이 다 되었다고 알려주는것이다.

 

자 그럼, 여기서 CPU의 상태가 등장한다. 운영체제마다 조금씩의 차이는 있지만, 대부분의 운영체제들은 기본적으로 아래의 상태들을 가진다.

 

CPU 상태

  • 생성상태
    • 이제 막 메모리에 적재되어 메모리를 할당받은 상태
    • 준비가 완료되었다면 준비 상태로
  • 준비상태
    • 당장이라도 CPU를 할당받아 실행 할 수 있지만 자신의 차례가 아니기에 대기중인 상태.
    • 자신의 차례가 된다면 실행상태로!
  • 실행상태(Running)
    • CPU를 할당받아 실행중인 상태
    • 할당된 시간 모두 사용시(타이머 인터럽트 발생 시) 준비 상태로 되돌아감
    • 실행 도중 입출력 장치를 이용하면 입출력 작업이 끝날때까지 대기 상태 유지
  • 대기상태(Blocked)
    • 프로세스가 실행 도중 입출력장치를 사용하는 경우
    • 입출력 작업은 cpu에 비해 느리기에 이 경우 대기 상태로 접어듬
    • 입출력 작업이 끝나면(입출력 완료 인터럽트를 받으면) 준비 상태로
  • 종료상태
    • 프로세스가 종료된 상태

계층구조

우리는 프로세스 실행 도중 시스템 호출을 통해 다른 프로세스 생성이 가능하다.

  • 새 프로세스를 생성한 프로세스 : 부모 프로세스
  • 부모 프로세스에 의해 생성된 프로세스 : 자식 프로세스
  • 부모, 자식 프로세스는 별개의 프로세스이기 때문에 별개의 pid를 가진다.

부모 프로세스는 fork()를 통해 자식 프로세스를 생성하고(자식 프로세스는 부모 프로세스의 자원을 상속받는다), 자식 프로세스는 exec()을 통해 새로운 프로그램을 실행할 수 있도록 함.

부모 프로세스의 fork() 리턴값은 자식 프로세스의 pid이고, 자식 프로세스의 fork()리턴값은 0이다.

 

부모 프로세스는 wait()을 통해 자식 프로세스가 종료 될때까지 기다리거나, kill(pid, SIGKILL)을 통해 강제종료 할 수 있다.

 

즉, 정말 직관적으로 fork()와 exec()을 표현하면,

 

1. 프로세스 A를 fork()를 이용하여 복제본을 만든다.

2. 복제본이 생성되었다(자식 프로세스). 하지만 지금 자식과 부모 프로세스는 똑같은 두개의 프로세스이다.

3. 자식 프로세스에게 부모 프로세스가 하는 일을 하라는 대신, 본인만의 프로세스를 실행시키고 싶으면 exec()를 이용하여 자신의 메모리 공간을 새로운 프로그램으로 덮어쓴다.

운영체제란 무엇일까? 우리가 흔히 알고있는 windows, macOS, Linux, Android, IOS등이 있다. 그렇다면 이러한 운영체제는 무슨 일을 할까?

 

컴퓨터의 운영체제는 프로그램 실행에 필요한 자원을 할당하고 프로그램이 올바르게 실행되도록 돕는 '프로그램' 이다.
컴퓨터의 "프로그램"은 컴퓨터의 메모리에 적재된다고 배웠었는데, 운영체제를 위한 메모리 공간이 따로 할당되어 있다. 아래 그림을 보자

운영체제를 간략한 그림으로 나타낸 모습

메모리에는 두가지 영역이 존재하는데, 커널 영역, 사용자 영역으로 나뉜다.

 

 커널 영역

  • 목적: 운영체제의 핵심 기능을 수행하는 코드와 데이터를 저장하는 영역이다. 여기에는 운영체제의 커널(kernel)이 위치하며, 시스템의 핵심 기능을 관리하고 실행한다.
  • 권한: 가장 높은 권한을 가지고 있어, 하드웨어 및 다양한 시스템 리소스에 접근할 수 있다.
  • 기능: 프로세스 스케줄링, 메모리 관리, 입출력 관리, 인터럽트 처리 등과 같은 핵심적인 시스템 기능을 담당한다.

사용자 영역

  • 목적: 사용자 애플리케이션 및 프로세스가 실행되는 영역으로, 실제 응용프로그램의 코드와 데이터가 위치한다.
  • 권한: 상대적으로 낮은 권한을 가지고 있어, 일반적인 응용프로그램이 시스템 자원에 직접 접근하지 못하도록 보호한다.
  • 기능: 사용자 애플리케이션의 실행, 데이터 처리, 파일 시스템 접근 등을 담당한다.

운영체제의 메모리 관리

위의 그림에서 우리는 메모장을 실행할때 컴퓨터에 "메모리 1000번지에 메모장을 실행시켜줘" 라거나, 프로그램을 종료할때 "1000번지에 있는 메모장을 종료해" 라고 하지 않는다. 우리는 단순 x버튼을 눌러 메모장을 닫지만, 저 너머의 운영체제는 1000번지로 찾아가 해제시켜준다.

 

 

그렇다면 운영체제를 사용할때의 장점은 무엇일까?

- 운영체제는 응용 프로그램들이 자원에 접근하려고 할때, 오직 자신을 통해서만 접근하도록 하여 자원을 보호한다.

 

즉, 운영 체제는 자원과 응용 프로그램의 소통의 다리 역할을 하는 매개체이다.

 

운영체제가 하드웨어에 접근하는 모습

 

마지막으로, 운영체제의 핵심 기능들은 무엇일까?

  • 프로세스(== 현재 실행중인 프로그램) 관리
    • 컴퓨터에서 여러 프로그램을 동시에 실행한다고 가정할때, 사실 컴퓨터는 여러가지의 프로그램들을 "동시에" 실행하고 있는것은 아니다. 커널 영역에 존재하는 운영체제가 매우 빠르게 "번갈아가며" 이 프로그램들을 관리한다.
  • 자원 접근 및 할당
    • CPU = (CPU 스케쥴링 : 어떤 프로세스를 먼저, 얼마나 오래 실행할까?)
    • 메모리 = (페이징, 스와핑)
    • 입출력 장치
  • 파일 시스템 관리

 

이번에는 캐시 일관성 에 대해 알아보자. 
 
캐시 일관성이란 멀티코어 프로세싱에서 자주 등장하는 개념인데, 한마디로 말해 여러 캐시의, 즉 하나 이상의 캐시의 데이터 불일치를 나타내는 용어이다.
 
예를 한번 들어보자.

위의 예시는 두개의 코어가 있다고 가정한다.
 
예를 들어, 메모리 주소 0x1234ABCD에 X = 1이라는 데이터가 있다고 해보자.
그 다음, CPU1에서 0x1234ABCD에 저장되어 있는 X변수를 읽어오고싶다고 해보자. 그러면 1이 읽힐 것이다. 그러면 이제 CPU2에서 같은 주소로 내려가 X를 읽는다고 해보자. 그러면 버스에서는 메인 메모리의 0x1234ABCD에 있는 X에 접근하여 1을 읽어올것이다. 이 상황에서는 문제가 없다.
 
하지만, 
 
CPU2에서 이제 X에 1 대신 2를 쓴다고 해보자. 이렇게 되면, CPU1과 CPU2가 바라보는 X값은 다를 것이다. 즉, 어떤 값이 유효한 값인지 알 수 없다. 
 
위 그림이 이해가 안된다면 아래는 캐시 일관성 문제를 표현한 테이블이다.

 
맨 왼쪽 열에 있는 Time step은 동작이 수행되는 과정을 시간별로 분류한것이다.
 
0. X에는 0이라는 정수가 존재한다.
1. CPU A는 X값을 메모리에서 읽어온다 => 0을 읽어와 캐시에 저장할것이다.
2. CPU B도 X값을 메모리에서 읽어온다 => 0을 읽어와 캐시에 저장할것이다.
(이 시점에서 두개의 캐시 A,B에는 각각 X에서 읽어온 0이라는 값이 저장된다.)
3. CPU A는 X값을 1로 바꾼다. 이때, 메모리에서 X는 1을 가지고 있다.
 
같은 문제가 발생한다. 어느 캐시에 있는 X값이 올바른 X값일까?
 
여기서 일관성 문제를 해결하기 위해 아래 세가지 상태변수가 등장한다.
 

  • Invalid[0]
  • Shared[1]
  • Exclusive[1]

 
※Invalid : 여러개의 프로세서중 하나의 프로세서에서 어느 한 메모리 주소에 "쓰기" 요청을 할때, 현재 쓰기 요청을 하는 프로세서를 제외한 모든 프로세서를 invalid(무효화) 하게 만드는것이다. 예를 들어, 위의 예시에서 CPU A가 X에 쓰기 요청을 한다면, A는 버스를 통해 다른 프로세서들에게 "내가 지금 X값을 변경하고 있으니 너희 나머지 모두들의 X값을 무효화 시킬게. 만약 너가 X를 바꾸거나 읽고 싶다면, 다시 메인 메모리로 돌아가 업데이트된 X값을 가져와서 처리해" 라는 동작이다. [쓰기 전용]
 
※ Exclusive : 위의 예시에서, 이제 A를 제외한 다른 프로세서들은 모두 invalid상태에 머물러 있다. 그렇다면 A는 무슨 상태일까? A는 여기서 Exclusive(독점적인) 상태이다. "오직" A만이 X의 값을 변경하거나 읽을 수 있는 상태라는 말이다.
 
※ Shared : invalid나 Exclusive한 상태가 아닌 상태이며, "안전하게" 즉, 캐시 블럭을 문제 앖이 읽어올수 있다는 뜻이다. [읽기 전용]

예시를 들어보자.
1. 메모리의 X에는 0이라는 정수가 저장되어 있다.
2. A가 X를 읽어온다(캐시에는 아무것도 없는 상태이므로 캐시 미스 발생), 0을 읽어와 캐시에 저장한다.
3. B가 X를 읽어온다(캐시에는 아무것도 없는 상태이므로 캐시 미스 발생), 0을 읽어와 캐시에 저장한다.
4. A는 X에 원래 있던 0이라는 값 대신 1을 쓴다(이 시점에서 A는 Exclusive를 가지고, 캐시 B에 있던 X값(0)은 무효화(invalidate)된다).
5.B가 이제 X를 읽어온다. 그러면 B는 업데이트 된 1을 제대로 읽어올것이다.(이때, Exclusive했던 A는 더이상 Exclusive하지 않게 된다.)
 
 

병렬 컴퓨팅(Parallel Computing) 이란 무엇일까?

 

하나의 컴퓨터 또는 프로세서가 아니라 여러개의 컴퓨터나 프로세서가 동시에 일처리를 하여 작업 속도를 향상시키는것을 말한다.

 

- 멀티코어 프로세서(intel Core i7, i9) : 여러개의 독립적인 프로세서들이 하나의 칩(CPU,GPU, 메모리) 안에 내장되어 있고, 병렬적으로 작업 수행이 가능하다. 예를들어, 네개의 코어가 있는 프로세서는 네개의 각기 다른 작업들을 독립적으로 처리 할 수 있게 해준다.

 

 

멀티프로세서(UMA)를 표현한 그림

위의 구조를 맨 위부터 자세히 살펴보자.

 

1. 프로세서 : 작업을 요청하는 곳이다. 프로세서가 작업을 요청하면, 원하는 작업이 캐시에 있는지 확인하러 캐시 레벨로 내려간다.

 

2. 캐시 : 캐시는 앞 글에서 설명했다. 모른다면 다시 글을 읽고 꼭 다시 보고 넘어가자!

 

3. Interconnection Network : 편하게 BUS라고 부른다. 이 버스는 컴퓨터 내부의 다양한 구성 요소들이 데이터를 주고 받을 수 있는 "통로" 라고 생각하면 편하다. 위 그림에서의 버스는 SRAM 과 DRAM과의 정보 전처리를 담당하는 역할을 한다. 버스의 속도와 너비는 컴퓨터의 성능과 효율에 큰 영향을 미치며, SRAM과 DRAM의 효율적인 통신을 위해 사용된다.

 

위 그림처럼 여러개의 프로세서가 있고, 각 프로세서는 독립적은 캐시를 가지고 있으며, 버스를 통해 DRAM과 상호작용 한다.

 

그러면 내가 위 그림 설명에서 UMA라고 하였는데, UMA는 무엇일까?

 

  • UMA(Uniform)

여기서의 uniform은 무엇을 의미하는것일까?

 

위의 UMA 그림을 다시 살펴보자. 여기서 첫번째 프로세서가 동작하는 방식은 대략 이렇다.

 

1. 프로세서에서 작업(데이터) 요청

2. 캐시 방문(캐시 히트/ 미스) [이 상황에서는 캐시 미스라고 하겠다]

3. 버스에게 메시지 전달 ("1번 프로세서에서 캐시 미스! DRAM에서 요청한 메모리를 가져다 주기 바람!")

4. 메시지를 들은 버스는 DRAM에서 프로세서가 요청한 데이터를 "복사" 하여 다시 캐시로 올려준다. 

 

이러한 일련의 작업들은 모든 프로세서에서 동일하게(uniform) 수행된다.

 

 

 

가상 메모리란 무엇일까? 

 

가상 메모리란 메모리가 실제보다 많아 보이게 하는 기술로, 각 프로그램에 실제 메모리 주소가 아닌 가상 메모리 주소를 할당한다. 이러한 방식은 멀티태스킹에서 자주 사용되며, 메인 메모리를 크게 보이게 하는 기술로 사용된다. 

 

이러한 가상 주소를 '가상 주소'(virtual address)또는 '논리 주소'(logical address)라고 부른다. 가상 주소 장치는 메모리 관리 장치에 의해 물리 주소로 변환되여 매핑된다. 

 

아래는 가상 메모리 주소가 실제 물리 메모리 주소로 어떻게 변환되고 매핑되는지 간략히 표현한 그림이다.

동작 과정을 설명하기전, 용어부터 간략히 알고 넘어가자.

 

  • 페이지 : 가상 메모리의 "페이지"는 캐시의 블럭과 같다. 위 그림에서 왼쪽 위의 테이블들을 보면, 총 12개의 인덱스가 존재하는데, 각 인덱스는 가상 메모리를 담고있는 "페이지" 라고 불린다.
  • 프로세서가 요청한 메모리가 가상주소 페이지에 존재하지 않으면, page fault라고 불리운다.
  • 물리 주소(physical address) : RAM 상에 존재하는 "실제" 데이터가 존재하는 주소이다.
  • 가상 주소 : 운영체제가 부여한 "가상"의 주소로 관리또한 OS에 의해 관리된다.

아래는 가상 메모리의 동작 과정을 간략히 표현한 그림이다.

 

가상 주소 -> 물리 주소의 변환 과정을 나타낸 그림

동작 과정은 대략 이러하다.

 

<위 그림안의 테이블에서 각 인덱스는 가상 페이지 번호를 나타낸다.>

1. 가상 메모리에서 요청한 주소를 따라 페이지 테이블로 이동한다. 페이지 테이블에서 원하는 주소가 있는 인덱스로 이동하여 valid bit을 확인한다. valid  bit는 항상 0 또는 1이다. 

 

2. Valid bit이 1이라면 물리 페이지 번호를 로드한다.

 

3. page offset(페이지 오프셋은 페이지의 시작점부터 원하는 데이터가 얼마나 먼 거리에 떨어져 있느냐를 판별할때 쓰이는데, 가상 주소와 물리 주소의 페이지 오프셋은 항상 같다. 그다음 이 오프셋과 로드된 물리 주소를 합쳐 물리 주소를 얻어낸다.

 

상당히 직관적이고 간단해 보인다.

 

위 그림에 보면 Page Table Register라는 레지스터가 보이는데,  이 레지스터는 일반적인 레지스터와의 역할이 다르다. 이 레지스터는 항상 메인 메모리 안의 페이지 테이블의 시작점을 가리킨다. 

 

메인 메모리 안의 페이지 테이블,,, 뭔가 이상한 점을 눈치 챘다면 캐시와 메인 메모리의 개념을 어느정도 이해하고 있는 것이다. 혹시나 개념을 모른다면 아래 글을 참고하면 좋을것 같다. 

 

https://jghdg1234.tistory.com/4

 

Memory Hierarchy(컴퓨터의 메모리 구조)

이번에는 컴퓨터의 메모리에 대해 조금 더 자세히 알아보자. 컴퓨터의 메모리 구조를 그림으로 간략하게 표현하면 아래와 같다.(물론 실제 메모리의 물리적인 모습은 훨씬 복잡하다.) 맨 위(레

jghdg1234.tistory.com

정말 간단히 요약하면, 캐시는 빠르고, 메인 메모리는 느리다. 그렇지만 페이지 테이블은 메인 메모리 상에 존재하므로 매번 프로세서에게 요청을 받을 때마다 메인 메모리에 접근하는것이다. 그렇다면, 메인 메모리에 접근하기 전에 더욱 빠르게 가상 주소를 물리 주소로 변환하는 방법은 없을까? 있다. 이 문제점을 해결하기 위해 나온것이 TLB(Translation Lookaside Buffer, 변환 색인 버퍼)이다. 

 

TLB는 CPU상에 존재하기 때문에 접근 속도가 메인 메모리보다 훨씬 빠르다. 또다른 fully-associative cache라고 생각하면 된다. 

 

동작 과정은 대략 이러하다.

 

1. TLB와 페이지 테이블을 동시에 확인한다.

 

2. TLB에 원하는 블럭을 찾으면(TLB hit) 물리 주소로 곧바로 매핑한다.

 

3. 찾지 못하면(TLB miss) 메인 메모리에 접근하며 페이지 테이블을 확인한 후, 물리 주소를 찾고 그 주소를 TLB의 빈 자리에 메인 메모리에서 찾은 주소를 보관해둔다. 그런데 만약 페이지 테이블에 조차 원하는 주소를 찾지 못하면 어떻게 될까?(page fault) 이 뒤로는 이제 OS의 영역이다. OS가 페이지를 어디선가 찾아오고 페이지 테이블을 업데이트 시켜 줄 것이다. 

 

아래는 TLB의 동작 과정을 간단히 나타낸 그림이다.

 

 

 

TLB를 이용한 가상주소 -> 물리 주소의 변환 과정을 나타낸 그림

맨 위부터 차례로 

 

1. 32비트 물리 주소를 두 부분으로 나눈다.(가상 페이지 번호, 가상 페이지 오프셋) - 가상 페이지 오프셋은 물리 페이지 오프셋과 항상 동일하게 매핑된다

 

2. 물리 페이지 번호를 얻기 위해서, TLB에 접근하여 모든 슬롯을 찾아본다. 원하는 태그를 찾으면, 물리 주소를 로드하여 물리 주소로 변환 시킨다. 이때, TLB miss가 발생하면 DRAM으로 내려가 page table을 확인한 후, page table에 원하는 주소가 있으면 TLB를 방금 찾은 Page table entry로 업데이트 한다.

 

3. 변환된 물리 주소를 다시 인덱스, 오프셋, 태그 비트를 추출 한 후 나머지 캐시 동작을 수행한다. 

 

캐시의 동작 과정을 잘 모르겠다면, 아래 글을 한번 읽어보면 좋을것 같다.

 

https://jghdg1234.tistory.com/5

 

컴퓨터 캐시의 동작 과정

메모리 , 캐시에 대한 기본적인 내용을 모른다면 앞의 글을 참고하면 좋을것같다. https://jghdg1234.tistory.com/4 Memory Hierarchy(컴퓨터의 메모리 구조) 이번에는 컴퓨터의 메모리에 대해 조금 더 자세히

jghdg1234.tistory.com

 

+ Recent posts