Post

[운영체제 Deep Dive #1] ls를 치면 무슨 일이 일어날까? - Process 생성의 모든 것

운영체제 Deep Dive #1 – 프로세스와 스케줄링: 프로세스 생성, 상태 전이, 스케줄러 동작 방식 분석.

[운영체제 Deep Dive #1] ls를 치면 무슨 일이 일어날까? - Process 생성의 모든 것

들어가며

터미널에서 ls를 입력하면 디렉토리 목록이 출력됩니다.

너무 당연한 동작처럼 보이지만, 사실 이 한 줄의 명령 뒤에서는 운영체제의 핵심 기능들이 동시에 동작합니다.

예를 들면 다음과 같습니다.

  • 새로운 프로세스 생성
  • 프로그램 메모리 로드
  • CPU 스케줄링
  • 파일 시스템 접근

그러나 실제로 Shell이 명령을 실행하는 코드는 생각보다 단순합니다.

1
2
3
4
5
6
pid_t pid = fork();          // 1. 프로세스 복제
if (pid == 0) {              // 2. 자식 프로세스
    execvp("ls", args);      // 3. ls 프로그램 실행
    exit(1);                 // exec 실패 시 종료
}
wait(NULL);                  // 4. 부모는 자식 종료 대기

단 5줄 정도의 코드입니다.

(이 코드는 제가 직접 구현한 nsh (Unix minishell)의 일부입니다.)

하지만 이 코드 안에는 다음과 같은 운영체제의 핵심 개념이 들어 있습니다.

  • fork() — 프로세스 생성
  • exec() — 프로그램 실행
  • wait() — 프로세스 관리

이번 글에서는 다음 질문을 중심으로 살펴보겠습니다.

“터미널에서 ls를 입력하고 Enter를 누르면 실제로 무슨 일이 일어날까?”


프로그램 vs 프로세스

ls가 실행되는 과정을 이해하려면 먼저 프로그램(Program)프로세스(Process)의 차이를 알아야 합니다.

이 두 개념은 운영체제에서 가장 기본이 되는 개념입니다.


프로그램 (Program)

프로그램은 디스크에 저장된 실행 파일입니다.

예를 들어 우리가 사용하는 ls 명령어도 사실은 하나의 파일입니다.

1
2
$ which ls
/bin/ls
1
2
$ ls -l /bin/ls
-rwxr-xr-x 1 root root 138K /bin/ls

이 파일 안에는 다음과 같은 것들이 들어 있습니다.

  • 프로그램 코드
  • 전역 데이터
  • 실행에 필요한 메타 정보 (ELF 헤더 등)

하지만 중요한 점은 다음입니다.

프로그램은 아직 실행되지 않은 상태입니다.

즉 단순히 디스크에 존재하는 파일일 뿐입니다.


프로세스 (Process)

프로세스는 실행 중인 프로그램의 인스턴스입니다.

즉 프로그램이 실제로 실행되면 운영체제는 다음과 같은 자원을 할당합니다.

  • 메모리
  • CPU 시간
  • 파일 디스크립터
  • 프로세스 ID (PID)

예를 들어 다음 명령을 실행해 보면

1
$ ps aux | grep ls

시스템에서 실행 중인 프로세스를 확인할 수 있습니다.

각 프로세스는 고유한 PID(Process ID) 를 가지며 운영체제가 이를 통해 관리합니다.


비유로 이해하기

이 관계는 다음 비유로 이해하면 쉽습니다.

1
2
프로그램 = 요리 레시피
프로세스 = 실제 요리 과정

레시피 하나로 여러 요리를 만들 수 있듯이

1
하나의 프로그램 → 여러 프로세스

가 동시에 실행될 수 있습니다.

예를 들어 다음 명령을 실행하면

1
2
3
vim file1.txt &
vim file2.txt &
vim file3.txt &

같은 vim 프로그램이지만 서로 다른 세 개의 프로세스가 생성됩니다.

각 프로세스는

  • 서로 다른 PID
  • 독립적인 메모리 공간
  • 독립적인 실행 흐름

을 가지게 됩니다.


ls를 입력하면 전체적으로 무슨 일이 일어날까?

이제 다시 처음 질문으로 돌아가 보겠습니다.

우리가 터미널에서 다음 명령어를 입력합니다.

1
$ ls

겉보기에는 단순히 파일 목록이 출력되는 것처럼 보이지만
실제로 내부에서는 꽤 많은 일이 일어납니다.

전체 흐름을 먼저 간단하게 정리해 보겠습니다.

1
2
3
4
5
6
1. 사용자가 터미널에서 ls 입력
2. Shell이 명령어를 읽음
3. Shell이 새로운 프로세스를 생성 (fork)
4. 자식 프로세스가 ls 프로그램으로 교체 (exec)
5. ls 프로그램 실행
6. 결과를 터미널에 출력

조금 더 자세히 보면 다음과 같은 구조입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
User
 
   ls 입력
 
Terminal
 
 
Shell (bash, zsh, nsh )
 
   fork()
 
Child Process
 
   exec("/bin/ls")
 
ls Program 실행
 
 
파일 목록 출력

여기서 중요한 포인트는 다음입니다.

ls를 실행하는 주체는 Shell입니다.

우리가 사용하는 터미널은 단순히 입출력 인터페이스일 뿐이고
실제로 명령어를 해석하고 실행하는 프로그램은 Shell입니다.

대표적인 Shell에는 다음과 같은 것들이 있습니다.

  • bash
  • zsh
  • fish

그리고 직접 Shell을 구현해 볼 수도 있습니다.

예를 들어 제가 작성한 미니 쉘 프로젝트인 nsh도 같은 원리로 동작합니다.

https://github.com/nahyun27/linux-minishell

Shell의 핵심 역할은 다음과 같습니다.

  • 사용자의 입력을 읽는다
  • 명령어를 파싱한다
  • 새로운 프로세스를 생성한다
  • 프로그램을 실행한다

이 과정에서 가장 중요한 시스템 콜이 바로 두 가지입니다.

1
2
fork()
exec()

이 두 가지가 바로 Linux 프로세스 생성의 핵심 메커니즘입니다.


fork() : 프로세스는 어떻게 생성될까?

이제 Shell이 새로운 프로세스를 만드는 방법을 살펴보겠습니다.

Linux에서 새로운 프로세스를 만들 때 사용하는 가장 중요한 시스템 콜은 바로 fork()입니다.

fork()현재 프로세스를 복제하여 새로운 프로세스를 만드는 시스템 콜입니다.

즉, 완전히 새로운 프로그램을 실행하는 것이 아니라
현재 실행 중인 프로세스를 그대로 복사한 프로세스가 하나 더 만들어집니다.


fork()의 동작 방식

fork()가 호출되면 운영체제는 다음과 같은 작업을 수행합니다.

  1. 현재 프로세스를 복제합니다.
  2. 새로운 프로세스에 PID를 할당합니다.
  3. 부모 프로세스와 자식 프로세스가 동시에 실행됩니다.

이때 기존 프로세스를 부모 프로세스 (Parent Process),
새롭게 생성된 프로세스를 자식 프로세스 (Child Process) 라고 합니다.

구조를 그림으로 표현하면 다음과 같습니다.

1
2
3
4
5
6
7
8
Parent Process
      
       fork()
      
 ┌─────────────┐
  Parent      
  Child       
 └─────────────┘

중요한 점은 두 프로세스가 동일한 코드에서 실행을 계속한다는 것입니다.

fork() 이후의 코드가 부모와 자식 모두에서 실행됩니다.


fork()의 반환값

부모와 자식 프로세스는 같은 코드를 실행하지만
fork()반환값(return value) 을 통해 서로를 구분할 수 있습니다.

1
pid_t fork(void);

반환값의 의미는 다음과 같습니다.

반환값의미
음수프로세스 생성 실패
0자식 프로세스
양수부모 프로세스 (자식의 PID)

fork() 예제 코드

다음은 간단한 fork() 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child process\n");
    } else {
        printf("Parent process\n");
    }

    return 0;
}

