Rocky 's Blog

쉘 내부 명령어와 외부 명령어

  • CS
  • 운영체제
2025. 11. 23.
게시글 썸네일

정의 및 특징


내부 명령어 (Built-in Commands)

쉘 프로그램 (/bin/bash/bin/zsh/bin/sh 등) 내부에 함수나 로직으로 이미 구현되어 있는 명령어다.

특징

  • 쉘 실행 파일의 코드 일부로 포함됨
  • 쉘이 시작될 때 메모리 (RAM) 에 함께 로드됨
  • 별도의 실행 파일로 존재하지 않음
  • 현재 쉘 프로세스 내부에서 직접 실행
  • 새로운 프로세스 생성 없음 (fork/exec 불필요)
  • PATH 환경 변수와 무관하게 항상 사용 가능
  • 예시

    cd          # 디렉터리 변경
    export      # 환경 변수 설정
    alias       # 별칭 설정
    history     # 명령어 이력 조회
    set         # 쉘 옵션 설정
    source (.)  # 스크립트 실행
    exit        # 쉘 종료
    echo        # 텍스트 출력 (bash 내장)
    pwd         # 현재 디렉터리 표시 (bash 내장)
    외부 명령어 (External Commands)

    별도의 실행 파일 (바이너리 또는 스크립트) 로 디스크에 저장되어 있는 명령어다.

    특징

  • 독립적인 실행 파일로 존재 (/bin/usr/bin/usr/local/bin 등)
  • 실행 시 디스크에서 읽어와야 함
  • 새로운 프로세스를 생성하여 실행 (fork + exec)
  • PATH 환경 변수에서 파일 위치를 검색
  • 별도의 메모리 공간에서 실행
  • 예시

    ls          # 디렉터리 내용 표시 (/bin/ls)
    cat         # 파일 내용 출력 (/bin/cat)
    grep        # 텍스트 검색 (/bin/grep)
    find        # 파일 검색 (/usr/bin/find)
    gcc         # C 컴파일러 (/usr/bin/gcc)
    python      # Python 인터프리터 (/usr/bin/python)
    PATH 란?

    PATH는 쉘이 외부 명령어(실행 파일)를 찾을 때 검색하는 디렉토리 목록을 담고 있는 환경 변수다.

    # PATH 내용 확인
    $ echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

    콜론(:)으로 여러 디렉터리 경로를 구분하고, 왼쪽부터 오른쪽 순서로 검색한다.

    PATH=/첫번째/디렉터리:/두번째/디렉터리:/세번째/디렉터리

    실행 방식의 차이


    내부 명령어 실행 과정
    Notion image

    코드 예시 (의사 코드)

    // bash 내부 구조 (단순화)
    int main() {
        while (1) {
            char *command = read_command();
    
            if (strcmp(command, "cd") == 0) {
                // 내부 명령어 - 직접 실행
                builtin_cd(args);  // 함수 호출
            } else if (strcmp(command, "export") == 0) {
                builtin_export(args);
            } else {
                // 외부 명령어 - fork/exec
                execute_external(command);
            }
        }
    }
    
    void builtin_cd(char *path) {
        if (chdir(path) != 0) {
            perror("cd failed");
        }
        // PWD 환경 변수 업데이트
        update_pwd();
    }
    외부 명령어 실행 과정
    Notion image

    코드 예시 (의사 코드)

    void execute_external(char *command, char **args) {
        // PATH에서 실행 파일 찾기
        char *path = find_in_path(command);  // "/bin/ls"
    
        pid_t pid = fork();  // 프로세스 복제
    
        if (pid == 0) {
            // 자식 프로세스
            execve(path, args, env);  // 프로그램 실행
            perror("exec failed");
            exit(1);
        } else {
            // 부모 프로세스 (쉘)
            int status;
            waitpid(pid, &status, 0);  // 자식 종료 대기
        }
    }

    왜 내부 명령어와 외부 명령어를 구분할까?


    쉘 상태 변경 필요성

    일부 명령어는 반드시 현재 쉘 프로세스의 상태를 변경해야 한다.

    cd (디렉터리 변경)

    내부 명령어로 구현해야 하는 이유

    # cd가 만약 외부 명령어라면?
    $ pwd
    /home/user
    
    $ cd /tmp  # 외부 프로세스에서 디렉터리 변경
    $ pwd
    /home/user  # 변경되지 않음!

    왜 그럴까?

  • 외부 명령어는 자식 프로세스에서 실행
  • 자식 프로세스가 chdir() 를 호출해도 자식의 디렉터리만 변경
  • 부모 (쉘) 의 현재 디렉터리는 그대로
  • 자식이 종료되면 변경 사항도 사라짐
  • 내부 명령어로 구현하면

    $ pwd
    /home/user
    
    $ cd /tmp  # 쉘 프로세스 자체에서 디렉터리 변경
    $ pwd
    /tmp  # 변경됨!
    export (환경 변수 설정)

    내부 명령어로 구현해야 하는 이유

    # export가 만약 외부 명령어라면?
    $ echo $MY_VAR
    (비어있음)
    
    $ export MY_VAR=hello  # 외부 프로세스에서 환경 변수 설정
    $ echo $MY_VAR
    (여전히 비어있음)

    왜 그럴까?

  • 환경 변수는 각 프로세스마다 독립적
  • 자식 프로세스가 환경 변수를 설정해도 부모에게 영향 없음
  • 자식이 종료되면 환경 변수도 사라짐
  • 내부 명령어로 구현하면

    $ echo $MY_VAR
    (비어있음)
    
    $ export MY_VAR=hello  # 쉘 자체의 환경 변수 설정
    $ echo $MY_VAR
    hello  # 설정됨!
    속도와 효율성

    외부 명령어 실행 비용

    1. fork() 시스템 콜: 프로세스 복제
    2. PATH 검색: 디스크에서 파일 찾기
    3. 파일 읽기: 디스크 I/O
    4. 메모리 로드: 프로그램을 RAM에 적재
    5. exec() 시스템 콜: 프로그램 실행
    6. exit()wait(): 종료 처리

    차이가 나는 이유

  • 내부 명령어: 메모리에서 직접 실행
  • 외부 명령어: fork, 디스크 읽기, exec 등의 오버헤드
  • RAM 에 상주시키는 이유


    디스크 vs RAM 속도 차이

    접근 속도 비교

    RAM 접근 시간:    ~100 나노초 (0.0001 밀리초)
    SSD 접근 시간:    ~0.1 밀리초
    HDD 접근 시간:    ~10 밀리초
    
    RAMSSD보다 1,000배 빠름
    RAMHDD보다 100,000배 빠름
    매번 디스크에서 읽는 경우의 문제

    외부 명령어 실행 과정 (디스크 읽기 포함)

    # 사용자가 ls를 100번 실행한다면?
    $ ls  # 1번: 디스크에서 /bin/ls 읽기 (10ms)
    $ ls  # 2번: 디스크에서 /bin/ls 읽기 (10ms)
    $ ls  # 3번: 디스크에서 /bin/ls 읽기 (10ms)
    ...
    $ ls  # 100번: 디스크에서 /bin/ls 읽기 (10ms)
    
    # 총 1000ms (1초) 소요

    문제점

    1. 매번 디스크 I/O 발생 → 느림
    2. 디스크 헤드 이동 시간 소요 (HDD 의 경우)
    3. 자주 사용하는 명령어일수록 비효율적
    내부 명령어로 RAM 에 상주시키는 경우

    쉘 시작 시 한 번만 로드

    부팅/쉘 시작 시:
    1. /bin/bash 실행 파일을 디스크에서 읽기
    2. bash 코드를 RAM에 로드
    3. 내부 명령어 함수들도 함께 RAM에 로드
    
    이후 명령어 실행 시:
    1. RAM에서 직접 함수 호출 (~0.0001ms)
    2. 디스크 접근 불필요

    장점

    # 사용자가 cd를 100번 실행한다면?
    $ cd /home  # 1번: RAM에서 직접 실행 (0.0001ms)
    $ cd /tmp   # 2번: RAM에서 직접 실행 (0.0001ms)
    $ cd /var   # 3번: RAM에서 직접 실행 (0.0001ms)
    ...
    
    # 총 0.01ms (거의 즉시)
    RAM 사용의 트레이드오프

    장점

    1. 속도: 디스크 접근 없이 즉시 실행
    2. 효율성: 프로세스 생성 오버헤드 없음
    3. 응답성: 사용자 경험 향상

    단점

    1. 메모리 사용: 쉘이 항상 RAM 공간 차지
    2. 유연성 제한: 쉘을 재시작하지 않고는 업데이트 불가

    균형점

    자주 사용하는 명령어 → 내부 명령어 (RAM 상주)
    - cd, export, alias, echo, pwd 등
    - 총 메모리 사용: 매우 작음 (수백 KB)
    
    가끔 사용하는 명령어 → 외부 명령어 (디스크 저장)
    - find, gcc, ffmpeg 등
    - 필요할 때만 로드 → RAM 절약
    페이지 캐시를 통한 최적화

    실제로 외부 명령어도 자주 사용하면 빨라진다.

    OS 의 페이지 캐시

    첫 실행:
    $ ls  # 디스크에서 읽기 (10ms)
          # OS/bin/ls를 페이지 캐시에 저장
    
    이후 실행:
    $ ls  # 페이지 캐시에서 읽기 (0.1ms)
    $ ls  # 페이지 캐시에서 읽기 (0.1ms)

    메커니즘

    1. 외부 명령어를 처음 실행할 때 디스크에서 읽음
    2. OS 가 자동으로 RAM 의 페이지 캐시에 저장
    3. 이후 실행 시 캐시에서 읽어옴 (디스크 접근 불필요)
    4. 메모리가 부족하면 자동으로 캐시 제거

    장점

  • 자주 사용하는 외부 명령어도 빠르게 실행
  • 명시적 관리 불필요
  • 메모리 효율적 (자동 관리)