개발👩‍💻/시스템프로그래밍

프로세스 다루기 1(10-2-1)

gigibean 2021. 6. 5. 02:23
728x90

프로세스와 관련된 여러가지 시스템 콜들에 대해서 설명한다.

 

fork: 이 fork 시스템 콜을 호출한 프로세스와 동일한 자식 프로세스를 생성한다.

exec: 6가지 종류가 있고, 프로세스 코드 부분을 다른 프로그램을 교체할 때 사용된다.

exit, _exit: exit은 프로세스를 종료하고 종료 상태를 부모 프로세스에게 전달하는 목적,

_exit은 exit과 거의 같지만, atexit함수를 통해서 등록된 함수들을 호출하지 않고 그냥 프로세스를 종료한다는 차이점이 있다.

atexit: exit에의해 종료될 때 호출될 함수를 등록할 때

 

fork

인자없이 fork()만 호출하면 된다.

함수로부터 리턴하는 것은 한번이 아닌 두번이다.

 

어떤 프로세스가 있는데 이 fork를 부르면 자신과 똑같은 자식프로세스를 만든다. 

자식프로세스가 생성되어 부모와 똑같은 프로그램을 돌린다.

그래서 fork를 불러 자식 프로세스가 호출되면 부모프로세스에서 fork()에 리턴값을 리턴한다.

그리고 자식 프로세스도 부모와 같은 프로그램이고, 처음부터 실행시작하는 것이아니라 fork로부터 리턴하는 시점부터 이어나간다. 따라서 자식 프로세스에서도 리턴한다. 그렇기 때문에 return이 두번이다.

 

프로세스를 구분하기 위해서 pid가 있다.

부모 프로세스에서 리턴되는 값은 자식 프로세스의 pid이다.

그리고 자식 프로세스에 경우 리턴할 때는 0을 리턴하게 된다. 항상

 

리턴되는 값을 보고 0인지 아닌지 보고 자식 프로세스로부터 리턴한 것인지 부모프로세스로부터 리턴된 것인지 알 수 있다.

만약자식프로세스를 만들지 못하는 경우, 생성이 안된 경우 부모 프로세스로부터 -1이 리턴되고, 1번만 리턴하게 된다.

 

그래서 유닉스의 경우에는 fork호출하게 되면 부모하고 똑같은 내용을 실행하는 자식프로세스가 만들어지게 된다. 

 

fork를 이용해서 자식 프로세스를 만들고 이어서 exec이라는 시스템콜 호출해서 자식 프로그램을 다른 프로그램으로 대체 시킬 수 있다. 예를들어 A라는 부모프로세스가 있고 이 프로세스에서 fork를 호출해서 A' 라는 자식프로세스를 만들었는데, 이를 exec이라는 시스템콜을 호출해서 B라는 프로세스로 바꿔버릴 수 있다.

이러한 방법으로 fork, exec 두단계를 이용해서 새로운 프로그램을 실행하도록하는 방법을 유닉스에서는 취하고 있다.

 

부모프로세스, 자식프로세스

부모 프로세스와 자식 프로세스는 별개의 프로세스이기 때문에 각각의 프로세스 구분하기위한 pid는 다 다르다.

ppid parent process id라는 것인데 해당 프로세스의 부모 프로세스를 의미한다.

 

프로세스란 실행되고 있는 프로그램을 프로세스라고 했는데, 프로그램을 실행시킨다면

디스크에 저장되어 있는 파일, 프로그램이 메인메모리로 적재된다.

파일의 경우 디스크의 파일 내용 뿐아니라 파일에 대한 속성, 아이노드를 저장한다.

프로세스도 마찬가지로 프로세스 관리하기 위해서 해당 관련 정보들을 프로세스 테이블에 저장한다.

프로세스 속성들이 프로세스 테이블에 저장되어있다.

umask, cwd, uid 같은 속성 정보들이 프로세스 테이블에 저장되어있다.

프로세스 만들어질 때마다 프로세스 테이블에 방을 한칸씩 차지하게 된다.

방을 배정하고 만들어진 프로세스의 속성 정보들을 저장을 한다.

프로세스들이 종료되면 더이상필요 없게 된다.

프로세스가 만들어질때 그 프로세스의 정보 저장하는 용도로 재사용 될 수있다.

 