이 코드를 실행하면 다음과 같은 결과가 나올 수 있습니다.

1
2
Parent process
Child process

또는

1
2
Child process
Parent process

출력 순서는 매번 달라질 수 있습니다.

그 이유는 부모와 자식 프로세스가 동시에 실행되기 때문입니다.

운영체제의 스케줄러가 어떤 프로세스를 먼저 실행하느냐에 따라
출력 순서가 달라질 수 있습니다.


중요한 특징

정리하면 fork()의 핵심 특징은 다음과 같습니다.

  • 현재 프로세스를 복제하여 새로운 프로세스를 만든다
  • 부모와 자식 프로세스가 동시에 실행된다
  • 두 프로세스는 서로 다른 PID를 가진다
  • fork() 이후의 코드는 두 번 실행된다

그런데 한 가지 의문이 생깁니다

fork()현재 프로세스를 복제한다고 했습니다.

그렇다면 운영체제는
프로세스의 어떤 정보를 기반으로 이 복제를 수행할까요?

프로세스에는 다음과 같은 정보들이 필요합니다.

  • PID
  • CPU 레지스터 상태
  • 프로그램 카운터
  • 메모리 정보
  • 열린 파일 정보

이러한 정보들을 운영체제가 관리하기 위해 사용하는 자료구조가 바로
PCB(Process Control Block) 입니다.


