지난 1.1, 1.2 포스팅을 통해 우리는 프로세스의 정의를 아래와 같이 확인한 바 있다.
Process : Instance of a Running Program
Process : 프로그램이 메모리에 '명령어의 시퀀스'로 적재된 후, CPU를 동작시킬 수 있는 상태가 되었을때, 그 프로그램을 일컫는 용어
프로그램이 '명령의 집합'이고, 이를 순차적으로 컴파일, 어셈블, 링크, 로드하면 메모리에 적재된다. Program Counter가 메모리 내의 '프로그램 Code 부분'을 가리키고, CPU는 이를 차례대로 실행하게 된다.
~> 이때, 마치 여러 프로세스가 각각의 CPU를 독점하고 있는 것처럼 보이지만, 실상은 단일 CPU가 Time-Sharing을 하면서 여러 프로세스를 처리하고 있다. (단일 CPU 기준)
~> 예를 들어 10ms의 Time Quantam을 기준으로 한다면, CPU가 Context Switch를 해가며 각 프로세스를 순차적으로 부분부분씩 처리하고, 이를 반복한다.
이러한 프로세스는, 다음과 같은 두 종류의 추상화를 제공한다.
~> 이 중, Logical Control Flow에 대해 조금 더 자세히 파헤쳐보자.
만약, 프로세스 p1과 p2가 있고, 단일 CPU가 있다고 해보자.
CPU 안에는 Register Set이 있고, 그 안에는 PC(Program Counter), IR(Instruction Register), 범용 레지스터(ex. EAX, EBX, ..)와 같은 레지스터들이 있다.
p1이 수행되고 있다고 하면, CPU의 PC는 p1의 가상 메모리 부분에서 code 영역의 명령을 가리키게 된다.
인터럽트가 걸리면, OS 커널 공간 안에 있는 Timer Interrupt Handler가 수행되어, 이 핸들러가 'Context Switch'를 수행한다.
이제 p2가 수행되기 시작한다. 커널 내의 p2와 관련된 저장물들이 있는 메모리 공간(Saved Registers)에서 정보들을 가져와 CPU의 Register Set에 Update한다. (p2를 복구하는 것)
~> 마치 프로세스들이 각각 자신만의 메모리와 CPU를 가진 것처럼 추상화되어 동작한다.
(Logical Control Flow + Private Address Space)
※ 메모리 : Kernel Space + User Space
User Space에는 프로그램이 적재되어 Stack-Heap-Data-Code 등의 부분이 각 프로세스마다 할당되고, Kernel Space에는 Context Switch에 필요한 Saved Registers 부분이 있다.
프로세스의 코드 부분이 수행될 때, 만약 프로세스의 명령어가 100개 있다면, CPU는 단순하게 그 100개의 명령어를 순차적으로 읽으면서 수행한다. 'Physical Control Flow(Fetch->Decode->Execute->WriteBack)'를 지켜가면서 말이다.
한편, 만약 두 프로세스를 돌릴 때는 어떨까?
Concurrent Processes : 두 개의 프로세스의 시작부터 끝날때까지의 시간 간격이 겹치는 경우, 이 두 프로세스의 관계를 Concurrent Processes라고 한다.
Sequential Processes : 반대로, 두 개의 프로세스의 시작-끝 시간 간격이 서로 겹치지 않는 경우, 두 프로세스의 관계를 Sequential Processes라고 한다.
~> 즉, 두 프로세스 간의 관계를 나타내는 용어이다.
~> Concurrent Processes의 Control Flow를 보면, 우리 눈에는 동시에 병렬적으로 각각 수행되고 있는 것처럼 보이지만, 실상은 물리적으로 Disjoint하다는 것이다. (Context Switch로 인해!)
Pa와 Pb가 있다. 둘 다 프로세스이고, Code+Stack의 가상 메모리 영역만 가진다고 해보자. Stack에는 지역변수들, Code엔 소스 코드가 들어간다. 두 프로세스는 메인 메모리 어딘가에 상주하고 있다.
이때, 메인 메모리는 User 부분과 Kernel 부분으로 나뉜다고 했다. ★★
만약 이때 System Call(Interrupt or Trap), 예를 들어서 read를 했다고 해보자.
OS가 '메인 메모리에 상주하고 있는(Memory-Resident)' 부분을 커널(Kernel)이라 하는데, 커널 부분의 코드는 마치 기존의 프로세스에 포함된 코드 영역처럼 돌아가게 된다.
커널 함수 부분과 프로세스 부분이 Not Separte인 것처럼 보인다는 것이다.
한편, Control Flow는 Context Switch를 통해 한 프로세스에서 다른 프로세스로 넘어간다고 했다.
예를 들어, Pa 프로세스가 실행될 때, Time Quantam이 10ms라 하면, 10ms만큼 돌았을 때, Timer Interrupt가 걸려 OS 커널 내의 함수인 Timer Interrupt Handler가 마치 Pa의 일부분인 것처럼 동작한다는 것이다.
~> 즉, Pa 프로세스가 타이머 인터럽트가 걸려 끝나고, Context Switch Overhead가 지나간 다음, Pb(Pnext) 프로세스가 수행된다는 것이다.
이제 프로세스에 대한 개념적인 분석을 넘어, 실제 코드 레벨 분석을 해보자.
우리가 Linux Shell에서 'ls -al'이라는 명령을 치면, files와 directories에 대한 Info가 화면에 출력된다.
한편, OS System Call 중에는 fork라는 Call이 있다.
fork : 프로세스를 생성하는 System Call이다.
참고로, 'fork'란 '갈라지다/나뉘다'라는 의미이다. "왜 갈라지는 것일까?"
UNIX 계열의 OS는 '모든 프로그램'에 대해 fork라는 System Call을 하여 프로세스화한다.
위의 예시에서, ls 프로세스는 '>' 프로세스가 fork하여 만들어진 것이다.
'>'는 ls의 parent process, ls는 '>'의 child process이다.
'>'는 Shell이라는 프로세스이다.
그렇다면, 이 Shell 프로세스는 누가 생성한 것일까?
fork에 대해 자세히 확인하기에 앞서, 잠시 Programming Skill에 대한 언급을 하겠다. 우리가 앞으로 계속해서 사용하게 될 Skill이기 때문에 소개한다.
일반적으로, 오류가 발생하면 Linux System에서는 함수의 반환값으로 -1을 부여하고, 전역 변수 'errno'에 에러의 원인을 명시한다.
~> 그래서, 시스템을 다루는 프로그래밍을 할 경우, 우리는 항상 에러 상황을 고려해야한다.
System-Level Function의 return값은 '에러 유무'를 알려주므로, 항상 이 반환값에 주의해야한다.
if ((pid = fork()) < 0) { // 이 fork가 System Call이다.
fprintf(stderr, "fork error: %s\n", strerror(errno)); // 에러 확인!
exit(0);
}
~> 위에서 언급한 fork함수를 사용하는 간단한 코드이다. fork는 System Call, 즉, System-Level Function으로, 언급한 것처럼 반환값을 체크해 에러 여부를 확인하고 있음을 주목하자.
시스템 콜 관련해서 에러가 날 경우, 이를 아예 명시적으로 "어떠한 에러가 났다!"를 화면에 출력하는 프로그래밍 스킬이 있다.
~> 이를 위해선, 일반적으로 아래와 같은 꼴의 'Error-Reporting Function'을 정의한다. (UNIX 계열 시스템 프로그래밍 상황이 기준이다.)
void unix_error_report(char *msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
~> 이러한 스킬을 적용하면, 위의 예시 코드를 아래와 같이 변형할 수 있다.
if ((pid = fork()) < 0)
unix_error_report("fork error");
~> 그리고, 이를 이용해 아래와 같이 '에러 보고 기능'을 탑재한 Fork 함수를 만들어 사용할 수 있다.
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0)
unix_error_report("Error in Fork()!");
return pid;
}
~> 우리는 이제 Fork라는 사용자 정의 함수로 fork를 사용해, 에러 핸들링까지 함께 수행할 수 있다. 어떤가, 가독성도 좋고 괜찮지 않은가? 이러한 프로그래밍 스킬은 System Programming에서 상당히 중요한 역량이다. 잘 기억해두자.
여담) 이러한 코딩 스타일을 'Stevens-Style Error-Handling Wrappers'라고 한다. (랩핑을 씌워놓은거 같다고 해서 Wrapper라고 한다.)
int main(void) {
pid_t pid;
int x = 0;
pid = Fork();
/* Child Process */
if (pid == 0) {
printf("child : x=%d\n", ++x);
exit(0);
}
/* Parent Process */
printf("parent: x=%d\n", --x);
exit(0);
}
(출력 예시)
parent: x=-1
child: x=1
(또는)
child: x=1
parent: x=-1
프로세스가 Code와 Stack 부분을 가진다고 해보자.
이때, CPU가 이 프로세스를 실행하는데, 'Error-Handling Wrapper' 처리가 된 Fork 프로시저를 만난다.
이때, 커널의 fork는 현재 프로세스(이를 Pa라고 부르자)와 똑같은 코드를 복사해 메인 메모리에 적재한다. Pa를 Parent, 새로 생기는 프로세스를 Child라고 한다.
fork라는 System Call은 현재 프로세스의 코드를 똑같이 복제해 메모리에 적재한다.
fork 함수는 parent에겐 0이 아닌 값을, child에겐 0을 반환한다. (구분을 위해)
이때, 두 프로세스에 대한 커널의 Saved Registers 부분에는 PC 값이 '논리적으로 fork 수행 직후에 위치한 명령어'를 가리킨다. 두 프로세스 모두가 말이다.
잊지말자, 현재 우리가 논하는 fork의 목적은 '프로세스 생성'이다.
~> 즉, 우리는 새로운 프로그램을 로딩할 때, fork System Call의 결과로 생긴 Child Process에 원하는 프로그램을 넣는(이 과정은 다음 포스팅에서 설명) 방식으로 수행하는 것이다.
~> 이때, Parent와 Child는 Concurrent Processes가 되므로 Timer 상황에 따라서 parent부터 수행될지, child부터 수행될지 알 수 없다. ★★
=> 그래서, 위의 출력 결과가 두 가지가 가능한 것이다.
※ Process ID를 얻는 함수
pid_t getpid(void) : 현재 Process의 PID 반환
pid_t getppid(void) : Parent Process의 PID 반환
Running : 프로세스가 실행중이거나, 또는 프로세스가 커널에 의해 스케쥴(Context Switch의 대상이 됌)된 후, 실행되기 전에 기다리고 있는 상태 (Running includes Ready)
Stopped = Blocked = Waited = Skipped : 프로세스가 수행이 잠시 Suspend되고, 아직 언제 실행될 것인지 스케쥴되지 않은 상태
실행되기로 예정되었으면 Ready(Running), 아직 스케쥴되지 않았으면 Waited(Stopped)
Terminated : 프로세스가 어떠한 Signal에 의해 종료된 상태
Done : 프로세스가 수행을 마치고 (정상) 종료된 상태
exit함수의 주요 특징 : return하지 않고 그냥 종료된다. (위에서 반환이라 표현한 것은 status 인자를 변경하는 것을 의미. exit 함수 자체는 반환이 void임.
Parent Process가 fork System Call을 이용해 새로운 Child Process를 생성한다.
fork함수 : int fork(void)
fork함수의 주요 특징 : 한 번 호출되면, 두 번의 반환을 수행한다. ★★★
우리는 'Process Graph'를 이용하여 Concurrent Program(Processes)의 Partial Ordering을 표현할 수 있다.
그래프의 각 Vertex는 '명령의 수행'을 나타낸다.
'a -> b'는 a가 b보다 먼저 수행됨을 의미한다.
그래프의 각 Edge는 현재 변수들의 값으로 레이블화할 수 있다.
그래프의 시작 Vertex는 In-Edge가 없다.
~> Process Graph를 어떻게 Topological Sorting하든, 그 결과는 Feasible한 Total Ordering이다.
~> 이때, 이 Partial Ordering은 좌에서 우로 흐르게 그리곤 한다.
금일 포스팅은 여기까지이다.