CS/OS

Operating System Concepts 정리 - Ch.03

VSFe 2021. 1. 10. 14:59

3.1 Process Concept

CPU의 활동은 무엇인가? → 시스템 마다 부르는 명칭이 다름. (일괄처리 시스템: job/시분할 시스템: task...) → 하지만 이런 것들은 꽤나 유사함 → 프로세스라고 부르자!

간단하게 보자면, 프로세스는 실행 중인 프로그램이라고 보면 된다.

그래서 프로그램 == 프로세스일까? 아니다. 프로그램은 디스크에 저장된 실행 파일 같은 것들이고, 이 파일이 메모리에 적재되어 실행이 되어야 프로세스라고 할 수 있다.

프로세스는 Program Counter와 Register를 갖고 있고, 메모리 상에서 다음과 같은 형태를 띄고 있다. (물론 일반적으로...)

  • Text Section: 코드
  • Data Section: 전역 변수
  • Stack Section: 복귀 주소나 지역 변수 같은 임시적인 자료
  • Heap Section: 동적으로 할당되는 메모리

그림을 보면 text랑 data는 뭔가 못 늘릴 것 같음 → 런타임 시점에 할당된 이후엔 고정. 당연히 stack이랑 heap은 고정되어 있겠지...?

함수를 실행하면, 함수의 매개변수, 지역변수, 함수 종료 후 복귀할 주소값을 담은 activation record을 stack에 담게 됨. 종료 되면 당연히 pop. → 따라서 동적으로 움직임! heap은 뭐 말할 필요도 없고...

잠깐 Activation Record에 대해서 알아보자.

OS가 프로그램을 실행하기 위해 main을 호출하면 main의 Activation Record가 생성됨.

Activation Record는 크게 네 부분으로 나눌 수 있는데,

  • Return Value: 피호출 함수가 호출 함수에게 넘기는 값
  • Value Parameter: 호출 함수가 피호출 함수에게 넘기려고 하는 값 (함수의 매개변수 떠올리면 편할듯...)
  • Return Address: 함수가 호출된 곳의 다음 주소. 함수가 종료되면 함수를 호출했던 부분의 다음 줄을 실행해야 하니, 주소도 함수를 호출한 주소가 아닌 다음 주소를 넘기는 것이다.
  • Local Variables: 지역변수들...

http://www.cs.utsa.edu/~wagner/CS3723/compilerd/act_rec.html

이걸로 여기를 조금 더 공부해보자...

참고로 프로그램이 같다고 해서 프로세스가 같은건 아니다. 당장 구글 크롬 창 두개 띄우면 서로 다른 프로세스거든... 그래서 Code Segment (Text Section) 이랑 Data Section의 내용물이 같을진 몰라도 Stack이랑 Heap은 천차만별임.

java는 JVM이라는 VM에서 동작하는데, 엄밀히 말하면 java 명령어를 통해 JVM 프로세스를 실행하고, java 명령어 뒤에 명시된 프로그램을 JVM 내 가상머신으로 돌림. 즉, 프로세스는 다른 프로세스가 동작할 수 있는 환경을 만들어주기도 한다는 소리!

프로세스는 'State'가 중요한데, 프로세스는 다음 5개 중 하나의 State에 놓여있다.

  • New: 프로세스가 생성 중
  • Running: 명령어들이 실행되고 있음
  • Waiting: 프로세스가 이벤트를 대기하고 있음 (입출력 완료/신호 수신)
  • Ready: 프로세스가 CPU에 스케쥴링 되기를 대기 중
  • Terminated: 프로세스가 종료 됨