PCB (Process Control Block) : 운영체제는 프로세스를 어떻게 관리할까?

앞에서 fork() 시스템 콜을 통해 새로운 프로세스가 생성된다는 것을 살펴보았습니다.

그렇다면 한 가지 질문이 생깁니다.

운영체제는 수많은 프로세스를 어떻게 관리할까요?

Linux 시스템에서는 동시에 수백 개 이상의 프로세스가 실행될 수 있습니다.

  • Shell
  • 브라우저
  • 에디터
  • 백그라운드 데몬
  • 시스템 서비스

이 모든 프로세스의 상태와 정보를 운영체제가 관리해야 합니다.

이를 위해 운영체제는 PCB(Process Control Block) 이라는 자료구조를 사용합니다.


PCB란 무엇인가?

PCB는 프로세스를 관리하기 위해 운영체제가 유지하는 정보 구조체입니다.

쉽게 말하면 다음과 같습니다.

1
PCB = 프로세스의 모든 정보를 담고 있는 관리 카드

운영체제는 각 프로세스마다 하나의 PCB를 생성하여 관리합니다.

즉 시스템에는 다음과 같은 구조가 존재합니다.

1
2
3
4
Process 1  →  PCB
Process 2  →  PCB
Process 3  →  PCB
...

운영체제는 이 PCB들을 통해 프로세스의 상태와 실행 정보를 추적합니다.


PCB에 저장되는 정보

PCB에는 프로세스를 관리하기 위한 다양한 정보가 저장됩니다.

대표적인 항목들은 다음과 같습니다.

1. 프로세스 ID (PID)

각 프로세스를 구별하기 위한 고유한 식별자입니다.

예를 들어 다음 명령을 실행하면

1
ps

현재 실행 중인 프로세스와 PID를 확인할 수 있습니다.


2. 프로세스 상태 (Process State)

프로세스의 현재 상태를 나타냅니다.

대표적인 상태는 다음과 같습니다.

  • Running : 현재 CPU에서 실행 중
  • Ready : 실행 준비 상태
  • Waiting : 이벤트를 기다리는 상태
  • Terminated : 실행 종료

운영체제의 스케줄러는 이 상태를 기반으로 어떤 프로세스를 실행할지 결정합니다.


3. 프로그램 카운터 (Program Counter)

프로세스가 다음에 실행할 명령어의 주소를 저장합니다.

CPU가 컨텍스트 스위칭을 할 때
이 값이 PCB에 저장되어 있어야 나중에 정확한 위치에서 다시 실행할 수 있습니다.


4. CPU 레지스터

프로세스 실행에 필요한 레지스터 값들이 저장됩니다.

예를 들면

  • Stack Pointer
  • Base Pointer
  • General Registers

이 값들은 컨텍스트 스위칭(Context Switching) 시에 매우 중요합니다.


5. 메모리 관리 정보

프로세스가 사용하는 메모리 영역에 대한 정보입니다.

예를 들면

  • Code Segment
  • Data Segment
  • Heap
  • Stack

또는

  • Page Table
  • Memory limits

같은 정보가 저장됩니다.


6. 파일 디스크립터 정보

프로세스가 열어 둔 파일 정보입니다.

예를 들어

  • 표준 입력 (stdin)
  • 표준 출력 (stdout)
  • 표준 에러 (stderr)

도 모두 파일 디스크립터로 관리됩니다.


fork()와 PCB의 관계

이제 다시 fork()로 돌아가 보겠습니다.

fork()가 호출되면 운영체제는 다음과 같은 작업을 수행합니다.

  1. 새로운 PCB를 생성합니다.
  2. 부모 프로세스의 PCB 내용을 복사합니다.
  3. 새로운 PID를 할당합니다.
  4. 자식 프로세스를 실행 상태로 만듭니다.

구조를 단순화하면 다음과 같습니다.

