콘텐츠로 이동

Process

CPU 가상화
가상화는 각 응용 프로그램에게 여러 개의 CPU인 것처럼 보이도록 하는 일을 한다. 각 응용 프로그램은 자신만 사용하는 CPU 를 가지게 되었다고 생각하지만 실제로는 한 개의 CPU만 있다.

요약

운영체제의 기본 개념인 프로세스에 대해서 소개
프로세스는 간단히 말하면 실행 중인 프로그램이다. 이 개념을 염두에 두고 프로세스 구현에 필요한 기법, 구현한 프로세스를 스케줄링하는 정책 등을 다음 장에서부터 알아 볼 것이다.
이러한 여러 방법들을 조합하여 운영체제가 CPU를 가상화하는 방식을 이해하자.

프로세스의 개념

일반적으로 프로세스는 실행 중인 프로그램으로 정의한다.
프로그램은 디스크 상에 존재하며 실행을 위한 명령어와 정적 데이터의 묶음이다. 이 명령어와 데이터 묶음을 읽고 실행하여 프로그램에 생명을 불어넣는 것이 운영체제이다.

핵심 질문 : CPU가 여러 개 존재한다는 환상을 어떻게 제공하는가
적은 개수의 CPU 밖에 없더라도, 운영체제는 어떻게 거의 무한 개에 가까운 CPU가 있는 듯한 환상을 만들 수 있을까?

운영체제는 CPU를 가상화하여 이러한 환상을 만들어 낸다. 하나의 프로세스를 실행 하고, 얼마 후 중단시키고 다른 프로세스를 실행하는 작업을 반복하면서 실제 하나 또는 소수의 CPU로 여러 개의 가상 CPU가 존재하는 듯한 환상을 만들어 낸다.
시분할(time sharing)이라 불리는 이 기법은 원하는 수 만큼의 프로세스를 동시에 실행할 수 있게
한다. 시분할 기법은 CPU를 공유하기 때문에, 각 프로세스의 성능은 낮아진다.

운영체제에서 CPU 가상화를 잘 구현하기 위해, 저수준의 도구와 고차원적인 “지능”이 필요하다. 저수준 도구를 메커니즘(mechanism) 이라 한다.
메커니즘은 필요한 기능을 구현하는 방법이나 규칙을 의미한다. CPU에서 프로그램 실행을 잠시 중단하고 다른 프로그램을 실행하는 것을 문맥 교환(context switch) 이라고 한다.
이 시분할 기법은 모든 현대 운영체제들이 채택하고 있다.

팁 : 시분할(Time sharing)공간 분할(Space sharing) 이용
시분할은 자원 공유를 운영체제가 사용하는 가장 기본 기법 중 하나이다. 한 개체가 잠깐 자원을 사용한 후, 다른 개체가 또 잠깐 자원을 사용하고, 그 다음 개체가 사용하면서 자원 (CPU 또는 네트워크 링크 등)을 많은 개체들이 공유한다.
시분할과 자연스럽게 대응되는 개념은 공간 분할(space sharing)이다.
공간 분할은 개체에게 공간을 분할해 준다. 공간 분할의 예로 디스크가 있다.
디스크는 자연스럽게 공간 분할할 수 있는 자원으로, 블럭이 하나의 파일에 할당되면 파일을 삭제하기 전에는 다른 파일이 할당될 가능성이 낮다.

운영체제의 정책(policy)은 운영체제 내에서 어떤 결정을 내리기 위한 알고리즘이다.
예를 들어, 실행 가능한 여러 프로그램들이 있을 때, 운영체제는 어느 프로그램을 실행시켜야 하는가? 운영체제의 스케줄링 정책(scheduling policy)이 이러한 결정을 내린다. 이러한 결정을 내리기 위하여 스케줄링 정책은 과거 정보 (예, 직전 1분 동안 어떤 프로그램이 자주 실행되었는지), 워크로드에 관한 지식 (예, 어떤 유형의 프로그램들이 실행되었는지), 및 성능 측정 결과 (예, 시스템이 대화 성능 혹은 처리량을 높이려 하는지)를 이용한다.

운영체제는 실행 중인 프로그램의 개념을 제공하는데, 이를 프로세스(process)라 한다.
전술한 바와 같이 프로세스는 실행 중인 프로그램이다. (특정 순간의) 프로세스를 간단하게 표현하려면, 실행되는 동안 접근했거나 영향을 받은 자원의 목록을 작성하면 된다.
프로세스의 구성 요소를 이해하기 위해서 하드웨어 상태(machine state)를 이해해
야 한다. 프로그램이 실행되는 동안 하드웨어 상태를 읽거나 갱신할 수 있다. 이때 가장 중요한 하드웨어 구성 요소는 무엇일까?
프로세스의 하드웨어 상태 중 가장 중요한 구성 요소는 메모리이다. 명령어는 메모리에 저장된다. 실행 프로그램이 읽고 쓰는 데이터 역시 메모리에 저장된다. 프로세스가 접근할 수 있는 메모리 (주소 공간(address space)이라 불림)는 프로세스를 구성하는
요소이다.
레지스터도 프로세스의 하드웨어 상태를 구성하는 요소 중 하나이다.
많은 명령어들이 레지스터를 직접 읽거나 갱신한다. 프로세스를 실행하는 데 레지스터도 빠질 수 없다.
프로세스의 하드웨어 상태를 구성하는 레지스터 중에 특별한 레지스터들이 존재한다.

프로그램 카운터(program counter, PC)는 프로그램의 어느 명령어가 실행 중인지를 알려준다. 프로그램 카운터는 명령어 포인터(instruction pointer, IP)라고도 불린다.
스택 포인터(stack pointer)와 프레임 포인터(frame pointer)는 함수의 변수와 리턴 주소를 저장하는 스택을 관리할 때 사용하는 레지스터이다.

프로그램은 영구 저장장치 (persistent storage)에 접근하기도 한다. 이 입출력 정보는 프로세스가 현재 열어 놓은 파일 목록을 가지고 있다.

팁 : 정책과 구현의 분리
많은 운영체제에서 공통된 설계 패러다임은 고수준 정책을 저수준 기법으로부터 분리하는 것이다. 기법은 시스템에 관해 “어떻게”라는 질문에 답을 제공하는것이라고 생각할 수 있다.
예를 들어, 운영체제는 어떻게 문맥 교환을 하는가? 등이 해당된다. 정책은 “어느 것”이라는 질문에 답한다. 예를 들어, 운영체제는 지금 당장 어느 프로세스를 실행시켜야 하는가? 등이 해당된다. 둘을 분리하면 정책을 변경할 때 기법의 변경을 고민하지 않아도 된다. 따라서 일반적인 소프트웨어 설계 원칙인 모듈성(modularity)의 한 형태이다.

프로세스 API 개요

운영체제가 반드시 API 로 제공해야 하는 몇몇 기본 기능
이 API들은 형태는 다르지만 모든 현대 운영체제에서 제공

  • 생성 (Create): 새로운 프로세스 생성

  • 제거 (Destroy): 프로세스 생성 인터페이스를 제공하는 것처럼 필요없는 프로세스를 중단시키는 API도 필요함

  • 대기 (Wait): 어떤 프로세스의 실행 중지를 기다림

  • 각종 제어 (Miscellaneous Control): 프로세스를 일시정지하거나 재개 (일시정지되었던 프로세스의 실행을 다시 시작)하는 기능을 제공한다.

  • 상태 (Status): 프로세스 상태 정보 얻기 (얼마 동안 실행되었는지 또는 프로세스가 어떤 상태에 있는지 등)

프로세스 생성: 프로그램에서 프로세스로

운영체제는 어떻게 프로그램을 준비하고 실행시키는가? 실제로 어떻게 프로세스를 생성하는가

