[OS] 프로세스의 생성과 복사
1. 프로세스의 생성
1.1. 프로세스가 생성되는 경우
- 시스템 부팅 과정에서 필요한 프로세스 생성
- 사용자 로그인 후 사용자 제어(대화) 위한 프로세스 생성
- bash
- explorer.exe
- finder.app 등
- 새 프로세스 생성하는 사용자 명령 (ex:
vi hello.c
) - 배치 작업 실행 시(
at
,batch
명령) - 사용자 응용 프로그램이 syscall로 새 프로세스 생성
단순히 메모리에 올라갔다고 프로세스가 되는 것이 아니다.
- 그냥 메모리에 올라가져‘만’ 있으면 아무 의미가 없다.
- PCB가 존재해 OS가 제어 가능한 형태가 되어야 함 → CPU 할당 받아 실행이 가능해진다.
1.2. 프로세스 생성 과정
- 생성할 실행파일 경로를 OS에 전달
- OS는 메모리에 프로그램 적재
- Code 영역에 프로그램 코드 적재
- Data 영역에 전역/정적 변수 할당
- 스택/힙은 아직 아무것도 없으므로 초기화만 시킴
- PCB 공간 할당 받고(malloc), 필요 정보 채움
- 프로세스 식별자 결정 → 새 PID 번호 할당
- 프로세스 정보 기록
- 프로세스 테이블에서 새 항목 할당
- 새로 할당된 프로세스 테이블에 PCB 연결
- PCB에 프로세스 상태를 ready로 표시하고, 준비 큐에 장착
1.3. 복사에 의한 프로세스 생성
프로세스 생성 과정은 이처럼 할 일이 많으므로, 다른 프로세스를 생성하기 위해 기존의 프로세스를 복사하여 생성하는 것이 상대적으로 간편하다. 현대 운영체제는 상당수가 이를 따르고 있다.
- Linux:
fork()
시스템 콜 - Windows
CreateProcess()
등 시스템 콜
엄밀히 말하면 윈도우에는
fork()
는 없다.CreateProcess()
는fork()
+exec()
에 가깝다.
UNIX 계열 OS는 시스템 부팅 시 0번 프로세스만 자체적으로 생성한다. 나머지 프로세스는 복제를 통해 생성한다. 이렇게 함으로써 얻는 이점은 아래와 같다.
- 자주 사용되는 프로세스에 대해 매번 반복할 필요 X (예:
bash shell
) - 관리상 편리해짐 (프로세스 계층 구조)
- Process 간 통신
2. fork() 시스템 콜
fork()
: 새로운 프로세스를 실행 중 프로세스로부터 복사하는 함수- 실행 중인 프로세스와 똑같은 프로세스가 하나 더 만들어진다.
int pid = fork();
- 부모 프로세스: fork를 호출한 프로세스
- 자식 프로세스: fork된 프로세스
- 부모 프로세스의 모든 환경, 메모리, PCB 등을 복사
- 부모와 동일한 모양이지만 독립된 주소 공간에 위치함
단, PCB에서 아래 내용은 달라진다.
- PID
- PPID(Parent PID): 부모의 PID로 초기화
- CPID(Chlid PID): 자식이 없을 경우 -1
- 메모리 관련 정보: 독립된 주소 공간을 소유하기 때문
2.1. 실행 과정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pid_t pid; //pid 변수 선언
pid = fork(); //자식 프로세스 생성
//자식 프로세스는 이 라인 밑에서부터 실행됨
if (pid > 0) {
/* 부모 프로세스가 실행할 코드 */
}
else if (pid == 0) {
/* 자식 프로세스가 실행할 코드 */
}
else {
/* fork() 오류 처리하는 코드 작성 */
}
- 자식은 부모의 PC(Program Counter)도 복제,
pid = fork();
그 다음 줄부터 실행된다.- 즉
pid = fork();
이전 라인은 실행이 불가능
- 즉
fork()
함수의 리턴값- 부모 프로세스: 자식 프로세스의 PID (> 0)
- 자식 프로세스: 0
- Error: 음수
code example for xNIX
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
27
28
29
30
31
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
int i, sum = 0;
pid = fork(); // fork! -> create a child process
if (pid > 0) { // Run by the parent
printf("Parent: fork()'s return == Child pid = %d\n", pid);
printf("Parent: pid = %d\n", getpid());
wait(NULL); // Wait for the child
printf("Parent has been finished\n");
return 0;
} else if (pid == 0) { // Run by the child
printf("\t-Child: fork()'s return pid = %d\n", pid);
printf("\t-Child: pid = %d, parent's pid = %d\n", getpid(), getppid());
for (i = 1; i <= 100; i++)
sum += i;
printf("\t-Child: sum = %d\n", sum);
return 0;
} else { // Error
printf("fork error");
return 0;
}
}
실행 결과
1
2
3
4
5
Parent: fork()'s return == Child pid = 482
Parent: pid = 481
-Child: fork()'s return pid = 0
-Child: pid = 482, parent's pid = 481
-Child: sum = 5050
2.2. 장점
- 프로세스 생성 속도 빠름
- 추가 작업 없이 자원 상속 가능
- 시스템 관리 효율적으로 가능 (프로세스 계층 구조)
2.3. 단점
- 매번 모든 Context의 복사본 만드는 것은 매우 비효율적
- 맨 처음 만든 프로그램 프로세스(0번 프로세스) 이외에는 다른 프로그램 동작 불가
- 해결책: exec() 시스템 콜 - fork() 후 실행
3. exec() 시스템 콜: Process Overlay
- 기존의 프로세스를 새 프로세스로 전환(재사용)하는 함수
- 현재 실행 중인 프로세스 주소 공간에 새로운 응용프로그램 적재 후 실행
fork()
: 새 프로세스를 복사하는 syscallexec()
: 프로세스 그대로 둔 채 내용만 바꾸는 syscallexeclp()
,execv()
,execvp()
, …
- 실행 파일을 로딩 후, 현재 프로세스 이미지 위에 단순이 덮어쓰는 것
프로세스를 새로 생성하는 것이 아니다.
- 프로세스 PID가 변경되지 않는다.
- 프로세스 메모리 공간(코드, 데이터, 힙, 스택)에 새 프로그램이 적재되는 것이다.
- 보통
fork()
통해 생성된 자식 프로세스가exec()
를 실행한다.- loader가 exec를 통해 호출된다.
3.1. 실행 과정
메모리
- Code 영역에 있는 기존의 내용을 지우고, 새로운 코드를 덮어씌움
- Data 영역이 새 변수로 채워지고, 힙/스택 영역이 리셋됨
PCB
- PID, PPID, CPID, 메모리 관련 정보는 유지
- 새 프로세스가 전환 되더라도, 종료 후 부모 프로세스로 돌아올 수 있음
- Program counter를 비롯한 register, 파일 정보 등이 모두 리셋됨
- 오버레이된 프로그램의 맨 처음부터 실행하게 된다.
4. wait() 시스템 콜
- 자식 프로세스가 끝날 때까지 무한히 기다리다가, 자식 프로세스가 종료되면 그 다음 코드부터 이어서 실행을 계속할 수 있게 하는 시스템 콜
- Linux shell에서의 예:
- foreground process 있는 경우: shell은 wait 상태
- 프로세스 실행 중 상태에서
Ctrl + Z
→ 프로세스 일시중단 → 쉘이 wait에서 빠져나온다.
code example for xNIX
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 <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
int status;
pid = fork(); //새 프로세스 분기
//자식 프로세스는 여기서부터 실행된다.
if (pid > 0) {
printf("Parent: Wating for the child\n");
wait(&status); // Wait.
printf("Parent: child's exit code=%d\n", WEXITSTATUS(status));
return 0;
} else if (pid == 0) {
execlp("/bin/ls", "ls", NULL); // run ls on the child
// 이게 실행되면 이 밑의 코드는 싹 무시된다!
// (자식은 절대 그 존재를 모른다)
} else {
printf("fork error!");
return 0;
}
}
실행 결과
1
2
3
4
Parent: Wating for the child
forkEx.c Makefile threadEx2.c waitEx.c # 자식 프로세스 /bin/ls 실행 결과
main replit.nix threadEx.c # 자식 프로세스 /bin/ls 실행 결과
Parent: child's exit code=0
5. exit() 시스템 콜
- 작업의 종료를 알리는 시스템 콜
- 종료를 명시적으로 알림으로서 부모는 자식이 사용하던 자원을 빨리 회수한다.
5.1. 종료 코드
- 부모 프로세스에게 상태나 종료의 이유를 전달하는 값 (ex:
exit(1)
) - 통상
0
을 정상 종료로, 나머지1 ~ 255
범위를 그 외 종료로 사용한다. main()
함수의 리턴 값이 곧exit()
함수의 종료 코드 값이다.return 4; == exit(4);
- 내부적으로
main()
의return
문은exit()
syscall이 실행되도록 컴파일된다. - OS가 자동적으로
exit()
호출되도록 해 프로그램을 종료시키도록 하게 한다.
종료 코드를 부모가 확인해야 최종적으로 자식 프로세스가 종료된다.
5.2. exit() 통한 프로세스 종료 과정
- 프로세스의 모든 자원 반환
- 코드, 데이터, 스택, 힙 등 모든 메모리 자원 반환
- 열어 놓은 파일/소켓 등을 닫는다.
- PCB에 프로세스 상태를
Terminated
로 변경, PCB에 종료 코드 저장- 아직 PCB가 프로세스 테이블에서 제거된 것이 아님
- 자식 프로세스들을
init
프로세스에게 입양 - 부모 프로세스에게
SIGCHLD
(일종의 종료 알림 신호) 전송- 부모의 의무:
SIGCHLD
신호 수신하고,wait()
시스템 호출로 자식의 종료 코드 읽어주기
- 죽은 자식이 남긴 정보를 확인해야 자식 프로세스의 PCB가 완전히 제거된다.
- 부모의 의무:
부모가 자식의 종료신호를 제 때 확인하지 못할 경우, 자식은 좀비 프로세스가 된다.
- PCB가 남아있으므로,
ps
명령어 등으로 존재를 확인할 수 있다.
code example for xNIX
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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid, zompid;
int status;
pid = fork();
if (pid > 0) { // parent's code
sleep(10); // sleep for 10 sec
zompid = wait(&status); // wait for the child
printf("Parent: child %d has been finished with %d\n", zompid,
WEXITSTATUS(status));
return 0;
} else if (pid == 0) { // child's code
printf("Child: I am done %d \n", getpid());
exit(100); // exit code 100
} else {
printf("fork error\n");
return 0;
}
}
실행 결과
1
2
Child: I am done 592 # 문장 출력 후 10초 뒤에 아래 문장이 출력된다.
Parent: child 592 has been finished with 100
자식 프로세스(PID 592)가 좀비(Status=Z)가 된 모습. 자식까지 모두 종료된 뒤에는 사라진다.
- 메모리 정리는 이미 모두 되었으므로, 점유하는 메모리 size = 0이다.
좀비 프로세스란, 단순히 PCB 정리가 안된 것 뿐이다.
6. 정리 in xNIX OS
fork()
→exec()
의 구조fork()
통해 프로세스 생성exec()
통해 필요한 프로세스 실행- 부모 프로세스: 생성을 한 프로세스 →
wait()
으로 대기 - 자식 프로세스: 생성된 프로세스 →
exit()
으로 종료 알림
- 모든 프로세스의 조상: init(1번 프로세스)
6.1. fork() → exec() 거치는 이유
- 프로세스 생성 과정 간소화
- 프로세스 관리 쉬워짐: 프로세스 계층 구조
- 예시:
abort()
(부모가 신호 보내서 자식 죽이는 syscall)
- 예시:
- 프로세스 간 통신(IPC)
- 프로세스는 독자적인 메모리 공간 가진 것처럼 운영
- 서로 간섭/통신 불가, 통신이 필요하면
SIGCHLD
같은 신호 보내기 - 또는 파일을 이용해 서로 의사소통
- 여기서
fork()
가 사용됨- 같은 파일 핸들러를 두 프로세스가 공유
- 여기서
1
2
3
4
5
6
7
8
9
10
11
int fd; // 파일 시스템 변수
foo() {
fd = open("pile"); // 파일을 연다.
if(fork() == 0) { // 자식일 경우
read(fd,...); // 하나의 파일로 서로 의사소통함
}
else { // 부모일 경우
write(fd,...); // 하나의 파일로 서로 의사소통함
}
}
(사족) 윈도우의 프로세스 생성 과정
CreateProcess()
함수 $ \approx $fork()
+exec()
fork()
와 차이점fork()
: 자식 프로세스가 부모 프로세스의 주소공간 상속밭음CreateProcess()
: 자식 프로세스에게 구체적으로 어떤 프로그램을 실행할 것인지 요구(주소값이 명확해야 한다.)
code example for Windows
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 <stdio.h>
#include <windows.h>
int main() {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
if (!CreateProcessA(NULL,
"C:\\windows\\system32\\notepad.exe",
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
printf("Error!\n");
return -1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
printf("Child has been finished\n");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
실행 결과
이 포스팅은 작성자의 CC BY-NC 4.0 라이선스를 준수합니다.