Rocky 's Blog

터미널 명령어 실행 과정

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

전체 흐름 요약


[유저 모드]
1. 사용자가 "ls -l" 입력
2. 쉘이 명령어 파싱
3. 외부 명령어임을 확인

[시스템 콜 → 커널 모드]
4. fork() 호출
   - 커널: 자식 프로세스 생성
   - 메모리 할당, PCB 생성

[유저 모드 복귀]
5. 부모와 자식 모두 fork() 이후 실행

[자식 프로세스 - 시스템 콜 → 커널 모드]
6. exec() 호출
   - 커널: 메모리를 ls 프로그램으로 교체
   - 프로그램 로드 및 시작

[유저 모드 복귀]
7. ls 프로그램 실행

[시스템 콜 → 커널 모드 (반복)]
8. open(), read(), stat(), write() 등
   - 커널: 파일 시스템 접근
   - 데이터 읽기 및 출력

[시스템 콜 → 커널 모드]
9. exit() 호출
   - 커널: 자원 해제, 종료 상태 보관

[부모 프로세스 - 시스템 콜에서 복귀]
10. wait() 완료
    - 자식 종료 감지
    - 좀비 프로세스 정리

[유저 모드]
11. 쉘이 새로운 프롬프트 표시
12. 다음 명령어 대기

1. 사용자가 터미널(쉘)에 명령어를 입력


사용자 명령어 입력
username@hostname:~$ ls -l /home

이 시점까지는 모든 작업이 유저 모드에서 일어난다.

