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