OS는 프로세스를 Process Control Block으로 표현한다. PCB는 수 많은 정보를 포함하며, 다음과 같은 것들은 포함한다.

  • Process State
  • Program Counter: 다음에 실행할 명령어의 주소를 가리킴.
  • Registers: Accumulator, Index Register, Stack Register, General-Purpose Register와 Conditional Code 정보가 포함됨. 인터럽트가 발생할 시 나중에 프로그램이 정상적으로 실행되기 위해, PC와함께 저장되어야 함.
  • CPU Scheduling Information: 우선순위, 스케쥴링 큐에 대한 포인터와 매개변수
  • Memory-management Information: 운영체제에 의해 사용되는 메모리 시스템에 따라 base/limit register, 또는 Page/segment table 정보를 포함함.
  • Accounting Information: CPU 사용 시간, 시간제한, 계정 번호, 프로세스 번호 등...
  • I/O State Information: 할당된 I/O장비와 열린 파일의 목록 저장

 

프로세스 내에는 스레드가 있다. 지금까지는 하나의 프로세스에 하나의 스레드가 할당된거라고 봐야 하지만, 여러 스레드가 병렬로 실행될 수 있다. 이럴 땐 PCB에 각 스레드에 대한 정보를 포함한다.

3.2 Process Scheduling

Process Scheduler는 CPU에서 실행 가능한 프로세스 중 하나를 선택한다. 만약에 프로세스가 여러개라면? 일단 CPU가 자유로워 질 때 까지 계속 대기해야지 뭐...

일단 프로세스가 시스템에 들어오면, Job Queue에 놓임. 해당 큐에는 시스템 안에 모든 프로세스가 들어가 있음. (메인 메모리에 존재) Ready 상태가 되면 Ready Queue로 이동함. (일반적으로 Linked List로 구현함) 참고로 Ready Queue의 header는 리스트의 첫번째 PCB와 마지막 PCB를 가리키는 포인터를 포함함. 당연히 Linked List를 만들어야 하니까 각각의 PCB는 Ready Queue에 있는 다음 프로세스를 가리키는 포인터 필드가 있을 수 밖에...

 

일단 Ready Queue에 놓이면 Dispatch 될 때 까지 Ready Queue에서 대기함. 이후 CPU가 할당되어 실행되면...

  • 입출력 요청을 해서 I/O Queue로 보내질 수 있음
  • 새로운 child Process를 생성하고 종료를 기다릴 수 있음
  • Interrupt 당해서 강제로 제거되고, 다시 Ready Queue로 넘어갈 수 있음

처음 두 경우는 Wating 상태에서 Ready 상태로 전환될거고, 이후에 큐에 다시 들어갈거임... 마지막으로 프로세스가 종료되면 모든 큐에서 삭제되고 PCB와 자원을 반납함.

보시다시피 다양한 스케쥴링 큐 사이를 왔다리 갔다리 할거임...

  • Long-term Scheduler: 실행될 수 있는 후보 프로세스중에서 선택해서 실행하기 위해 메모리에 적재 (자주 일어나지 않음.)
  • Short-term Scheduler: 실행 준비가 완료되어 있는 프로세스 중에서 선택해서, CPU를 할당함. (자주 발생함.)

당연히 자주 발생하는게 빨라야 마음이 편하니, Short-term이 훨씬 빨라야함.

Long-term은 아무래도 프로세스를 생성하다보니 간격이 조금 여유있음. 다만 이건 메모리에 있는 프로세스의 수를 제어하는 중요한 역할을 함. 근데, 갯수를 일정하게 유지한다면 당연히 하나가 나가야 새로 할당하겠지? 그래서 자주 발생하지 않음...

참고로 UNIX와 Windows 같은 시분할 시스템은 Lorm-term이 없으며, 모든 새로운 프로세스는 전부 Short-term에 넣는다. 물론 medium-term을 도입해서, 메모리에서 일부 프로세스를 제거하고, 추후에 swapping을 통해 다시 메모리로 불러와서 중단 지점에서 다시 실행을 재개함.

위에서 인터럽트 이야기를 했었는데, 실제로 인터럽트가 발생하면 인터럽트 처리 후에 상태를 다시 복원해야 하므로 현재 상태를 저장할 필요가 있음. 이러한 정보를 Context라고 하는데, 이는 PCB에 표현 됨. 아무튼, CPU를 다른 프로세스로 교환하려면 이전의 상태를 보관하고 새로운 프로세스로 전환해야 하는데, 이 작업을 Context Switch라고 부름.

