5. Interlude: Process API

 

예시를 통하여 알기 쉽게 Process API 세 가지를 우선 알아볼 것이다.

 

아래는 LINUX나 net과 같은 unix계통의 운영체제에서 돌아가는 코드 예시이다. 이러한 unix계통의 운영체제는 새로운 프로세스를 만들 때 fork()라는 api를 사용한다. (SYSTEM CALL의 종류)

위 예시는 p1.c라는 프로그램을 fork라는 함수(시스템 콜)을 호출하여 새로운 프로세스를 생성한다. 프로그램이 직접 새로운 프로세스를 생성할 순 없기 때문에 프로그램이 fork()를 호출하면 제어권이 운영체제로 넘어가고 운영체제 안의 시스템콜 sys_fork()함수가 실행되어 프로세스를 생성하는 것이다.

unix 시스템에서는 새로운 프로세스를 만들 때 완전히 새로운 것을 만드는게 아니라 fork라는 시스템콜을 호출한 프로세스를 그대로 복사하여 동일한 코드와 데이터(스택과 힙)을 가진 프로세스를 만드는 것이다.

저번 포스팅에서 create하는 api는 함수의 주소공간과 (memory), register (Program Counter 포함)를 복사한다고 했다. 그렇다면 p1에서 fork를 통해 복사할 때, 복사된 p2는 어디에서부터 실행될까? 함수니까 main부터? 아니면 p1의 복사된 fork실행 부분부터?

방금 register의 PC도 복사한다고 말했듯이, p1의 fork가 실행될 당시의 pc가 가리키는 (현재 수행중인 명령어) 것 또한 그대로 복사하여 p2에 전달하기 때문에 복사된 p2는 fork에서부터 실행된다. 

 


 

p1은 p2라는 프로세스를 생성했기 때문에 p1은 부모 프로세스라고 불리고, p2는 자식 프로세스라고 불린다. 여기서 운영체제 안에서는 다음과 같은 일이 일어난다.

여기서 p1에 반환하는 리턴값은 p2의 pid를 반환하게 된다. (자식의 pid)

p2또한 fork부분의 중간부터 실행될텐데 p2에게는 0이라는 리턴값을 fork에 반환한다.

만약 어떤 문제로 인하여 자식 프로세스를 생성할 수 없을 때 fork는 부모에게 에러가 발생했다는 의미의 -1값을 반환한다. 

위의 예제 코드를 실행시키고 나면 다음 둘 중 하나를 출력한다.

그 이유는 운영체제에 따라 p1을 먼저 처리하는지, p2를 먼저 처리하는지 다르기 때문이다. p1을 먼저 처리한다면 time sharing에 의하여 p1실행 이후 멈췄다가 p2실행의 반복일 것이다.

 

 


 

 

fork() System call을 알아봤으니 이번엔 wait() System call을 알아보자.

wait 시스템 콜은 부모 프로세스에 의해 호출이 되고, 자식 프로세스가 죽을때까지 기다려주는 시스템 콜이다.

wait의 반환값으론 자식의 pid가 반환된다. 마지막 else절의 printf문을 보면 rc값으로 자식의 pid, wc 또한 자식의 pid, (int)getpid는 자신의 pid값을 반환받게 된다.

wait를 사용하면 항상 child가 먼저 출력되고 나중에 parent가 출력되는 것을 볼 수 있다. 운영체제가 부모 프로세스를 먼저 실행시키던지, 자식 프로세스를 먼저 실행시키던지 상관없이 time sharing에 의하여 부모 프로세스의 차례가 올 때, wait로 자식프로세스가 죽을때까지 잠시 멈췄다가 실행될 것이다.

 

 


 

마지막으로 exec() System Call에 대해 알아보자. exec()을 호출하면 새로운 프로그램이 로딩되면서 실행된다. 

exec계열의 함수는 6가지가 있는데 그 중 execvp라는 함수에 대한 예시를 보자.

execvp는 매개변수를 두 개 갖는다. 첫번째는 파일명을 갖고 두번째는 main함수에 들어가는 argv[]가 들어간다.

 이 예제에서 실행할 프로그램은 wc라는 프로그램이다. 이 프로그램은 파일안의 행의 수, 단어의 수, 문자의 수를 계산해서 보여주는 프로그램이다. 이러한 프로그램은 디스크 어딘가에 존재할 것이다. 만약 p1의 자식프로세스 p1_c가 execvp로 wc를 실행하게 되면 기존에 있던 p1의 코드,데이터, 힙 스택은 지워지고 wc의 코드와 데이터가 그 위에 덮어씌워질 것이다. 스택과 힙 또한 원래의 초기상태로 바뀔것이다. 

 

두번째 parameter인 argv[]를 설명하기 앞서 예를들어보자.

리눅스 터미널에서 ./ls -al을 입력하면 main()의 argc( argument count )는 ls, -al 2개가 있으니 2가 되고, argv( argument vector )는 각 토큰의 문자열을 가리킨다.

두 개의 문자열 ls와 -al을 동시에 프로그램에 전달하기 위해 argv[]라는 배열을 만들어서 0번인덱스에 ls의 포인터값을, 1번 인덱스에 -al의 포인터값을 넣어준다. 이러한 문자열에서 첫 번째 문자의 주소는 char형의 포인터이다. 따라서 main의 argv[]는 char* 형태를 저장할 수 있는 배열이다. 

 

 

wc 프로그램을 execvp로 실행하기 위해선 파일명과 argv[]가 필요한데 파일명은 wc로 줄 것이고, argv[]는 각 토큰의 문자열을 가리킨다. 위의 코드에서 argv의 첫 번째는 wc문자열을, 두 번째는 p3.c 문자열을 가리킬 것이다. 이렇게 만들어진 argv는 wc의 메인함수로 전달된다. 

execvp함수를 실행한 후 printf문은 실행되지 않는다. execvp를 호출함과 동시에 프로세스의 코드, 데이터, 스택, 힙 모두 wc프로그램으로 덮어씌워졌기 때문이다. wc의 실행이 끝나고 나면 else부분이 실행된다. 

 

 


 

앞서 배웠던 fork와 exec 같이 프로세스를 생성하는 일을 굳이 두 단계로 나눠야 할까?

자식 프로세스를 fork로 생성하고 거기에 exec으로 덮어씌우는 이유는 fork와 exec 사이에서 부모가 아닌 자식 프로세스가 부가적인 일을 할 수 있기 때문이다.