목차

  1. fork() & exec()
  2. fork(), exec()의 차이점
  3. exec() 관련 함수
  4. System Call 에서의 fork, exec
  5. fork() 예시
  6. exec() 예시

fork() & exec()

fork()exec()는 모두 한 프로세스가 다른 프로세스를 실행시키기 위해 사용하게 된다.

exec에는 execl, execv등 여러가지 함수군을 가지고 있다. exec의 함수군에 대해서는 아래쪽에서 차이를 간단히 정리하고자 한다.


fork(), exec()의 차이점

우선 fork()와 exec()의 차이점에 대해 설명하고자 한다.

 

fork() 시스템 호출은 새로운 프로세스를 위한 메모리를 할당한다는 것이다. 그리고 fork()를 호출한 프로세스를 새로운 공간으로 전부 복사하게 되고, 원래 프로세스는 원래 프로세스대로 작업을 실행하고 fork()를 이용해서 생성된 프로세스도 그 나름대로 fork() 시스템 콜이 수행된 라인의 다음 라인부터 실행이 된다. (새로 생성된 프로세스는 원래의 프로세스와 똑같은 코드를 가지고 있다.)

 

반면, exec() fork()처럼 새로운 프로세스를 위한 메모리를 할당하지 않고, exec()를 호출한 프로세스가 아닌 exec()에 의해 호출된 프로세스만 메모리에 남게 된.

 

간단히 정리하면, fork()의 결과는 프로세스가 하나 더 생기는 것이다.( = 프로세스 id- PID 가 완전히 다른 또 하나의 프로세스가 생기는 것). 반면 exec()실행의 결과로 생성되는 새로운 프로세스는 없고, exec()를 호출한 프로세스의 PID가 그대로 새로운 프로세스에 적용이 되며, exec()를 호출한 프로세스는 새로운 프로세스에 의해 덮어 쓰여지게 된다.

 


exec() 관련 함수

이제, exec()관련 함수들에 대해 조금 간단히 다뤄볼까 한다. exec는 execl, execv, execlp, execvp 등이 있다.

이들에 대하여 본 4가지만 간단히 구분하는 방법을 정리해보았다.

 

exec를 먼저 l계열(execl, execlp)과 v계열(execv, execvp)로 나누어 설명하겠다.

l계열 : 인자를 열거하는 방식이 나열형

v계열 : 인자를 열거하는 방식이 배열형

 

다음으로 p가 붙은 계열(execlp, execvp), 안붙은 계열(execl, execv)로 나누어 정리하면,

p가 안붙은 계열 : 경로를 지정해주면 ,현재/절대경로를 기준으로 찾게 된다.(경로로 실행파일을 지정)

p가 붙은 계열(path) : path에 잡혀있으면 실행된다.(실행파일의 이름만 지정)

 

exec계열은 첫번째 인자의 코드가 들어오고 나머지 기존에 exec아래에서 실행해야 할 코드는 전부 잃어버리게 된다는 점을 가지고 있다고 보면 된다.


System Call에서의 fork, exec

System Call로 하드웨어를 컨트롤하면서 여러가지 작업을 할 수 있다.

System Call을 활용한 작업영역을 3가지로 구분한다.

 

  1. File I/O
  2. Process Control
  3. InterProcess Communication

fork와 exec는 그 중 Process Control 영역이다.

 

fork와 exec는 따로 얘기할 수 없다.

fork는 독자적으로 쓰일 수 있겠으나 exec는 독자적으로 쓰기에는 너무 한정적이다.

 

프로그램에서 exec를 호출하게 되면 현재 메모리에 상주하고 있는 이후 프로그램은 무시되어 버린다.

exec로 호출되는 프로그램이 현재 메모리에 올라와 있는 프로그램을 덮어서 로딩되기 때문이다.

 

그러나 별도의 메모리 공간을 할당하고 그 할당된 공간에서 exec를 실행하게 되면 다른 메모리 공간에서 실행되고 있는 원래의 Process는 자기 갈 길을 갈 수 있다. 이런 일을 하는 것이 바로 fork이다.

 