Context Switch가 발생하면, 커널은 과거 프로세스의 Context를 PCB에 저장하고, 스케쥴 된 새로운 프로세스의 저장된 Context를 꺼내옴. 다만 이 시간동안 시스템은 다른 일을 할 수 없기 때문에, 순수한 오버헤드 시간으로 볼 수 있다. 즉, Context Switch가 잦으면 손해다... 일단 속도는 메모리의 속도, 복사되어야 할 레지스터의 수, 특수 명령어의 존재에 따라 좌우된다.

3.3 Operation on Processes

프로세스는 여러 개의 새로운 프로세스들을 생성할 수 있다. 이런 경우 생성하는 프로세스를 부모 프로세스, 새로운 프로세스는 자식 프로세스라고 부른다. (즉, 트리 구조라고 볼 수 있다!)

대부분의 OS는 유일한 PID (Process ID) 를 사용해서 프로세스를 구분한다.

일단 시스템이 부팅되면 systemd라는 프로세스가 생성이 됨. (호환성을 위해 init.d라는 이름으로도 된다고 들었는데.... 나중에 찾아봐야겠다.)

login 프로세스에 대해서 이야기를 하자면, 시스템에 직접 로그인을 하는 클라이언트를 관리하는 책임을 짐. 그러니까 자연스럽게 bash가 login의 자식일 수 밖에...

일반적으로 자식을 생성하면, 자식은 자신의 임무를 달성하기 위해 CPU시간이나, 메모리, 파일 등의 자원이 필요하다. OS로부터 직접 받을 수 있짐나, 부모가 가진 자원의 부분 집합만을 사용하도록 제한될 수 있다.

또한, 부모 프로세스는 자식에게 초기화 데이터를 전달할 수 있다. (ex. img.jpg라는 파일을 출력하는 프로세스가 있다면, 부모로부터 파일 경로를 받을 수 있고, 그럼 그 프로세스는 해당 경로에 있는 파일을 출력하기만 하면 됨.)

프로세스가 새로운 프로세스를 생성하면, 두 프로세스는 다음과 같이 실행할 수 있다.

  • 부모는 자식과 병행하게 실행된다.
  • 부모는 일부 또는 모든 자식이 종료될 때 까지 기다린다.

공간 측면에서는 다음과 같은 가능성이 있다.

  • 자식 프로세스는 부모의 복사본이다. (즉, 똑같은 프로그램과 데이터를 가짐.)
  • 자신에게 적재될 새로운 프로그램을 갖고 있다.

POSIX에서의 예시

  • fork()라는 System call로 새로운 프로세스를 생성함.
  • 새로운 프로세스는 원래 프로세스의 주소 공간의 복사본으로 구성. (즉, 부모가 쉽게 자식과 통신 가능) → 자식은 0 return, 부모는 자식의, PID return.
  • fork() 이후 한 프로세스가 exec()를 통해 자신의 메모리 공간을 새로운 프로그램으로 교체 함.
  • 만약 자식이 실행하는 동안 대기해야 한다면, wait() System call을 통해 Ready Queue에서 자신을 제거한다.
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();
		/*
		새로운 프로세스를 생성함. 자식 프로세스는 fork() 이후 코드부터 실행됨.
		*/

    if(pid < 0) { // 생성 실패
        fprintf(stderr, "Fork Failed");
        return 1;
    } else if(pid == 0) { // 리턴 값이 0: 자식
        execlp("/bin/ls", "ls", NULL);
				// execlp(const char *file, const char *arg0, ... const char *argn, (char *) 0);
				// file에 지정한 파일을 실행하여 arg0 ~ argn까지를 인자로 함. 끝은 항상 NULL.
				// 실행이긴 하지만, 결국 exec 계열이기 때문에 자식 프로세스를 덮어씌워버림.
    } else { // 리턴 값이 pid: 부모
        wait(NULL);
        printf("Child Complete");
    }

    return 0;
}

