Interview/OS

Linux OS exec()

김 정출 2024. 9. 27. 17:45

exec() 함수는 LinuxUnix 기반 운영체제에서 실행 중인 프로세스를 새로운 프로그램으로 교체하는 시스템 호출입니다. 이 시스템 호출은 fork()와 함께 자주 사용되며, *현재 프로세스를 완전히 대체하고 새로운 프로그램*을 실행하는 데 사용됩니다.

exec() 함수는 한 프로세스 내에서 실행되던 코드, 데이터, 스택을 새로운 프로그램으로 덮어씌우고, PID는 유지되면서 프로그램이 실행되는 방식입니다. exec() 함수는 여러 변형(execl, execp, execv, execle 등)이 있지만, 기본적으로 내부 동작 방식은 동일합니다.

exec()의 내부 동작 과정

  1. 프로세스 준비 상태
    • exec()는 현재 실행 중인 프로세스에서 호출됩니다. 이 프로세스는 이미 존재하는 상태이므로 PID(프로세스 ID)파일 디스크립터와 같은 정보는 그대로 유지됩니다.
    • 그러나 호출 후 이 프로세스는 완전히 새로운 프로그램으로 교체됩니다. 즉, 기존의 코드, 데이터, 스택은 사라지고 새로운 프로그램의 코드로 대체됩니다.
  2. 파일 시스템에서 프로그램 찾기
    • exec() 함수는 새로운 프로그램을 실행할 파일 경로를 입력으로 받습니다. 이 파일은 일반적으로 실행 가능한 바이너리(예: ELF 파일) 또는 스크립트일 수 있습니다.
    • 커널은 파일 시스템에서 해당 프로그램을 찾고, 이 프로그램이 실행 가능한지 확인합니다. (즉, 파일이 존재하는지, 실행 권한이 있는지, 올바른 형식인지 등)
  3. 새로운 프로그램의 메모리 로드
    • 프로세스의 메모리 공간을 초기화합니다.
      • 기존 프로세스의 텍스트 영역(코드 영역), 데이터 영역, 스택 영역을 모두 해제하고, 새로운 프로그램을 위한 새로운 메모리 영역을 할당합니다.
    • ELF 파일을 메모리에 적재하는 과정:
      • exec()가 호출된 후, 커널은 ELF(Executable and Linkable Format) 파일의 헤더를 읽고, 프로그램의 코드와 데이터를 메모리로 적재합니다.
      • 파일의 텍스트 섹션코드 영역으로, 데이터 섹션데이터 영역으로 적재됩니다. 이때 각 섹션은 적절한 권한(읽기, 쓰기, 실행)을 부여받습니다.
    • 스택 영역도 새롭게 초기화됩니다. 이를 통해 새로운 프로그램은 자체적인 스택을 사용할 수 있게 됩니다.
  4. 프로세스 컨텍스트 초기화
    • 기존 프로세스의 레지스터 값, 프로그램 카운터(PC), 스택 포인터 등은 새로운 프로그램에 맞게 재설정됩니다.
    • 프로세스의 시작 주소는 새로운 프로그램의 엔트리 포인트로 설정됩니다. (즉, 새로운 프로그램의 첫 번째 명령어로 PC가 이동합니다.)
    • 새로운 프로그램이 시작되면, 이전 프로그램의 레지스터 상태나 프로그램 카운터는 더 이상 유효하지 않습니다.
  5. 환경 변수와 인자 처리
    • exec() 호출 시 전달된 명령줄 인자(argv)환경 변수(envp)는 새로운 프로세스에 전달됩니다.
    • 이들은 새로운 프로그램의 스택에 저장되고, 프로그램이 시작될 때 main() 함수로 전달됩니다.
    • main() 함수는 보통 다음과 같은 형태로 시작됩니다:
    • int main(int argc, char *argv[], char *envp[]);
  6. 파일 디스크립터 유지
    • exec()파일 디스크립터를 닫지 않습니다. 기존에 열려 있던 파일 디스크립터는 그대로 유지되며, 새로운 프로그램에서도 이 파일 디스크립터를 사용할 수 있습니다.
      • 예를 들어, 표준 입출력(stdin, stdout, stderr)의 파일 디스크립터는 새로운 프로그램에서도 그대로 유지됩니다.
      • 이 특징은 자식 프로세스가 파일 디스크립터를 열어 놓은 상태에서 exec()를 호출해도, 파일 디스크립터가 그대로 유지되면서 프로그램 실행 전후로 입출력이 이어지는 데 유용하게 쓰입니다.
  7. 메모리 매핑과 가상 메모리 설정
    • 가상 메모리 공간은 새로운 프로그램에 맞게 재설정됩니다.
    • 프로그램의 텍스트 영역(코드), 데이터 영역, , 스택이 새롭게 구성되며, 필요한 경우 동적으로 할당된 메모리나 메모리 매핑 영역도 초기화됩니다.
    • 동적 라이브러리(예: glibc)*는 프로그램 실행 전에 메모리에 로드되며, 이를 통해 동적 링크가 처리됩니다.
  8. 시그널 핸들러 리셋
    • exec() 호출 후, 기존 프로세스에 설정된 시그널 핸들러는 기본값으로 초기화됩니다.
    • 이는 새로운 프로그램이 이전 프로그램의 시그널 처리 로직을 그대로 상속하지 않도록 하기 위한 것입니다.
  9. 새로운 프로그램 실행
    • 이제 모든 준비가 끝나면, 커널은 새로운 프로그램을 실행합니다.
    • 프로그램이 첫 번째 명령어로 실행을 시작하며, 이전 프로그램의 상태는 완전히 덮어씌워져 복구할 수 없습니다.
  10. exec()는 반환하지 않음
    • exec() 호출 이후에는 새로운 프로그램으로 완전히 대체되므로, 기존 프로세스는 더 이상 존재하지 않습니다.
    • 따라서 exec()는 성공적으로 실행되면 결코 반환되지 않으며, 이후의 코드는 실행되지 않습니다.
    • 만약 exec()가 실패하면, 예외적으로 1을 반환하며 실패 원인은 errno를 통해 확인할 수 있습니다.