유저 모드의 특징

  • 제한된 권한으로 실행
  • 메모리, 하드웨어에 직접 접근 불가
  • 자신의 메모리 공간만 접근 가능
  • 시스템 자원 사용 시 반드시 커널에 요청해야 함
  • 쉘의 명령어 처리 준비
    1. 사용자가 Enter 키를 누름
    2. 쉘이 입력된 문자열을 읽음
    3. 명령어를 파싱(분석):
    4. 명령어 타입 확인

    2. 명령어 타입 확인


    내장 명령어 (Built-in Commands)

    쉘 자체에 구현되어 있는 명령어로, 쉘이 직접 처리한다.

    예시

  • cd: 디렉터리 변경
  • exit: 쉘 종료
  • export: 환경 변수 설정
  • alias: 별칭 설정
  • source (또는 .): 스크립트 실행
  • 처리 방식

    // 의사 코드
    if (is_builtin_command(command)) {
        execute_builtin(command, args);  // 쉘이 직접 실행
        return;  // fork/exec 불필요
    }
    외부 명령어 (External Commands)

    별도의 실행 파일로 존재하는 프로그램으로, 새로운 프로세스를 생성해야 한다.

    예시

  • ls: 디렉터리 내용 표시 (/bin/ls)
  • cat: 파일 내용 출력 (/bin/cat)
  • grep: 텍스트 검색 (/bin/grep)
  • 실행 파일 위치 찾기

    # PATH 환경 변수에서 검색
    PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
    
    # 각 디렉터리에서 "ls" 찾기
    /usr/local/bin/ls  (없음)
    /usr/bin/ls        (없음)
    /bin/ls            (찾음!)

    3. 외부 명령어는 새로운 프로세스를 생성하고 실행해야 하므로, 여러 시스템 콜이 필요하다.


    프로세스 생성 및 실행 과정
    // 쉘의 명령어 실행 의사 코드
    pid_t pid = fork();  // 시스템 콜 1: 프로세스 생성
    
    if (pid < 0) {
        // fork 실패
        perror("fork failed");
    } else if (pid == 0) {
        // 자식 프로세스
        execve("/bin/ls", args, env);  // 시스템 콜 2: 프로그램 실행
        perror("exec failed");  // exec 실패 시에만 도달
        exit(1);
    } else {
        // 부모 프로세스 (쉘)
        int status;
        waitpid(pid, &status, 0);  // 시스템 콜 3: 자식 종료 대기
        // 자식이 종료되면 여기서 계속 진행
    }
    시스템 콜이란?

    유저 모드에서 커널 모드로 진입하는 인터페이스다. 사용자 프로그램이 운영체제의 서비스를 요청하는 방법이다.

    모드 전환 과정

    1. 사용자 프로그램이 시스템 콜 호출
    2. 소프트웨어 인터럽트 발생 (trap, syscall 명령)
    3. CPU가 유저 모드 → 커널 모드 전환
    4. 커널이 요청 처리
    5. 결과 반환
    6. CPU가 커널 모드 → 유저 모드 복귀

    4. fork() 시스템 콜로 현재 프로세스를 복제하여 새로운 프로세스를 생성하고, 부모는 wait()를 추가 호출한다.


    fork()의 역할

    현재 프로세스(부모)를 복제하여 새로운 프로세스(자식)를 생성한다.

    커널에서의 작업

    1) 새로운 프로세스 생성

  • 새로운 PID(Process ID) 할당
  • 프로세스 제어 블록(PCB) 생성
  • 프로세스 테이블에 등록
  • 2) 메모리 할당

  • 부모 프로세스의 메모리 공간 복사 (Copy-on-Write 방식으로 최적화)
  • 코드(Code), 데이터(Data), 힙(Heap), 스택(Stack) 복제
  • 독립적인 메모리 공간 제공
  • 3) 자원 복사

  • 열려있는 파일 디스크립터 복사
  • 환경 변수 복사
  • 현재 작업 디렉터리 복사
  • 시그널 핸들러 복사
  • 4) 부모-자식 관계 설정

  • 자식 프로세스의 PPID(Parent PID)를 부모의 PID로 설정
  • 프로세스 계층 구조 형성
  • fork() 반환값
    pid_t pid = fork();
    
    // 부모 프로세스에서: pid > 0 (자식의 PID)
    // 자식 프로세스에서: pid == 0
    // 실패 시: pid < 0

    fork() 호출 후, 부모와 자식 모두 fork() 다음 줄부터 실행된다.

    wait() 시스템 콜 - 부모의 대기

    부모 프로세스(쉘)는 자식이 종료되기를 기다린다.

    // 부모 프로세스 (쉘)
    int status;
    pid_t result = waitpid(pid, &status, 0);  // 시스템 콜
    
    if (WIFEXITED(status)) {
        int exit_code = WEXITSTATUS(status);
        // exit_code == 0: 정상 종료
        // exit_code != 0: 오류 발생
    }

    wait()의 역할

    1. 자식 프로세스가 종료될 때까지 부모를 블록(대기)
    2. 자식의 종료 상태 수집
    3. 좀비 프로세스 정리 (완전히 제거)
    4. 자식이 사용하던 PID 재사용 가능하게 함
    쉘로 제어권 복귀
    # 자식 프로세스 종료 후
    username@hostname:~$ _  # 커서가 다시 프롬프트에 표시

    쉘의 동작

    1. wait() 시스템 콜에서 복귀
    2. 자식의 종료 상태 확인
    3. 필요시 종료 상태 출력
    4. 새로운 프롬프트 표시
    5. 다음 명령어 입력 대기 (유저 모드)

    5. exec() 시스템 콜로 현재 프로세스의 메모리 공간을 새로운 프로그램으로 완전히 교체한다.


    커널에서의 작업

    1) 실행 파일 검증

  • /bin/ls 파일이 존재하는지 확인
  • 실행 권한이 있는지 확인
  • 실행 가능한 형식(ELF 등)인지 확인
  • 2) 메모리 공간 교체

    기존 프로세스 메모리:
    ┌─────────────┐
    │ bash 코드    │  ← 쉘의 코드
    ├─────────────┤
    │ bash 데이터   │  ← 쉘의 데이터
    ├─────────────┤
    │ 힙           │
    ├─────────────┤
    │ 스택         │
    └─────────────┘
    
    exec() 실행 후:
    ┌─────────────┐
    │ ls 코드      │  ← ls 프로그램의 코드
    ├─────────────┤
    │ ls 데이터     │  ← ls 프로그램의 데이터
    ├─────────────┤
    │ 힙           │  ← 새로 초기화
    ├─────────────┤
    │ 스택         │  ← 새로 초기화
    └─────────────┘

    3) 프로그램 로드

  • 실행 파일의 코드 섹션을 메모리에 로드
  • 데이터 섹션을 메모리에 로드
  • BSS 섹션(초기화되지 않은 전역 변수) 초기화
  • 스택과 힙 영역 설정
  • 4) 프로그램 시작

  • 프로그램의 진입점(entry point, 보통 main() 함수)으로 이동
  • 명령줄 인자(argv)와 환경 변수(env) 전달
  • exec() 계열 함수들
    // 다양한 형태의 exec 함수
    execl("/bin/ls", "ls", "-l", NULL);
    execv("/bin/ls", argv);
    execve("/bin/ls", argv, envp);  // 가장 기본적인 형태
    execlp("ls", "ls", "-l", NULL);  // PATH에서 검색

    exec()가 성공하면, 이후의 코드는 절대 실행되지 않는다. 프로세스가 완전히 새 프로그램으로 바뀌기 때문이다.

    6. 프로그램 실행


    파일 시스템 접근

    ls 프로그램이 실행되면, 디렉터리 내용을 읽기 위해 추가 시스템 콜을 사용한다.

    // ls 프로그램의 내부 동작 (단순화)
    
    // 1. 디렉터리 열기
    int fd = open("/home", O_RDONLY | O_DIRECTORY);  // 시스템 콜
    if (fd < 0) {
        perror("open failed");
        exit(1);
    }
    
    // 2. 디렉터리 내용 읽기
    struct dirent *entry;
    DIR *dir = fdopendir(fd);  // 시스템 콜
    
    while ((entry = readdir(dir)) != NULL) {  // 시스템 콜
        // 각 파일/디렉터리 정보 가져오기
        struct stat file_stat;
        stat(entry->d_name, &file_stat);  // 시스템 콜
    
        // 파일 정보 출력
        printf("%s\n", entry->d_name);  // write 시스템 콜 내부 사용
    }
    
    // 3. 디렉터리 닫기
    closedir(dir);  // 시스템 콜
    주요 시스템 콜들

    파일 관련

  • open(): 파일 열기
  • read(): 파일 읽기
  • write(): 파일 쓰기
  • close(): 파일 닫기
  • stat(): 파일 정보 가져오기
  • 디렉터리 관련

  • opendir(): 디렉터리 열기
  • readdir(): 디렉터리 항목 읽기
  • closedir(): 디렉터리 닫기
  • 출력 관련

  • write(): 표준 출력으로 데이터 쓰기
  • write(STDOUT_FILENO, "hello\n", 6);
    커널의 역할

    1) 권한 검사

  • 프로세스가 해당 파일/디렉터리에 접근 권한이 있는지 확인
  • UID/GID 기반 권한 체크
  • 접근 제어 목록(ACL) 확인
  • 2) 파일 시스템 접근

  • 디스크에서 데이터 읽기
  • 버퍼 캐시 사용으로 성능 최적화
  • 파일 디스크립터 관리
  • 3) 자원 관리

  • 메모리 할당/해제
  • 파일 디스크립터 관리
  • 프로세스 스케줄링
  • 7. 프로그램이 작업을 완료하면 exit() 시스템 콜을 호출한다.


    명령 종료

    ls 프로그램이 작업을 완료하면:

    // ls 프로그램의 마지막
    exit(0);  // 시스템 콜: 정상 종료
    // 또는
    exit(1);  // 시스템 콜: 오류 종료
    exit() 시스템 콜

    커널의 처리

    1. 프로세스가 사용하던 모든 자원 해제:
    2. 프로세스 상태를 "종료됨(Zombie)"으로 변경
    3. 부모 프로세스에게 SIGCHLD 시그널 전송
    4. 종료 상태 코드(exit status) 보관