Laonbud-Notice

fork()와 exec() 본문

개발/리눅스

fork()와 exec()

developers 2011. 11. 23. 22:51
이전 포스팅에서 System Call과 SubRoutine의 차이를 살펴보았습니다.

이미 아시다시피 System Call로 하드웨어를 컨트롤 하면서 여러가지 작업을 할 수 있습니다.
그러면 fork와 exec는 어떤 작업을 하는 걸까요?
이전에 System Call을 활용한 작업 영역을 3가지로 구분하였습니다.
File I/O, Process Control, InterProcess Communication이 그 3가지였는데,
fork와 exec는 그 중 Process Control 영역입니다.

fork와 exec은 따로 얘기할 수 없습니다.
fork는 독자적으로 쓰일 수 있겠으나 exec은 독자적으로 쓰기에는 너무 한정적입니다.
프로그램에서 exec을 호출하게 되면 현재 메모리에 상주하고 있는 이후 프로그램은 무시 되어 버립니다.
exec로 호출되는 프로그램이 현재 메모리에 올라와 있는 프로그램을 덮어서 로딩되기 때문이죠.
그러나 별도의 메모리 공간을 할당하고 그 할당된 공간에서 exec를 실행하게 되면 다른 메모리 공간에서 실행되고 있는 원래의 Process는 자기 갈길을 갈 수 있다. 이런 일을 하는 것이 바로 fork입니다.

Process Control을 한다는데, 그럼 Process는 뭘까요?
Process와 자주 비교 언급되는 것은 바로 Thread입니다.
둘의 차이는 독립된 메모리 공간을 할당 받냐 아니냐의 차이입니다.
 
(별도의 메모리 공간을 가지고 있기 때문에 Process에서 Process를 제어하는 것은 쉽지 않습니다.
그러나 불가능한 일은 아니죠.)

식상하지만 여전히 Process와 Thread를 이해하기 가장 쉬운 방법은 그림입니다.

이 그림은 Process와 Thread를 잘 나타내는데, 네모 박스는 하나의 프로세스를 나타내고 실은 말 그대로 쓰레드를 나타냅니다. 하나의 프로세스에 여러 개의 쓰레드가 있을 수는 있지만 하나의 쓰레드에 여러 개가 있을 수는 없습니다.
쓰레드는 독립적으로 실행될 수 없고, 하나의 프로세스에 종속되기 때문입니다.
하나의 프로세스는 운영체제의 가상 메모리 공간에 독립적인 할당 공간에서 로딩이 됩니다. 쓰레드는 프로세스에 종속되기 때문에 마찬가지로 할당된 메모리 공간에서 움직입니다. 그러므로 메인 Procedure에서 선언된 변수나 함수는 그 프로세스에서 일을하는 모든 쓰레드가 접근할 수 있게 됩니다. 그러나 쓰레드가 동작하는 순서는 프로그래머가 동기화 할 수 없는 경우가 많습니다.(쓰레드의 많은 예제중에 여러개의 쓰레드에서 전역 변수 값을 1씩 올리며 출력하는데 순서대로 올라가지 않는 예제가 많은데요. 이것을 생각하시면 쉽게 이해를 하실 수 있을 것입니다.)
프로세스와 쓰레드를 설명할 수 있는 쉬운 예는 곰플레이어와 같은 프로그램입니다.
AVI 파일을 클릭하면 이 프로그램은 자막이 있으면 자막을 보여주고, 사운드를 들려주고, 영상을 보여주며 전체화면으로 바꾸면 끊기지 않고 전체화면으로 바꿔줍니다. 이것은 단일 프로세스의 단일 쓰레드로는 구현이 불가능하죠.(불가능하다기 보다 대단히 어려운 과정을 거쳐야겠죠...) 사운드를 들려주는 쓰레드, 영상을 보여주는 쓰레드, 프레임을 조정하는 쓰레드, 자막을 관리하는 쓰레드가 각자 자기 할일을 하고 있으며 각각의 쓰레드가 CPU의 자원을 독점적으로 쓰지 않고 적절히 양보하면서 쓰기 때문에 가능합니다.(물론 더불어 CPU 성능이 비약적인 발전이 있어왔기 때문에...) 