Pasted image 20221214105236.png

  • 코드 영역 : 프로그램 본문이 기술된 곳(읽기 전용)
  • 데이터 영역 : 코드가 실행되면서 사용하는 변수나 파일 등의 각종 데이터를 모아놓은 곳(읽기/쓰기)
  • 스택 영역 : 운영체제가 프로세스를 실행하기 위해 부수적으로 필요한 데이터를 모아놓은 곳(숨김 영역)

프로그램 실행을 위하여 운영체제가 하는 첫 번째 작업은 프로그램 코드와 정적 데이터 (static data, 예를 들어, 초기값을 가지는 변수) 를 메모리, 프로세스의 주소 공간에 탑재(load)하는 것이다.

프로그램은 디스크 또는 요즘 시스템에서는 플래시-기반 SSD에 특정 실행 파일 형식으로 존재한다.
코드와 정적 데이터를 메모리에 탑재하기 위해서 운영체제는 디스크의 해당 바이트를 읽어서 메모리의 어딘가에 저장해야 한다.

어떤 프로그램이든 실행시키기 전에 운영체제는 프로그램의 중요 부분을 디스크에서 메모리로 탑재해야 한다.

초기 운영체제들은 프로그램 실행 전에 코드와 데이터를 모두 메모리에 탑재하였다.
현대의 운영체제들은 이 작업을 늦추었다. 즉, 프로그램을 실행하면서 코드나 데이터가 필요할 때 필요한 부분만 메모리에 탑재한다.

해당 개념에서 더 나아가 페이징(paging)스와핑(swapping) 에 대해서 더 배울 예정임.

코드와 정적 데이터가 메모리로 탑재된 후, 프로세스를 실행시키기 전에 운영체제가 해야 할 일이 몇 가지 있다.
일정량의 메모리가 프로그램의 실행시간 스택(run-time stack, 혹은 그냥 스택) 용도로 할당되어야 한다.
C 프로그램은 지역 변수, 함수 인자, 리턴 주소 등을 저장하기 위해 스택을 사용한다.
운영체제는 스택을 주어진 인자로 초기화한다. 특히, main()함수의 인자인 argc와 argv 벡터를 사용하여 스택을 초기화한다.

운영체제는 프로그램의 힙(heap)을 위한 메모리 영역을 할당한다. C 프로그램에서 힙은 동적으로 할당된 데이터를 저장하기 위해 사용된다.

프로그램은 malloc()을 호출하여 필요한 공간을 요청하고 free()를 호출하여 사용했던 공간을 반환하여 다른 프로그램이 사용할 수 있도록 한다.

힙은 연결 리스트, 해시 테이블, 트리 등 크기가 가변적인 자료 구조를 위해 사용된다. 프로그램이 실행되면 malloc() 라이브러리 API 를 호출하여 메모리를 요청하고, 운영체제가 이를 충족하도록 메모리를 할당한다.

운영체제는 또 입출력과 관계된 초기화 작업을 수행한다.

예를 들어, Unix 시스템 에서 각 프로세스는 기본적으로 표준 입력 (STDIN), 표준 출력 (STDOUT), 표준 에러 (STDERR) 장치에 해당하는 세 개의 파일 디스크립터(file descriptor)를 넣는다.

이 디스크립터들을 사용하여 프로그램은 터미널로부터 입력을 읽고 화면에 출력을 프린트 하는 작업을 쉽게 할 수 있다.
(입출력, 파일 디스크립터 등에 관해서는 이후 영속성 파트에서 다룬다.)

코드와 정적 데이터를 메모리에 탑재하고, 스택과 힙을 생성하고 초기화하고, 입출력 셋업과 관계된 다른 작업을 마치게 되면, 운영체제는 프로그램 실행을 위한 준비를 마치게 된다.

프로그램의 시작 지점 (entry point), 즉 main()에서부터 프로그램 실행을 시작하는 마지막 작업만이 남는다.
main() 루틴으로 분기함으로써 (다음 장에서 이를 가능하게 하는 특수 기법 설명 나옴), 운영체제는 CPU를 새로 생성된 프로세스에게 넘기게 되고 프로그램 실행이 시작된다.

프로세스 상태

프로세스가 무엇인지 (이 개념에 대해 명확히 정의하겠지만), 어떻게 생성되는지 (대략이라도) 알게 되었으므로, 프로세스의 상태(state)에 대해 논의해 보자. 프로세스 상태의 개념은 초기 컴퓨터 시스템에서 등장하였다.
프로세스 상태를 단순화하면 다음 세 상태 중 하나에 존재할 수 있다.

Pasted image 20221214145446.png

  • 실행 (Running): 실행 상태에서 프로세스는 프로세서에서 실행 중이다. 즉, 프로세스는 명령어를 실행하고 있다.

  • 준비 (Ready): 준비 상태에서 프로세스는 실행할 준비가 되어 있지만 운영체제가 다른 프로세스를 실행하고 있는 등의 이유로 대기 중이다.

  • 대기 (Blocked): 프로세스가 다른 사건을 기다리는 동안 프로세스의 수행을 중단시키는 연산이다.
    흔한 예 : 프로세스가 디스크에 대한 입출력 요청을 하였을 때 프로세스는 입출력이 완료될 때까지 대기 상태가 되고, 다른 프로세스가 실행 상태로 될 수 있다.

그림에서 보듯이 프로세스는 준비 상태와 실행 상태를 운영체제의 정책에 따라 이동한다.
프로세스는 운영체제의 스케줄링 정책에 따라 스케줄이 되면 준비 상태에서 실행 상태로 전이한다.

Pasted image 20221214105331.png

실행 상태에서 준비 상태로의 전이는 프로세스가 나중에 다시 스케줄 될 수 있는 상태가 되었다는 것을 의미한다.

프로세스가 입출력 요청 등의 이유로 대기 상태가 되면 요청 완료 등의 이벤트가 발생할 때까지 대기 상태로 유지된다.
이벤트가 발생하면 프로세스는 다시 준비 상태로 전이되고 운영체제의 결정에 따라 바로 다시 실행될 수도 있다.

Structures (ready List, Process Control Blocks, and so forth)

3.2 Structures (ready list, process control blocks, and so forth) – Wachemo University e-Learning Platform
프로세스 제어 블록(Process Control Block, 줄여서 PCB)은 특정한 프로세스를 관리할 필요가 있는 정보를 포함하는 운영 체제 커널의 자료 구조이다.
작업 제어 블록(Task Control Block, 줄여서 TCB) 또는 작업 구조라고도 한다.
PCB는 운영 체제가 프로세스를 표현한 것이다.

  • 반드시 기억해야할 운영체제 자료 구조
    1. Process Control Block
    2. Process List
    3. Ready List
    4. Register information: context

PCB 포함 정보

  • Process state
  • Program counter
  • Stack pointer: 메모리 정보
  • Value of CPU registers when the process is suspended.
  • CPU scheduling information such as priority
  • The area of memory used by the process (memory management information)
  • Accounting information: amount of CPU time used, time limits, process no, etc.
  • I/O status information: list of I/O devices such as tape derives allocated for the process, a list of open files, etc.
  • Pointer to the next process’s PCB

Process control block include s CPU scheduling, I/O resource management, file management information etc.
The PCB serves as the repository for any information which can vary from process to process. Loader/linker sets flags and registers when a process is created.

If that process gets suspended, the contents of the registers are saved on a stack and the pointer to the particular stack frame is stored in the PCB.

By this technique, the hardware state can be restored so that the process can be scheduled to run again.

Example) Thread Structure in Pintos

Pintos(핀토스) 시작하기 | by Jeong Hyeon Lee | Jul, 2020 | Medium | Togather View

Pintos(핀토스)는 80x86 아키텍처를 위한 단순한 운영체제 프레임워크
Pasted image 20221218181750.png

흥미롭게도 핀토스는 디버깅 위해서 프로세스 이름 정의할 수 있음.

Pasted image 20221218185243.png

Pasted image 20221218185307.png