Process Control을 한다는데, 그럼 Process는 뭘까?

Process 와 자주 비교 언급되는 것은 바로 Thread이다.

 

둘의 차이는 독립된 메모리 공간을 할당 받냐 안받냐의 차이이다.

이 그림은 Process 와 Thread 를 잘 나타낸다.

네모 박스는 하나의 프로세스를 나타내고, 실은 말 글대로 쓰레드를 나타낸다.

하나의 쓰레드는 독립적으로 실행될 수 없는데, 왜냐면 하나의 프로세스에 종속되기 때문이다.

 

하나의 프로세스는 운영체제의 가상 메모리 공간에 독립적인 할당 공간에서 로딩이 된다.

쓰레드는 프로세스에 종속되기 때문에 마찬가지로 할당된 메모리 공간에서 움직인다.

그러므로 메인 Procedure에서 선언된 변수나 함수는 그 프로세스에서 일을 하는 모든 쓰레드가 접근할 수 있게 된다.

그러나 쓰레드가 동작하는 순서로 프로그래머가 동기화 할 수 없는 경우가 많다.

(쓰레드의 많은 예제 중에 여러개의 쓰레드에 전역변수 값을 1씩 올리며 출력하는데 순서대로 올라가지 않는 것을 생각해 보아라)

 

프로세스와 쓰레드를 설명할 수 있는 쉬운 예는 곰플레이어와 같은 프로그램이다.

avi 파일을 클릭하면 이 프로그램은 자막이 있으면 자막을 보여주고, 사운드를 들려주고, 영상을 보여주며 전체화면으로 바꾸면 끊기지 않고 전체화면으로 바꿔준다. 이것은 단일 프로세스의 단일 쓰레드로는 구현이 불가능하다.(불가능하다기 보다 대단히 어려운 과정을 거쳐야 한다.) 사운드를 들려주는 쓰레드, 영상을 보여주는 쓰레드, 프레임을 조정하는 쓰레드, 자막을 관리하는 쓰레드가 각자 자기 할일을 하고 있으며 각각의 쓰레드가 CPU의 자원을 독점적으로 쓰지 않고 적절히 양보하면서 쓰기 때문에 가능하다.

 

여기서 살피고자 하는 fork() 와 exec()는 Process Control을 한다고 했으니,

한 프로그램에서 곰플레이어와 같은 쓰레드를 호출하는 것과 같은 Control을 하는 것이 아닐까?

아니면 곰플레이어 같은 Process에서 자막관리 쓰레드 같은 여러 쓰레드를 Control하는 것을 얘기하는 것일까?

 

그러면 지금부터 fork와 exec에 대해 자세히 살펴보자.


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_t0이면 자식, 0보다 크면 부모이다. (-1이면 Error가 발생한 것이다)

 

다음 코드는 테스트 코드인데 대단히 단순한 구조라는 것을 볼 수 있다.


출력은 다음과 같다.

위 프로그램에서 pid = fork()가 실행되는 순간, 위 프로세스와 똑같은 프로세스 하나가 별도의 메모리 공간에 생성된다.

이때 두 프로세스의 변수 값, PC(Program Count)값은 정확히 똑같다. 단, pid값만 유일하게 달라진다.

따라서 pid값을 이용해 Child Process가 할 일을 하게 하고, Parent Process가 할 일을 하게 하면 된다.

 

이때 Child Process가 우선적으로 실행이 되지만, 실행 시간이 길 경우 운영체제의 Process Scehuler에 따라서 Child Process들과 Parent Process가 비동기식으로 경합하게 된다.


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가 완료되면 다시 대기 상태로 돌아간다고 볼 수 있다. 

 

'🚦 Server > Operating System' 카테고리의 다른 글

10. Process Scheduling (1)  (0) 2020.04.29
pthread  (0) 2020.04.29
09. Threads (3)  (0) 2020.04.27
PA1: My Simple Shell  (0) 2020.04.27
08. Threads (2)  (0) 2020.04.23
복사했습니다!