여기서 살피고자 하는 fork()와 exec()은 Process Control을 한다고 했으니,
한 프로그램에서 곰플레이어 같은 쓰레드를 호출하는 것과 같은 Control을 하는 것이 아닐까요?
아니면 곰플레이어같은 Process에서 자막관리 쓰레드 같은 여러 쓰레드를 Control하는 것을 얘기하는 것일까요?

정답은 전자입니다.

그러면 지금부터 fork와 exec에 대해서 자세히 살펴보겠습니다.
1.fork()
헤더 파일 : <unistd.h>
함수 원형 : pid_t fork(void);
->사실 pid_t라는 구조형 때문에 <sys/types.h>도 헤더 파일에 포함해 주어야 합니다. pid_t 대신에 int를 사용해도 무리가 없지만...(왜냐하면 #define pid_t int이기 때문이죠!) 
이 fork는 현재 프로세스에서 다른 프로세스를 만듭니다. 현재 프로세스를 부모 프로세스(Parent Process)라고 하고, 만들어진 다른 프로세스를 자식 프로세스(Child Process)라고 합니다. fork가 리턴하는 값 pid_t는 그래서 대단히 중요한데, 이 값에 따라 내가 부모인지, 자식인지 알 수 있습니다. pid_t가 0이면 자식, 0보다 크면 부모입니다.(-1이면 Error가 발생한 것이구요.)
다음 코드는 테스트 코드인데요. 대단히 단순한 구조라는 것을 보시면 쉽게 이해하실 것입니다. 

출력은 다음과 같습니다.

위 프로그램에서 pid = fork()가 실행되는 순간, 위 프로세스와 똑같은 프로세스 하나가 별도의 메모리 공간에 생성됩니다.
이때 두 프로세스의 변수 값, PC(Program Count)값은 정확히 똑같습니다. 단, pid값만 유일하게 달라집니다.
따라서 pid값을 이용해 Child Process가 할일을 하게 하고, Parent Process가 할 일을 하게 하면 됩니다.
 
이때 Child Process가 우선적으로 실행이 되지만, 실행 시간이 길 경우 운영체계의 Process Scheduler에 따라서 Child Process들과 Parent Process가 비동기식으로 경합하게 됩니다.

2. exec
-> exec이라는 함수명은 사실 없습니다. exec은 어떤 일을 하는 family 명칭으로 보는 게 더 정확합니다.
exec family가 하는 일은 현재 실행되는 프로세스에서 다른 프로세스 일을 하게 하는 것입니다. 예를 들어 어떤 문서에서 어떤 문자들이 출현했는지를 판단한 뒤, 출현한 경우 문서내에서 자주 쓰이는 키워드를 추출한다면 2가지 프로세스로 나눌 수 있을 것입니다. 이때 자주 쓰이는 키워드를 추출하는 프로그램이 별도로 만들어져 있거나 만들었다면, 어떤 문자들이 출현한 문서를 찾는 프로그램에서 exec family를 이용해 만들어진 프로그램 object를 이용할 수 있습니다.
다음 테스트 코드를 보면 쉽게 이해 하실 수 있을 것입니다.
 
출력 결과는 다음과 같습니다.

printf문을 이용해 문자열을 출력한뒤, execl을 이용한 ls -l을 실행한 결과가 보여집니다.
그런데 의문점이 들 수 있는데요. perror함수의 결과가 보여져야 하는 것이 아닐까요?
혹은 perror가 왜 execl 뒤에 있어야 할까요? 조건문이 필요하지 않을까요? 
이 이유의 해답은 exec family는 다른 Process를 실행시킬 때 현재 Process 메모리 공간에 덮어 써 버린다는 것입니다. 따라서 exec 함수가 성공하면 perror는 실행되지 않습니다. 이미 ls -l 프로세스가 메모리 공간에 덮어 써 버려졌고, 이후 명령들은 찾을 수 없기 때문입니다. 그러나, exec가 실패했다면 ls -l 프로세스가 메모리 공간에 로딩 되어지지 않기 때문에 perror가 실행 될 것입니다.

우리가 exec을 호출할때 많은 경우에는 원래의 Process가 사라져 버리는 걸 원치 않을 것입니다. 이럴 때 필요한 방법은 우리가 배웠죠. 바로 fork() 입니다. 즉 자식 Process에서 새로운 Process를 로딩해서 일을 하게 하면 되는 것입니다. 다음과 같은 소스가 바로 그런 예입니다.

출력 결과는 다음과 같습니다.


wait((int*) 0)는 child Process가 종료될 때까지 기다립니다. 그리고 "ls completed"를 출력하게 됩니다.
사실 fork와 exec은 shell의 동작 방식을 잘 설명하는데 이용될 수 있는데, 사용자가 shell에서 명령을 하면 그것을 수행하고 완료하면 다시 shell로 돌아가게 됩니다. shell이 fork를 한뒤 exec를 행하고 Child Process가 완료되면 다시 대기 상태로 돌아간다고 볼 수 있는 것이죠.
<위 소스코드는 Keith Haviland 등이 쓴, Unix System Programming에서 가져 온 것입니다.>

마지막으로 위에서 언급한 exec family에 대해서 설명 요약하면 다음과 같습니다.
먼저, exec family의 함수들과 함수 형태를 살펴보겠습니다.

1)int execl(const char *path, const char* arg0, ..., const char* argn, (char*) 0);
2)int execlp(const char* file, const char* arg0. ..., const char* argn, (char*) 0);
3)int execle(const char* pah, const char* arg0, ..., const char* argn, char* const envp[]);
4)int execv(const char* path, char* const argv[]);
5)int execvp(const char* file, char* const argv[]);
6)int execve(const char *path, char* const argv[], char* const envp[]);