Bash
all_list  

모든 프로세스 리스트를 보여주는 명령어

운영체제가 프로세스를 생성할 때 마다 thread 구조를 list 에 넣는다.
제거하면 list 에서도 제거한다.

Bash
ready_list  

ready to run 상태의 프로세스 저장하는 리스트 (ready-queue)

Example) The xv6 Kernel Process Structure (Cont.)

Xv6, a simple Unix-like teaching operating system
GitHub - mit-pdos/xv6-public: xv6 OS

Xv6, a simple Unix-like teaching operating system
Pasted image 20221218181801.png

  • 봐야할 요소
    프로세스 상태
    pid
    여기는 priority field 가 없다. (흥미로운 특징)
    사용중인 파일에 대한 정보
    context (settable registers the process is using )

핀토스랑 다르게 레지스터 정보에 대해서 상세히 담고 있다. (Value of CPU registers when the process is suspended.)

Pasted image 20221218181809.png

두 개의 프로세스가 어떻게 전이될 수 있는지를 보여주는 예를 보자.
실행 중인 두 프로세스가 있다고 하자. 각 프로세스는 오직 CPU만 사용하고 입출력을 실행하지 않는다.

이 경우, 각 프로세스의 상태 전이는 아래와 같이 될 수 있다.

Pasted image 20221214105345.png

다음 예에서는 첫 번째 프로세스가 어느 정도 실행한 후에 입출력을 요청한다.
그 순간 프로세스는 대기 상태가 되고 다른 프로세스에게 실행 기회를 준다.

Pasted image 20221214105405.png

그림 7.4가 시나리오의 진행 과정을 보인다.
자세히 살펴보면, Process0은 입출력을 요청하고 요청한 작업이 완료되기를 기다린다.
프로세스는 디스크를 읽거나 네트워크로부터 패킷을 기다릴 때 대기 상태로 전이한다.
운영체제는 Process0이 CPU를 사용하지 않는다는 것을 감지하고 Process1 을 실행시킨다.
Process1이 실행되는 동안 입출력은 완료되고 Process0은 준비 상태로 다시 전이된다. 결국 Process1은 종료되고 Process0이 실행되어 종료된다.

위와 같은 간단한 예에서조차 운영체제가 내려야 할 결정이 매우 많다는 사실에 주목해야 한다.
우선 시스템은 Process0이 입출력을 요청할 때 Process1의 실행여부를 결정해야 한다.
Process1을 실행하기로한 결정은 CPU를 계속 동작시키므로 자원 이용률을 높인다.
또한, 시스템은 Process0이 요청한 입출력이 완료되었을 때, Process0 을 바로 실행하지 않고 실행 중이던 Process1을 계속 실행하였다.
운영체제는 스케줄러를 통해 이러한 결정을 내린다. 운영체제의 스케줄러는 이후 파트에 자세히 나온다.

자료 구조—프로세스 리스트 (시스템에 존재하는 모든 프로세스에 대한 정보, 이 리스트의 각 노드는 프로세스 제어 블럭이다.)
운영체제에는 다양한 중요 자료 구조들이 많이 존재한다.
프로세스 리스트가 그 중 첫 번째이다. 이 자료 구조는 단순하다.
다수의 프로그램을 동시에 실행할 수 있는 모든 운영체제는 이와 유사한 자료 구조를 가지고 있고, 이 자료 구조를 이용하여 시스템에서 실행 중인 프로그램을 관리한다.
프로세스의 관리를 위한 정보를 저장하는 자료 구조를 프로세스 제어 블럭(Process Control Block, PCB)이라 부른다.

자료구조

운영체제도 일종의 프로그램이다.

다른 프로그램들과 같이 다양한 정보를 유지하기 위한 자료 구조를 가지고 있다.

예를 들어, 프로세스 상태를 파악하기 위해 준비 상태의 프로세스들을 위한 프로세스 리스트(process list)와 같은 자료 구조를 유지한다.
또한, 어느 프로세스가 실행 중인지를 파악하기 위한 부가적인 자료 구조도 유지한다.

운영체제는 또 대기 상태인 프로세스도 파악해야 한다.
입출력 요청이 완료되면 운영체제는 적절한 프로세스를 깨워 준비 상태로 다시 전이시킬 수 있어야 한다.

Pasted image 20221214105442.png

그림 7.5는 xv6 커널에서 각 프로세스를 추적하기 위해 운영체제가 필요로 하는 정보를 보이고 있다 [Cox+08]. Linux, Mac OS X, 또는 Windows 같은 운영체제들도 이와 비슷한 프로세스 구조를 가지고 있다.

그림을 통해서 운영체제가 관리하는 있는 프로세스 정보를 알 수 있다.
레지스터 문맥(register context) 자료 구조는 프로세스가 중단되었을 때 해당 프로세스의 레지스터값들을 저장한다.
이 레지스터값들을 복원하여 (예, 해당 값을 실제 물리 레지스터에 다시 저장) 운영체제는 프로세스 실행을 재개한다.

문맥 교환(context switch) 이라고 알려진 이 기법은 이후 파트에 다시 자세히 배울 것이다.

그림에서 실행, 준비, 대기 외에 다른 상태들이 존재하는 것을 볼 수 있다.
초기(initial) 상태를 가지는 시스템도 있다. 프로세스가 생성되는 동안에는 초기 상태에 머무른다.

프로세스는 종료되었지만 메모리에 남아있는 상태인 최종(inal)상태도 있다.
(Unix-기반 시스템에서 이 상태는 좀비(zombie) 상태[1] 라고 불린다).
이 상태는 프로세스가 성공적으로 실행했는지를 다른 프로세스 (보통은 부모(parent)프로세스) 가 검사하는 데 유용하다.

이를 위하여 최종 상태를 활용한다 (Unix-기반 시스템에서는 프로세스가 성공적으로 종료되었으면 0을, 그렇지 않으면 0이 아닌 값을 반환한다).
부모 프로세스는 자식 프로세스의 종료를 대기하는 시스템 콜을 호출 (예, wait()) 한다. 이 호출은 종료된 프로세스와 관련된 자원들을 정리할 수 있다고 운영체제에 알리는 역할도 한다.

  1. 이 좀비들은 제거 명령으로 쉽게 제거할 수 있으나 일반적으로 다른 방법을 사용하여 제거한다.

Pasted image 20221214152837.png

부모가 자식을 생성한 후 자식의 종료를 기다리는 경우

Pasted image 20221214152841.png
자식이 부모보다 먼저 종료한 경우


시뮬레이션 숙제

시뮬레이션 숙제는 논의된 주제를 이해하였는지 확인할 수 있도록 실행 가능한 시뮬레이터 형태로 제공된다. 시뮬레이터는 통상 python 프로그램 형태로 제공된다.

이 프로그램을 사용하면 랜덤 시드를 이용하여 다른 문제를 출제할 수 있고 -c 플래그를 지정하면 주어진 문제에 대한 해답을 만들 수 있다. 이렇게 생성된 해결책과 비교하여 당신의 해결책을 점검할 수 있다.
-h 또는 - -help 옵션을 지정하고 시뮬레이터를 실행시키면 시뮬레이터가 제공하는 모든 옵션에 대한 정보를 얻을 수 있다.

각 시뮬레이터와 함께 제공되는 README 파일은 시뮬레이터를 실행시키는 방법에대해 상세히 설명한다. 각 플래그에 대한 상세한 설명을 확인할 수 있다.

process-run.py 프로그램은 프로세스가 실행되면서 변하는 프로세스의 상태를 추적할 수 있고, 프로세스가 CPU를 사용하는지 (예, add 명령어 실행) 입출력을 하는지 (예, 디스크에 요청을 보내고 완료되기를 기다린다)를 알아볼 수 있다.