Windows API에서의 예시

 

프로세스가 마지막 문장을 끝내면 exit() System Call을 호출해서 자신의 삭제를 요청함. 이 시점에서 상태 값을 반환함. 또한 물리/가상 메모리, 열린 파일, I/O Buffer 등의 자원을 반납함.

다만, 부모가 적당한 System Call을 통해 프로세스의 종료를 유발할 수 있고, 사용자 또한 임의적으로 중단시킬 수 있다.

그럼 부모가 자식을 왜 죽일까? 너무 콩가루 집안 같다.

  • 자식이 자신에게 할당된 자원을 초과해서 사용 → 당연히 부모가 자식의 상태를 검사해야 함.
  • 자식에게 할당된 작업이 필요 없어질 때
  • 부모가 exit를 하는데, OS 차원에서 부모 exit 이후에 자식이 살아있는 것을 금지 하는 경우

위에서 볼 수 있듯이, 부모 프로세스가 종료된 이후에 자식이 존재하는걸 금하기 때문에 부모 종료전 순차적으로 모든 자식 프로세스가 종료되어야 한다. → Cascading Termination

아까 위 코드를 보면 딱히 exit()를 명시하지 않았는데, 일반적으로 main의 return 값을 통해 exit(0); 이런식으로 간접적으로 호출함.

pid_t pid; int status; pid = wait(&status);

부모가 wait을 통해 어느 자식이 종료되었는지 구별할 수 있도록 wait는 pid를 반환함.

일단 프로세스가 종료되면 PCB의 해당 항목은 부모가 wait()를 실행하기 전 까지 남아 있다. 이런 프로세스를 Zombie Process라고 부르는데, 아주 짧은 시간만 그렇고, 부모가 wait()를 호출하면 좀비의 PID와 PCB내의 해당 항목이 반환됨.

참고로 부모가 wait()를 호출하지 않고 그냥 종료하면 고아가 되어 버리는데... 이런 경우엔 (UNIX 기준) 부모를 init/systemd로 지정해서 해결함.

Interprocess Communication

프로세스는 독립적이거나 협력적인 프로세스이다. 이분법적이네...

그렇다면 프로세스는 왜 협력을 해야할까?

  • Information Sharing: 여러 사용자가 동일한 정보에 흥미를 가질 수 있으니, 이런 정보를 프로세스들이 병행적으로 접근할 수 있게 해야함.
  • Computation Speed-up: 특정 태스크를 빨리 실행하려고 함 → 이걸 서브태스크로 나눠, 각각 다른 서브태스크와 병렬로 실행되게 할 수 있음. (물론 실질적인 속도를 높이려면 멀티 코어여야 함...)
  • Modularity: 2장에서 모듈형 시스템을 언급했었는데, 이를 구현하기 위해 프로세스를 분할 할 수 있다.
  • Convenience: 개별 사용자들이 한 순간 많은 Task를 가지고 있을 수 있음.

협력하는 프로세스들은 데이터와 정보를 교환할 수 있는 InterProcess Communication (IPC) 기법을 필요로 한다.

프로세스의 통신에는 기본적으로 Shared Memory 방식과 Message Passing 모델이 있음. 전자의 경우는 공유되는 메모리의 영역이 구축되어 그 영역에 데이터를 읽고 쓰고, 후자의 경우에는 메시지를 서로 교환함.

 

잔지의 경우는 System call이 메모리 영역 구축때만 사용되어 속도가 빠르고, 일단 구축되면 일반적인 메모리처럼 접근이 가능하기 때문에 커널의 도움이 필요 없다. 후자의 경우는 충돌을 회피할 필요가 없기 때문에 적은 양의 데이터를 교환하는데 유용하고, 분산 시스템에서 구현하기 쉬움. 참고로, 많은 코어를 가진 시스템에서는 공유 메모리가 성능이 떨어지는데, 공유 데이터가 여러 캐시 메모리를 이주하기 때문에 성능 저하가 발생하기 때문이다.