테이블의 크기 유한하기 때문에 프로세스의 만들수잇는 개수는 제한되어있다.

프로세스 정보 저장할 수 있는 배열이라고한다면, 그 프로세스 테이블에 저장할 수 있는 정보는 100개 밖에 안되기 때문에 이를초과해서 저장하는 것은 불가능하게된다.

fork를 사용해서 자식프로세스 만들지 못하는경우 -1을 리턴한다고 했는데, 이와 같은 경우를 예로 들 수 있다.

 

프로그램이 메인메모리에 적재되어 실행된다고 했는데,

 

 

text

맨 아래 보면 text라고 되어 있다. 이것은 작성한 프로그램을 컴파일 하게 되면 기계어코드가 나오는 게 이를 저장할 영역이다.

 

stack

변수의 종류가 있는데 지역, 전역 변수가 있다. 예를 들어 메인함수에서 선언된 지역변수들이 있을 것이다. 프로그램이실행을 했을 때 지역변수들을 위해 할당되어야 하는 부분이 stack이라는 부분이다. 여기에 지역변수를 위한 공간이다. stack은 메인함수를 위한 것이고 추가 함수들의 지역변수는 별도의 공간을 할당 받아서 그 함수의 지역변수를 저장한다.

이와 같이 함수를호출하는 깊이가 깊어질 수록 stack의 크기는 점점 커져갈 수밖에 없다.

이렇게 하다가 return을 하면 여기서 선언된 지역변수들은 다 사라진다.

따라서 해당함수 지역변수를 담기위해 사용되었던 스택 공간들은 모두 사라진다.

스택의 크기는 줄어든다.

 

-> 함수 호출할때마다 스택의 크기 커지고, 반환할때마다 줄어든다.

 

uninitialized data, initialized data

지역변수 뿐아니라 전역변수도 있다.

전역변수는 크기가 정해져 있다. 

전역변수 프로그램 실행도중 할당할 수는 없으니

해당 크기는 정해져 있다.

그래서 정해진 크기의 공간을 차지하게 된다.

구분을 해 놓은 것은 전역변수 선언할 때, int a, int a= 1 처럼 초기화가 된 전역벽수와 초기화 되지 않은 전역변수를 구분한다.

 

heap

힙에 경우 프로그램짤때 동적으로 메모리 할당하는데, 프로그램이 실행되는 동안 동적으로 할당받고 반납할 수 있다.

할당된 공간의 크기가 늘어나면 힙사이즈가 늘어난다. 반납하게 되면 힙사이즈가 줄어든다.

프로그램이 실행되는 동안 커지거나 작아지거나 할 수 있다.

 

command-line arguments and environment variable

프로그램 컴파일 후 실행하며 커멘드상에서 인자값을 줬을 때 이 값을 여기에 저장한다.

argc, argv 같이.

 

fork()에 의한 프로세스 복사

 

새로운 프로세스 만들어지면 메인메모리 적제되고 속성 기록하기 위해 프로세스 테이블에 부분을 차지하고 있다.

fork로 프로세스가 만들어 지면

부모프로세스와 똑같은 자식 프로세스 만들어지기 때문에 메인 메모리 적재되는 프로세스 부모 프로세스 복사 붙여넣기와 같다.

자식 프로세스도 하나의 프로세스이기때문에 프로세스테이블에 바 하나가 할당된다.

속성들이 여러가지가 있는데 자식프로세스에게 카피가 되어 넘어간다.

부모 프로세스의 커런트 워킹 디렉토리가 어디냐 하는 것이 자식도 똑같이 물려받고, 유저 아이디도 똑같이 물려받는다.

그런데 fork에 의해 부모 프로세스가 차지하고있는 메모리영역을 자식프로세스 복사한다.

 

살펴본바와 같이 메모리에 있는 부분들도 공유할 수없고 복사하게 된다.

그런데 텍스트영역은 기계어 코드고 실행되는 동안 바뀌지 않기 때문에 이 부분은 공유할  수 있다. 부모 프로세스와 자식프로세스 똑같은 기계어 사용한다.

복사한다 했지만 부모 프로세스에서 텍스트영역은 복사를 할 필요 없고 텍스트 영역 제외한 나머지 영역들 데이터와 관련된 부분들 복사하게 된다.