문제

  1. 다음과 같이 플래그를 지정하고 프로그램을 실행시키시오: ./process-run.py -l 5:100, 5:100.
    CPU 이용률은 얼마가 되어야 하는가 (예, CPU가 사용 중인 시간의 퍼센트?)
    그러한 이용률을 예측한 이유는 무엇인가?
    -c 플래그를 지정하여 예측이 맞는지 확인하시오.

  2. 이제 다음과 같이 플래그를 지정하고 실행시키시오 :
    ./process-run.py -l 4:100, 1:0.
    이 플래그는 4개의 명령어를 실행하고 모두 CPU만 사용하는 하나의 프로세스와 오직 입출력을 요청하고 완료되기를 기다리는 하나의 프로세스를 명시한다. 두 프로세스가 모두 종료되는 데 얼마의 시간이 걸리는가?
    -c 플래그를 사용하여 예측한 것이 맞는지 확인하시오.

  3. 옵션으로 지정된 프로세스의 순서를 바꾸시오 : ./process-run.py -l 1:0, 4:100.
    이제 어떤 결과가 나오는가? 실행 순서를 교환하는 것은 중요한가? 이유는 무엇인가?
    (언제나처럼 -c 플래그를 사용하여 예측이 맞는지 확인하시오.)

  4. 자, 다른 플래그에 대해서도 알아보자. 중요한 플래그 중 하나는 -S로서 프로세스가 입출력을 요청했을 때 시스템이 어떻게 반응하는지를 결정한다.
    이 플래그가 SWITCH_ON_END 로 지정되면 시스템은 요청 프로세스가 입출력을 하는 동안 다른 프로세스로 전환하지 않고 대신 요청 프로세스가 종료될 때까지 기다린다.
    입출력만 수행하는 프로세스와 CPU 작업만 하는 프로세스 두 개를 실행시키면 어떤 결과가 발생하는가?(-l 1:0,4:200 -c -S SWITCH_ON_END)

  5. 이번에는 프로세스가 입출력을 기다릴 때마다 다른 프로세스로 전환하도록 플래그를 지정하여 같은 프로세스를 실행시켜 보자 (-l 1:0,4:100 -c -S SWITCH_ON_IO). 이제 어떤 결과가 발생하는가?
    -c를 사용하여 예측이 맞는지 확인하시오.

  6. 또 다른 중요한 행동은 입출력이 완료되었을 때 무엇을 하느냐이다.
    -I IO_RUN_LATER 가 지정되면 입출력이 완료되었을 때 입출력을 요청한 프로세스가 바로 실행될 필요가 없다.
    완료 시점에 실행 중이던 프로세스가 계속 실행된다.
    다음과 같은 조합의 프로세스를 실행시키면 무슨 결과가 나오는가? (./process-run.py -l 3:0, 5:100, 5:100, 5:100 -S SWITCH_ON_IO -I IO_RUN_LATER -c -p)
    시스템 자원은 효과적으로 활용되는가?

  7. 같은 프로세스 조합을 실행시킬 때 -I IO_RUN_IMMEDIATE를 지정하고 실행시키시오.
    이 플래그는 입출력이 완료되었을 때 요청 프로세스가 곧바로 실행되는 동작을 의미한다.
    이 동작은 어떤 결과를 만들어 내는가?
    방금 입출력을 완료한 프로세스를 다시 실행시키는 것이 좋은 생각일 수 있는 이유는 무엇인가?

  8. 이제 다음과 같이 무작위로 생성된 프로세스를 실행시켜 보자.
    예를 들면, -s 1 -l 3:50,3:50, -s 2 -l 3:50,3:50, -s 3 -l 3:50,3:50.
    어떤 양상을 보일지 예측할 수 있는지 생각해 보시오.
    -I IO_RUN_IMMEDIATE를 지정했을 때와 -I IO_RUN_LATER를 지정했을 때 어떤 결과가 나오는가?
    -S SWITCH_ON_IO-S SWITCH_ON_END의 경우에는 어떤 결과가 나오는가?


Process API

핵심 질문 : 프로세스를 생성하고 제어하는 방법
프로세스를 생성하고 제어하려면 운영체제가 어떤 인터페이스를 제공해야 하는가?
유용하고 편하게 사용하기 위해서 이 인터페이스는 어떻게 설계되어야 하는가?

  • 핵심 주제
    1. Unix 프로세스를 다루는 API 중 fork(), exec()wait()
    2. Seperation of fork() and exec() (Unix)
  • IO redirection
  • Pipe

fork() System Call

Child 프로세스 생성

Pasted image 20221214143700.png

실행 중인 프로세스로부터 새로운 프로세스를 복사하는 함수이다.
이때 실행하던 프로세스는 부모 프로세스, 새로 생긴 프로세스는 자식 프로세스로서 부모-자식 관계가 된다.

  • 부모 프로세스 영역과 자식 프로세스 영역과 다른 부분
    1. 프로세스 구분자(PID)
    2. 메모리 관련 정보
    3. 부모 프로세스 구분자(PPID)와 자식 프로세스 구분자(CPID)

자식 프로세스는 부모와 같은 메모리를 차지한다.

자식 프로세스는 자신의 주소 공간, 자신의 레지스터, 자신의 PC 값을 갖는다.

  • Child process is allocated separate memory space from the process.
    The child process has the same memory contents as the parents.
  • The child process has its own registers, and program counter register (PC)
  • The newly created process becomes independent after it is created.
    독립적인 프로그램으로서 스케줄링 됨
  • for parent, fork() returns PID of of child process
    for child process, fork() returns 0

p1.c (fork() 예제)

The fork() System Call
Pasted image 20221214153304.png

<그림 8.1> fork() 호출 (p1.c)

p1.c 프로그램에서 어떤 일들이 벌어지는지 더 자세히 보도록 하자.
실행이 시작될 때 프로세스는 PID가 포함된 “hello world …” 메시지를 출력한다.
그러고 fork( ) 함수를 호출한다.

  • 결과 1

    Bash
    prompt> ./p1  
    hello world (pid:29146)  
    hello, I am parent of 29147 (pid:29146)  
    hello, I am child (pid:29147)  
    prompt>  
    

  • 결과 2

    Bash
    prompt> ./p1  
    hello world (pid:29146)  
    hello, I am child (pid:29147)  
    hello, I am parent of 29147 (pid:29146)  
    prompt>  
    

fork() 호출 직후를 살펴보자.
운영체제 입장에서 보면. 프로그램 p1이 2개가 존재한다. 두 프로세스가 모두 fork()에서 리턴하기 직전이다.

새로 생성된 프로세스는 (= 자식 프로세스) main()함수 첫 부분부터 시작하지 않았다는 것을 알 수 있다.
(“hello, world” 메시지가 한 번만 출력되었다는 것에 유의하기 바란다).

자식 프로세스는 fork()를 호출하면서부터 시작되었다. (pid === 0 )

자식 프로세스는 부모 프로세스와 완전히 동일하지는 않다.

fork() 시스템 콜의 반환 값이 서로 다르다. fork() 로 부터 부모 프로세스는 생성된 자식 프로세스의 PID를 반환받고, 자식 프로세스는 0을 반환받는다.

이 반환 값의 차이로 인해, 그림 8.1과 같이 부모와 자식 프로세스가 서로 다른 코드를 실행하는 프로그램을 쉽게 작성할 수 있다.
이 프로그램의 출력 결과가 항상 동일하지는 않다.

단일 CPU 시스템에서 이 프로그램을 실행하면, 프로세스가 생성되는 시점에는 2개 (부모와 자식) 프로세스 중 하나가 실행된다.

위의 출력 예에서는 부모 프로세스 실행 후에 자식 프로세스가 실행되거나, 그 반대 경우도 발생할 수 있다.

CPU 스케줄러(scheduler)는 실행할 프로세스를 선택한다. (스케줄러는 이후에 더 배울 것임)

