본문 바로가기

컴퓨터/언어,프로그래밍

fork()와 exec() [펌]

이전에 System Call과 SubRoutine의 차이를 살펴보았기 때문에,
이제 System Call이 무슨 일을 하는지 대충 알 수 있다.
(그동안 글은 시간에 쫓겨 자세히 쓸 수 없었지만, 적당한 일 하나를 끝낸 뒤라 시간적 여유가 있어 자세히 써볼까 한다.)

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하는 것을 얘기하는 것일까?
정답은 전자다.

지금부터 느낌상 정답을 맞춘 것을 구체화 해본다.
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하게 된다.
참 긴글이 되어 버렸군.
 
 
 
제주삼다수, 2L,... 오뚜기 진라면 매운... 상하목장 유기농 흰... 남양 프렌치카페 카... 고려인삼유통 홍삼 ... 종근당건강 오메가3... 요이치 카링 유무선...