프로세스 생성

프로세스 생성

기본 프로세스는 text, data, bss, heap, stack의 공간을 생성 한 후 프로세스 이미지를 해당 공간에 업로드하고 실행을 시작한다.

fork() 와 exec() 시스템콜

  • fork() : 새로운 프로세스 공간을 별도로 만들고, fork() 시스템 콜을 호출한 프로세스 (부모 프로세스) 공간을 모두 복사
    • 별도의 프로세스 공간을 만들고, 부모 프로세스 공간의 데이터를 그대로 복사( 부모 프로세스는 그대로 살아 있음.)
  • exec() : 호출한 현재 프로세스 공간의 text, data, bss 영역을 새로운 프로세스의 이미지로 덮어씌움
    • 별도의 프로세스 공간을 만들지 않음. (덮어 씌워 지는 거라 부모 프로세스 없음.)

fork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){
pid_t pid;
printf("Before fork() call\n");
pid = fork();

if (pid == 0)
printf("this is Child process. PID is %d\n", pid);
else if (pid > 0)
printf("this is Parent process. PID is %d\n", pid);
else
printf("fork() is failed\n");
return 0;
}

fork()가 실행되면 부모 프로세스와 동일한 자식 프로세를 별도의 메모리 공간에 생성한다. 자식 프로세스에 pid 값은 0으로 리턴된다. 이때 동일한 pc 값을 가지기 때문에 이후에 코드가 실행되고 위에서 예시에선 pid > 0 일때 코드 가 출력되고 다음으로 pid == 0 일때 코드가 출력된다.

exec()

인자로 덮어씌워질 프로세스 정보를 넘겨준다. exec는 여러가지 함수 패밀리들이 있다. pc 다음부터는 코드가 덮어씌워지기 때문에 제대로 동작했으면 exec를 후출한 다음줄 부터는 실행이 안될 것이다.

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
printf("execute ls\n");
execl("/bin/ls", "ls", "-l", NULL);
perror("execl is failed\n");
exit(1);

}

execl은 첫번재 인수로 디렉토리 이름이 명시된 명령어를 받고 다음부터 명령어 플래그등을 인수로 받는다. 갯수는 상관 없이 받을 수 있지만 마지막 인수는 NULL 로 끝나야 한다.
실행 시키면 perror 부분에 코드는 실행되지 않고 덮어씌워진 ls 명령이 실행된다.

execlp 디폴트 환경변수 패스 값을 사용하기 때문에 명령어만 넘겨주면 된다. execlp("ls", "ls", "-l", NULL)

execle() 환경변수를 지정하고 넘겨준다.

1
2
char *envp[] = {"user=dave", "PATH=/bin", (char *)0};
execle("ls", "ls", "-al", NULL, envp);

execv는 인자로 변수로 만들어서 넣을 수 있다.

1
2
char *arg[] = {"ls", "-al", NULL};
execv("/bin/ls", arg);

fork와 exec

fork : 부모 프로세스로부터 새로운 프로세스 공간을 만들고 부모 프로세스 데이터를 복사(fork)

exec : 새로운 프로세스를 위한 바이너리를 새로운 프로세스 공간에 덮어 씌움 (exec)

wait() 시스템 콜

wait() 함수를 사용하면, fork() 함수 호출시, 자식 프로세스가 종료할 때까지, 부모 프로세스가 기다림. 브모 프로세스가 자식 프로세스보다 먼저 죽는 경우를 막기 위해 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int pid;
int child_pid;
int status;
pid = fork();
switch(pid) {
case -1:
perror("fork is failed\n");
break;
case 0:
execl("/bin/ls", "ls", "-la", NULL);
perror("execl is failed\n");
break;
default:
child_pid = wait(NULL);
printf("ls is complete\n");
printf("Parent PID (%d), Child PID (%d)\n", getpid(), child_pid);
exit(0);

}
}

execl를 호출하면 부모 프로세스가 덮어씌어 지니까 fork를 통해 자식 프로세스 생성. case 0 인 경우는 자식 프로세스 인 겨으로 여기에 ls 명령어를 덮어씌움.

ls 명령어 출력

wait()를 통해서 자식 프로세스가 끝나기를 기다렸다가 부모 프로세스를 실행 시킬 수 있음.

copy-on-write

리눅스에 프로세스 크기는 4GB에 크기를 가지는데 fork()로 복제할때마다 전체를 복사하려고 하면 오래 걸린다.
따라서 자식 프로세스를 생성했다면 우선은 부모 프로세스에 페이지를 같이 굥유하는 방법으로 생성한다.
그리고 페이지를 읽는 것이 아니라 써야 할때 페이지를 복사하고 분리하는 방법을 사용한다.

장점

  • 프로세스 생성 시간을 줄일 수 있다.
  • 새로 생성된 프로세스에 새롭게 할당되어야 하는 페이지 수도 최소화 된다.

프로세스 종료

exit() 세스템콜 : 프로세스 종료

c 언어에서 main 함수에서 return 을 호출하는 것과 exit()을 호출하는 것에는 어떤 차이가 있을까?

c 언어 실행 파일은 우선 _start() 함수를 호출하고 그 안에서 main() 함수를 호출함. return 하면 main 함수가 끝나고 다음 exit()함수를 호출함.

만약 main 함수에서 exit() 함수를 호출했다면 바로 프로세스를 종료시키는 것임.

exit() 시스템 콜

부모 프로세스는 status & 0377 계산 값으로 자식 프로세스 종료 상태 확인 가능

기본 사용

1
2
exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);

주요 동작

  • atexit() 에 등록된 함수 실행
  • 열려 있는 모든 입출력 스트림 버퍼 삭제
  • 프로세스가 오픈한 파일을 모두 닫음
  • tempfile() 함수를 생성한 임시 파일 삭제

atexit() 함수

프로세스 종료시 실행될 함수를 등록하기 위해 사용한다. 등록된 함수를 등록된 역순서대로 실행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void){
void exithandling(void);
void goodbymessage(void);
int ret;

ret = atexit(exithandling);
if (ret != 0) perror("Error in atexit\n");
ret = atexit(goodbymessage);
if (ret != 0) perror("Error in atexit\n");
exit(EXIT_SUCCESS);
}

void exithandling(void) {
printf("exit handling\n");
}

void goodbymessage(void) {
printf("see you again!\n");
}

역순이기 때문에 goodbymessage 가 먼저 호출 됨.

wait() 시스템 콜

  • wait() 함수를 사용하면 fork()로 생성된 자식 프로세스가 종료될때까지 부모 프로세스가 기다림
  • 자식 프로세스가 완전히 끝나면 좀비 프로세스가 가지고 있던 최소 정보도 삭제하고 부모 프로세스에 SIGCHLD 시그널이 보내짐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int pid;
int child_pid;
int status;
pid = fork();
switch (pid) {
case -1:
perror("fork is failed\n");
break;
case 0:
execl("/bin/ls", "ls", "-al", NULL);
perror("execl is failed\n");
break;
default:
child_pid = wait(&status);
if (WIFEXITED(status)) {
printf("Child process is normally terminated\n");
}
exit(0);
}
}

wait의 리턴값은 종료된 자식 프로세스의 pid 값임.

status 정보를 통해 기본적인 자식 프로세스 관련 정보를 확인할 수 있음

1
int WIFEXITED(status);

자식 프로세스가 정상 종료 시 리턴값은 0 이 아닌 값이 됨.