스케줄러의 동작은 일반적으로 상당히 복잡하고 상황에 따라 다른 선택이 이루어지기 때문에, 어느 프로세스가 먼저 실행된다라고 단정하는 것은 매우 어렵다.

이 비결정성(nondeterminism)으로 인해 멀티 쓰레드 프로그램 실행 시 다양한 문제가 발생한다.
(이 책의 제2편 병행성 부분에서 비결정성에 대해 자세히 다룸.)

자식 프로세스와 부모 프로세스 간 순서를 강제하기 위해서는 어떤 방법이 필요할까?

wait() System Call

부모가 자식 프로세스가 종료할 때 까지 커널 내에서 대기하는 시스템 호출 함수 (waitpid() 도 있음)

  • When the child process is created, wait() in the parent process won’t return until the child has run and exited.
  • The parent and the child does not have any dependency.
  • In some cases, the application wants to enforce the order in which they are executed, e.g. the parent exits only after the child finishes.

이 예제에서 (p2.c) 부모 프로세스는 wait() 시스템 콜을 호출하여 자식 프로세스 종료 시점까지 자신의 실행을 잠시 중지시킨다. 자식 프로세스가 종료되면 wait()는 리턴한다.

정리 해야 할 자식 프로세스를 정리 하지 않은 채 부모 프로세스가 종료 되면, 고아 프로세스, 좀비 프로세스가 생성 될 수 있음
따라서 wait() 호출을 사용 하여 이러한 것을 방지 함

부모 - 자식 프로세스 동기화를 위해서도 사용 됨

p2.c (wait() 예제)

Pasted image 20221214153824.png
<그림 8.2> fork() 와 wait() 호출 (p2.c)

주의 ! wait() 함수는 인자가 없다.

wait() 호출을 위와 같이 부모 프로세스 코드에 추가하면 프로그램은 항상 동일한 결과를 출력한다.

  • Result (Deterministic)
    Bash
    prompt> ./p2  
    hello world (pid:29266)  
    hello, I am child (pid:29267)  
    hello, I am parent of 29267 (wc:29267) (pid:29266)  
    prompt>  
    

우리는 이제 wait() 함수를 통해서 자식 프로세스가 먼저 출력을 수행하도록 보장할 수 있다.
부모 프로세스가 먼저 실행되면 곧바로 wait()를 호출한다. 이 시스템 콜은 자식 프로세스가 종료될 때까지 리턴하지 않는다.
부모가 먼저 실행되더라도 자식 종료 후 wait()가 리턴한다. 그런 후 부모 프로세스가 출력한다.

wait()가 자식 프로세스가 종료하기 전에 리턴하는 몇 가지 경우가 있다.
자세한 사항은 man 페이지를 참조하면 됨

exec() System Call

The exec family of system calls :: Operating systems 2018
fork() 는 부모 클론이라면 exec() 는 부모랑 다른 프로그램 실행하고 싶을 때!
기존의 프로세스를 새로운 프로세스로 전환하는 함수로 기존 프로세스의 구조를 재활용한다.

Pasted image 20221214144158.png

이 시스템 콜은 자기 자신이 아닌 다른 프로그램을 실행해야 할 때 사용한다.
p2.c 의 fork() 시스템 콜은 자신의 복사본을 생성하여 실행한다.
자신의 복사본이 아닌 다른 프로그램을 실행해야 할 경우에는 바로 exec() 시스템 콜이 그 일을 한다 (그림 8.3).

이 메서드는 호출한 그 자신과 또 다른 프로그램을 실행하고 싶을 때 씀

  • exec( ) Parameter: 2개
    1. The name of the binary file (실행할 파일)
    2. The array of arguments (that needs to be passed to the exec function)

Os needs to load a new binary image, initialize a new stack, initialize a new heap for the new program.

Pasted image 20221218193746.png

C
char *argv[3];  

argv[0] = "echo";  
argv[1] = "hello";  
argv[2] = 0;  

exec("/bin/echo", argv); // 실행하고자 하는 바이너리 파일 이름, 한번에 실행할 배열  
printf("exec error\n")  

p3.c (exec() 예제)

Pasted image 20221214154850.png
<그림 8.3> fork(), wait(), exec() 호출하기 (p3.c)

이 예에서 자식 프로세스는 wc 프로그램을 실행하기 위해 execvp() 시스템 콜을 호출한다.

처음에는 fork() 호출함.
부모 프로세스 wait() 걸고 자식 프로세스 끝날 때 까지 기다림 (blocked)
자식 프로세스 실행

  • p3 Result
    Bash
    prompt> ./p3  
    hello world (pid:29383)  
    hello, I am child (pid:29384) # 자식 프로세스 실행  
    29 107 1030 p3.c # wc 프로그램 실행  
    hello, I am parent of 29384 (wc:29384) (pid:29383) # 부모 프로세스 실행  
    prompt>  
    

wc 프로그램은 단어의 개수를 세는 프로그램이다.
자신의 소스 파일인 p3.c를 인자로 하여 wc를 실행하고 소스 코드의 행 개수, 단어의 개수, 바이트의 개수를 알려 준다.

사실 exec() 시스템 콜은 6가지 변형이 존재한다. xecl(), execle(), execlp(), execv(), execvp(), execve()

exec() 시스템 콜은 다음과 같은 과정으로 수행된다.
실행 파일의 이름과 (예, wc) 약간의 인자가 (예, p3.c) 주어지면 해당 실행 파일의 코드와 정적 데이터를 읽어 들여 현재 실행 중인 프로세스의 코드 세그멘트와 정적 데이터 부분을 덮어 쓴다.

힙과 스택 및 프로그램 다른 주소 공간들로 새로운 프로그램의 실행을 위해 다시 초기화된다.

그런 다음 운영체제는 프로세스의 argv와 같은 인자를 전달하여 프로그램을 실행시킨다.
새로운 프로세스를 생성하지는 않는다. 현재 실행 중인 프로그램을 (p3) 다른 실행 중인 프로그램으로 (wc) 대체하는 것이다.

자식 프로세스가 exec()을 호출한 후에는 p3.c는 전혀 실행되지 않은 것처럼 보인다.
exec() 시스템 콜이 성공하게 되면 p3.c는 절대로 리턴하지 않는다.

  • Replace the existing contents of the memory with the new memory contents from the new binary file.
  • exec() does not return. It starts to execute

fork() exec() 를 분리한 이유

팁 : 올바른 선택하기 (Lampson’s Law)
Lampson은 매우 자주 올바른 선택을 했기 때문에 그를 기념하여 이 법칙에 그의 이름을 붙였다.
“Hints for Computer Systems Design”
Lampson이 말한 바 같이 “올바르게 선택하라. 추상화도 단순함도 올바른 선택을 대체할 수 있다.”
올바른 선택을 해야만 한다. 올바른 선택은 다른 어떤 대안보다 나아야 한다.
프로세스 생성을 위한 API를 설계하는 방법은 많다. 그러나 fork()와 exec()의 조합은 UNIX OS 에서 단순하면서 매우 강력하다.

여기서 이런 의문이 생길 수 있다. 새로운 프로세스를 생성하는 간단한 작업 같은데, 왜 이런 이상한 인터페이스를 사용할까?
왜 fork() 와 exec() 를 분리했을까? (그냥 forkAndExec(“ls”, “ls -l”) 쓰면 안되나?)
fork()/exec() 조합이 프로세스를 생성하고 조작하는 강력한 방법이다.

Unix의 쉘을 구현하기 위해서는 fork()와 exec()을 분리해야 한다. 그래야만 쉘이 fork()를 호출하고 exec()를 호출하기 전에 코드를 실행할 수 있다.

이때 실행하는 코드에서 프로그램의 환경을 설정하고, 다양한 기능을 준비한다.

새 프로그램을 실행하고 IO rediction, pipe(shell 프로그래밍의 핵심) 를 가능하게 한다.

Pasted image 20221214161158.png