기계어 코드와 관련된 코드는 하나만 존재하면 된다.

이를 자식, 부모가 공유하게 된다.

 

프로세스 테이블에 저장되는 여러가지 내용들중에 프로그램 카운트 부분이 있다. 프로그램의 어디까지 실행했는지에 대한건데 이것도 자식프로세스에 똑같이 복사된다.

그래서 같은 프로그램 카운트를 갖는데,

자식프로세스가 새로 만들어졌는데, 이는 프로그램 처음부터 시작하는 것이 아니라 부모 프로세스에서 fork를 불렀으면, 막 호출했을때의 상황의 프로그램 카운트 부분을 복사한다.

fork를 리턴하는 시점부터 자식프로세스를 시작한다.

 

어떤 프로세스가 돌아가고 있다.

쭉 실행하다가 포크를 호출한다.

호출하면서 자식 프로세스가 만들어졌다.

자식 프로세스도 부모와 똑같은 프로그램이기 때문에 위와 같다.

부모는 포크까지 실행하기 때문에그 아랫부분 부터 실행해나아가고 자식도 마찬가지이다.

 

자식프로세스가 만들어질때 포크를호출할 때 상태를 부모로 부터 물려받는다.

cpu의 상태 등을 자식프로세스가 물려받아

자식프로세스에 기록된다.

커런트 워킹 디렉토리또한 새로 생선된 자식 프로세스 테이블에 기록된다.

umask, 기타 환견변수 등 속성정보도 포크호출할때의 값들이 자식 프로세스에 기록된다.

 

유저 아이디에 대한 것도 기록되는데,

유저 아이디가 2가지로 세분이 되어 있다.

실제 유저 아이디인 real user id(ruid), 실질적인 유저아이디 effective user id(euid)

이에대한 자세한 내용은 따로 나중에 살펴본다.

 

fork() 사용

포크를 호출하면 자식 프로세스가 만들어지고 내부적으로 어떤일이 일어나는지 살펴봤는데 이번에는 코딩하는상황에서 포크를 어떻게 사용하는 것인지 살펴보자

 

포크는 두번 리턴되는데, 부모->자식프로세스아이디, 자식->0 리턴된다.

 

… (A) …
if ((pid = fork()) == 0) { /* 자식 프로세스가 수행할 부분 */
… (B) …
}
else if (pid > 0) { /* 부모 프로세스가 수행할 부분 */
… (C) …
}
else { /* fork 호출이 실패할 경우 수행할 부분 */
… (D) …
}
… (E) …

위와 같은 방식으로 사용되는데, 

fork()를 불르고 리턴되는 값을 pid 저장한다. 이것이 0인지확인한다 이것이 참이면 -> 자식프로세스이다.

B라는 부분은 자식프로세스에서만 실행된다.

0이 아니라고 할 때 더 큰값이면, 부모프로세스 C는 부모프로세스임을 의미한다.

B부분은 자식프로세스에서만, C부분은 부모 프로세스에서만 D는 음수 값으로 호출에 실패할 경우이다.

 

E부분은 if문과 상관없는 부분으로 부모든 자식이든 다 실행한다.

 

예를 한번 들어보자

int main() {
	int i, j, k;
	for (i = 0;i < 5;i++) {
		sleep(1); 
    	printf("before fork :i = %d\n",i);
	}
	if (fork() > 0) { /* parent */
		for (j = 0; ; j++) {
			sleep(2); printf("parent : j = %d\n", j);
		}
	}
	else { /* child */
		for (k = 0; ; k++) {
			sleep(3); printf("child : k = %d\n", k);
		}
	}
	return 0;
}

fork를한 리턴값이 0보다 큰지, 아닌지 if문을 통해서 구분하여 부모프로세스와 자식프로세스에서 해야할 부분을 구분한다.

부모프로세스는 2초에 한번식 j를 출력, 자식은 3초에한번씩 k를 출력하는프로그램을 만들었다.

 

이 파일을 컴파일하고 실행을 시키면 포크하기 전에 루프를 다섯번 돌면서 i값을출력한다.

루프를돌고나면 포크를한다.

 

두개의 프로세스가 독립적으로 따로 따로 돌아가게 된다.

이 프로그램을 이번에는 다시 실행시켜서 결과를 다른 파일로 가게해서 백그라운드로 돌아가게 해보자

 