3.5 IPC in Shared-Memory Systems

통신하는 프로세스들이 공유 메모리 영역을 형성해야 한다. 일반적으로는 공유 메모리 세그먼트를 생성하는 프로세스의 주소 공간에 위치하는데, 통신하는 다른 프로세스들은 해당 세그먼트를 자신의 주소 공간에 추가해야 한다.

OS는 일반적으로 한 프로세스가 다른 프로세스에 접근하는 것을 금지하는데, 공유 메모리를 사용하게 되면 이 제약 조건을 제거해야 한다. 그래야 공유 영역을 읽고 쓸 수 있다.

위치나 형식은 프로세스들에 의해 결정되며, OS는 간섭하지 않음. 또한, 프로세스들은 동시에 동일한 위치에 쓰지 않도록 해야 한다.

상황을 예로 들어서, Producer (생산자) - Consumer (소비자) 모델을 떠올려 보자. (해당 모델을 예로 들면, 컴파일러는 어셈블리 코드를 생산하고, 어셈블리는 이것을 소비해서 목적 프로그램을 만들고, 이를 로더가 소비한다.) 생산자와 소비자가 병행으로 실행되려면, 생산자가 정보를 채워 넣고 소비자가 소모할 수 있는 버퍼를 하나 만들어 주면 좋을 것 같음.

두 가지 유형의 버퍼가 사용되는데, 하나는 unbounded buffer라고 해서 버퍼의 크기가 고정되지 않은 경우인데, 이 경우 생산자가 언제든 새로운 항목을 생산할 수 있는 경우이다. 다른 하나는 bounded buffer라고 해서 버퍼의 크기가 고정되어 있는 경우인데, 버퍼가 비어 있으면 소비자는 반드시 대기 해야 하며, 모든 버퍼가 꽉 차 있으면 생산자가 대기 해야 한다.

#define BUFFER_SIZE 10

typedef struct {
 /* Some Code... */
} item;

item buffer[BUFFER_SIZE];
int in = 0;
int out = 0;

이런 bounded buffer를 하나 만들어 보자.

두개의 논리 포인터 in/out을 가지고 있는데, in은 다음으로 비어 있는 위치를 가리키고, out은 첫번째 차 있는 위치를 가리킨다. in == out 이면 버퍼는 비어 있고, (in + 1) % BUFFER_SIZE == out 이면 꽉 차 있다.

그럼 생산자와 소비자의 코드를 만들어 보자.

// Producer
item next_produced;

while (true) {
    while (((in + 1) % BUFFER_SIZE) == out)
		; // Wait
		buffer[in] = next_produced;
		in = (in + 1) % BUFFER_SIZE;
}
// Consumer
item next_consumed;

while (true) {
		while (in == out)
		;
    next_consumed = buffer[out];
    out = (out + 1) % BUFFER_SIZE;
}

BUFFER_SIZE - 1 까지만 버퍼에 수용할 수 있음...

3.6 IPC in Message-Passing System

동일한 주소 공간을 공유하지 않고도 프로세스들이 통신을 하고, 그들의 동작을 동기화 할 수 있도록 하는 기법을 제공함 → 네트워크에 연결된 다른 컴퓨터가 존재할 수 있는 환경이면 아주 좋겠네!

메시지 전달 시스템은 최소 두 가지 연산을 제공하는데, send(message)receive(message) 이다.

P와 Q가 통신을 원하면, 반드시 서로 메시지를 주고 받아야 하고, 이들 사이에 communication Link가 형성이 되어야 한다. 물리적인 구현은 다 빼고... 그래서 어떻게 논리적으로 구현을 해야 할까?

  • 직접 또는 간접 통신
  • 동기식 또는 비동기식 통신
  • 자동 또는 명시적 버퍼링