쉘은 단순한 사용자 프로그램이다. 쉘은 프롬프트를 표시하고 사용자가 무언가 입력하기를 기다린다. 그리고 명령어를 입력한다 (예, 실행 프로그램의 이름과 필요한 인자).

대부분의 경우 쉘은 파일 시스템에서 실행 파일의 위치를 찾고 명령어를 실행하기 위하여 fork()를 호출하여 새로운 자식 프로세스를 만든다.

그런 후 exec()의 변형 중 하나를 호출하여 프로그램을 실행시킨 후 wait()를 호출하여 명령어가 끝나기를 기다린다. 자식 프로세스가 종료되면 쉘은 wait()로부터 리턴하고 다시 프롬프트를 출력하고 다음 명령어를 기다린다.

fork()와 exec()을 분리함으로써 쉘은 많은 유용한 일을 조금 쉽게 할 수 있다.
다음 예를 생각해 보자.

I/O Redirection

Bash
prompt> wc p3.c > newfile.txt  
  • wc w3.c‘ 의 결과를 newfile.txt 에 저장하자.
  • Shell is a program that fork() and exec() the command with argument.
    • % ls l → shell calls fork() and exec("ls", "ls-l");
    • Before calling exec("wc"), "wc w3.c" , the shell closes STDOUT (close(1)) and opens newfile.txt(open(“newfile.txt”))

위의 예제에서 wc 프로그램의 출력은 newfile.txt라는 출력 파일로 방향이 재지정된다 ( > 표시가 재지정을 나타낸다).

자식이 생성되고 exec()이 호출되기 전에 표준 출력(standard output) 파일을 닫고 newfile.txt 파일을 연다.
이런 작업을 해 놓으면 곧 실행될 프로그램인 wc의 출력은 모두 화면이 아니라 파일로 보내진다.

p4.c 예제

Pasted image 20221214161249.png
<그림 8.4> 입출력 재지정의 모든 것 (p4.c)

그림 8.4에 작업을 수행하는 프로그램이 나와 있다.

Unix 시스템은 미사용 중인 파일 디스크립터를 0번부터 찾아 나간다.
이 경우, STDOUT_FILENO가 첫 번째 사용 가능 파일 디스크립터로 탐색되어, open()이 호출될 때 할당된다. 표준 출력 파일을 닫았기 때문이다.

이후 자식 프로세스가 표준 출력 파일 디스크립터를 대상으로 하는 모든 쓰기, 예를 들어 printf()에 의한 쓰기는 화면이 아니라 새로 열린 파일로 향하게 된다.

이 출력 결과에는 두 가지의 흥미로운 점이 있다. 첫째, p4를 실행하면, 화면에 아무 일도 일어나지 않는다.

그러나 실제로는 다음과 같은 일이 발생하였다.

프로그램 p4 는 fork()를 호출하여 새로운 자식 프로세스를 생성하고 execvp()를 호출하여 wc 프로그램을 실행시킨다. 출력이 p4.output 파일로 재지정되었기 때문에 화면에는 아무것도 출력되지 않는다. 출력 파일을 cat해 보면 wc를 실행시켰을 때 얻을 수 있는 모든 출력이 파일에 저장된다.

Unix 파이프가 이와 유사한 방식으로 구현되지만 pipe() 시스템 콜을 통하여 생성된다.
이 경우, 한 프로세스의 출력과 다른 프로세스의 입력이 동일한 파이프에 연결된다.

  • p4.c의 실행 결과
Bash
prompt> ./p4  
prompt> cat p4.output  
32 109 846 p4.c  
prompt>  

한 프로세스의 출력은 자연스럽게 다음 프로세스의 입력으로 사용되고, 명령어 체인이 형성된다.

파일에서 특정 단어가 몇 번 나오는지 세는 예제를 생각해 보자.
파이프와 grep과 wc를 사용하면 쉽게 할 수 있다. grep foo file | wc -l 을 명령어 프롬프트에 입력하면 된다.

File Descriptor

File descriptor - Wikipedia
리눅스 - 파일 디스크립터 :: Developer Ahn
Site Unreachable

흔히 유닉스 시스템에서 모든 것은 파일이라고 한다.
일반적인 정규파일(Regular File)에서부터 디렉토리(Directory), 소켓(Socket), 파이프(PIPE), 블록 디바이스, 캐릭터 디바이스 등등 모든 객체들은 파일로써 관리된다. 유닉스 시스템에서 프로세스가 이 파일들을 접근할 때에 파일 디스크립터(File Descriptor)라는 개념을 이용한다.

파일 디스크립터는 ‘0이 아닌 정수’, ‘Non-negative Integer’ 값이다. 즉, 음수가 아닌 0과 양수인 정수 값을 갖는다. (unsigned int 값이라고 보면 된다.)

프로세스가 실행 중에 파일을 Open 하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중에 사용하지 않는 가장 작은 값을 할당해 준다. 그 다음 프로세스가 열려있는 파일에 시스템 콜을 이용해서 접근 할 때, FD 값을 이용해 파일을 지칭 할 수 있다.

프로그램이 프로세스로 메모리에서 실행을 시작 할 때, 기본적으로 할당되는 파일 디스크립터 표가 있다.

바로 표준 입력(Standard Input), 표준 출력(Standard Output), 표준 에러(Standard Error)이다.
이 들에게 각각 0, 1, 2 라는 정수가 할당되며, POSIX 표준에서는 STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO로 참조된다. 이 매크로는 헤더 파일에서 찾아 볼 수 있다.

0이 아닌 정수로 표현되는 파일 디스크립터는 0 ~ OPEN_MAX (요즘 links 버전에서는 256까지 가능) 까지의 값을 가질 수 있으며, OPEN_MAX 값은 플랫폼에 따라 다르다.

Pasted image 20221218200609.png

파일 디스크립터

  • 시스템으로부터 할당 받은 파일을 대표하는 0이 아닌 정수 값
    file descriptor is an integer that represents a file, a pipe, a directory and a device.
  • 프로세스에서 열린 파일의 목록을 관리하는 테이블의 인덱스
    A process uses a file descriptor to open a file and directory.
  • Each process has its own file descriptor table.

    Pasted image 20221218200632.png
    0 (Standard Input = STDIN)
    1 (Standard Output = STDOUT),
    2 (Standard Error = STDERR)

  • open()
    새로운 파일 오브젝트 생성, offset 가짐
    fd 리스트 차례로 훑으면서 가장 첫번 째로 나오는 빈칸에 파일 오브젝트를 포인터로 가르킴.

Allocate a new file object, allocate new file descriptor and set the newly allocated file descriptor to point to the new file object.
when allocating the new file descriptor, it uses the smallest ‘free’ file descriptor from the file descriptor table.

  • close()
    리스트에서 삭제 포인터 삭제
    deallocate the file descriptor ‘fd’
    deallocate the file object if there is no file descriptor associated with it.

  • fork()
    copies the file descriptor table from the parent to child process.

  • exec()
    retains the file descriptor table.
    → loads new binary
    → initial new stack, new heap.
    → reuse file descriptor table

Pasted image 20221218200905.png

Pasted image 20221218201342.png

Offset

시작해야 할 지점
https://man7.org/training/download/lusp_fileio_slides.pdf

Pasted image 20221218201503.png
MMAP – TutorialsDaddy

C
fd = open( pathname, flags, mode ) // integer  
// pathname 이 가리키는 파일을 열고 열린 파일을 이후 호출에서 참조 할 때 쓸 파일 디스크립터를 리턴.  
// flags는 파일을 읽기, 쓰기, 둘다를 위해 열 지 말 지를 지정 한다.  
read = read( fd, buffer, count ) // standard input, stream 타입,   
// fd가 가리키는 파일에서 최대 count 바이트를 읽어 buffer에 저장.  
written = write( fd, buffer, count )  
// 버퍼에서 최대 count 바이트를 fd가 가리키는 열려 있는 파일에 쓴다.  
close(fd);  