1
2
3
4
5
Parent PCB
     │
     │ fork()
     ▼
Parent PCB      Child PCB

fork()는 단순히 코드를 복제하는 것이 아니라
PCB와 프로세스 실행 환경 전체를 복제
하는 과정입니다.


하지만 아직 끝이 아닙니다

여기까지 보면 fork() 이후에는
Shell의 복사본이 하나 더 실행되는 것처럼 보입니다.

하지만 우리가 ls를 실행할 때 실제로 실행되는 것은
Shell이 아니라 /bin/ls 프로그램입니다.

그렇다면 다음 질문이 등장합니다.

fork()로 생성된 자식 프로세스는 어떻게 ls 프로그램을 실행할까?

이 과정을 담당하는 시스템 콜이 바로 exec() 입니다.


exec() : 프로세스는 어떻게 새로운 프로그램을 실행할까?

앞에서 fork()를 통해 새로운 프로세스가 생성되는 과정을 살펴보았습니다.

하지만 fork()만으로는 우리가 원하는 프로그램을 실행할 수 없습니다.

왜냐하면 fork()현재 프로세스를 복제할 뿐이기 때문입니다.

즉 Shell이 fork()를 호출하면 생성되는 자식 프로세스는
여전히 Shell 프로그램의 복사본입니다.

하지만 우리가 ls를 입력했을 때 실행되어야 하는 프로그램은
Shell이 아니라 /bin/ls 입니다.

그렇다면 Shell은 어떻게 자식 프로세스를 ls 프로그램으로 실행할까요?

이때 사용되는 시스템 콜이 바로 exec() 입니다.


exec()의 역할

exec()현재 프로세스의 프로그램을 새로운 프로그램으로 교체하는 시스템 콜입니다.

즉 새로운 프로세스를 만드는 것이 아니라
현재 프로세스의 메모리 공간을 완전히 다른 프로그램으로 덮어씁니다.

정리하면 다음과 같습니다.

1
2
fork()  새로운 프로세스 생성
exec()  프로세스를 다른 프로그램으로 교체

exec() 동작 과정

Shell이 ls 명령을 실행할 때의 흐름은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
Shell
 │
 │ fork()
 ▼
Child Process (Shell 복사본)
 │
 │ exec("/bin/ls")ls 프로그램 실행

즉 전체 과정은 다음과 같습니다.

1
2
3
4
5
1. Shell이 fork() 호출
2. 자식 프로세스 생성
3. 자식 프로세스가 exec() 호출
4. 자식 프로세스가 ls 프로그램으로 교체
5. ls 실행

exec() 예제 코드

다음은 fork()exec()를 함께 사용하는 간단한 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {
        execl("/bin/ls", "ls", NULL);
    }

    return 0;
}

이 프로그램을 실행하면

1
$ ./a.out

다음과 같이 현재 디렉토리의 파일 목록이 출력됩니다.

1
2
3
file1.txt
file2.txt
main.c

즉 프로그램이 자식 프로세스를 생성한 뒤 ls 프로그램을 실행하게 됩니다.


exec()가 호출되면 어떤 일이 일어날까?

exec()가 호출되면 현재 프로세스의 메모리는 다음과 같이 변경됩니다.

1
2
3
4
5
6
Before exec()

Process
 ├─ Code : shell
 ├─ Heap
 ├─ Stack
1
2
3
4
5
6
After exec()

Process
 ├─ Code : ls
 ├─ Heap
 ├─ Stack

프로세스 자체는 유지되지만 실행 중인 프로그램이 완전히 바뀌게 됩니다.

여기서 중요한 점은 다음과 같습니다.

  • PID는 그대로 유지됩니다
  • 프로세스 자체는 동일합니다
  • 실행 중인 프로그램만 교체됩니다

fork + exec 패턴

Linux에서 새로운 프로그램을 실행할 때는 거의 항상 다음 패턴이 사용됩니다.

1
2
3
fork()
exec()
wait()

흐름을 정리하면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Shell
 │
 │ fork()
 ▼
Child
 │
 │ exec()
 ▼
Program 실행
 │
 │
Parent
 │
 │ wait()
 ▼
Child 종료 대기

이 구조가 바로 Linux 프로세스 실행의 기본 패턴입니다.


이제 모든 퍼즐이 맞춰집니다

지금까지 살펴본 내용을 정리해 보면
우리가 터미널에서 ls를 입력했을 때 내부에서는 다음과 같은 일이 일어납니다.