$ ./a.out > a&
[1] 21249
$ ps -l

아래와 같은 결과가 출력된다.

a.out이 돌아가고 있고, 이는 21249이다.

 

다시한번 ps -l을 실행하면

위와 같이 하나의 프로세스가 더 실행되게 된다.

a.out이 하나더생겼는데 이중 하나는 자식프로세스이다.

각각의 프로세스의 pid를 보면 다르다.

ppid를 보면 21251의 부모를 보면 21249이다

즉, 위에 있는 a.out이 부모 프로세스, 아래 있는 것이 포크로 만들어진 자식프로세스이다

 

이렇게 두개의 프로세스가 있다.

그런데 여기서 자식 프로세슬 죽이면

kill -9 21251
ps -l

 그럼 당연히 자식프로세스는 죽고 부모프로세스만 돌아가고 있을 것이다.

defunct라고 되어 있는데, 자식프로세스가 이렇게변경되어 있다.

죽였지만 리스트에는 나온다.

이는 좀비라는 것이다. 좀비프로세스라고 한다.

자식프로세스는 좀비프로세스 상태이다.

그래서 앞에 Z라고 나와있다.

 

자식 프로세스를 죽였지만 ps에 나온다.

자식프로세스가 죽었을 때 부모 프로세스가 자식프로세스의 종료상태를 가져가야하는데 부모프로세스가 아직 이것을 하지 않았기 때문에 남아있는 것이다. 이것을 여기서 defunct라고 표시했다.

 

이상태에서 부모프로세스를 죽여보자

kill -9 21249
ps -l

확인을 해보면

부모 프로세스가 죽었으니 당연히 없어졌다. 그리고 아까 좀비상태로 남아있던 자식프로세스도 나타나지 않는다.

 

그다음 똑같은 프로그램을 다시 실행시켜보자.

그럼 아직 자식이 만들어지기 전이기 때문에 하나프로세스만 있고 프로그래밍한대로 5초가 지나면 자식이 만들어질 것이다.

그래서 다시한번 ps를해보면

자식프로세스까지만들어진다.

 

이상황에서 부모프로세스를 죽이면

부모프로세스는 죽였으니 사라졌지만

자식프로세스는 ppid 가 1로 변했다.

부모프로세스가 죽어버려서 얘를 고아프로세스라고 부른다.

1번이라는 프로세스가 얘를 양자로 삼아간것이다.

1번프로세스는 init process라고한다. 리눅스를 부팅할때 제일 처음 만들어지는 프로세스 중 하나이다.

부모프로세스가 init 프로세스로 바뀐 것이다.

 

고아, 좀비 프로세스는 나중에 추가적으로 살펴본다.

 

자식 프로세스는 open된 파일에 대한 정보도 물려받는다.

부모프로세스가 파일을 3개를 열었다고 하자 오픈된 파일정보가 프로세스 테이블에 기록되어있다.

포크해서 자식프로세스 만들면 프로세스 테이블에서 많은 부분들이 카피된다고 했는데 여기서 오픈된 파일의 정보도 포크할 때 카피가 된다.

자식프로세스가 만들어져서 프로세스테이블에 방한칸을 배정했다고 할 때 여러가지 속성정보도 카피할 때 이 오픈된 파일정보도 카피된다.

오픈된 파일에 대한 테이블의 위치도 카피가 되어 자식프로세스도 같은 파일 테이블을 가리키게 된다.

이것이 중요한 이유는, 여기에 offset 값이 있기 때문이다.

부모프로세스가 파일 오픈한 다음에 어느 부분까지 I/O를 했다면, 그리고 이것을 1000bytes라고 한다면

이상태에서 부모가 포크를 하면 자식이 오픈된 파일의 정보도 카피했다. 

자식이 0이라는 파일디스크립트 이용해서 읽게되면 파일의 처음부터가 아니라 오프셋이 있는 곳부터 읽어들인다.

그다음 자식프로세스가 읽게되면 500을 읽었으면 파일테이블의 오프셋은 1500이 될 것이다. 여기서 또 부모에서 리드를 하면 1500부터 읽어 나가게된다.

자식 프로세스는 부모프로세스의 오픈된 파일의 정보도 물려받게 되며, 오프셋을 공유하게 된다.

 

 

반응형