fd는 해당 프로세스의 open file 을 관리하는 구조체 배열의 index.

그 구조체의 index 가 가리키는 객체가 dentry 라는 객체이고, 그 dentry 객체는 또다시 해당 파일을 나타내는 inode 객체를 가리키게 된다..

커널 구조체중 struct files_struct 보시면 struct file fd_array 라는 배열이 있다.
실제로 open()을 통해 얻는 fd 는 저 구조체의 index 를 나타낸다.

일반적으로 0, 1, 2 index 는 std-in/std-out/error 와 관련된 fd 로 미리 할당이되고, 보통 open()을 하게 되면 fd 값은 3이 된다.

3 번 index 로 test.txt 를 찾는 방법은 우선 fd_array[3] 이 pointing 하고 있는 file 구조체의 f_dentry 를 얻어오게 된다.

dentry 란 것은 directory entry 를 의미하는데, 리눅스에서 디렉토리에 접근을 빠르게 하기 위한 구조체로 사용하고 있다. open() 시스템 콜을 호출하게 되면, test.txt 가 존재하는 위치와 관련되어 dentry 구조체가 만들어진다.

dentry 구조체는 관련된 inode 구조체를 가리키는 필드를 포함한다.
따라서 open(“test.txt’,…) 함수로 호출된 파일은 test.txt 에 대한 dentry 생성, inode 생성(또는 읽음) 후에 해당 프로세스의 open 파일 관리 구조체인 files_struct 의 fd_array 의 비어있는 위치에 test.txt 의 dentry 를 포인팅하고 그 index 인 3을 넘겨주는 것이다.

이후 사용자가 3번을 가지고 시스템 콜을 수행하게되면, 해당 프로세스의 files_struct 의 fd_array index 를 통해서 관련 inode에 접근할 수 있게 되는 것임.

I/O Redirector

부모-자식 관계 프로세스 간에 파일을 이용한 통신

Pasted image 20221218215341.png

% cat input.txt

Bash
char *argv[2];  

argv[0] = "cat";  
argv[1] = 0;  
if (fork() == 0) {  
close(0);  
open("input.txt", O_RDONLY);  
exec("cat", argv);  
}  

C
char buf[512];  
int n;  

for(;;) {  
    n = read(0, buffer, sizeof(buffer)); // Standard Input  
    // read from input.txt  
    if (n == 0) break;  
    if (n < 0) {  
      fprintf(2, "read error\n"); // Standard Error  
      exit();  
    }  
    if(write(1, buffer, n) == n) {  
      fprintf(2, "write error\n"); // Standard Output  
      exit();  
    }  
}  

read from standard input
write from standard output
open() 함수는 fd(file descriptor)를 반환 => 해당 파일에 접근할 수 있는 일종의 열쇠
쓰기 읽기 연산 맨 앞에는 언제나 fd를 사용함

ex: write(fd, "Test", 5)
fd(com.txt)파일에 Test라는 문자열을 쓰라는 의미
“Test” 문자열 크기가 5B이기 때문에 5 명시 (문자열 끝을 알리는 null포함해서 총 5B)

Pasted image 20221218211319.png

Pasted image 20221218211327.png

Pipe |

프로세스 간 통신

Pipe 간단 개요: 더 쉽게 알아보기

Pipe란 무엇인가?

두 프로세스는 pipe | 를 통해서 정보를 주고 받는다.

Bash
❯❯❯ ps aux | grep python  

위 명령어를 프로세스가 하나라 생각할 수 있지만 사실 2개이다.
ps aux, grep python 이렇게 각기 다른 프로세스를 돌리는 것이다.

pipe는 총 2개의 파일을 생성한다..
하나는 왼쪽 프로세스가 자신의 결과값을 write하는 파일, 다른 하나는 오른쪽에서 read하는 파일이다.

파이프를 기준으로 왼쪽 프로세스 ps aux의 결과 값이 오른쪽 프로세스 grep python input 값으로 들어간다.
실제 수도관 파이프 처럼 한쪽에서는 물을 넣어주고 다른 한쪽에서는 그 물을 받는 형태와 같다.
따라서 소켓과는 다르게 한 방향으로만 통신할 수 있다. (일반 파이프의 경우)

Bash
% echo hello world | wc  
  • Output to STDOUT of one process is fed to STDIN of another process.
  • Implemented with dup() and pipe()
  • Key innovation of UNIX shell.
    Pasted image 20221218205540.png
    파이프를 생성한 후에는 read()와 write() 시스템 콜을 사용하여 접근할 수 있다.

일반 파이프는 파이프를 생성한 프로세스 이외에는 접근을 할 수 없기 때문에 통상 부모 프로세스가 파이프를 생성하고 fork()로 생성한 자식 프로세스와 통신하기 위해 사용한다.
Pasted image 20221218212154.png
[운영체제] 파이프 — Vanilla 개발 일기

dup() System Call

  • Duplicate file descriptor: dup() System Call

    find an empty slot from the beginning of the file descriptor table.

Pasted image 20221218210311.png

Pasted image 20221218210321.png

Pasted image 20221218210338.png

Pasted image 20221218210344.png

Pasted image 20221218210349.png

Pasted image 20221218210355.png
랄라라 :: IPC - 파이프(PIPE)의 개념
파이프의 특성 중 유의할 것은 파이프는 fork() 함수에 의해 복사 되지 않는다는 것임

fork() 함수에 의해서 프로세스가 생성되면, 자식 프로세스는 부모가 사용하던 변수를 복사하게 된다.

하지만 파이프의 경우 복사되는것이 아니라 File Descriptor 를 공유하게 된다.
즉 자식 프로세스와 부모 프로세스가 같은 파이프를 가리키게 된다.

파이프는 운영체제에서 임시로 생성되는 파일이고, 접근 가능한 방법은 File Descriptor(파일 디스크립터) 를 공유하는 방법만이 존재한다.

Pasted image 20221218213004.png
C언어 파이프를 이용한 IPC 함수 pipe() :: 바다야크

Duplicated File Descriptors (intraprocess)

https://man7.org/training/download/lusp_fileio_slides.pdf
A process may have muliple FDs referring to same OFD

  • Achieved using dup() or dup2()

Pasted image 20221218211659.png

Duplicated File Descriptors (between processes)

Two process may have FDs referring to same OFD

  • Can occur as a result of fork()
    Pasted image 20221218211808.png

Distinct Open File Table Entries Referring to Same File

Two processes may have FDs referring to distinct OFDs that refer to same inode

  • Two processes independently open()ed same file
    Pasted image 20221218211923.png

Pasted image 20221218212038.png

Pipe 종류

동기 및 겹치는 파이프 I/O - Win32 apps | Microsoft Learn

  1. Anonymouse pipe (일반/익명 파이프)
    일반적으로 파이프라고 하면 이름 없는 파이프(익명 파이프)를 의미한다.
    일반적으로 부모 프로세스와 자식 프로세스 간에 데이터를 전송하는 명명되지 않은 단방향 파이프다.
    익명 파이프는 항상 로컬이므로 네트워크를 통한 통신에는 사용할 수 없다.

핸들을 기반으로 통신한다.
핸들을 주고받을 수 있는 프로세스들 (흔히 부모-자식)처럼 특별한 관계가 있는 프로세스들끼리만이 통신할 수 있는 기법이다.
익명의 파이프는 부모와 자식 프로세스만이 파일 디스크립터를 공유하므로 다른 프로세스는 파이프를 사용하여 통신이 불가능하다.
일반 파이프는 프로세스들이 통신을 마치고 종료하면 일반 파이프는 없어지게 되고 부모-자식 관계여야만 하는 등 제약 조건들이 많다.

  1. Named Pipe (지명 파이프)
    파이프 서버와 하나 이상의 파이프 클라이언트 간의 통신을 위해 명명된 단방향 또는 이중 파이프다.
    Named Pipe는 파이프의 이름만 알고있다면 어떠한 관계에 있는 프로세스라도 통신이 가능하게 된다.
    명명된 파이프의 모든 인스턴스는 동일한 파이프 이름을 공유하지만 각 인스턴스에는 자체 버퍼와 핸들이 있으며 클라이언트/서버 통신을 위한 별도의 통로를 제공한다.
    인스턴스를 사용하면 여러 파이프 클라이언트에서 동일한 명명된 파이프를 동시에 사용할 수 있다.
    FIFO라 불리는 특수파일, 서로 관련없는 프로세스 간 통신에 사용된다.