Naming

프로세스 중 통신을 원하는걸 찾아내려면 일단 식별을 해야함.

직접 통신 하에선, 통신을 원하는 각 프로세스는 수신자와 송신자의 이름을 명시해야 함. 여기서는 send와 receive를 이렇게 명시함.

  • send(P, message): 프로세스 P에게 메시지를 전송함
  • receive (Q, message): 프로세스 Q로부터 메시지를 수신한다.

통신 연결은 다음과 같은 특성을 가짐

  • 통신을 원하는 프로세스의 쌍들 사이에 연결이 자동적으로 구축됨. 즉, 통신을 하기 위해선 서로의 신원만 알면 됨.
  • 연결은 정확히 두 프로세스들 사이에만 연관됨
  • 통신하는 프로세스들의 쌍 사이에는 정확히 하나의 연결만 존재함.

다시 말해서, 주소 방식에서 대칭성을 보인다고 할 수 있음. 다만 살짝 꼬면... 비대칭적으로 가능하기도 함. (receive를 임의의 프로세스로 부터 메시지를 수신하는 것으로 바뀜 → 변수 id는 통신을 발생시킨 프로세스의 이름으로 설정됨.)

단점이라면 프로세스 지정으로 인한 제한된 모듈성. 식별자를 하나 바꾸면 다른 부분을 다 찾아서 전부 바꿔야 함.

간접 통신에서 메시지들은 mailbox 또는 port로 송신되고, 그것으로부터 수신됨. 프로세스들에 의해 메시지들이 넣어지고, 제거된다. 메일박스는 고유의 id를 갖고 있는데, 예를 들어 POSIX의 메시지 큐는 메일박스를 식별하기 위해 정수 값을 사용함. 이 기법에서 프로세스는 다수의 메일박스를 통해 다양한 프로세스와 통신할 수 있음.

그렇다고 통신을 어떻게 정의할까? 두 프로세스가 같은 공유 메일박스를 가지면 됨. 즉, send와 receive를

  • send (A, message): 메시지를 메일박스 A로 송신함.
  • receive (A, message): 메시지를 메일박스 A로부터 수신함.

통신 연결은 다음과 같은 특성을 가짐

  • 프로세스간 연결은 이들 프로세스가 공유 메일박스를 가질 때만 구축됨.
  • 연결은 두 개 이상의 프로세스와 연관될 수 있다.
  • 통신하고 있는 각 프로세스 사이에는 다수의 서로 다른 연결이 존재할 수 있고, 각 연결은 하나의 메일박스에 대응함.

만약 프로세스 P1, P2, P3가 하나의 메일박스를 공유한다고 해보자. P1이 메일박스로 메시지를 보내고, P2와 P3가 receive를 하면, 누가 받을까?

  • 하나의 링크는 최대 두 게의 프로세스만 연관되도록 설정함 → 지정 가능
  • 한 순간에 최대 하나의 프로세스만 receive 연산을 실행하도록 허용 → 먼저 한 놈이 임자
  • 어느 프로세스가 메시지를 수신할지 시스템이 임의로 선택하게 함. → 즉, 시스템에서 정의된 알고리즘으로 전달함 (ex. 라운드 로빈)

메일박스는 OS나 프로세스에 의해 소유될 수 있다. 일단 프로세스가 소유하는 경우를 보면, 소유자와 사용자를 구분할 수 있다. 또한 프로세스가 종료되면 메일박스가 삭제되니, 사용하고 있던 다른 프로세스들에게 메일박스를 사용할 수 없다고 알려야 한다. OS는 프로세스가 메일박스와 관련하여

  • 새로운 메일박스를 생성함
  • 메일박스를 통해 메시지를 송신하고 수신함
  • 메일박스를 삭제함

과 같은 연산을 지원해야 함.

OS가 소유하는 메일박스는 자체적으로 존재한다. 당연히 특정한 프로세스에 의해 예속되지 않음.