1
2
3
4
1. Shell이 사용자 입력을 읽는다
2. Shell이 fork()로 자식 프로세스를 만든다
3. 자식 프로세스가 exec()로 ls 프로그램을 실행한다
4. ls가 디렉토리 내용을 출력한다

하지만 아직 한 가지 과정이 남아 있습니다.

Shell은 자식 프로세스가 끝날 때까지 기다려야 합니다.

이 역할을 하는 시스템 콜이 바로 wait() 입니다.


wait() : 부모 프로세스는 왜 자식 프로세스를 기다릴까?

앞에서 fork()exec()를 통해 새로운 프로그램이 실행되는 과정을 살펴보았습니다.

하지만 아직 한 가지 중요한 과정이 남아 있습니다.

바로 부모 프로세스가 자식 프로세스의 종료를 기다리는 과정입니다.

Linux에서 이 역할을 하는 시스템 콜이 바로 wait()입니다.


wait()의 역할

wait()부모 프로세스가 자식 프로세스가 종료될 때까지 기다리도록 하는 시스템 콜입니다.

Shell이 ls를 실행하는 과정을 다시 보면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
Shell
 │
 │ fork()
 ▼
Child
 │
 │ exec("/bin/ls")ls 실행

여기서 Shell이 아무것도 하지 않고 계속 실행된다면 어떤 일이 발생할까요?

예를 들어 다음과 같은 상황을 생각해 보겠습니다.

1
2
$ ls
$ echo hello

만약 Shell이 자식 프로세스를 기다리지 않는다면
ls가 끝나기도 전에 다음 명령을 입력받게 될 수 있습니다.

이렇게 되면 출력이 섞이거나 예상하지 못한 동작이 발생할 수 있습니다.

그래서 Shell은 wait()를 호출하여 자식 프로세스가 종료될 때까지 기다립니다.


wait() 동작 방식

wait()는 다음과 같이 동작합니다.

1
pid_t wait(int *status);

부모 프로세스가 wait()를 호출하면

  1. 자식 프로세스가 종료될 때까지 대기합니다.
  2. 자식 프로세스가 종료되면
  3. 종료된 자식 프로세스의 PID를 반환합니다.

또한 자식 프로세스의 종료 상태(exit status) 도 확인할 수 있습니다.


fork + exec + wait 패턴

이제 우리가 살펴본 시스템 콜들을 하나로 묶어 보겠습니다.

Linux에서 프로그램을 실행할 때 일반적으로 다음 패턴이 사용됩니다.

1
2
3
fork()
exec()
wait()

이를 코드 형태로 표현하면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {
        execl("/bin/ls", "ls", NULL);
    } else {
        wait(NULL);
    }

    return 0;
}

이 프로그램의 동작 흐름은 다음과 같습니다.

1
2
3
4
5
6
1. 부모 프로세스가 fork() 호출
2. 자식 프로세스 생성
3. 자식 프로세스가 exec()로 ls 실행
4. 부모 프로세스는 wait()로 자식 종료 대기
5. ls 실행 종료
6. 부모 프로세스 다시 실행

이 구조가 바로 Linux에서 프로그램을 실행하는 기본적인 방식입니다.


Zombie Process

여기서 wait()가 중요한 또 다른 이유가 있습니다.

부모 프로세스가 wait()를 호출하지 않으면
종료된 자식 프로세스는 Zombie Process가 될 수 있습니다.

좀비 프로세스는 실행은 끝났지만 PCB가 시스템에 남아있는 상태입니다.

1
2
3
4
5
6
7
Parent (wait 안함)
        │
        ▼
Child 종료
        │
        ▼
Zombie Process

이 상태에서는 시스템 자원이 완전히 정리되지 않습니다.

그래서 부모 프로세스는 반드시 wait()를 호출하여
자식 프로세스를 수거(reap) 해야 합니다.


Orphan Process

또 다른 상황도 있습니다.

부모 프로세스가 먼저 종료되고
자식 프로세스가 계속 실행되는 경우입니다.

이를 Orphan Process라고 합니다.

Linux에서는 이런 경우 다음과 같이 처리됩니다.

1
2
3
4
5
6
7
부모 프로세스 종료
        ↓
자식 프로세스의 PPID 변경
        ↓
PID 1 프로세스(init 또는 systemd)가 부모가 됨
        ↓
init이 wait() 호출

즉 고아 프로세스는 init 프로세스가 자동으로 관리합니다.