더 자세히 얘기하자면 리눅스에서는 Named Pipe를 mkfifo를 통해서 임시 파일을 만드는 방식으로 사용하지만 윈도우는 아예 내부 객체를 만들어서 구현한다고 한다.

반대로 지명 파이프는 양방향 통신이 가능하며, 부모-자식 관계도 필요로 하지 않는다(동일한 기계 내에는 존재해야 한다.) 일단 지명 파이프가 구축되면 여러 프로세스들이 이를 사용하여 통신할 수 있다.

UNIX에서 지명 파이프는 FIFO라고 불린다. mkfifo() 시스템 콜을 이용하여 파이프를 생성하고 open(), read(), write(), close() 시스템 콜로 조작한다.

Windows에서는 CreateNamePipe()함수를 사용하여 생성되고 클라이언트는 ConnectNamedPipe()함수를 사용하여 지명 파이프에 연결할 수 있다. 지명 파이프를 통한 통신을 위해 ReadFile()과 WriteFile()함수를 사용할 수 있다.

여타 Api 들

Unix 시스템에는 fork(), exec(), 및 wait() 외에 많은 프로세스 관련 인터페이스가 있다.
예를 들면, kill() 시스템 콜은 프로세스에게 시그널(signal)을 보내는 데 사용된다.
시그널은 프로세스를 중단시키고 (block), 삭제하는 등의 작업에 사용된다.

시그널이라는 운영체제의 메터니즘은 외부 사건을 프로세스에게 전달하는 토대이다. 이 기반 구조는 시그널을 보내거나 전달받는 방법을 모두 포함한다.

유용한 명령어들이 많이 있다. 예를 들면, ps 명령어는 어떤 프로세스가 실행 중인지 알아보기 위하여 사용된다. ps의 유용한 플래그를 알기 위해서는 man 페이지를 읽어 보라.
top 역시 시스템에 존재하는 프로세스와 그 프로세스가 CPU 및 다른 자원들을 얼마나 사용하고 있는지를 보여 주기 때문에 매우 유용하다.
top 명령어를 여러 번 실행할 경우 자기 스스로가 가장 많은 자원을 사용하고 있다고 지적하는 것이 재밌는 부분이다.
매우 자기중심적인 명령어라고 할 수 있다. 마지막으로 다양한 CPU 측정기가 제공되어 시스템의 부하 정도를 알 수 있다.
예를 들어, Raging Menace software 사의 MenuMeter를 Macintosh의 toolbar에 실행시키면 어느 때건 CPU의 이용률을 점검할 수 있다. 일반적으로 현재 벌어지고 있는 일에 대해 더 많은 정보를 가질수록 더 유리하다.

여담 : RTFM — man 페이지를 읽자 (manual page)
특정 시스템 콜이나 라이브러리 콜을 언급할 때 매뉴얼 페이지 또는 간략하게 man 페이지를 읽으라는 말을 여러 번 들었을 것이다. man 페이지는 Unix 시스템에 존재하는 문서의 원형이다.
이른바 web이라고 불리는 것이 존재하기 전에 만들어졌다는 것을 기억하라.
man 페이지를 읽는 것은 시스템 프로그래머로서 성장하는 데 매우 중요한 단계이다.
이 페이지들 속에는 매우 유용한 정보가 셀 수 없이 숨어 있다. 특히, 유용한 페이지는 사용 중인 쉘 (예, tcsh 또는 bash)의 페이지와 프로그램에서 사용한 시스템 콜에 (반환 값이 무엇이고 어떤 에러 조건이 존재하는지 보기 위하여) 관한 페이지다.

Bash
$ man [보고 싶은 메뉴얼 이름]  

bro: just get to the point!

  1. 파이프는 생성될때 2개의 파일을 생성한다. (하나는 write, 하나는 read)
  2. 수도관처럼 한쪽방향으로만 진행된다. (write 파일에서 쓰면 read 파일에서 읽는다.)
  3. fork를 하게되면 자식 프로세스는 부모 프로세스의 파일 디스크립터를 상속받는다.
  4. 상속 때문에 두 프로세스 모두 write과 read파일을 open하게 되는데, parent는 반드시 write를 닫아줘야되고 (EOF 조건을 만족해야 read를 멈출 수 있으므로) child는 read를 닫는 것이 권장된다.

과제

  1. fork()를 호출하는 프로그램을 작성하라. fork()를 호출하기 전에 메인 프로세스는 변수에 접근하고 (예, x) 변수에 값을 지정하라 (예, 100). 자식 프로세스에서 그 변수의 값은 무엇인가?
    부모와 자식이 변수 x를 변경한 후에 변수는 어떻게 변했는가?

  2. open() 시스템 콜을 사용하여 파일을 여는 프로그램을 작성하고 새 프로세스를
    생성하기 위하여 fork()를 호출하라. 자식과 부모가 open()에 의해 반환된 파일 디스크립터에 접근할 수 있는가?
    부모와 자식 프로세스가 동시에 파일에 쓰기 작업을 할 수 있는가?

  3. fork()를 사용하는 다른 프로그램을 작성하라. 자식 프로세스는 “hello”를 출력하고 부모 프로세스는 “goodbye”를 출력해야 한다. 항상 자식 프로세스가 먼저 출력하게 하라. 부모가 wait()를 호출하지 않고 할 수 있는가?

  4. fork()를 호출하고 /bin/ls를 실행하기 위하여 exec() 계열의 함수를 호출
    하는 프로그램을 작성하라. exec()의 변형 execl(), execle(), execlp(),
    execv(), execvp(), execve() 모두를 사용할 수 있는지 시도해 보라. 기본적으로는 동일한 기능을 수행하는 시스템 콜에 여러 변형이 있는 이유를 생각해 보라.

  5. wait()를 사용하여 자식 프로세스가 종료되기를 기다리는 프로그램을 작성하라.
    wait()가 반환하는 것은 무엇인가? 자식 프로세스가 wait()를 호출하면 어떤
    결과가 발생하는가?

  6. 코딩이 싫지만 컴퓨터 과학자가 되기 원한다면 (a) 컴퓨터 과학 이론 분야에서 잘 하거나 (b) 지금까지 들어 왔던 “컴퓨터 과학”이라는 것에 대해 다시 생각해 보아야 한다.

  7. 위 문제에서 작성한 프로그램을 수정하여 wait() 대신에 waitpid()를 사용하라.
    어떤 경우에 waitpid()를 사용하는 것이 좋은가?

  8. 자식 프로세스를 생성하고 자식 프로세스가 표준 출력 (STDOUT_FILENO)을 닫는 프로그램을 작성하라. 자식이 설명자를 닫은 후에 아무거나 출력하기 위하여 printf()를 호출하면 무슨 일이 생기는가?

  9. 두 개의 자식 프로세스를 생성하고 pipe() 시스템 콜을 사용하여 한 자식의 표준 출력을 다른 자식의 입력으로 연결하는 프로그램을 작성하라.

강의 자료

추가 참고 자료

쉽게 배우는 운영체제 Chapter03
운영체제(Operating System) - 프로세스와 프로세스 관리
14. Process(3)


마지막 업데이트 : 2025년 4월 23일
작성일 : 2023년 4월 2일