Synchronization

메시지 전달은 blocking/non-blocking 방식으로 전달된다.

  • Blocking Sending: 송신하는 프로세스는 메시지가 수신 프로세스 또는 메일박스에 의해 수신될때까지 블락된다.
  • Non-Blocking Sending: 송신하는 프로세스가 메시지를 보내고 작업을 재시작한다.
  • Blocking Receiving: 수신자는 메시지가 도착할 때 까지 봉쇄된다.
  • Non-Blocking Receiving: 수신자는 유효한 메시지를 받거나 Null을 받는다.

만약 송/수신이 둘다 Blocking이면 송수신자 간에 Rendezvous를 이루게 됨. 그럼 동기화 문제는 안 해도 됨... (그냥 보내고 대기하고 받을때까지 대기하고...)

Buffering

통신하는 프로세스들에 의해 교환되는 메시지는 임시 큐에 존재하게 됨. 큐는 다음과 같은 종류가 있다.

  • Zero Capacity: 최대 길이가 0. 즉 링크는 대기하는 메시지들을 가질 수 없으므로, 송신자는 수신자가 보낼때 까지 무한 대기.
  • bounded Capacity: 큐는 유한한 길이 n을 가짐. 만약 큐의 공간이 남았음녀 송신자는 대기하지 않고 실행을 계속하고, 아니면 공간 생길때 까지 대기
  • unbounded Capacity: 대기할 이유가 없음.

Zero Capacity Queue의 경우, Buffering이 없다고 흔히 말한다.

3.8 Communication in Client-Server Systems

IPC에서 다뤘던 기법들은 클라이언트의 서버 시스템의 통신에도 사용할 수 있다.

이 챕터에서는 클라이언트 서버에서 사용 가능한 세 가지 통신 전략에 대해 다뤄보자.

Socket

Socket이란 통신의 Endpoint를 의미한다. 네트워크를 통해 통신하는 한 쌍의 프로세스는 각각 하나씩의 소켓이 필요하다. 각 소켓의 식별은 IP주소와 포트번호를 통해 식별한다.

일단 지정된 포트에 클라이언트 요청 메시지가 도착하기를 기다리고, 요청이 수신되면 서버는 클라이언트 소켓으로부터 연결 요청을 수락함으로써 연결이 진행된다.

클라이언트 프로세스가 연결을 요청하면 호스트 PC가 포트 번호를 부여하는데, 이 땐 1024보다 큰 임의의 정수가 된다. (1024 미만의 포트번호는 표준 서비스를 구현하는데 사용됨. → 실제로 포트를 할당할 때 1024 미만인 경우는 루트권한이 필요한 경우가 많음!!)

 

특정 프로세스가 X에 연결을 요청했을 때, 다음과 같이 1625번 포트를 할당 받고 웹서버에 접속하는 환경을 보여준다.

모든 연결은 유일해야 하므로, 다른 클라이언트 프로세스가 X에 요청하게 될 경우, 1625번이 아닌 다른 포트 번호를 할당 받을 것이다.

소켓은 분산된 프로세스 간 널리 사용되고 효율적이긴 한데 너무 저수준이라 바이트 스트림만 통신할 수 있다.

Remote Procedure Calls

원래는 네트워크에 연결되어 있는 두 시스템 사이의 통신에 사용하기 위하여 프로시저 호출 기법을 분리해 생각하기 위한 방편으로 설계됨.

IPC와는 다르게 RPC에서 전달되는 메시지는 구조화 되어 있음. 즉 패킷 수준 이상! 또한 각 메시지에는 원격지 포트에서 listen 중인 RPC 데몬의 주소가 지정되어 있고 실행되어야 할 함수의 식별자, 그리고 그 함수에게 전달되어야 할 매개변수가 포함됨.

포트는 단순히 각 메시지 패킷의 시작부분에 포함되는 정수임. 원격 프로세스가 어떤 서비를 받고자 하면 그 서비스에 대응되는 적잘한 포트 주소로 메시지를 보내야 한다.