정리

지금까지 우리는 다음 세 가지 핵심 시스템 콜을 살펴보았습니다.

  • fork() : 새로운 프로세스 생성
  • exec() : 프로세스를 새로운 프로그램으로 교체
  • wait() : 부모 프로세스가 자식 프로세스 종료 대기

이 세 가지 시스템 콜이 함께 동작하면서
우리가 터미널에서 입력한 명령어가 실제 프로그램으로 실행됩니다.


전체 실행 흐름

사용자가 ls 명령을 입력했을 때의 흐름을 간단히 정리하면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
User
 │
 │ ls 입력
 ▼
Shell
 │
 │ fork()
 ▼
Child Process
 │
 │ exec("/bin/ls")ls 프로그램 실행
 │
 │ 디렉토리 읽기
 │ 파일 목록 출력
 ▼
프로세스 종료
 │
 │ wait()
 ▼
Shell 복귀

즉,

1
2
3
4
5
6
7
사용자 입력
 Shell 명령어 해석
 fork() 자식 프로세스 생성
 exec() 프로그램 실행
 프로그램 작업 수행
 wait() 종료 회수
 Shell 복귀

이 과정이 우리가 명령어를 입력하는 순간 매우 빠르게 실행됩니다.


실제 Shell 코드 예시

제가 만든 미니 Shell 프로젝트 nsh에서도 동일한 구조를 사용합니다.

프로젝트 주소
https://github.com/nahyun27/linux-minishell

핵심 명령 실행 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
pid_t pid = fork();

if (pid < 0) {
    perror("fork failed");
}
else if (pid == 0) {
    execvp(args[0], args);
    exit(EXIT_FAILURE);
}
else {
    waitpid(pid, NULL, 0);
}

이 코드가 바로 Unix Shell의 기본 실행 패턴입니다.

1
2
3
fork()
  exec()
  wait()

즉,

  1. fork()로 자식 프로세스를 생성하고
  2. 자식 프로세스가 exec()로 프로그램을 실행하고
  3. 부모 프로세스가 wait()로 종료 상태를 회수합니다.

실제로 확인해보기: strace

이 과정은 strace라는 도구로 직접 확인할 수 있습니다.

1
strace ls

출력 일부를 보면 다음과 같습니다.

1
2
3
4
5
execve("/usr/bin/ls", ["ls"], ...) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_DIRECTORY) = 3
getdents64(3, ...)
write(1, "file1.txt\nfile2.txt\n", 20)
exit_group(0)

이 시스템 콜들을 보면 ls 프로그램이 내부적으로 어떤 작업을 하는지 알 수 있습니다.

1
2
3
4
디렉토리 열기
→ 파일 목록 읽기
→ stdout 출력
→ 프로세스 종료

즉 우리가 단순히

1
$ ls

라고 입력했을 뿐이지만 내부에서는

  • fork()
  • exec()
  • open
  • getdents
  • write
  • exit

같은 여러 시스템 콜이 순차적으로 실행되고 있는 것입니다.


다음으로 떠오르는 질문

지금까지는 단일 명령어만 살펴봤습니다.

하지만 실제로 우리는 다음과 같은 명령을 훨씬 자주 사용합니다.

1
ls | grep .c

이 명령은 다음과 같은 의미입니다.

  • ls의 출력 결과를
  • grep .c 프로그램의 입력으로 전달

즉 두 개의 프로그램이 파이프로 연결되어 동시에 동작합니다.

그렇다면 여기서 자연스럽게 다음 질문이 생깁니다.

ls | grep .c는 내부적으로 어떻게 동작할까?

단순한 fork() → exec() 패턴만으로는
이 동작을 설명하기에 무언가 하나가 더 필요합니다.

그 정체가 바로 Pipe(파이프) 입니다.


다음 글에서 다룰 내용

다음 글에서는 다음과 같은 내용을 살펴보겠습니다.

  • Pipe란 무엇인가
  • ls | grep .c 명령의 내부 동작
  • pipe() 시스템 콜
  • dup2()와 표준 입출력 리다이렉션
  • Shell이 파이프라인을 만드는 방법

즉 다음 글에서는 Linux Shell의 파이프라인 구조를 실제 코드와 함께 분석해보겠습니다.

더 알아보기

추천 자료

실습:


태그: #운영체제 #Process #fork #exec #Linux #Shell #nsh

This post is licensed under CC BY 4.0 by the author.