이전 포스팅을 보면 알겠지만, 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을 실행중이라는 말이다. 

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

 

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

 

모든 프로세스는 실행을 위해  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 스케쥴링 : 어떤 프로세스를 먼저, 얼마나 오래 실행할까?)
    • 메모리 = (페이징, 스와핑)
    • 입출력 장치
  • 파일 시스템 관리

 

+ Recent posts