모두 비슷한 형식에 약간씩의 변형이 있는 것을 알 수 있습니다. 이 중 가장 헷갈리는 부분이, 맨 첫 인자가 패쓰명에 파일이 포함되어야 하는지, 파일 이름만인지, 디렉토리 명인지 등인데요. 따라서 다음과 같이 확실히 정리하고 넘어갈 필요가 있습니다.

path -> 실행파일까지 포함한 경로명 ex)/bin/ls <- ls가 실행파일이며, 경로명에 꼭 포함되어야 합니다.
file -> 실행파일만 ex)ls 

그렇다면 둘의 차이는 뭘까요? path인 경우는 어디에 실행파일이 있는지 알수 있지만, file은 그렇지 못합니다.
따라서 file과 같이 실행시킬 수 있는 것들은 환경 변수의 Path에 디렉토리가 꼭 설정이 되어 있어야 합니다. 
끝에 e로 끝나는 함수들은 모두 환경 변수를 지정할 수 있는데, 그렇지 않은 것들은 그냥 Null값을 전달합니다.
또한 exec다음 오는 글자가 l인 것들은 list로 인자를 전달하고, v로 끝나는 것들은 array로 argv[] 형태로 전달합니다.
이때 주의할 점은 인자의 첫번째는 파일명이 되어야 합니다. 이것이 조금 헷갈리게 하는 것은 사실인데...
c 프로그래밍에서 첫번째 인자, argv[0]은 프로그램명이었던 것을 생각해보세요. ^^;;

위 내용들을 종합해서 간단하게 그림으로 나타낸 것이 다음 그림입니다.
(인터넷에서 본 그림인데 출처는 정확히 기억나지 않네요.)


또한, 위 그림은 실제 수행 되는 것을 나타내는데, 모든 exec family는 결과적으로 execve를 call하게 됩니다.



'개발 > 리눅스' 카테고리의 다른 글

fork()와 exec()  (6) 2011.11.23
UNIX 파일 시스템의 주요 디렉토리  (0) 2011.11.23
System Call과 SubRoutine의 차이  (0) 2011.11.14
리눅스는...  (0) 2011.11.14
6 Comments
댓글쓰기 폼