RPC는 클라이언트가 원격 호스트의 프로서지러르 호출하는 것을 마치 자기의 프로시저를 호출하는 것처럼 해준다. RPC 시스템은 클라이언트 쪽에 stub을 제공하여 통신을 하는 데 필요한 자세한 사항들을 숨겨준다.

만약 클라이언트와 서버의 데이터 표현 방식이 다르다면? (ex. Big-endian, Little-endian) 그런 경우를 대비해 클라이언트에서 데이터를 보내기 전 XDR(eXternal Data Representation) 형태로 바꾸어서 보낸다. 수신측에서는 매개변수를 풀어내면서 자기 기종의 형태로 데이터를 받아서 넘겨준다.

Pipe

초기 UNIX 시스템에서 제공하는 IPC 기법의 하나였었다. 다만 통신할 때는 여러 제약을 가진다.

우선 파이프를 구현하기 전, 4가지 문제를 고려해야 하는데,

  • 파이프가 단방향, 양방향 통신을 허용하는가?
  • 양방향 통신이 허용된다면 Half-Duplex인가 Full-Duplex인가?
  • 통신하는 두 프로세스 간 부모-자식 같은 특정 관계가 존재하는가?
  • 파이프는 네트워크를 통해서 통신이 가능한가, 아니면 동일한 기계 내 존재하는 두 프로세스 끼리만 통신할 수 있는가?

Ordinary Pipe

생산자-소비자 형태로 두 프로세스 간의 통신을 허용한다. 생산자는 한쪽으로만 쓰고, 소비자는 다른쪽으로만 읽는다. 결과적으로 단방향 통신이라고 봐야 한다. 만약에 양방향 통신이 필요하면 파이프를 두개 만들어야 할 것이다.

UNIX에서 일반 파이프는 다음 함수를 통해 구축한다.

pipe(int fd[]);

이 함수는 fd[] File Descripter를 통해 접근되는 파이프를 생성한다. fd[0]은 읽기 종단, fd[1]은 쓰기 종단으로 동작한다. 그런데 UNIX는 파이프도 파일로 취급하기 때문에, read()와 write()가 사용이 가능하다.

 

다만 Ordinary Pipe는 생성한 프로세스 이외엔 접근이 불가하다. 다만 자식 프로세스가 생성되면 자원들을 상속받는데, 여기에 파일 (파이프도 일종의 파일이므로)도 포함되어 있으므로 통상적으로 자식 프로세스와 함께 이용한다.

Windows의 경우는 어떨까? 여기서는 Anonymous Pipe라고 부르는데, Windows의 경우 자식 프로세스를 생성할 때 어떤 속성을 상속받는지 명시해야 한다.

Named Pipe

Ordinary Pipe는 프로세스가 통신을 마치고 종료하면 그냥 없어진다.

Named Pipe는 양방향 통신이 가능하며, 부모-자식 관계도 필요하지 않다. 일단 생성이 되면 여러 프로세스들이 이를 사용하여 통신할 수 있다.

UNIX에서는 FIFO라고 부른다. 일반 파일 시스템 처럼 존재하며, mkfifo()라는 System Call을 통해 생성되고, 일반적인 open(), read(), write(), close()로 조작된다. 물론 양방향 통신이라고 했지 Full-Duplex라고 하진 않았으므로, 여기서도 일반적으로 2개의 FIFO를 사용한다. 추가로 통신하는 프로세스는 같은 기계 내 존재해야 한다.

Windows의 경우 Full-Duplex를 허용하며, 다른 기계 내 프로세스 간 파이프 전송도 지원한다.

'CS > OS' 카테고리의 다른 글

Operating System Concepts 정리 - Ch.05  (0) 2021.01.21
Operating System Concepts 정리 - Ch.04  (0) 2021.01.15
Operating System Concepts 정리 - Ch.02  (2) 2021.01.02