Process
Program 과 Processor 랑은 다른 개념, Process 는 실행가능한 Program의 instance 이다.
(* Program: 실행가능한 file)
(* Processor: CPU)
여러개의 프로그램들이 동시에 같은 시스템에서 실행 될 수 있는데, 이런 동시 실행은 context switch 를 통해 이루어진다.
이때 context란 프로세스가 필요로 하는 정보들인 Program Counter(* PC)나 register, main memory들을 담은 상태 정보를 의미한다.
Virtual Address Space
각각의 프로세스가 사용하는 가상의 메모리 공간.
(* 아래의 이미지는 리눅스 환경에서의 프로세스의 가상 메모리 공간입니다.)
위 가상공간의 주소 값으로 보아 32bits 운영체제임을 알 수 있다.
Unused
안쓰는 공간.
접근하려 하면 exception을 리턴한다.
Read-only code and data | Read/write data
실행가능한 파일이 저장되어있는 공간.
실질적인 Machine Codes들이 저장되어있는 공간이다.
Read-only data는 리터럴 데이터같이 수정이 필요없는 데이터를 의미한다.
Read/write data는 static variables와 같은 읽기도, 쓰기도 할 수 있는 데이터를 의미한다.
Run-time heap
Heap 영역은 run time에 동적으로 접근 가능한 메모리 영역이다.
C언어에서 malloc과 같은 라이브러리를 통해 접근하고 데이터를 할당할 수 있다.
Memory mapped region for shared libraries
Shared Libraries는 말 그래도 공유되는 라이브러리라는 의미로, C에서 stdlib(* standard library)나 math library와 같은 라이브러리를 의미한다.
printf() 와 같은 함수도 여기에 저장되어있다.
User stack(created at runtime)
이 stack은 유저의 address공간의 가장 위에 존재하는 공간이다.
지역변수나 여러 함수들이 저장되어있어 함수 호출을 통해 동적으로 접근할 수 있는 공간이다.
Kernal virtual memory
Kernal은 OS의 부분중 하나로, 메모리 안에 항상 위치해있다.
응용프로그램은 접근할 수 없는 공간이다.
추가
해당 가상공간의 크기를 알고있으면 감을 잡기 편하다.
해당 공간의 가장 아래의 주소는 0x00000000, 가장 위 주소는 0xffffffff 이므로 2^32 bytes가 할당되어있다.
따라서 가상 공간의 크기는 4GB이다. 또한 Kernal의 크기는 1GB이다(* 리눅스 환경에서, Window의 경우 2GB이다.)
Logical Control Flows
각각의 프로세스는 자기 자신의 logical control flow를 갖고 있다.
동시에 진행되는 통제 흐름들은 사실 물리적으로 배타적인(* disjoint)한 프로세스들로 이루어져있다.
하지만, 이를 우리는 병렬적으로 진행되는 흐름으로 볼 수 있다.
(* 진정한 병렬 프로세스는 코어의 개수에 의존한다.)
Context Switching
통제 흐름은 어느 한 프로세스에서 또 다른 프로세스로 context switch를 통해 넘어갈 수 있다.
이때 kernal code는 프로세스 가상 공간의 가장 위에 위치한 kernal memory에 위치한 코드이다.
즉, kernal은 독립된 프로세스가 아닌, 사용자 프로세스의 일부로서 존재한다.
(* OS는 사용자 프로세스에 수동적인 시스템이다. 즉, 자신이 불려지길 기다리며, PC가 오기를 기다린다.)
fork(void)
새로운 프로세스를 만드는 함수.
fork를 부르는 프로세스(* patent process)와 동일한 또 다른 프로세스(* child process)를 만든다.
child process에는 0을 리턴해주고, parent process에는 자식의 process ID(* pid)를 리턴해준다.
만약 error가 발생한다면 -1을 리턴해주고, 또한 errno를 통해 원인을 알 수도 있다.
(* errno는 전역변수로 프로세스가 만들어질때 생성되는 변수이다.)
(* fork()는 한 번의 호출로 두 개의 리턴값을 주는 특이한 함수이다.)
만약 어떤 프로세스에서 fork를 호출한다면,
자식 프로세스에서의 PC는 fork를 호출한 line부터 코드를 실행시키기 시작한다.
(* 모든 데이터에 대해서 복사가 일어나는 것은 아니다. 이전에 언급했던 프로세스의 크기를 생각해보자.
하나의 프로세스가 가지는 가상 공간은 32bits 운영체제 기준 4GB이다. 메모리의 크기가 16GB라고 한다면 4개의 프로세스만 만들 수 있다는 의미이다. 그렇기 때문에 프로세스의 가상 공간에 비해 실제로 할당되는 공간은 더 작다. OS가 필요할때마다 디스크에서 정보를 가져오는 것이다. fork를 통한 프로세스의 생성도 비슷하다. 모든 데이터를 복사한다면 용량이 부족해진다. 따라서 COW(* Copy on Write), Write작업이 있을 때, 그 공간만 복사하고, 막상 사용하지 않거나, 읽기(* Read)만 하는 공간이라면 parent의 가상 공간을 공유한다.)
errno
에러를 나타내는 값을 리턴했을 때, 그 이유를 알려주는 값이다.
이는 가장 마지막에 일어난 에러를 기준으로 만들어지기에 에러가 여러번 발생했다면, 덮어쓰기될 수 있다.
(* errno는 0으로 초기화되지 않기 때문에, errno를 0으로 만들어줌으로써 error가 어디서 발생했는지 알 수 있다.)
perror()
시스템의 에러 메시지를 보여주는 메소드이다.
아래의 코드는 이를 사용하는 예시이다:
rpid = fork();
if (rpid < 0) {
perror("Fork failed:");
return -1;
}
else if (rpid == 0) {
printf("Child\n");
}
else {
printf("Parent\n")
}
// output:
// Fork failed:
// (error messages)
에러의 종류는 다음과 같다:
- EAGAIN: 시스템이 정해놓은 최대 프로세스의 개수를 초과하는 경우
- ENOMEM: 메모리가 부족한 경우
- ENOSYS: fork가 지원되는 플랫폼이 아닌 경우
Manual(man) page
해당 command가 어떤건지 알 수 있는 명령어.
함수 이름 뒤에 숫자의 종류는 다음과 같다:
- 1: user commands
- 2: system call
- 3: library functions
fork examples #1
핵심: parent process와 child process는 x값을 공유할까? child process의 PC는 어디를 가르킬까?
void fork1()
{
int x = 1;
pid_t pid = fork()
if (pid == 0) { // fork() 의 return 값이 0이면 해당 프로세스가 child라는 의미
printf("Child has x = %d\n", ++x);
} else { // return 값이 0이 아니면 해당 프로세스가 parent라는 의미
printf("Parent has x = %d\n", --x);
}
// printf("Bye from process %d with x = %d\n", getpid(), x);
}
// output:
// parent process' output: 0
// child process' output: 2
결론: 공유하지 않는다.
(* 하지만 부모와 자식 프로세스에서 x의 포인터를 통해 주소값을 찍으면 같은 값이 나온다.
??? x는 공유하지 않는데 왜 주소값이 같을까? 왜냐하면 주소값 자체가 가상공간의 주소이기 때문이다. 실제로 저장되어있는 물리적 공간은 다르지만, 가상 공간에서의 위치는 같기 때문에 주소값이 같은 값으로 나오는것이다.)
fork example #2
void fork2(){
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
결과
fork example #3, #4, #5
void fork3(){
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
void fork4(){
printf("L0\n");
if (fork() != 0){
printf("L1\n");
if (fork() != 0){
printf("L2\n");
fork();
}
}
printf("Bye\n");
}
void fork5(){
printf("L0\n");
if (fork() == 0){
printf("L1\n");
if (fork() == 0){
printf("L2\n");
fork();
}
}
printf("Bye\n");
}
결과
exit()
프로세스를 종료하는 코드
일반적으로 상태값은 0으로 주어진다.
main 함수에서의 return 0 와는 다르다. return은 함수를 종료하는 것이고, exit(0)는 프로세스를 종료하는 것이다.
atexit()
매개변수에 해당하는 함수를 일단 실행하고 process를 종료한다.
void cleanup(void){
printf("cleaning up\n");
}
void fork6(){
atexit(cleanup);
fork();
exit(0);
}
// output:
// cleaning up
// (destroy the process)
'[학교 수업] > [학교 수업] 시스템 프로그래밍' 카테고리의 다른 글
[시스템 프로그래밍] Signal Programming (0) | 2024.09.24 |
---|---|
[시스템 프로그래밍] Signals (0) | 2024.09.20 |
[시스템 프로그래밍] Linux Commands (0) | 2024.09.20 |
[시스템 프로그래밍] Linux Installation (0) | 2024.09.13 |
[시스템 프로그래밍] Multi-process Programming (0) | 2024.09.11 |