exec() 동작 과정 요약

  1. 프로세스는 현재의 메모리 공간을 버리고, 새로운 프로그램의 메모리 공간을 로드합니다.
  2. 파일 시스템에서 실행 파일을 찾고, 실행 가능한지 확인합니다.
  3. ELF 파일을 메모리에 로드하고, 가상 메모리 공간을 재구성합니다.
  4. 명령줄 인자와 환경 변수를 새 프로그램에 전달합니다.
  5. 파일 디스크립터는 유지되며, 이를 통해 입출력 연속성을 보장합니다.
  6. 시그널 핸들러는 초기화되고, 새로운 프로그램의 엔트리 포인트로 제어가 넘어갑니다.
  7. exec() 호출은 성공 시 반환되지 않으며, 기존 프로세스는 완전히 새로운 프로그램으로 대체됩니다.

예시 코드

다음은 fork()exec()를 함께 사용하는 예시 코드입니다.

#include <stdio.h>
#include <unistd.h>

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

    if (pid == 0) {
        // 자식 프로세스에서 exec()를 호출하여 새로운 프로그램 실행
        char *args[] = {"/bin/ls", "-l", NULL};  // 실행할 프로그램과 인자들
        execv("/bin/ls", args);                 // 새로운 프로그램으로 대체

        // execv()가 성공했다면, 이 코드는 실행되지 않음
        perror("execv failed");
    } else if (pid > 0) {
        // 부모 프로세스는 자식 프로세스가 새로운 프로그램을 실행할 동안 계속 실행
        printf("Parent process continues.\\n");
    } else {
        // fork 실패 시
        perror("fork failed");
    }

    return 0;
}

이 코드에서 자식 프로세스는 fork() 이후 execv()를 호출하여 /bin/ls 프로그램을 실행하고, 부모 프로세스는 계속해서 자신의 코드를 실행합니다. execv()가 호출된 이후에는 자식 프로세스는 ls 프로그램으로 완전히 대체됩니다.

결론

exec()는 기존 프로세스를 완전히 새로운 프로그램으로 교체하는 시스템 호출입니다. 이 과정에서 PID는 유지되지만, 메모리 공간, 명령어, 스택 등이 모두 새로 초기화됩니다. 이를 통해 기존 프로세스의 자원을 활용하면서도 완전히 새로운 프로그램을 실행할 수 있습니다. fork()와 함께 사용하여 새로운 프로세스를 생성하고, 이 프로세스를 통해 다른 프로그램을 실행하는 구조를 자주 사용합니다.